AngularAtlased. 22.2

Ch. 17Security & globalisation75 / 75

Internationalisation (i18n)

$localize, the i18n attribute, ICU plural/select, a build per language vs runtime translations, and locale-based formatting.

~0 min read Official docs

Angular has built-in internationalization through the @angular/localize package. You mark the texts in templates with the i18n attribute, and in TypeScript with $localize. The CLI extracts them into a translation file (XLIFF, JSON…), and at build time it produces one variant of the application per language, with the texts already replaced. The result is as fast as the untranslated application: no key lookup at runtime.

Besides texts, i18n also means plurals and formatting. “2 files”, but “20 files” in Romanian is “20 de fișiere”; 1,499.50 in English and 1.499,50 in Romanian; the date with the name of the day in the user’s language. This is where ICU expressions and locale data come in.

Why it matters
Adding i18n later is expensive: every hard-coded text has to be found, marked and tested. Starting with it costs almost nothing, and correct plurals and formats matter even in an application that is only in Romanian.

Marking the texts

ng add @angular/localize installs the package and loads @angular/localize/init as a polyfill. The i18n attribute marks an element; its optional value has the form meaning|description@@id. The meaning and the description help the translator. A custom id (@@cart.title) keeps the translation from being lost when you change the source text. For attributes you use i18n-alt, i18n-title etc. In code, $localize is a tagged template with the same syntax, and the interpolations receive names (${count}:count:).

ng add @angular/localize
# adds @angular/localize/init to the polyfills and the types to tsconfig — without it, i18n and $localize don't start

ICU: plural and select

ICU expressions choose the text according to a value. plural uses the CLDR categories of the language (LOCALE_ID), plus exact matches (=0). select chooses according to a string. Romanian has three categories:

  • one for 1;
  • few for 0, 2–19 and 101–119;
  • other for the rest (20, 100, 120…), where “de” appears: “20 de fișiere” (20 files).

The rule is in the Angular locale data (@angular/common/locales/ro). ICU expressions work in any template, even without translations.

LiveICU with the rules of the Romanian language (real template)

You uploaded 2 files as editor.

CLDR category for 2 in Romanian:few

Try 0, 1, 2, 19, 20, 101 and 120 and watch how the form and the CLDR category change. Change the role for the select.

Extraction and translation files

ng extract-i18n gathers all the marked messages. By default it writes messages.xlf (XLIFF 1.2); with --format you can also choose XLIFF 2, XMB, JSON or ARB. You copy the file for each language (messages.en.xlf), the translators fill in the <target> elements, and the ids link the source to its translation.

ng extract-i18n --output-path src/locale             # default: messages.xlf (XLIFF 1.2)
ng extract-i18n --format=xlf2 --output-path src/locale
ng extract-i18n --format=json --output-path src/locale

Build per language vs runtime translations

The standard approach is at build time: in angular.json you declare sourceLocale and locales, and with "localize": true the CLI produces one folder per language (/ro/, /en/…). The server chooses the folder according to the URL or the user’s preference. Changing the language means navigating to another folder, therefore a reload.

The alternative is at runtime: a single build, and in main.ts you load the translations with loadTranslations() before bootstrap. Here too, changing the language means a reload, because the texts marked with i18n in templates are evaluated when the components are created. If you need to change the language without a reload, use a runtime translation library.

{
  "projects": {
    "shop": {
      "i18n": {
        "sourceLocale": "ro",
        "locales": {
          "en": "src/locale/messages.en.xlf",
          "de": "src/locale/messages.de.xlf"
        }
      },
      "architect": {
        "build": {
          "options": { "localize": true }
        }
      }
    }
  }
}
LiveReal $localize, with translations loaded at runtime

Your cart

You have 3 items in your cart.

The messages are $localize calls with ids (@@cart.title), translated with loadTranslations(). This demo has its own buttons and does not follow the site’s language.

Switch between ro, en and de. The demo calls clearTranslations()/loadTranslations() and re-evaluates the $localize messages (in a computed). Change the number of products: the {$count} placeholder is kept in the translation.

