| |

Angular 86 🅰️ Internationalization (i18n)

⏱️ Estimated read: 15 min | |

Internationalization is the process of designing an application so that it can be adapted to various languages and regions without engineering changes. Angular provides built-in internationalization tools, commonly abbreviated as i18n, to help you achieve this. The framework’s approach is primarily compile-time: translations are extracted from your source code, translated, and then merged back into the application during the build process, producing a separate, optimized bundle for each language .

This chapter covers the complete i18n workflow: setting up locale definitions, marking text for translation, handling plurals and alternate text, extracting translation files, building localized variants, and deploying them.

Key point: Angular’s built-in i18n compiles a separate build for each language. This means the translated strings are baked directly into the JavaScript bundles, resulting in zero runtime performance cost and no flash of untranslated content. The trade-off is that adding a new language requires a new build and deployment, and users cannot switch languages in the browser without loading a different application bundle .


Why Angular’s i18n exists

Building a multilingual application involves more than just replacing strings. It requires formatting dates, numbers, and currencies according to local conventions. It requires handling plural forms, which vary dramatically between languages. It requires managing translation files, coordinating with translators, and deploying multiple application variants.

Angular’s built-in i18n addresses these concerns through a unified pipeline. The ng extract-i18n command scans your templates for marked text and produces an industry-standard translation file. Translators work on that file. The build process then merges the translations back into the application, producing a complete, localized variant. The framework handles the complexities of locale-specific formatting and plural rules automatically .

This approach differs from runtime translation libraries like ngx-translate, which load translation files at runtime and allow in-browser language switching. The built-in i18n prioritizes runtime performance and correctness over operational flexibility .


a. Setting Up Locale Definitions

The first step is to define your source locale and target locales in the angular.json workspace configuration file. The source locale is the language used in your application’s source code, and it defaults to en-US. Target locales are the languages you want to support .

The configuration lives under the i18n property of your project:

{
  "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"
          }
        }
      }
    }
  }
}

The sourceLocale specifies the language of your source code. The locales map defines each target locale with its translation file path and an optional subPath. The subPath determines the directory where that locale’s build output will be placed, and it is also used to adjust the baseHref in the generated HTML .

Locale identifiers follow the Unicode locale identifier standard, which is based on BCP 47 tags. A locale ID consists of a language identifier and an optional locale extension, separated by a hyphen. For example, en-US is English as used in the United States, while en-CA is English as used in Canada. The differences between these locales include grammar, punctuation, and formats for currency, decimal numbers, and dates .


b. Marking Text for Translation

Angular uses the i18n attribute to mark text in component templates for translation. The attribute is added to an element, and Angular treats the element’s content as a translatable unit .

<h1 i18n>Welcome to our application</h1>
<p i18n>This text will be translated.</p>
<button i18n>Submit</button>

The i18n attribute can carry two optional metadata values: a description and a meaning. Both are passed to the translator and help disambiguate text that could be translated in multiple ways .

<button i18n="Submit button|The button that submits the form">Submit</button>

The format is description|meaning. The description explains the context, and the meaning distinguishes between identical source strings that have different translations. A classic example is the word “Close.” As a verb on a button, it means “to close something.” As an adjective, it means “nearby.” The meaning attribute tells the translator which interpretation is correct .

Text can also be marked for translation using the i18n attribute on inline elements within a larger block. Angular handles the surrounding text and the marked element as a single translatable unit.

Pluralization and alternate text are handled using ICU message syntax directly in the template. Angular supports plural and select expressions .

<span i18n>
  {count, plural,
    =0 {No items}
    =1 {One item}
    other {{{count}} items}
  }
</span>

The ICU syntax is expressive but fragile. A translation that loses a brace produces a build error rather than a runtime error, which is a safer failure mode .


c. Extracting, Translating, and Merging

The extraction step produces a translation file from the marked text in your templates. The Angular CLI command is ng extract-i18n .

ng extract-i18n --output-path src/locale --out-file messages.xlf --format xlf2

The --format option supports xlf, xlf2, xmb, json, arb, and legacy-migrate. The default is xlf. For modern projects, xlf2 is the recommended format .

The extracted file contains translation units, each with an ID and the source text. Translators edit the <target> elements, leaving the <source> untouched. The unit IDs tie each translation to its source text. When the source text changes, the unit ID changes, signaling that the translation needs review .

After translation, the localize option in angular.json tells the build process to produce localized variants. Setting "localize": true builds all defined locales. Setting it to an array of locale IDs builds only those locales .

ng build --localize

