Service

WrI18n

Reactive i18n service. Active locale lives on a signal, catalogs load lazily through a swappable loader (HTTP or static), and double-brace placeholders interpolate at render time. Scopes let feature modules ship and load their own catalogs. Keep ngwr's own strings with provideWrI18nBaseCatalogs rather than spreading wrEn into your catalog: 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 — ngwr's components fall back to English and a wrT read of a lost key renders the raw key.

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.

Active locale:en

Pipe: ngwr i18n demo — Hello, Ada!

Directive:Hello, Ada!

Common keys:Save / Cancel / Loading…

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.

Which one to reach for.
CallReturnsWhere it may runUse it for
useI18nText(input, key, fallback)Signal<string>Injection contextA component string a consumer can override with an input
readI18nText(key, fallback)Signal<string>Injection contextThe same, with no override input to forward
useI18nFormatter(key, fallback)(params) => stringInjection contextA label that interpolates per row
i18n.translate(key, params?)Signal<string>AnywhereA reactive string built after construction
i18n.t(key, params?)stringAnywhereA 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.

NameDescriptionTypeDefault
WrI18nInjectable service.service
localeActive 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.

NameDescriptionTypeDefault
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.

NameDescriptionTypeDefault
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.

NameDescriptionTypeDefault
wrTrequiredTranslation key. Required.string
wrTParamsInterpolation params for {{name}} tokens.WrI18nParams | nullnull
wrTScopeOptional scope — scoped lookup first, then root fallback.string | nullnull

See also