The demo re-evaluates the $localize calls in code. Texts marked with i18n in templates do not change this way, which is why real applications reload the page when the language changes.

Formatting by locale

The date, number, currency and percent pipes (and the functions formatDate, formatNumber…) use LOCALE_ID. For any language other than en-US you must register the data with registerLocaleData. This site does it for ro, with RON as the default currency (DEFAULT_CURRENCY_CODE). All the pipes also accept an explicit locale parameter.

import { registerLocaleData } from '@angular/common';
import localeRo from '@angular/common/locales/ro';
import { DEFAULT_CURRENCY_CODE, LOCALE_ID } from '@angular/core';

registerLocaleData(localeRo);   // required for any locale other than en-US

export const appConfig: ApplicationConfig = {
  providers: [
    { provide: LOCALE_ID, useValue: 'ro' },              // used by date, number, currency, percent, ICU
    { provide: DEFAULT_CURRENCY_CODE, useValue: 'RON' },
  ],
};
LiveThe same value, four locales
LocaleDateNumberCurrency (EUR)Percent
rovineri, 9 octombrie 20261.234.567,891.499,50 €18,8 %
enFriday, October 9, 20261,234,567.89€1,499.5018.8%
deFreitag, 9. Oktober 20261.234.567,891.499,50 €18,8 %
ja2026年10月9日金曜日1,234,567.89€1,499.5018.8%
The date, number, currency and percent are formatted with formatDate/formatNumber/formatCurrency/formatPercent, for ro, en, de and ja. Pay attention to the separators, the order of the elements and the currency symbol.

How this site switches language

The site does not use build-time i18n for its own text. The interface language is a signal: the LangStore service (in core/i18n), with lang() and the values 'en' or 'ro'. The button in the topbar calls toggle(), which saves the choice in localStorage under the key atlas-lang. On startup the key is read; if it is missing, the language is English. An effect keeps the lang attribute of the <html> element in sync.

Switching does not reload the page: text is evaluated again when the signal changes. There are two kinds of bilingual text:

  • The interface (topbar, menus, search) has keys in messages.ts, typed with MessageKey. The t pipe translates them: {{ 'topic.minRead' | t: { n: 4 } }}. TypeScript requires every key to exist in both languages.
  • The texts of this site live in dictionaries, one JSON file per language (i18n/en/…, i18n/ro/…, i18n/de/…). Each text has a key and is read with the t pipe: {{ 'signals:title' | t }}. The pipe is impure, so it is re-evaluated when the language changes.

The prose of a page is written in its template in both languages (@if (lang.lang() === 'en')), and its code is in two .snippets files. The $localize demo above is a different mechanism with its own buttons: it does not follow the site’s language.

When YES, when NO

Built-in i18n (a build per language)
  • Public applications, SEO, performance: each language is an optimized build, with no keys at runtime.
  • Languages known at build time and changed rarely (the preference in the account, the /en/ URL).
  • Teams with professional translators who work in XLIFF.
  • Works well with SSR / prerender: one variant per language (see SSR).
Runtime translation library
  • Changing the language without a reload is a product requirement.
  • The translations come from a CMS and change without a deploy.
  • Dozens of languages, and you don’t want dozens of builds.
  • Small internal applications in a single language: just LOCALE_ID + pipes + ICU, without extraction.

Common pitfalls

  • Concatenating texts ('You have ' + n + ' items'): the word order differs between languages. Use a single message with a placeholder, or an ICU expression.
  • Plurals with n === 1 ? … : …: in Romanian you get “20 fișiere” instead of “20 de fișiere”. Use the ICU plural.
  • Messages without a custom id: the generated id changes with any correction of the text, and the translation is lost.
  • Hard-coded formats ('dd/MM/yyyy', toFixed(2) + ' lei'): they bypass the locale. Use the pipes.
  • Unregistered locale: LOCALE_ID set to ro without registerLocaleData throws an error at the first pipe.

Cheat sheet

