Angular 86 🅰️ Internationalization (i18n)
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
| Attribute | Purpose | Example |
|---|---|---|
i18n | Mark text for translation | <h1 i18n>Title</h1> |
i18n-title | Translate an attribute | <img i18n-title title="Logo"> |
i18n-plural | Plural forms (ICU) | {count, plural, =0 {...} other {...}} |
i18n-select | Alternate text (ICU) | {gender, select, male {...} female {...}} |
i18n-<attr> | Translate a specific attribute | <input i18n-placeholder placeholder="Name"> |
The ICU Expression Types
| Expression | Purpose | Example |
|---|---|---|
plural | Count-based forms | {count, plural, =0 {none} =1 {one} other {#}} |
select | Category-based forms | {gender, select, male {He} female {She} other {They}} |
selectordinal | Ordinal forms | {place, selectordinal, one {#st} two {#nd} other {#th}} |
The CLI Commands
| Command | Purpose |
|---|---|
ng extract-i18n | Extract translations from source |
ng build --localize | Build all locale variants |
ng build --localize=fr | Build only French variant |
ng serve --localize=fr | Serve French variant in dev |
The Translation Formats
| Format | Extension | Recommended |
|---|---|---|
| XLIFF 1.2 | .xlf | Legacy |
| XLIFF 2.0 | .xlf | Yes |
| XML Message Bundle | .xmb | Legacy |
| JSON | .json | Not for built-in i18n |
| ARB | .arb | Flutter only |
The angular.json i18n Options
| Option | Purpose |
|---|---|
sourceLocale | Source language (default: en-US) |
locales | Map of target locales to translation files |
subPath | Output directory for the locale |
translation | Path to the translation file |
localize | Build 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
ng serve --localize fails with multiple locales | Dev server supports only one locale | Use --localize=fr for a specific locale |
| Translations not applied | localize not set in angular.json | Add "localize": true to build options |
| Missing translation error | i18nMissingTranslation: "error" | Add the translation or change the setting |
| ICU expression breaks build | Missing brace or malformed syntax | Check brace balance and ICU syntax |
| Base href wrong | subPath not configured | Add subPath to the locale definition |
| Translation file not found | Wrong path in angular.json | Verify 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
| Item | Value |
|---|---|
| i18n attribute | Marks text for translation |
| i18n-title | Translates an attribute |
| ICU plural | {count, plural, =0 {...} other {...}} |
| ICU select | {gender, select, male {...} other {...}} |
| Extraction command | ng extract-i18n |
| Default format | xlf (XLIFF 1.2) |
| Recommended format | xlf2 (XLIFF 2.0) |
| Build command | ng build --localize |
| Configuration | i18n in angular.json |
| Source locale | sourceLocale in angular.json |
| Target locales | locales map in angular.json |
| Output subpath | subPath per locale |
| Missing translation | i18nMissingTranslation |
| Build-time translation | AOT 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
i18nattribute 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
pluralandselectsyntax 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. ThesourceLocalespecifies the source language. Thelocalesmap defines each target locale with its translation file path and an optionalsubPath. - The extraction command produces a standard translation file.
ng extract-i18nscans 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
localizeoption produces localized builds. Setting"localize": truebuilds 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-Languageheader 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.
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!