skip to content

Localization Workflow

Angular's @angular/localize flow marks template and code text, extracts it to translation files, and merges each locale into its own build at compile time. Interviewers probe the build-time trade-off.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

11

In Angular, what does the LOCALE_ID token control, and how do it and the locale data get set for a German build?

level: juniorimportance: must knowfreq 55%

answer

  1. formatting, not translation
  2. date, number, currency, percent
  3. plural categories need it too
  4. a CLDR data file per locale
  5. the localized build wires both for you

basics

~20 s

LOCALE_ID names the active locale; the date, number, currency and percent pipes and ICU plural rules read it together with that locale's CLDR data. A --localize build sets both per locale; otherwise you provide LOCALE_ID and register the data yourself.

solid answer

~40 s

`LOCALE_ID` is an `InjectionToken<string>` from `@angular/core` that says which locale the app runs in. It does not translate text; it selects **locale data**, the CLDR-derived tables for date patterns, number symbols, currency formats and plural rules, which `DatePipe`, `DecimalPipe`, `CurrencyPipe`, `PercentPipe` and ICU plural expressions use. By default its value comes from `$localize.locale`, which a localized build inlines as a string literal per locale, falling back to `en-US`. So with `ng build --localize` the German variant gets `LOCALE_ID` `de` and its locale data automatically. In a single-locale app you do it by hand: `registerLocaleData(localeDe)` from `@angular/common` plus `{ provide: LOCALE_ID, useValue: 'de' }`. Providing a non-English ID without its data makes the pipes throw a missing-locale-data runtime error (`NG0701`).

code

ts · 11 lines
ts
import { ApplicationConfig, LOCALE_ID } from '@angular/core';
import { registerLocaleData } from '@angular/common';
import localeDe from '@angular/common/locales/de';

// Only for an app that is NOT built with --localize:
// the localized build registers data and sets the locale per variant itself.
registerLocaleData(localeDe);

export const appConfig: ApplicationConfig = {
  providers: [{ provide: LOCALE_ID, useValue: 'de' }],
};

go deeper

for a junior

Recall that LOCALE_ID sets the locale used by the date, number, currency and percent pipes, and that it does not translate text.

for a middle

Explain locale data registration, the fallback from region to language, and why a localized build needs no manual provider while a hand-set app does.

for a senior

Diagnose NG0701 and wrong-format reports in production, and decide between registering data manually and relying on the CLI's per-locale builds.

for a principal

Consider how locale, currency and region are modeled separately in the product, since conflating them causes wrong prices in markets sharing a language.

## Two separate concerns Localizing an Angular app has two halves that are easy to confuse: - **Translation**: replacing marked source text with a translated message. - **Formatting**: rendering dates, numbers, currencies, percentages and plural categories the way a locale expects: `1.234,56 €` in German, `¥1,235` in Japanese. `LOCALE_ID` belongs to the second half. It is an `InjectionToken<string>` exported from `@angular/core` whose value is a Unicode locale ID such as `en-US`, `de` or `ja`. ## What reads `LOCALE_ID` | Consumer | What the locale changes | | --- | --- | | `DatePipe` | Month and day names, date patterns, first day of week | | `DecimalPipe` | Decimal and grouping separators | | `CurrencyPipe` | Symbol placement, separators, currency digits | | `PercentPipe` | Percent format and separators | | ICU `plural` in templates | Which plural category (`one`, `few`, `other`) a number falls into | Each of these looks up **locale data** for the ID: generated CLDR tables shipped in `@angular/common/locales` (for example `@angular/common/locales/de`). Only English (`en`) data is built into `@angular/core`; every other locale's data must be registered before use. ## Where the value comes from The token's default factory resolves in this order: 1. A `LOCALE_ID` provided by a parent injector, if any. 2. Otherwise the global locale: `$localize.locale` when it is set, else `en-US`. The second step is what makes localized builds work without configuration. During compile-time translation, the CLI's inliner replaces the expression that reads `$localize.locale` with a **string literal for the locale being built**, so each variant's default `LOCALE_ID` is its own locale. ## A localized build: automatic With `ng build --localize` (or `"localize": true` in the build options) for `en-US`, `de` and `ja`, the CLI, for each variant: - inlines the translations for that locale; - sets the locale so `LOCALE_ID` resolves to it; - **includes and registers the locale data** for it; - sets `<html lang>` and the base href. No `registerLocaleData` and no `LOCALE_ID` provider are needed. ## A single-locale app: manual An app that is simply German, built without the localize pipeline, sets both pieces itself: ```ts import { registerLocaleData } from '@angular/common'; import localeDe from '@angular/common/locales/de'; import { LOCALE_ID } from '@angular/core'; registerLocaleData(localeDe); export const appConfig = { providers: [{ provide: LOCALE_ID, useValue: 'de' }], }; ``` When data is looked up, Angular tries the exact ID, then its language part (`de-AT` falls back to `de`), then, for English IDs only, the built-in `en` data. If nothing matches, it throws a runtime error, `NG0701`, with the message `Missing locale data for the locale "de"`. The **global variants** in `@angular/common/locales/global/*` register themselves on import, which is an alternative to calling `registerLocaleData`. ## Common mistakes - Expecting `LOCALE_ID` to translate text. It only affects formatting and plural selection. - Providing `LOCALE_ID` without registering the data, which fails at the first pipe call. - Registering data for `de` and expecting `de-CH` number formats. The fallback to `de` avoids an error but gives German, not Swiss, separators. - Assuming `CurrencyPipe` switches currency with the locale. The locale sets the **format**; the currency code defaults to `USD` unless you pass one or provide `DEFAULT_CURRENCY_CODE`. ## Summary - `LOCALE_ID` picks locale data; the pipes and ICU plurals consume it. - Localized builds set the ID and register the data per variant. - Hand-set apps need both `registerLocaleData` and a `LOCALE_ID` provider.

  • In a ja build of an Angular app, why can {{ price | currency }} still show US dollars?
    The locale controls how a currency is formatted, not which currency it is. `CurrencyPipe` uses the code you pass or, if none, the `DEFAULT_CURRENCY_CODE` token, which defaults to `USD`. Pass `'JPY'` or provide `DEFAULT_CURRENCY_CODE` for the Japanese variant.
  • An Angular app provides LOCALE_ID 'de-AT' but only registered 'de' data. What happens?
    Locale data lookup tries the exact ID first and then the language part, so `de-AT` falls back to the registered `de` data. The pipes work, but with German rather than Austrian specifics. Register the `de-AT` data if the differences matter.

