@etyma/angular
v1.0.0
Published
Signal-native, zoneless-first Angular bindings for Etyma: typed translation keys, lazy locale catalogs and SSR transfer.
Maintainers
Readme
@etyma/angular
Angular bindings for Etyma.
Signal-native, standalone, zoneless-first. No NgModule, no RxJS in the public API, and no dependency on Zone.js — the package still works inside a Zone-based application, it just never needs one.
Semver-stable public API. The documented exports, generated formats, diagnostic codes and CLI exit semantics follow semantic versioning; see what the stability promise covers.
Install
pnpm add @etyma/core @etyma/angularRequires Angular 21 or 22.
Use
// app.config.ts
import { provideEtyma } from '@etyma/angular';
import { i18n } from './i18n/i18n';
export const appConfig: ApplicationConfig = {
providers: [provideZonelessChangeDetection(), provideEtyma(i18n)],
};// nav.ts
import { injectI18n } from '@etyma/angular';
import { i18n } from './i18n/i18n';
@Component({
template: `
<a [routerLink]="i18n.path('/docs')">{{ t('nav.docs') }}</a>
<p>{{ t('footer.rights', { year: 2026 }) }}</p>
`,
})
export class Nav {
protected readonly i18n = injectI18n(i18n);
protected readonly t = this.i18n.t;
}Passing the definition is a type-level argument — the instance comes from the injector
either way — and it is what makes t('nav.dcos') a compile error. For a source written
with defineMessages() it also types params, so t('footer.rights') without { year } is
an error in a strict template too. Calling injectI18n()
with no argument works too; the keys are then plain strings.
API
provideEtyma(definition, options?)— registers Etyma.options.initialLocaleloads and activates a locale during application initialization, for an application that decides its locale some way other than from the URL.injectI18n(definition?)— theEtymaI18nservice, typed against the definition.injectT(definition?)— just the translate function.EtymaI18n—locale,directionandreadysignals;t,parts,hasandpath;load,activateandsetLocale.
Advanced: integration tokens. Applications do not need these; a routing integration does.
@etyma/analog is built on exactly these two, and a custom router integration can be too.
ETYMA_DEFINITION— theI18nDefinitiongiven toprovideEtyma(). Inject it where the definition is needed without creating the service, such as a route guard deciding whether a URL segment is a locale.ETYMA_LOCALE_SWITCH— a(locale) => Promise<void>thatsetLocale()delegates to when provided. Provide one to make switching language a navigation in your own routing scheme, asprovideEtymaAnalog()does; without it,setLocale()isactivate().
Server rendering
The service is created per application injector, which on a server means per request. Every piece of mutable state — the active locale, the catalogs loaded so far — lives on the instance, so two requests rendering two languages at once share nothing.
The active locale and catalogs loaded while rendering are written to Angular's
TransferState, so the browser's first i18n state matches the server-rendered HTML and it
does not fetch the same catalog again. The source catalog is left out: it is already in the
JavaScript bundle.
For AnalogJS applications, @etyma/analog
adds locale-prefixed routing, request-scoped locale resolution and the localized <head>.
Licence
MIT.