MARKING      i18n="meaning|description@@id" · i18n-title="@@id" · $localize`:@@id:text ${x}:name:`
ICU          {n, plural, =0 {…} one {…} few {…} other {…}} · {x, select, a {…} other {…}}
             Romanian: one = 1 · few = 0, 2–19, 101–119 · other = 20+, 120+ (“20 de fișiere” = 20 files)
EXTRACTION   ng extract-i18n [--format=xlf|xlf2|xmb|json|arb] [--output-path src/locale]
BUILD        angular.json: i18n.sourceLocale + i18n.locales · "localize": true → one bundle per language
RUNTIME      loadTranslations() before bootstrap · clearTranslations() · new language = reload
FORMATTING   registerLocaleData + LOCALE_ID + DEFAULT_CURRENCY_CODE · pipes with the locale parameter
ALTERNATIVE  runtime translation libraries (e.g. Transloco, ngx-translate) — language change without reload

Ch. 17 · 0 of 4 complete

Finished this lesson?

Mark it and it shows up on your map: in the contents and on the home page.

Marks itself as done once you have read the whole lesson.

  1. Security: XSS, sanitisation & CSP
  2. Authentication: tokens & refresh
  3. OAuth 2.1 & OpenID Connect with PKCE
  4. Internationalisation (i18n)
See your whole map

