Angular 87 🅰️ Internationalization and Localization in Depth
The previous chapter covered the fundamentals of Angular’s internationalization pipeline: marking text with the i18n attribute, extracting translations, and building localized variants. This chapter goes deeper. It covers the $localize function for translating strings in TypeScript code, the ICU expression system for plurals and selections, the TRANSLATIONS token for runtime translation loading, the distinction between internationalization and localization as separate phases, and the architectural decisions that determine which i18n strategy is right for a given application.
The goal is not to repeat the fundamentals. It is to cover the parts of the i18n system that are invisible until you need them: translating strings that live in component code rather than templates, handling plural rules that vary by language, and understanding the trade-offs between compile-time and runtime translation.
Key point: Angular’s i18n system is not a single feature. It is a pipeline with two distinct phases. Internationalization is the preparation: marking text, configuring locales, and ensuring the application is structured to support multiple languages. Localization is the production: extracting the marked text, translating it, and building a separate application variant for each locale . The @angular/localize package provides the tools for both phases .
Why the depth matters
The basic i18n workflow — mark, extract, translate, build — handles the common case. It does not handle the cases that appear once an application grows beyond a single developer and a single language.
The code problem. The i18n attribute only works in templates. A validation message defined in a TypeScript file, a notification string constructed in a service, or a dynamic label built in a component cannot be marked with i18n. The $localize function solves this. It is a tagged template literal that marks strings in TypeScript code for translation, using the same extraction pipeline as the template attribute .
The plural problem. English has two plural forms: singular and plural. Polish has four. Arabic has six. A naive count === 1 ? 'item' : 'items' check is wrong for most of the world. Angular’s ICU expression system encodes the plural rules for each locale and selects the correct form at build time .
The placement problem. Compile-time translation produces a separate bundle per locale. Runtime translation loads translation files on demand and switches languages without a page reload. The two approaches have different deployment topologies, different performance characteristics, and different use cases. Choosing the wrong one means either operational complexity or runtime cost .
The token problem. The TRANSLATIONS token allows translation content to be provided at runtime, which is how runtime translation libraries integrate with Angular’s i18n system. Understanding this token is the bridge between the compile-time and runtime approaches .
a. The $localize Function
The $localize function is the TypeScript equivalent of the i18n attribute. It is a global tagged template literal that marks strings for translation in component code, services, and any other TypeScript file .
The simplest use is a static string:
const greeting = $localize`Hello, world`;
The extraction tool finds this string, adds it to the translation file, and the build replaces it with the translated version for each locale. The syntax is the same as the i18n attribute, and the translation file format is identical.
$localize supports metadata through a colon-prefixed syntax. The metadata is prepended to the string and separated by colons:
const message = $localize`:greeting|Welcome message:Hello, world`;
The format is :meaning|description:string. The meaning and description are optional, but they serve the same purpose as in the i18n attribute: they help translators disambiguate strings that have different translations depending on context.
Interpolation works with the standard template literal syntax. Placeholders are named so that translators can reorder them:
const welcome = $localize`Hello, ${username}:name:!`;
The :name: after the interpolation names the placeholder. When the string is extracted, the translator sees the placeholder by name rather than by position, and can move it within the translated string without breaking the interpolation .
ICU expressions work inside $localize the same way they work in templates. A plural expression is written directly in the string:
const countMessage = $localize`{count, plural, =0 {No items} =1 {One item} other {{{count}} items}}`;
The extraction tool preserves the ICU syntax, and the build process selects the correct form based on the locale. The ICU expression is not a runtime feature — it is resolved at build time, and the resulting JavaScript contains the selected form for that locale .
One important limitation: $localize is a global function that must be available when the code runs. In a standard Angular application, the @angular/localize package provides it. If the package is not installed, the $localize calls fail at runtime. The ng add @angular/localize command installs the package and sets up the necessary polyfills .
b. ICU Expressions in Depth
ICU (International Components for Unicode) is a message format standard that Angular uses for pluralization and selection. The syntax is written directly in templates and $localize strings. It is expressive, and it is easy to break — a missing brace produces a build error rather than a runtime error .
The plural expression selects a form based on a numeric value. The syntax is:
{count, plural, =0 {No items} =1 {One item} other {# items}}
The =0 and =1 cases are exact matches. The other case is the fallback. The # character is replaced with the numeric value. For English, this is sufficient. For other languages, the CLDR plural rules define additional categories: zero, one, two, few, many, and other .
The categories are language-specific. Polish uses one, few, and many. Arabic uses zero, one, two, few, many, and other. The translator is responsible for providing the correct forms for each locale. The Angular extractor preserves the ICU syntax, and the build process selects the form that matches the locale’s plural rules .
The select expression selects a form based on a string value. It is used for categories like gender, role, or status:
{gender, select, male {He} female {She} other {They}}
The select expression does not have an =value form for exact matches. The keys are the category names, and other is the fallback. The translator provides the correct form for each category in each locale.
The selectordinal expression selects a form based on ordinal numbers — first, second, third. It is used for rankings, positions, and ordinals:
{place, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}
The ordinal categories follow the same CLDR rules as the plural categories, but the forms are ordinal rather than cardinal .
The extraction tool handles nested ICU expressions, but the complexity grows quickly. A plural inside a select inside a plural is difficult to translate and difficult to test. The Angular documentation recommends keeping ICU expressions as flat as possible and testing them in every supported locale .
c. The TRANSLATIONS Token and Runtime Options
The TRANSLATIONS token provides an alternative to compile-time translation. It allows translation content to be provided at runtime, which is how runtime translation libraries integrate with Angular’s i18n system .
The token is used at bootstrap. The translation file content — in XTB, XLF, or XLF2 format — is provided as a value:
import { LOCALE_ID, ApplicationConfig, TRANSLATIONS } from '@angular/core';
const translations = `...`; // content of messages.fr.xlf
export const appConfig: ApplicationConfig = {
providers: [
{ provide: TRANSLATIONS, useValue: translations },
{ provide: LOCALE_ID, useValue: 'fr' },
],
};
When TRANSLATIONS is provided, Angular replaces the $localize calls with the translated strings at runtime rather than at build time. This is the mechanism that runtime translation libraries use. They load the translation file, parse it, and provide the content through the token .
The runtime approach has different trade-offs from the compile-time approach. The application bundle contains the translation infrastructure rather than the translated strings, which means a single build can serve multiple locales. But the translation lookup happens at runtime, and the language cannot be changed without reloading the application or re-bootstrapping it with a different translation file .
The choice between compile-time and runtime translation is architectural. Compile-time translation produces smaller bundles, faster startup, and no runtime lookup cost. Runtime translation produces a single bundle, allows language switching without a page reload, and decouples translation updates from deployments. The Angular documentation recommends compile-time for applications where each locale is deployed separately, and runtime for applications where users switch languages themselves .
Internationalization vs. localization. The two terms are often used interchangeably, but they describe different phases of the same process. Internationalization is the preparation: adding the @angular/localize package, marking text with i18n and $localize, configuring locales in angular.json, and ensuring the application’s layout can accommodate longer translated strings and right-to-left languages . Localization is the production: extracting the marked text, translating it, building the localized variants, and deploying them. Internationalization is done once. Localization is done for each target locale.
Complete Example Session
This session demonstrates $localize in TypeScript, ICU expressions in both templates and code, and the TRANSLATIONS token.
// ============================================
// PART 1: THE $LOCALIZE FUNCTION IN CODE
// ============================================
// src/app/notifications.ts
export function getWelcomeMessage(username: string): string {
return $localize`Welcome back, ${username}:username:!`;
}
export function getItemCountMessage(count: number): string {
return $localize`{count, plural, =0 {No items} =1 {One item} other {{{count}} items}}`;
}
export function getRoleMessage(role: 'admin' | 'user' | 'guest'): string {
return $localize`{role, select, admin {Administrator} user {User} guest {Guest} other {Unknown}}`;
}
// ============================================
// PART 2: THE ICU EXPRESSION IN A TEMPLATE
// ============================================
// src/app/app.component.html
<div class="notifications">
<p i18n="Welcome message|Greeting displayed after login">
Welcome back, {{ userName }}!
</p>
<p i18n="Item count|Number of items in the cart">
{cartCount, plural,
=0 {Your cart is empty}
=1 {You have one item}
other {You have {{cartCount}} items}
}
</p>
<p i18n="User role|Role of the current user">
{userRole, select,
admin {You are an administrator}
user {You are a regular user}
guest {You are browsing as a guest}
other {Unknown role}
}
</p>
</div>
// ============================================
// PART 3: THE EXTRACTION FILE WITH $LOCALIZE
// ============================================
// src/locale/messages.xlf (excerpt)
<trans-unit id="notifications.welcome">
<source>Welcome back, <x id="username" equiv-text="${username}"/>!</source>
<target></target>
</trans-unit>
<trans-unit id="notifications.itemCount">
<source>{count, plural, =0 {No items} =1 {One item} other {{{count}} items}}</source>
<target></target>
</trans-unit>
// ============================================
// PART 4: THE FRENCH TRANSLATION
// ============================================
// src/locale/messages.fr.xlf (excerpt)
<trans-unit id="notifications.welcome">
<source>Welcome back, <x id="username" equiv-text="${username}"/>!</source>
<target>Bon retour, <x id="username"/> !</target>
</trans-unit>
<trans-unit id="notifications.itemCount">
<source>{count, plural, =0 {No items} =1 {One item} other {{{count}} items}}</source>
<target>{count, plural, =0 {Aucun article} =1 {Un article} other {{{count}} articles}}</target>
</trans-unit>
// ============================================
// PART 5: THE TRANSLATIONS TOKEN
// ============================================
// src/app/app.config.ts
import { ApplicationConfig, TRANSLATIONS, LOCALE_ID } from '@angular/core';
const frenchTranslations = `
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en-US" trgLang="fr">
<file id="ngi18n" original="ng.template">
<unit id="notifications.welcome">
<segment>
<source>Welcome back, <ph id="0" equiv="INTERPOLATION" equiv-text="{{ username }}"/>!</source>
<target>Bon retour, <ph id="0" equiv="INTERPOLATION" equiv-text="{{ username }}"/> !</target>
</segment>
</unit>
</file>
</xliff>
`;
export const appConfig: ApplicationConfig = {
providers: [
{ provide: TRANSLATIONS, useValue: frenchTranslations },
{ provide: LOCALE_ID, useValue: 'fr' },
],
};
// ============================================
// PART 6: THE RUNTIME LANGUAGE SWITCH
// ============================================
// src/app/language-switcher.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class LanguageSwitcherService {
private http = inject(HttpClient);
async switchToLocale(locale: string): Promise<void> {
// Load the translation file for the locale
const translations = await firstValueFrom(
this.http.get(`/assets/i18n/messages.${locale}.xlf`, { responseType: 'text' })
);
// Store the translations and reload the application
// In a real implementation, this would use the TRANSLATIONS token
// at bootstrap or re-bootstrap the application with the new translations.
sessionStorage.setItem('translations', translations);
sessionStorage.setItem('locale', locale);
window.location.reload();
}
}
// ============================================
// PART 7: THE CONFIGURATION WITH SOURCE LOCALE
// ============================================
// angular.json
{
"projects": {
"my-app": {
"i18n": {
"sourceLocale": "en-US",
"locales": {
"fr": {
"translation": "src/locale/messages.fr.xlf",
"subPath": "fr"
},
"de": {
"translation": "src/locale/messages.de.xlf",
"subPath": "de"
}
}
}
}
}
}
// ============================================
// PART 8: THE DEVELOPMENT WORKFLOW
// ============================================
// To develop against a single locale:
ng serve --configuration=fr --localize=fr
// The development server only supports a single locale at a time.
// Building multiple locales during development is not supported.
// To verify all locales build correctly:
ng build --localize
// To extract updated translations after changing source text:
ng extract-i18n --output-path src/locale --out-file messages.xlf --format xlf2
// ============================================
// PART 9: THE MISSING TRANSLATION CONFIGURATION
// ============================================
// angular.json
{
"architect": {
"build": {
"options": {
"i18nMissingTranslation": "error"
}
}
}
}
// Options: "error" (fail the build), "warning" (log and continue),
// "ignore" (silently skip). "error" is recommended for production
// to catch untranslated strings before deployment.
// ============================================
// PART 10: THE DEPLOYMENT CONFIGURATION
// ============================================
// The build produces one directory per locale:
// dist/my-app/
// ├── en-US/
// │ └── index.html
// ├── fr/
// │ └── index.html
// └── de/
// └── index.html
// The server routes requests based on the Accept-Language header
// or the URL path prefix. The Nginx configuration from the
// previous chapter handles this routing.
The ten parts cover $localize in code, ICU expressions in templates, the extraction file with $localize, the French translation, the TRANSLATIONS token, the runtime language switch, the configuration with source locale, the development workflow, the missing translation configuration, and the deployment configuration.
Quick Reference
The $localize Syntax
| Form | Example |
|---|---|
| Simple | $localize\Hello“ |
| With metadata | $localize\:meaning |
| With interpolation | $localize\Hello, ${name}:name:“ |
| With ICU plural | $localize\{count, plural, =0 {none} other {#}}“ |
| With ICU select | $localize\{gender, select, male {He} other {They}}“ |
The ICU Expression Types
| Expression | Purpose | Categories |
|---|---|---|
plural | Count-based forms | zero, one, two, few, many, other |
select | Category-based forms | Language-specific keys |
selectordinal | Ordinal forms | one, two, few, other |
The Translation Phases
| Phase | Activity | Tools |
|---|---|---|
| Internationalization | Mark text, configure locales | i18n attribute, $localize, angular.json |
| Localization | Extract, translate, build | ng extract-i18n, ng build --localize |
The Translation Strategies
| Strategy | Bundle | Language Switch | Deployment |
|---|---|---|---|
| Compile-time | One per locale | Page reload | Separate per locale |
| Runtime | Single | In-app | Single deployment |
The Key Tokens
| Token | Purpose |
|---|---|
LOCALE_ID | Current locale for formatting |
TRANSLATIONS | Translation content for runtime |
Best Practices
✅ Do This:
// Use $localize for strings in TypeScript
const message = $localize`Hello, ${name}:name:!`; // ✅
<!-- Use ICU for plurals -->
{count, plural, =0 {No items} other {{{count}} items}} <!-- ✅ -->
// Use "error" for missing translations in production
"i18nMissingTranslation": "error" // ✅
# Extract with a stable format
ng extract-i18n --format=xlf2 // ✅
// Use named placeholders in $localize
$localize`Hello, ${username}:username:!` // ✅
❌ Don’t Do This:
// Don't concatenate translated strings
const message = $localize`Hello` + ', ' + name; // ❌
<!-- Don't use naive plural checks -->
{{ count === 1 ? 'item' : 'items' }} // ❌ use ICU
// Don't use "ignore" for missing translations in production
"i18nMissingTranslation": "ignore" // ❌
// Don't forget the $localize polyfill
// Import @angular/localize/init in main.ts // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
$localize not defined | Package not imported | Add @angular/localize/init to polyfills |
| ICU expression breaks build | Missing brace | Check brace balance |
| Translation not applied | localize not set | Add "localize": true to angular.json |
| Placeholder lost in translation | Translator removed it | Use named placeholders and review |
| Runtime switch requires reload | Compile-time translation | Use runtime strategy or reload |
| Missing translation error | i18nMissingTranslation: "error" | Add the translation or change setting |
Real-World Examples
1. Simple $localize
const greeting = $localize`Hello, world`;
2. $localize with Metadata
const message = $localize`:greeting|Welcome:Hello, world`;
3. $localize with Interpolation
const welcome = $localize`Hello, ${username}:username:!`;
4. ICU Plural in Code
const count = $localize`{count, plural, =0 {No items} other {{{count}} items}}`;
5. ICU Select in Code
const role = $localize`{role, select, admin {Admin} other {User}}`;
6. TRANSLATIONS Token
{ provide: TRANSLATIONS, useValue: translations }
7. LOCALE_ID Token
{ provide: LOCALE_ID, useValue: 'fr' }
8. Extraction Command
ng extract-i18n --format=xlf2
9. Build Localized
ng build --localize
10. Missing Translation Config
{ "i18nMissingTranslation": "error" }
Visual
The i18n Pipeline
┌──────────────────────────────────────────────┐
│ ANGULAR I18N PIPELINE │
│ │
│ Internationalization (prepare): │
│ ├─ Mark text with i18n attribute │
│ ├─ Mark code with $localize │
│ ├─ Configure locales in angular.json │
│ └─ Use ICU for plurals and selects │
│ │
│ Localization (produce): │
│ ├─ ng extract-i18n → messages.xlf │
│ ├─ Translator fills <target> elements │
│ ├─ messages.fr.xlf, messages.de.xlf │
│ └─ ng build --localize │
│ │
│ Output: │
│ ├─ dist/en-US/index.html │
│ ├─ dist/fr/index.html │
│ └─ dist/de/index.html │
│ │
└──────────────────────────────────────────────┘
The $localize vs i18n Attribute
┌──────────────────────────────────────────────┐
│ WHERE TO USE EACH │
│ │
│ i18n attribute: │
│ ├─ HTML templates │
│ ├─ Element content │
│ └─ Attribute values │
│ │
│ $localize function: │
│ ├─ TypeScript code │
│ ├─ Services │
│ ├─ Component class properties │
│ └─ Utility functions │
│ │
│ Both feed the same extraction pipeline. │
│ │
└──────────────────────────────────────────────┘
The ICU Plural Categories
┌──────────────────────────────────────────────┐
│ CLDR PLURAL CATEGORIES │
│ │
│ English: │
│ one, other │
│ │
│ Polish: │
│ one, few, many, other │
│ │
│ Arabic: │
│ zero, one, two, few, many, other │
│ │
│ Japanese: │
│ other (no plural distinction) │
│ │
│ The translator provides the correct forms. │
│ Angular selects the form at build time. │
│ │
└──────────────────────────────────────────────┘
The Compile-Time vs Runtime
┌──────────────────────────────────────────────┐
│ COMPILE-TIME (default) │
│ │
│ ├─ One bundle per locale │
│ ├─ No runtime lookup cost │
│ ├─ No flash of untranslated content │
│ └─ Language switch = page reload │
│ │
├──────────────────────────────────────────────┤
│ RUNTIME (TRANSLATIONS token) │
│ │
│ ├─ Single bundle │
│ ├─ Loads translations on demand │
│ ├─ Language switch without reload │
│ └─ Runtime lookup cost │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
$localize | Tagged template literal for code |
| Metadata syntax | :meaning|description:string |
| Named placeholder | ${value}:name: |
| ICU plural | {count, plural, =0 {...} other {...}} |
| ICU select | {role, select, admin {...} other {...}} |
| ICU selectordinal | {place, selectordinal, one {...} other {...}} |
TRANSLATIONS token | Runtime translation content |
LOCALE_ID token | Current locale for formatting |
| Internationalization | Preparation phase |
| Localization | Production phase |
| Extraction command | ng extract-i18n |
| Build command | ng build --localize |
Key takeaways:
$localizeis the TypeScript equivalent of thei18nattribute. It marks strings in component code, services, and utility functions for translation. The extraction tool finds them, the translator translates them, and the build replaces them for each locale .- ICU expressions handle plurals and selections. The
plural,select, andselectordinalsyntax is written directly in templates and$localizestrings. Angular uses the CLDR plural rules for each locale to select the correct form. The translator provides the forms for each language . - The
TRANSLATIONStoken enables runtime translation. It provides translation content at bootstrap, which is how runtime translation libraries integrate with Angular. The token is the bridge between the compile-time and runtime strategies . - Internationalization and localization are distinct phases. Internationalization is the preparation: marking text, configuring locales, and structuring the application for multiple languages. Localization is the production: extracting, translating, and building the localized variants .
- Compile-time translation is the default and the recommended approach for most applications. It produces smaller bundles, faster startup, and no runtime lookup cost. The trade-off is that language switching requires a page reload, and adding a language requires a new build and deployment .
- Runtime translation is the alternative for applications where users switch languages themselves. It loads translation files on demand and allows in-app language switching. The trade-off is a runtime lookup cost and a larger bundle .
- The missing translation setting determines build behavior. Setting
i18nMissingTranslationto"error"fails the build when a translation is missing, which is recommended for production. The"warning"and"ignore"settings are useful during development .
Remember: Angular’s i18n system has two layers. The visible layer is the i18n attribute and the extraction pipeline. The deeper layer is the $localize function for code, the ICU expression system for plurals and selections, and the TRANSLATIONS token for runtime translation. Understanding both layers is what separates a basic translation setup from an internationalization strategy that works for every locale the application supports.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!