Translate

Internationalization that lives with your features instead of in one app-level bundle. Type-safe, signal-based, and built for monorepos, but works for single-repo apps too.

@mmstack/translatenpm

npm install @mmstack/translate @formatjs/intl

The usual Angular i18n setup keeps every string in one catalog the whole app loads up front. That grows into a bottleneck, and in a monorepo it means unrelated teams editing the same file. This library takes a different path, and three things make it worth a look.

#Translations live with the feature

A namespace belongs to the feature or library that uses it, not to the app. In a monorepo, the quote library ships its own quote namespace and its own locale files, and nothing else needs to know they exist. You register a namespace with a lazy loader, so its translations arrive with the route that needs them rather than in the initial bundle.

Nothing here assumes a monorepo. A single app groups its own strings the same way, one namespace per feature area, and gets the same on-demand loading. See Namespaces for the loading model.

Note: We usually export routes from index.ts in our nx monorepo, the root route attaches the resolver, so the rest of the repo knows nothing about the features translation needs.

#A wrong key is a build error

Translations are plain TypeScript, not JSON or XLIFF. A namespace is a const of ICU messages, and the compiler reads its shape: the nested keys become the valid keys, and each message's placeholders become the required parameters. So t('quote.greeting', { name }) is checked like any other typed call. Rename a key and every call site fails to compile. Drop a required argument and the call fails. You find these at build time, not from a user.

#It is all signals

Everything you read is a signal: t(), the formatters, and the active locale. So the same call works in a template, a computed, or an effect, and it updates on its own when the locale changes, with no pipe and no subscription.

@Component({
  template: `<h1>{{ t('quote.pageTitle') }}</h1>`,
})
export class QuoteComponent {
  protected readonly t = injectQuoteT();
  // the heading re-renders on its own when the locale changes
}

Calling t() in a template stays cheap even with object parameters. Angular hands the same parameter object back across renders that don't change it, and the store keys its cached signals by that object through a WeakMap, so a render reuses one fine-grained signal rather than re-creating it or diffing the params.

#What it looks like

One file defines the strings, one registers them, and a component reads them. This is the whole loop.

// quote.namespace.ts: the strings
const ns = createNamespace('quote', {
  pageTitle: 'Famous Quotes',
  errors: { minLength: 'Must be at least {min} characters.' },
});

// quote.t.ts: register it, get a typed t() and a route resolver
export const [injectQuoteT, resolveQuoteTranslations] = registerNamespace(
  () => import('./quote.namespace'),
  { 'sl-SI': () => import('./quote-sl.translation') },
);

// quote.component.ts: read it
export class QuoteComponent {
  protected readonly t = injectQuoteT();
  readonly title = this.t('quote.pageTitle');                     // ok
  readonly hint = this.t('quote.errors.minLength', { min: '5' }); // min required
}

The key on the component is autocompleted from the namespace, and the { min } argument is required because the ICU message declares it.

#Where to go next

You want toRead
Wire up locales (one build for all, or one per language)Configuration
Define translations and register them for lazy loadingNamespaces
Read a key in code or a template, switch language at runtime Reading translations
Format a price, a date, or a relative time like "3 days ago" Formatters
Round-trip JSON with translators and gate CI on shapeTooling