npm package: @ngx-runtime-i18n/angular
Description: Angular bindings for @ngx-runtime-i18n (signals-first, SSR-safe)
Latest version: 2.0.0

# @ngx-runtime-i18n/angular

Lean, SSR‑safe Angular wrapper around `@ngx-runtime-i18n/core`.

- Signals‑first service (`I18nService`) and `I18nPipe` for ergonomic templates
- Optional `I18nCompatService` (RxJS) for non‑signals apps
- SSR‑aware: TransferState snapshot on the server, hydration‑safe on the client
- Cancellation‑aware language switching (rapid toggles won’t corrupt state)
- Lazy Angular **locale data** per language to power pipes (`DatePipe`, `DecimalPipe`, ...)
- Configurable fallback chains with in-memory or localStorage catalog caching

Peer support: `@angular/* >=16 <21`

---

## Install

Always install both packages explicitly:

```bash
npm i @ngx-runtime-i18n/angular @ngx-runtime-i18n/core
```

---

## Directory layout (recommended)

```
your-app/
  src/
    public/
      i18n/
        en.json
        hi.json
        de.json
```

At runtime, catalogs are fetched from `/i18n/<lang>.json` by default in our examples.

---

## Quick Start (CSR)

```ts
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideRuntimeI18n } from '@ngx-runtime-i18n/angular';
import { appRoutes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(appRoutes),
    provideRuntimeI18n(
      {
        defaultLang: 'en',
        supported: ['en', 'hi', 'de'],
        fallbacks: ['de'],
        fetchCatalog: (lang, signal) =>
          fetch(`/i18n/${lang}.json`, { signal }).then((r) => {
            if (!r.ok) throw new Error(`Failed to load catalog: ${lang}`);
            return r.json();
          }),
        onMissingKey: (k) => k,
      },
      {
        localeLoaders: {
          en: () => import('@angular/common/locales/global/en'),
          hi: () => import('@angular/common/locales/global/hi'),
          de: () => import('@angular/common/locales/global/de'),
        },
        options: {
          autoDetect: true,
          storageKey: '@ngx-runtime-i18n:lang',
          cacheMode: 'storage',
          cacheKeyPrefix: '@ngx-runtime-i18n:catalog:',
          preferNavigatorBase: true,
        },
      }
    ),
  ],
};
```

**Template usage**

```html
<h1>{{ 'hello.user' | i18n:{ name: username } }}</h1>
<p>{{ 'cart.items' | i18n:{ count: items().length } }}</p>
```

**Component usage**

```ts
import { Component, inject } from '@angular/core';
import { I18nService, I18nPipe } from '@ngx-runtime-i18n/angular';

@Component({
  standalone: true,
  imports: [I18nPipe],
  template: `
    <button (click)="switch('de')">Deutsch</button>
    <div *ngIf="i18n.ready()">{{ i18n.t('hello.user', { name: 'Ashwin' }) }}</div>
  `,
})
export class SomeCmp {
  i18n = inject(I18nService);
  switch(lang: string) {
    if (this.i18n.ready()) this.i18n.setLang(lang);
  }
}
```

---

## DX helpers

`I18nService` exposes synchronous helpers that pair nicely with Angular signals during development:

- `getCurrentLang()` — snapshot the current language without subscribing to `lang()`.
- `getLoadedLangs()` — inspect which catalogs are resident in memory.
- `hasKey(key, lang = current)` — check catalog coverage without formatting.

```ts
const lang = i18n.getCurrentLang();
const loaded = i18n.getLoadedLangs();
const missingLegacy = !i18n.hasKey('legacy.title');
```

Render these in diagnostics panels or dev tools to confirm when catalogs hydrate.

## Switching & preloading

`I18nService` now exposes two additional signals:

- `switching()` becomes `true` as soon as you call `setLang()` with a new language, so you can show a spinner or disable controls.
- `activeSwitchLang()` mirrors the requested language (`null` otherwise), letting you render “Switching to fr…” helper text.

It also adds preload helpers that warm the same loaders used by `setLang()` without mutating the active language:

- `preloadLang(lang)` for a single language (includes fallback catalogs).
- `preloadLangs(langs)` for parallel prefetches (good when the user’s likely languages are known up front).
- `preloadFallbackChain(lang?)` to hydrate a language and its configured fallbacks (defaults to the active lang if none is passed).

```ts
const i18n = inject(I18nService);

// Disable the switcher while the next catalog downloads.
readonly onSwitch = (lang: string) => {
  if (i18n.switching()) return;
  i18n.setLang(lang);
};

// Warm caches for a user who usually toggles between German and Hindi.
await i18n.preloadLangs(['de', 'hi']);

// Preload the active route’s fallback chain before navigation.
await i18n.preloadFallbackChain('fr');
```