The build process uses ahead-of-time (AOT) compilation and applies the translations during the build. Each locale produces a complete, separate application variant in its own subdirectory . The lang attribute of the <html> element is set to the locale, and the baseHref is adjusted to include the subPath .

Deployment involves serving each locale variant from its own subdirectory. Users are redirected to their preferred language using the Accept-Language HTTP header. Nginx and Apache configurations handle the routing and fallback logic. The typical pattern is to serve the default language at the root and redirect to locale subdirectories based on browser preferences .


Complete Example Session

This session sets up i18n for an Angular application with English source and French and German targets.

// ============================================
// PART 1: THE ANGULAR.JSON CONFIGURATION
// ============================================

// 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"
          }
        }
      },
      "architect": {
        "build": {
          "options": {
            "localize": true,
            "outputPath": "dist/my-app"
          }
        }
      }
    }
  }
}

// ============================================
// PART 2: THE COMPONENT TEMPLATE
// ============================================

// src/app/app.component.html
<header>
  <h1 i18n="Application title">Task Manager</h1>
  <nav>
    <a routerLink="/tasks" i18n>Tasks</a>
    <a routerLink="/settings" i18n>Settings</a>
  </nav>
</header>

<main>
  <p i18n="Welcome message|Greeting displayed on the home page">
    Welcome back, {{ userName }}!
  </p>

  <div class="task-count" i18n>
    {taskCount, plural,
      =0 {No tasks}
      =1 {One task}
      other {{{taskCount}} tasks}
    }
  </div>

  <button i18n="Submit button|The button that submits the form">Submit</button>
</main>

// ============================================
// PART 3: THE EXTRACTION COMMAND
// ============================================

// Terminal
ng extract-i18n --output-path src/locale --out-file messages.xlf --format xlf2

// Output: src/locale/messages.xlf
// The file contains all marked text with unit IDs and source strings.

// ============================================
// PART 4: THE TRANSLATION FILE (FRENCH)
// ============================================

// src/locale/messages.fr.xlf (excerpt)
<?xml version="1.0" encoding="UTF-8" ?>
<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="app_title">
      <notes>
        <note category="description">Application title</note>
      </notes>
      <segment>
        <source>Task Manager</source>
        <target>Gestionnaire de tâches</target>
      </segment>
    </unit>
    <unit id="welcome_message">
      <notes>
        <note category="description">Welcome message</note>
        <note category="meaning">Greeting displayed on the home page</note>
      </notes>
      <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>

// ============================================
// PART 5: THE BUILD COMMAND
// ============================================

// Terminal
ng build --localize

// Output structure:
// dist/my-app/
// ├── en-US/          (source locale, no subPath)
// │   └── index.html
// ├── fr/             (French, subPath: "fr")
// │   └── index.html
// └── de/             (German, subPath: "de")
//     └── index.html

// ============================================
// PART 6: THE NGINX CONFIGURATION
// ============================================

// nginx.conf
http {
  map $http_accept_language $accept_language {
    ~*^fr fr;
    ~*^de de;
    ~*^en en;
  }

  server {
    listen 80;
    server_name localhost;
    root /www/data;

    # Fallback to default language
    if ($accept_language ~ "^$") {
      set $accept_language "en";
    }

    # Redirect root to preferred language
    rewrite ^/$ /$accept_language permanent;

    # Serve locale-specific builds
    location ~ ^/(fr|de|en) {
      try_files $uri /$1/index.html?$args;
    }
  }
}

// ============================================
// PART 7: THE LANGUAGE SWITCHER
// ============================================

// src/app/language-switcher.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-language-switcher',
  template: `
    <select (change)="switchLanguage($event)">
      <option value="en">English</option>
      <option value="fr">Français</option>
      <option value="de">Deutsch</option>
    </select>
  `,
})
export class LanguageSwitcherComponent {
  switchLanguage(event: Event): void {
    const locale = (event.target as HTMLSelectElement).value;
    window.location.href = `/${locale}/`;
  }
}

// The switcher navigates to the locale-specific subdirectory.
// This triggers a full page load of the localized build.

// ============================================
// PART 8: THE DATE AND NUMBER FORMATTING
// ============================================