All 75 pages · type to filter

  • 01What's new in Angular 21 & 22All the important changes in v21 and v22, briefly, with a link to the page that explains each one.Start
  • 01Your first app, step by stepA reading list built from an empty component to a finished app — signals, @for, inputs and outputs, a service and a form, one step at a time.Start
  • 01Quick decisions“Where do I put this?”, “Signal or Observable?”, “Shared or feature?” — short answers to everyday questions.Start
  • 01Angular interview questions60 real interview questions for junior, mid and senior, with the answer to give, the follow-up and the red flags. Plus a mock interview.Start
  • 02Standalone & bootstrapHow an app starts without NgModule: bootstrapApplication, app.config.ts, imports in the component.Modern fundamentals
  • 02Data binding & eventsInterpolation, property and attribute bindings, events with $event and key filters, and how [( )] is built — what goes into the DOM and what comes back.Modern fundamentals
  • 02Control flow: @if, @for, @switchThe block syntax that replaces *ngIf/*ngFor/ngSwitch, with track, @empty and exhaustive checks.Modern fundamentals
  • 02@let and the new template expressionsLocal variables with @let, arrow functions, spread, regex, instanceof, comments inside tags.Modern fundamentals
  • 02@defer — lazy loading in templatesLoad heavy components only when needed: triggers, prefetch, placeholder, loading, error.Modern fundamentals
  • 02Class & style bindingsWhy [class.x] and [style.x] replaced ngClass and ngStyle (with automatic migration from v21).Modern fundamentals
  • 02Component styles & encapsulation:host, the three encapsulation modes, theming with CSS variables, and why ::ng-deep is a last resort.Modern fundamentals
  • 02Pipes: when and howPure vs impure pipes, custom pipes, and how you choose between a pipe, computed() and a method.Modern fundamentals
  • 03signal() and computed()The reactive primitives: writable state, derived state, equality, untracked and immutability rules.Signals & reactivity
  • 03effect() — when YES, when NOSide effects synchronised with signals: localStorage, logging, imperative APIs. And their traps.Signals & reactivity
  • 03linkedSignal() — editable derived stateA signal that resets when its source changes, but that you can also set manually.Signals & reactivity
  • 03resource, rxResource, httpResourceAsync data as signals: status, value, error, reload, automatic cancellation. Stable since v22.Signals & reactivity
  • 03debounced() — delayed signalsThe “debounce” version of a signal, without RxJS. Perfect for search.Signals & reactivity
  • 03Lists: search, filter, sort, paginateA chain of computed signals that recalculates only what changed, with the filters kept in the URL so a link reopens the same view.Signals & reactivity
  • 03Observables from zeroWhat an Observable is, subscribe and unsubscribe, the operators you use every week, cold vs hot — and when a signal is enough.Signals & reactivity
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — and when RxJS remains the right choice.Signals & reactivity
  • 04input() and output()Inputs as signals (required, transform, alias) and typed outputs. Data down, events up.Components: communication
  • 04model() — two-way bindingBanana-in-a-box [( )] with a single API. When to use it, and when input + output is better.Components: communication
  • 04viewChild & contentChildQueries as signals: viewChild, viewChildren, contentChild, contentChildren, the read option.Components: communication
  • 04Lifecycle & DestroyRefWhich hooks still matter in the signals era, afterNextRender, DestroyRef and typed ngOnChanges.Components: communication
  • 04Content projectionng-content with select, multiple slots, default content and the trap of conditional projection.Components: communication
  • 04ng-template & ngTemplateOutletTemplates as values: render props in Angular, typed context and customising components.Components: communication
  • 05Smart vs DumbContainer vs presentational: who holds the state, who only displays, and how to split them properly.Component design
  • 05Generic components <T>A correctly typed DataTable<T> or Select<T>, with templates for cells.Component design
  • 05Dynamic componentsNgComponentOutlet, createComponent with inputBinding/outputBinding, and a dashboard built from config.Component design
  • 05Directives & host compositionAttribute directives, the host: {} metadata and the Directive Composition API (hostDirectives).Component design
  • 05Custom form controlsFormValueControl (Signal Forms) vs ControlValueAccessor: a star rating, in both variants.Component design
  • 05Change detectionOnPush is the default from v22, zone.js is gone from v21. What triggers rendering and how not to get burned.Component design
  • 06inject() & @Service()The new @Service() decorator from v22, inject() vs the constructor, and the injection context.Dependency Injection
  • 06Providers & the injector hierarchyRoot vs route vs component, useClass/useValue/useFactory/useExisting, and the optional/self/skipSelf modifiers.Dependency Injection
  • 06InjectionToken & provideX()Typed configuration, provideFeature() functions, makeEnvironmentProviders and initializers.Dependency Injection
  • 06injectAsync() — lazy servicesLoad a heavy service only when you need it, with prefetch on idle.Dependency Injection
  • 07Folder structurecore / shared / features, what goes into each, and what a feature looks like inside.Application architecture
  • 07Shared vs featureWhen to move a component into shared and (especially) when NOT to. A concrete checklist.Application architecture
  • 07Interfaces & modelsInline interface or separate file? DTO vs domain model, type vs interface, where to put them.Application architecture
  • 07State management with signalsSimple signal stores, facades, local vs global state, and when you need NgRx.Application architecture
  • 07Style guide & namingThe current official conventions: file names without suffixes, protected, readonly, member order.Application architecture
  • 07Barrels, aliases & boundariesindex.ts, path aliases, a feature’s public API and dependency rules that can be checked automatically.Application architecture
  • 07Errors: @boundary & ErrorHandlerThe new @boundary block from v22.2, the global ErrorHandler, HTTP errors and errors from resource.Application architecture
  • 08Modern routing & lazy loadingloadComponent, child routes, parameters as inputs and parameter inheritance (the new default in v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate and resolve as functions, with redirects via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — the basicsForms built on signals: form(), [formField], schema-based validation. Stable since v22.Forms
  • 09Signal Forms — advancedCustom and cross-field validation, async/HTTP with debounce, dynamic lists, disabled/hidden, reusable schemas.Forms
  • 09Typed Reactive FormsTyped FormGroup, nonNullable, FormArray — and how they coexist with Signal Forms.Forms
  • 10HttpClient, interceptors & the API layerHttpClient without setup (v21), Fetch by default (v22), functional interceptors and an API layer with DTO mapping.HTTP & data
  • 11Performance — a checklistWhat really matters: OnPush + signals, track, @defer, lazy routes, images, bundles and hydration.Quality & performance
  • 11Testing with VitestVitest is the default runner from v21. TestBed, components with signals, services, HTTP and harnesses.Quality & performance
  • 11DevTools: look inside your appChrome DevTools and Angular DevTools step by step: what each panel shows, with exercises that work on this site.Quality & performance
  • 11Angular AriaHeadless, accessible components that you style however you like: tabs, accordion, menu, listbox...Quality & performance
  • 12SSR, prerender & render modesWhen to render on the server, when at build time and when only in the browser: RenderMode per route, app.routes.server.ts and the real costs.SSR & hydration
  • 12Hydration: event replay & incrementalHow the browser takes over server HTML without re-rendering it: full hydration, event replay, @defer (hydrate on …) and mismatch errors.SSR & hydration
  • 12Server-compatible codewindow doesn’t exist on the server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, the HTTP cache and PendingTasks.SSR & hydration
  • 13Micro frontendsWhen it is worth splitting an app into several deployments and when it is just complexity: Native Federation, Module Federation, iframes, Web Components.Architecture at scale
  • 13Web Components with Angular ElementscreateCustomElement: an Angular component used as an HTML tag anywhere, plus communication between apps through events and BroadcastChannel.Architecture at scale
  • 13Monorepo, libraries & boundariesA workspace with several apps and libraries: ng generate library, Nx, secondary entry points, publishing and dependency rules.Architecture at scale
  • 13Hexagonal architecture (ports & adapters)A domain without Angular, ports as abstract classes and interchangeable adapters: HTTP, localStorage or in-memory, without touching the UI.Architecture at scale
  • 14Signals under the hoodThe reactive graph: producers and consumers, push-pull, why computed is lazy and glitch-free, the diamond problem and when effects run.Advanced reactivity
  • 14Advanced RxJS: higher-order operatorsswitchMap, mergeMap, concatMap and exhaustMap on a live diagram, plus retry with backoff, shareReplay and how to avoid leaks.Advanced reactivity
  • 14NgRx SignalStoreLarge-scale state management on signals: withState, withComputed, withMethods, rxMethod, entities and hooks — and when a simple store is enough.Advanced reactivity
  • 14State machines with signalsNo more impossible states: a typed machine on signals, with transitions, guards and a live state diagram.Advanced reactivity
  • 15WebSockets: real time that survives the networkA typed socket as signals: reconnect with backoff and jitter, heartbeat, an outbox while offline, and SSR safety.Real time & offline
  • 15Optimistic updates & rollbackAn instant UI that stays honest: snapshot and rollback, out-of-order responses, idempotency keys.Real time & offline
  • 15Offline-first: Service Worker & cachingThe Angular Service Worker: caching strategies, update prompts and an outbox for changes made offline.Real time & offline
  • 16Web WorkersHeavy computation without blocking the UI: a real worker in the page, message passing and when it is not worth it.Advanced UI & performance
  • 16Huge lists: virtual scroll100,000 rows without lag: CDK virtual scroll, itemSize, trackBy and the alternatives (pagination, infinite scroll).Advanced UI & performance
  • 16Modern animations & View Transitionsanimate.enter and animate.leave instead of the deprecated @angular/animations, CSS transitions and View Transitions on navigation.Advanced UI & performance
  • 16Memory leaks: finding and fixing themSubscriptions, timers and listeners that outlive their component — how to spot them in DevTools and close them.Advanced UI & performance
  • 17Security: XSS, sanitisation & CSPWhat Angular does automatically against XSS, when bypassSecurityTrust becomes dangerous, plus CSP with nonce, Trusted Types and XSRF.Security & globalisation
  • 17Authentication: tokens & refreshAn interceptor that attaches the token, refreshes once on 401 and replays the queued requests — plus where you keep the token.Security & globalisation
  • 17OAuth 2.1 & OpenID Connect with PKCEThe authorization code flow with PKCE, step by step: verifier and challenge, state and nonce, the ID token, and why a BFF is safer.Security & globalisation
  • 17Internationalisation (i18n)$localize, the i18n attribute, ICU plural/select, a build per language vs runtime translations, and locale-based formatting.Security & globalisation