Use this to preload catalogs during login, before showing a heavy route, or while waiting for other async work; the currently active language and UI stay stable.

---

## Fallback chains

- Configure `RuntimeI18nConfig.fallbacks?: string[]` to build an ordered lookup. Resolution always runs as **active language → each configured fallback → `defaultLang`**.
- Values are deduped automatically and trimmed against `supported`, so accidental repeats or unsupported tags are ignored.
- Missing keys emit a single dev-mode warning and then flow through `onMissingKey()`.

## Catalog caching

- `RuntimeI18nOptions.cacheMode` chooses your strategy:
  - `none` keeps only the active fallback chain in memory (good for memory-constrained apps).
  - `memory` (default) caches every loaded catalog for the current session.
  - `storage` hydrates catalogs from `localStorage`, serves them instantly, and refreshes them in the background. Use `cacheKeyPrefix` to isolate multiple apps.
- LocalStorage I/O never runs on the server, so SSR stays deterministic when you seed TransferState.

---

## SSR + Hydration

See `apps/demo-ssr` in this repo for a complete Express + Angular SSR demo (including TransferState seeding and catalog fallbacks).

On the server, use the exported helper to seed TransferState with the same keys that `provideRuntimeI18n()` reads on the client:

```ts
// i18n.server.providers.ts
import { Provider } from '@angular/core';
import { RuntimeI18nSsrSnapshot, provideRuntimeI18nSsr } from '@ngx-runtime-i18n/angular';

export function i18nServerProviders(snapshot: RuntimeI18nSsrSnapshot): Provider[] {
  return provideRuntimeI18nSsr(snapshot);
}
```

`RuntimeI18nSsrSnapshot.bootstrap` holds the active language catalog and `catalogs` can optionally seed additional locales. Everything defaults to the same prefix as `provideRuntimeI18n()` (`@ngx-runtime-i18n/core`), but pass `stateKeyPrefix` to both helpers when you override it.

Use the same `provideRuntimeI18n(...)` on both server and client app bootstraps. The wrapper reads TransferState on the client first and only fetches missing catalogs as needed.

---

## Options & Tokens

### `provideRuntimeI18n(config, { localeLoaders?, options?, stateKeyPrefix? })`

- **`config.defaultLang: string`** — fallback language.
- **`config.fallbacks?: string[]`** — ordered fallback catalog chain before the default.
- **`config.supported: string[]`** — allowed languages (authoritative list).
- **`config.fetchCatalog(lang, signal?)`** — async catalog loader (should be idempotent; honor `AbortSignal`).
- **`config.onMissingKey?: (key) => string`** — transform missing keys (dev‑only suggestion: return the key).

**`localeLoaders`** — map of language to dynamic imports of Angular locale data (enables localized pipes).  
**`options.autoDetect`** — on first boot: persisted → navigator → default.  
**`options.storageKey`** — localStorage key for the chosen language (falsy to disable).  
**`options.cacheMode`** — `'none' | 'memory' | 'storage'` for catalog caching strategy (default: `'memory'`).  
**`options.cacheKeyPrefix`** — storage prefix when `cacheMode === 'storage'`.  
**`options.preferNavigatorBase`** — map `en-GB` → `en` if `en` is in `supported`.  
**`stateKeyPrefix`** — advanced: customize TransferState keys if you embed multiple i18n instances.

### Services & Pipe

- **`I18nService`** — signals‑first: `lang()`, `ready()`, `t(key, params?)`, `setLang(lang)`
- **`I18nCompatService`** — RxJS equivalent for non‑signals codebases
- **`I18nPipe`** — `{{ 'path' | i18n:{...} }}` (pure=false; listens to `lang` only)

---

## Pitfalls & Gotchas

- **Angular pipes not localizing** — Ensure you defined `localeLoaders` for the language you’re testing.
- **Hydration mismatch** — Always seed TransferState on SSR; the wrapper is hydration‑safe when the first paint uses server data.
- **404 for catalogs** — Place files under `src/public/i18n` so they serve as `/i18n/*.json` in dev/prod.
- **Rapid language toggles** — Supported; the wrapper cancels in‑flight fetches. Your `fetchCatalog` must respect `AbortSignal`.

---

## Versioning & Support

- Angular: `>=16 <21`
- Node: LTS recommended
- SemVer: breaking changes will bump major versions.

---

## License

MIT