// src/app/app.component.ts
import { Component, LOCALE_ID, Inject } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <p>{{ today | date:'fullDate' }}</p>
    <p>{{ price | currency }}</p>
    <p>{{ percentage | percent }}</p>
  `,
})
export class AppComponent {
  today = new Date();
  price = 1234.56;
  percentage = 0.42;
}

// Angular's built-in pipes automatically use the locale
// specified in LOCALE_ID, which is set by the build process
// for each localized variant.

// ============================================
// 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 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
// Then merge the new units into the existing translation files.

The ten parts cover the angular.json configuration, the component template, the extraction command, the French translation file, the build command, the Nginx configuration, the language switcher, date and number formatting, the missing translation configuration, and the development workflow.


Quick Reference

The i18n Attributes

AttributePurposeExample
i18nMark text for translation<h1 i18n>Title</h1>
i18n-titleTranslate an attribute<img i18n-title title="Logo">
i18n-pluralPlural forms (ICU){count, plural, =0 {...} other {...}}
i18n-selectAlternate text (ICU){gender, select, male {...} female {...}}
i18n-<attr>Translate a specific attribute<input i18n-placeholder placeholder="Name">

The ICU Expression Types

ExpressionPurposeExample
pluralCount-based forms{count, plural, =0 {none} =1 {one} other {#}}
selectCategory-based forms{gender, select, male {He} female {She} other {They}}
selectordinalOrdinal forms{place, selectordinal, one {#st} two {#nd} other {#th}}

The CLI Commands

CommandPurpose
ng extract-i18nExtract translations from source
ng build --localizeBuild all locale variants
ng build --localize=frBuild only French variant
ng serve --localize=frServe French variant in dev

The Translation Formats

FormatExtensionRecommended
XLIFF 1.2.xlfLegacy
XLIFF 2.0.xlfYes
XML Message Bundle.xmbLegacy
JSON.jsonNot for built-in i18n
ARB.arbFlutter only

The angular.json i18n Options

OptionPurpose
sourceLocaleSource language (default: en-US)
localesMap of target locales to translation files
subPathOutput directory for the locale
translationPath to the translation file
localizeBuild all locales (true) or specific (["fr"])

Best Practices

✅ Do This:

<!-- Use the meaning attribute for ambiguous text -->
<button i18n="Close button|Close the dialog">Close</button>      <!-- ✅ -->
<!-- Use ICU for plurals -->
<span i18n>{count, plural, =0 {No items} other {{{count}} items}}</span> <!-- ✅ -->
// Set i18nMissingTranslation to "error" for production
"i18nMissingTranslation": "error"                                // ✅
# Use xlf2 for modern translation files
ng extract-i18n --format xlf2                                     // ✅
# Test each locale build before deployment
ng build --localize                                               // ✅

❌ Don’t Do This:

<!-- Don't concatenate translated strings -->
<p>{{ 'Hello' | translate }} {{ userName }}</p>                  <!-- ❌ use a single unit -->
// Don't use "ignore" for missing translations in production
"i18nMissingTranslation": "ignore"                                // ❌
# Don't edit the source file after extraction
# Always re-extract to get new unit IDs
ng extract-i18n  // then edit the source manually              // ❌
<!-- Don't use ICU braces without care -->
<span i18n>{count, plural, =0 {none} other {# items}</span>       <!-- ❌ missing brace -->

Common Pitfalls

PitfallWhy It HappensFix
ng serve --localize fails with multiple localesDev server supports only one localeUse --localize=fr for a specific locale
Translations not appliedlocalize not set in angular.jsonAdd "localize": true to build options
Missing translation errori18nMissingTranslation: "error"Add the translation or change the setting
ICU expression breaks buildMissing brace or malformed syntaxCheck brace balance and ICU syntax
Base href wrongsubPath not configuredAdd subPath to the locale definition
Translation file not foundWrong path in angular.jsonVerify the translation path

Real-World Examples

1. Marking Text

<h1 i18n>Welcome</h1>

2. Marking with Metadata

<button i18n="Submit|Form submission">Submit</button>

3. Plural Form

<span i18n>{count, plural, =0 {No items} =1 {One item} other {{{count}} items}}</span>

4. Select Form

<span i18n>{gender, select, male {He} female {She} other {They}}</span>

5. Extract Command

ng extract-i18n --output-path src/locale --format xlf2

6. Build Localized

ng build --localize

7. Serve Single Locale

ng serve --localize=fr

8. Configure Locale

{ "fr": { "translation": "src/locale/messages.fr.xlf", "subPath": "fr" } }

9. Missing Translation

{ "i18nMissingTranslation": "error" }

10. Language Switcher

window.location.href = `/${locale}/`;

Visual

The i18n Build Pipeline

┌──────────────────────────────────────────────┐
│  ANGULAR I18N PIPELINE                       │
│                                              │
│  Source code with i18n attributes            │
│       │                                      │
│       ▼                                      │
│  ng extract-i18n ──> messages.xlf            │
│       │                                      │
│       ▼                                      │
│  Translator edits <target> elements          │
│       │                                      │
│       ▼                                      │
│  messages.fr.xlf, messages.de.xlf            │
│       │                                      │
│       ▼                                      │
│  ng build --localize                         │
│       │                                      │
│       ├──> dist/en-US/index.html             │
│       ├──> dist/fr/index.html                │
│       └──> dist/de/index.html                │
│                                              │
│  Each variant is a complete, localized app.  │
│                                              │
└──────────────────────────────────────────────┘

The Locale Configuration

┌──────────────────────────────────────────────┐
│  angular.json i18n                           │
│                                              │
│  "i18n": {                                   │
│    "sourceLocale": "en-US",                  │
│    "locales": {                              │
│      "fr": {                                 │
│        "translation": "src/locale/fr.xlf",   │
│        "subPath": "fr"                       │
│      },                                      │
│      "de": {                                 │
│        "translation": "src/locale/de.xlf",   │
│        "subPath": "de"                       │
│      }                                       │
│    }                                         │
│  }                                           │
│                                              │
│  subPath determines the output directory.    │
│  It also adjusts the baseHref.               │
│                                              │
└──────────────────────────────────────────────┘

The Deployment Structure

┌──────────────────────────────────────────────┐
│  DEPLOYMENT                                  │
│                                              │
│  dist/                                       │
│  ├── en-US/                                  │
│  │   └── index.html  ← default language      │
│  ├── fr/                                     │
│  │   └── index.html  ← French                │
│  └── de/                                     │
│      └── index.html  ← German                │
│                                              │
│  Server routes based on:                     │
│  ├─ Accept-Language header                   │
│  └─ URL path (/fr/, /de/)                    │
│                                              │
│  Fallback: serve en-US if language unknown.  │
│                                              │
└──────────────────────────────────────────────┘

The ICU Plural Syntax

┌──────────────────────────────────────────────┐
│  ICU PLURAL                                  │
│                                              │
│  {count, plural,                             │
│    =0 {No items}        ← exact match        │
│    =1 {One item}        ← exact match        │
│    one {# item}         ← language category  │
│    few {# items}        ← language category  │
│    many {# items}       ← language category  │
│    other {# items}      ← fallback           │
│  }                                           │
│                                              │
│  # is replaced with the count.               │
│  Categories follow CLDR rules per language.  │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
i18n attributeMarks text for translation
i18n-titleTranslates an attribute
ICU plural{count, plural, =0 {...} other {...}}
ICU select{gender, select, male {...} other {...}}
Extraction commandng extract-i18n
Default formatxlf (XLIFF 1.2)
Recommended formatxlf2 (XLIFF 2.0)
Build commandng build --localize
Configurationi18n in angular.json
Source localesourceLocale in angular.json
Target localeslocales map in angular.json
Output subpathsubPath per locale
Missing translationi18nMissingTranslation
Build-time translationAOT compilation

Key takeaways:

  • Angular’s built-in i18n uses compile-time translation. Translations are merged into the application during the build, producing a separate optimized bundle for each locale. There is no runtime lookup cost and no flash of untranslated content .
  • The i18n attribute marks text for translation. It can carry a description and a meaning to disambiguate ambiguous source strings. The meaning attribute is essential for words like “Close” that have different translations depending on context .
  • ICU expressions handle plurals and alternate text. The plural and select syntax is written directly in the template. Angular uses the CLDR plural rules for each language, so the correct forms are selected automatically .
  • Locale definitions go in angular.json. The sourceLocale specifies the source language. The locales map defines each target locale with its translation file path and an optional subPath .
  • The extraction command produces a standard translation file. ng extract-i18n scans the templates and generates an XLIFF file. Translators edit the <target> elements. The unit IDs tie translations to source text and change when the source changes .
  • The localize option produces localized builds. Setting "localize": true builds all defined locales. Setting it to an array of locale IDs builds only those locales. Each locale produces a complete application variant in its own subdirectory .
  • Deployment requires server-side routing. Each locale variant is served from its own subdirectory. The server reads the Accept-Language header and redirects users to their preferred language. Nginx and Apache configurations handle the routing and fallback logic .

Remember: Angular’s i18n is a compile-time system. You mark text with the i18n attribute, extract it to a standard translation file, translate it, and build a separate application variant for each language. The framework handles the complexities of locale-specific formatting and plural rules. The trade-off is operational: adding a language requires a new build and deployment, and users cannot switch languages in the browser without loading a different bundle. For applications where each locale is deployed separately and runtime performance matters most, this is the right approach.

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!