Why ngwr provides this
Angular's built-in i18n bakes translations in at build time — one bundle per locale, no runtime switching. This service swaps catalogs at runtime, scopes keys per feature, and interpolates parameters.
Live demo
Switch the active locale — every binding on the page updates without a re-render.
Reading a key from TypeScript
There are two families here and picking the wrong one fails at runtime rather than at compile time. useI18nText(), readI18nText() and useI18nFormatter() all call inject(), so they only run in an injection context — a field initializer, a constructor, or inside runInInjectionContext(). Call one from a click handler, a setTimeout, a route resolver body or a module-level constant and Angular throws NG0203. The service's own t() and translate() have no such rule: you already hold the instance, so they work anywhere.
| Call | Returns | Where it may run | Use it for |
|---|---|---|---|
useI18nText(input, key, fallback) | Signal<string> | Injection context | A component string a consumer can override with an input |
readI18nText(key, fallback) | Signal<string> | Injection context | The same, with no override input to forward |
useI18nFormatter(key, fallback) | (params) => string | Injection context | A label that interpolates per row |
i18n.translate(key, params?) | Signal<string> | Anywhere | A reactive string built after construction |
i18n.t(key, params?) | string | Anywhere | A one-shot read — a toast message, a log line |
import { Component, inject } from '@angular/core';
import { WrI18n, readI18nText } from 'ngwr/i18n';
@Component({ /* … */ })
export class InvoiceCard {
private readonly i18n = inject(WrI18n);
// Field initializer — an injection context. Fine.
protected readonly heading = readI18nText('invoice.heading', 'Invoice');
protected onCopy(): void {
// readI18nText('invoice.copied', 'Copied') here throws NG0203:
// a click handler is not an injection context.
this.toast.show(this.i18n.t('invoice.copied')); // this is the way
}
}
// Outside a component altogether — a resolver, an interceptor, a helper:
const label = runInInjectionContext(injector, () => readI18nText('x', 'X'));readI18nText() returns a signal, and that is not a style choice: catalogs land a microtask after the first change-detection pass even with the static loader, so anything that read a plain string at construction time would freeze the English fallback for the life of the app. Keep the signal and call it in the template.
Service
Everything on the injected WrI18n instance.
| Name | Description | Type | Default |
|---|---|---|---|
WrI18n | Injectable service. | service | — |
locale | Active locale signal. Read-only — use use(locale) to write. | Signal<string> | defaultLocale |
use(locale) | Switch the active locale. Ignored if outside availableLocales. Persists via WrStorage. | (locale: string) => void | — |
t(key, params?, scope?) | Eager translate. Returns the value, or the missing-handler fallback. | (key, params?, scope?) => string | — |
translate(key, params?, scope?) | Reactive translate — Signal<string> that re-evaluates on locale + catalog changes. | (key, params?, scope?) => Signal<string> | — |
registerScope(scope) | Register a feature scope. Catalogs auto-load on every locale change. | (scope: string) => Promise<WrI18nCatalog> | — |
available() | Available locales — pass-through from config. | () => readonly string[] | — |
Providers
Wire the service up at bootstrap. Pick exactly one loader.
| Name | Description | Type | Default |
|---|---|---|---|
provideWrI18n(options?) | Root provider. Pass defaultLocale, availableLocales, an optional missingHandler, and an optional loader. Every field is optional — called bare it takes defaultLocale from Angular's LOCALE_ID. | (options: ProvideWrI18nOptions = {}) => EnvironmentProviders | — |
provideWrI18nStaticLoader(catalogs, scopes?) | Inline catalogs at bootstrap. Best for small apps and SSR. catalogs is keyed by locale; scopes is keyed by scope name and then by locale, for a lazy feature that ships its own strings. A loader serves only the catalogs you pass it and ngwr registers none of its own: pass only your own keys here and add ngwr's with provideWrI18nBaseCatalogs. | (catalogs: WrI18nStaticCatalogs, scopes?: WrI18nStaticScopedCatalogs) => Provider | — |
provideWrI18nBaseCatalogs(catalogs) | Register ngwr's shipped catalogs (wrEn, wrRu, …) underneath the loader, keyed by locale. The lookup walks them key by key once the loader's catalog misses, so your keys win and a namespace both sides define keeps both halves. Use it instead of { ...wrEn, ...yours }: a spread is shallow, so any namespace you share with ngwr, e.g. common, validation, table, keeps only the side spread last, and the other side's keys stop resolving with nothing logged. | (catalogs: WrI18nBaseCatalogs) => Provider | — |
provideWrI18nHttpLoader({ path, rootPath? }) | Fetch JSON catalogs at runtime. {locale} and {scope} tokens interpolate. | ({ path, rootPath? }) => Provider | — |
Template helpers
The same service, reached from a template.
| Name | Description | Type | Default |
|---|---|---|---|
WrTPipe — `| wrT[: params][: scope]` | Impure pipe. Re-evaluates on every CD cycle. | pipe | — |
WrTDirective — `[wrT]="key" [wrTParams] [wrTScope]` | Writes textContent of the host element on locale change. | directive | — |
useI18nFormatter(key, fallback) | Returns a (params) => string helper for the given key. The second argument is the English fallback the helper serves when no catalog carries the key — it is required, not a scope. | (key: string, fallback: string) => (params?: WrI18nParams) => string | — |
[wrT] inputs
The directive form, bound on any element.
| Name | Description | Type | Default |
|---|---|---|---|
wrTrequired | Translation key. Required. | string | — |
wrTParams | Interpolation params for {{name}} tokens. | WrI18nParams | null | null |
wrTScope | Optional scope — scoped lookup first, then root fallback. | string | null | null |