skip to content

When would you use Angular's runtime loadTranslations instead of per-locale builds, and what constraints does that approach bring?

level: seniorimportance: should knowfreq 28%

answer

  1. one artifact, translations fetched at startup
  2. must run before anything is translated
  3. messages are translated once
  4. a flat ID-to-message map
  5. locale data is now your job

basics

~20 s

loadTranslations from @angular/localize applies translations in the browser, so one build serves every locale. Translations must load before bootstrap, a language switch needs a reload, and LOCALE_ID and locale data must be set by hand.

solid answer

~50 s

Runtime translation keeps `$localize` as a real function in the bundle; before bootstrap you fetch a translation map and call `loadTranslations(map)` from `@angular/localize`, whose keys are message IDs and whose values use `{$PLACEHOLDER}` syntax, the same shape as the simple JSON extraction format. It fits when one build artifact must serve many locales, or when the locale is only known at startup, for example from a tenant setting. The constraints: translations must be loaded **before** any tagged message is first evaluated, because each message is translated once; loading new translations later does not change text already translated, so a language switch still needs a reload; you must set `$localize.locale` or provide `LOCALE_ID` and register the locale data yourself; the package must be a runtime dependency (`ng add @angular/localize --use-at-runtime`); and every user pays an extra request at startup.

code

ts · 29 lines
ts
// main.ts
import { loadTranslations } from '@angular/localize';
import { registerLocaleData } from '@angular/common';
import { bootstrapApplication } from '@angular/platform-browser';

const localeData: Record<string, () => Promise<{ default: unknown }>> = {
  de: () => import('@angular/common/locales/de'),
  ja: () => import('@angular/common/locales/ja'),
};

async function start(): Promise<void> {
  const stored = localStorage.getItem('locale') ?? 'en-US';
  const locale = stored in localeData ? stored : 'en-US';

  if (locale !== 'en-US') {
    const response = await fetch(`/i18n/${locale}.json`);
    const file: { translations: Record<string, string> } = await response.json();
    loadTranslations(file.translations);
    registerLocaleData((await localeData[locale]()).default);
  }
  $localize.locale = locale; // LOCALE_ID's default reads this

  // Import the application only after translations are loaded.
  const { App } = await import('./app/app');
  const { appConfig } = await import('./app/app.config');
  await bootstrapApplication(App, appConfig);
}

start().catch((err) => console.error(err));

go deeper

for a junior

Recall that loadTranslations applies translations in the browser, so one build can serve several languages.

for a middle

Explain the ID-to-message map format, why it must be loaded before bootstrap, and why LOCALE_ID and locale data must be set manually.

for a senior

Choose between runtime and compile-time translation for a real deployment, covering startup cost, language switching by reload, and missing-translation detection.

for a principal

Judge whether single-artifact deployment or tenant-driven locales justify runtime translation's costs, and who owns publishing the translation maps.

## Two ways to apply translations Angular compiles every `i18n` marker into a `$localize` tagged string. What happens to those calls decides the strategy: | | Compile-time (`localize` option) | Runtime (`loadTranslations`) | | --- | --- | --- | | Where translation happens | Build inliner replaces `$localize` calls with literals | `$localize` looks messages up in the browser | | Artifacts | One app copy per locale | One app for all locales | | Startup cost | None | Fetch and parse a translation map | | `LOCALE_ID` and locale data | Set per variant by the CLI | Your code sets them | | Language switch | Navigate to another sub-path | Reload after loading other translations | ## When runtime translation fits - **One artifact, many locales.** A platform deploys a single build and adds languages by publishing translation files, without rebuilding. - **Locale known only at startup.** A tenant or account setting decides the language, and sub-path routing is not wanted. - **Build-time budget.** Many locales multiply build and deploy size under compile-time translation. ## How it works `loadTranslations` takes a map from **message ID** to **target message**. Placeholders are written `{$NAME}`, for example `"Sie haben {$itemCount} Artikel"`, the same shape the simple JSON extraction format produces. It installs a `translate` function on `$localize`; afterwards each tagged string is looked up by ID. A message with no entry falls back to its source text, with a console warning. `clearTranslations()` removes loaded translations. The translations live on the global `$localize` object, so they are shared by every Angular app running in the same page. ## The ordering constraint The `@angular/localize` API docs are explicit: `$localize` messages are **processed once**, when the tagged string is first encountered, and loading new translations later does not change messages already translated. In practice: 1. Fetch the translation map and call `loadTranslations` **before** `bootstrapApplication`. 2. Import the application code **after** loading, with a dynamic `import()`, if any module evaluates `$localize` at load time, such as a module-level constant. 3. To switch language, store the choice and **reload** the page. ## Locale data and `LOCALE_ID` Nothing registers locale data for you on this path. Set `$localize.locale`, which `LOCALE_ID`'s default reads, or provide `LOCALE_ID` explicitly, and call `registerLocaleData` for the locale, typically after a dynamic import of `@angular/common/locales/<id>` so only the needed locale is downloaded. ## Setup - `ng add @angular/localize --use-at-runtime` puts the package in `dependencies` rather than `devDependencies`. Like the plain `ng add`, it adds the `@angular/localize/init` polyfill, which installs the global `$localize`. - Produce the per-locale maps from your translation files. The simple JSON format matches `loadTranslations` directly; XLIFF would need converting. ## Costs to state in an interview - An extra network request on every cold start, and a blank or source-language flash if bootstrap is not gated on it. - Every user downloads the source-language text of all messages in the bundle plus a translation map, and the translate function runs in the browser. - There are no per-locale URLs or per-locale cached bundles unless you add them yourself. - Missing translations surface only at runtime, as source-language text. For most public sites, per-locale builds remain the default; runtime loading is the exception you justify with deployment or tenancy needs.

  • A user picks Japanese from a menu and the Angular app calls loadTranslations with the ja map. Why does most of the UI stay English?
    Each `$localize` message is translated once, when first encountered. Messages already evaluated keep their text, and loading new translations does not re-render or retranslate them. Persist the choice, reload the page, and load the Japanese map before bootstrap.
  • Why does runtime translation need @angular/localize in dependencies rather than devDependencies?
    With compile-time translation the package is used only by build tooling, so a dev dependency suffices. With runtime translation, `loadTranslations` and the `$localize` translate function execute in the browser, so the package is part of the shipped application. `ng add @angular/localize --use-at-runtime` sets that up.

saying these in an interview costs you the question

  • loadTranslations lets you switch language live without a reload
  • Translations can be loaded after bootstrap in a component
  • loadTranslations reads XLIFF files directly
  • The CLI registers locale data for runtime translation too
  • Runtime translation has no startup cost