| |

Angular 87 🅰️ Internationalization and Localization in Depth

⏱️ Estimated read: 16 min | |

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

FormExample
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

ExpressionPurposeCategories
pluralCount-based formszero, one, two, few, many, other
selectCategory-based formsLanguage-specific keys
selectordinalOrdinal formsone, two, few, other

The Translation Phases

PhaseActivityTools
InternationalizationMark text, configure localesi18n attribute, $localize, angular.json
LocalizationExtract, translate, buildng extract-i18n, ng build --localize

The Translation Strategies

StrategyBundleLanguage SwitchDeployment
Compile-timeOne per localePage reloadSeparate per locale
RuntimeSingleIn-appSingle deployment

The Key Tokens

TokenPurpose
LOCALE_IDCurrent locale for formatting
TRANSLATIONSTranslation 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

PitfallWhy It HappensFix
$localize not definedPackage not importedAdd @angular/localize/init to polyfills
ICU expression breaks buildMissing braceCheck brace balance
Translation not appliedlocalize not setAdd "localize": true to angular.json
Placeholder lost in translationTranslator removed itUse named placeholders and review
Runtime switch requires reloadCompile-time translationUse runtime strategy or reload
Missing translation errori18nMissingTranslation: "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

ItemValue
$localizeTagged 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 tokenRuntime translation content
LOCALE_ID tokenCurrent locale for formatting
InternationalizationPreparation phase
LocalizationProduction phase
Extraction commandng extract-i18n
Build commandng build --localize

Key takeaways:

  • $localize is the TypeScript equivalent of the i18n attribute. 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, and selectordinal syntax is written directly in templates and $localize strings. Angular uses the CLDR plural rules for each locale to select the correct form. The translator provides the forms for each language .
  • The TRANSLATIONS token 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 i18nMissingTranslation to "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.

Was this helpful?


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!