LOCALE_ID is the country selector on a label printer: switching it to Germany changes how dates and prices are printed, but it does not rewrite the words on the label, and the printer still needs Germany's format cartridge installed.

saying these in an interview costs you the question

  • LOCALE_ID translates the template text
  • Providing LOCALE_ID is enough; locale data loads itself
  • A localized build still needs a manual LOCALE_ID provider
  • CurrencyPipe switches currency to match the locale
  • Locale data for every language is bundled by default
open as a page

In Angular, how do you mark template text, an element attribute, and a string in component code for translation?

level: juniorimportance: must knowfreq 55%

basics

~20 s

Template text gets the i18n attribute on its element, an attribute value gets i18n-<attribute> (for example i18n-title), and strings in TypeScript use the $localize tagged template literal. <ng-container i18n> marks text without adding an element.

open as a page

With the Angular CLI localize option, what does building en-US, de and ja variants produce, and how are those variants served?

level: middleimportance: must knowfreq 45%

basics

~20 s

The CLI compiles the app once, then inlines each locale's translations into a separate copy with its own directory, base href, html lang, LOCALE_ID and locale data. A server or Angular SSR redirects the root to the best-matching locale sub-path.

open as a page

In an Angular template, how do you use ICU plural and select expressions for a cart summary that varies by item count and shopper gender?

level: middleimportance: must knowfreq 48%

basics

~20 s

Inside i18n-marked text, write {count, plural, =0 {...} =1 {...} other {...}} for counts and {gender, select, female {...} male {...} other {...}} for string choices. Plural categories follow the locale's rules, other is the fallback, and clauses nest.

open as a page

In Angular i18n, why do generated message IDs churn between releases, and when are custom IDs the better choice?

level: middleimportance: must knowfreq 42%

basics

~20 s

A generated ID hashes the message text, placeholders included, plus its meaning, so any wording, markup or meaning change creates a new ID and orphans the old translation. Custom @@ids stay stable but can silently go stale.

open as a page

In Angular i18n, what does ng extract-i18n produce, which file formats can it write, and how do you create each language's translation file?

level: juniorimportance: should knowfreq 35%

basics

~20 s

ng extract-i18n collects every marked message into one source-language file, messages.xlf in XLIFF 1.2 by default. It can also write XLIFF 2.0, XMB, JSON or ARB. Each language gets a copy, such as messages.fr.xlf, whose targets a translator fills in.

open as a page

In Angular's i18n metadata meaning|description@@customId, what does each part do, and which parts affect the message ID?

level: middleimportance: should knowfreq 38%

basics

~20 s

The description is context for translators and never changes the message ID. The meaning disambiguates identical text and is hashed into the generated ID with the text. A custom @@id replaces the generated ID entirely.

open as a page

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%

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.

open as a page

A reviewer rejects Angular code that builds a cart message from two $localize fragments around a count; why is that wrong, and how should it be marked?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Fragments become separate translation units, so translators cannot see the whole sentence, reorder words or agree the noun with the count. Mark one whole message with a named placeholder, and put count-dependent wording in an ICU plural.

open as a page

In Angular i18n, an agency returned messages.de.xlf but the German build still shows some English; how are missing translations handled, and how do you catch them before release?

level: seniorimportance: should knowfreq 30%

basics

~20 s

An untranslated message is built with its source text; i18nMissingTranslation makes that a warning (default), an error or ignored. Units without a target fall back to source with only a parse warning, so CI also needs a diff check.

open as a page

In an Angular XLIFF translation file, how must translators handle placeholders and ICU plural units, and what happens when they get them wrong?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

Placeholders such as <x id="INTERPOLATION"/> may move but never be renamed or removed. An ICU becomes an ICU placeholder plus its own unit, where translators rewrite case texts and add plural categories. Unknown placeholder names fail the build.

open as a page