Internationalization (i18n) & Localization
Angular has framework-built-in internationalization (i18n) features, enabling standardized localization implementation for large-scale multi-region applications.
1. Angular Built-in i18n
Angular's standard i18n consists of the @angular/localize package and the Angular CLI. You add the i18n attribute to translatable content in templates, extract translation sources with the CLI, and the files that translators have filled in are merged in at build time.
ng add @angular/localize
Markup with the i18n Attribute
<h1 i18n="@@home.title">Welcome</h1>
<!-- Adding meaning and description gives translators context -->
<button i18n="button|confirm the order@@order.submit">Place order</button>
<!-- Attribute values use i18n-attributeName -->
<img src="logo.png" alt="Company logo" i18n-alt="@@home.logoAlt" />
<!-- Use ng-container to avoid an extra element -->
<ng-container i18n="@@home.lead">This app is built with Angular.</ng-container>
The format is i18n="meaning|description@@customId", and everything after @@ is the translation ID. If you don't specify an ID, one is generated from the content, so even a minor change to the source text changes the ID and loses the translation. Always add a custom ID.
For strings in code, use the $localize tagged template literal.
const message = $localize`:@@cart.empty:Your cart is empty`;
Extracting Translation Files
ng extract-i18n --format xlf2 --output-path src/locale
This generates messages.xlf. Translators copy it to something like messages.ja.xlf and fill in the translations. You can choose from these formats.
| Format | --format | Characteristics |
|---|---|---|
| XLIFF 1.2 | xlf | Default. Widest support among translation (CAT) tools |
| XLIFF 2.0 | xlf2 | Newer standard. Recommended for new projects |
| XMB / XTB | xmb | Google-style format. Extract as xmb, translations in xtb |
| ARB | arb | JSON-based format shared with Flutter |
| JSON | json | Simple format that is easy for humans to read and write |
Build-time Locale Switching
Define locales in angular.json.
{
"projects": {
"my-app": {
"i18n": {
"sourceLocale": "en",
"locales": {
"ja": { "translation": "src/locale/messages.ja.xlf" },
"zh-Hans": { "translation": "src/locale/messages.zh-Hans.xlf" }
}
},
"architect": {
"build": {
"options": { "localize": true }
}
}
}
}
}
ng build --localize # Build all locales
ng build --localize=ja # A specific locale only
ng serve --configuration=ja # One locale at a time during development
Per-locale output is generated such as dist/my-app/browser/en/, ja/, and zh-Hans/, and your web server or CDN routes to each by URL path or subdomain.
The Next.js App Router has no built-in i18n mechanism (the Pages Router had only locale routing), so it is common to rely on third-party runtime libraries such as next-intl, react-i18next, or react-intl. The big difference is that Angular has a standard mechanism that statically embeds translations at build time.
2. Build-time i18n vs Runtime i18n
| Aspect | Build-time i18n (Angular standard) | Runtime i18n (Transloco, etc.) |
|---|---|---|
| Applying translations | Statically embedded into templates at build time | JSON loaded and substituted at runtime |
| Number of builds | One per locale | One |
| Switching language | Navigate to a different URL (page reload) | Instant, no reload |
| Runtime cost | No translation logic, smallest bundle | Library plus fetching translation JSON |
| Build time | Grows with the number of locales | Constant |
| Updating translations | Rebuild and redeploy required | Can be reflected by swapping JSON |
| Type safety / validation | Missing translations easy to detect at extraction | Key validation needs separate tooling |
Rules of thumb for choosing:
- Sites where SEO matters and each locale has its own URL (including SSR / SSG) suit build-time i18n.
- Business apps behind a login where users switch language instantly from a setting suit runtime i18n.
- With many locales (10 or more), build time and output size balloon, so consider runtime i18n.
- The two are not mutually exclusive. However, define clear roles so the same text isn't managed twice.
3. Runtime Translation Libraries
@ngx-translate/core
The long-standing de facto library, still widely seen in existing projects. It centers on TranslateService and the translate pipe.
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideTranslateService, provideTranslateHttpLoader } from '@ngx-translate/core';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideTranslateService({
lang: 'en',
fallbackLang: 'en',
loader: provideTranslateHttpLoader({ prefix: '/assets/i18n/', suffix: '.json' }),
}),
],
};
@Component({
selector: 'app-header',
imports: [TranslatePipe],
template: `
<h1>{{ 'header.title' | translate }}</h1>
<p>{{ 'header.welcome' | translate: { name: userName() } }}</p>
<button (click)="change('ja')">日本語</button>
`,
})
export class HeaderComponent {
private translate = inject(TranslateService);
userName = signal('Taro');
change(lang: string): void {
this.translate.use(lang);
}
}
Note that the configuration API changed in v16 and later (provideTranslateService, lang / fallbackLang, and so on) and differs between major versions. Check the documentation for the version you use when adopting it.
@jsverse/transloco
A popular modern library often chosen as the next option after ngx-translate. It offers lazy-loaded scopes, Signal support, and a CLI / plugins for key extraction and missing-key detection.
import { provideTransloco } from '@jsverse/transloco';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideTransloco({
config: {
availableLangs: ['en', 'ja'],
defaultLang: 'en',
reRenderOnLangChange: true,
prodMode: !isDevMode(),
},
loader: TranslocoHttpLoader, // A loader that fetches assets/i18n/{lang}.json
}),
],
};
@Component({
selector: 'app-profile',
imports: [TranslocoPipe],
template: `
<h2>{{ 'profile.title' | transloco }}</h2>
<p>{{ t('profile.greeting', { name: name() }) }}</p>
`,
})
export class ProfileComponent {
private transloco = inject(TranslocoService);
name = signal('Taro');
t = (key: string, params?: Record<string, unknown>) => this.transloco.translate(key, params);
switchTo(lang: string): void {
this.transloco.setActiveLang(lang);
}
}
Splitting Locale Files with Lazy Loading
Putting the whole app in one huge JSON file makes the initial load heavy. With Transloco you can define scopes per feature (route) and fetch them only when needed.
export const ADMIN_ROUTES: Routes = [
{
path: '',
component: AdminComponent,
providers: [provideTranslocoScope('admin')], // assets/i18n/admin/en.json, etc.
},
];
assets/i18n/
en.json # common
ja.json
admin/
en.json # admin screens
ja.json
ngx-translate can split similarly, for example by adding a loader per feature (an extension library may be needed).
For new development, Transloco; if you already have ngx-translate assets, continuing without forcing a migration is the realistic guideline. The maintenance status of ngx-translate changes over time, so check the latest status when adopting it.
4. Date, Number, and Currency Localization
The built-in pipes format according to the current locale (LOCALE_ID).
| Pipe | Purpose | Example |
|---|---|---|
DatePipe | Dates and times | date: 'longDate' |
CurrencyPipe | Currency | currency: 'JPY' |
DecimalPipe | Numbers with digit grouping | number: '1.0-2' |
PercentPipe | Percentages | percent: '1.1-1' |
<p>{{ today | date: 'fullDate' }}</p>
<p>{{ price | currency: 'JPY' : 'symbol' : '1.0-0' }}</p>
<p>{{ 1234567.891 | number: '1.0-2' }}</p>
Setting LOCALE_ID and Registering Locale Data
LOCALE_ID defaults to en-US, and with build-time i18n the locale being built is set automatically. To switch at runtime, register locale data and provide LOCALE_ID.
import { registerLocaleData } from '@angular/common';
import localeJa from '@angular/common/locales/ja';
import localeEn from '@angular/common/locales/en';
registerLocaleData(localeJa);
registerLocaleData(localeEn);
providers: [{ provide: LOCALE_ID, useValue: 'ja-JP' }]
LOCALE_ID is evaluated once at application startup. To change the display locale of pipes at runtime, either provide a factory that returns LOCALE_ID dynamically, or specify the locale explicitly via the pipe's trailing arguments (date: 'short' : undefined : 'en').
Browser-standard APIs such as Intl.DateTimeFormat / Intl.NumberFormat need no locale data registration and add no bundle size via registerLocaleData. They are an option when the pipes can't meet your requirements.
5. Pluralization and Gender Handling (ICU Format)
Angular's standard i18n supports the ICU message format, so you can write plurals (plural) and branches (select) in templates. The syntax takes the form {variable, plural, ...}.
<span i18n="@@items.count">
{count, plural, =0 {No items} one {There is 1 item} other {There are {{ count }} items}}
</span>
Here count is the expression to branch on, =0 is an exact value match, and one / other are the plural categories of each language. Japanese has no plural distinction so other alone suffices, but when translating into languages such as English it expands to one / other (some languages, such as Russian, also have few / many).
The select expression branches on string values. It applies not only to gender but also to states and categories.
<span i18n="@@post.author">
{gender, select, male {He commented} female {She commented} other {A comment was posted}}
</span>
Plurals and select can be nested.
<span i18n="@@invite.message">
{gender, select,
male {{count, plural, =0 {He invited no one} other {He invited {{ count }} people}}}
other {{count, plural, =0 {No one was invited} other {{{ count }} people were invited}}}
}
</span>
To use ICU with runtime libraries, use the @jsverse/transloco-messageformat plugin for Transloco or ngx-translate-messageformat-compiler for ngx-translate. Note that, unlike react-intl / next-intl in React, which support ICU out of the box, Angular's runtime libraries need an additional package.
6. RTL (Right-to-Left) Layout Support
To support RTL languages such as Arabic and Hebrew, there are two basics: switching the document direction and using direction-independent styles.
Dynamic dir Attribute Switching
@Injectable({ providedIn: 'root' })
export class DirectionService {
private doc = inject(DOCUMENT);
private rtlLangs = new Set(['ar', 'he', 'fa', 'ur']);
apply(lang: string): void {
const dir = this.rtlLangs.has(lang) ? 'rtl' : 'ltr';
this.doc.documentElement.setAttribute('dir', dir);
this.doc.documentElement.setAttribute('lang', lang);
}
}
Call apply() from your language-switching logic (for example TranslocoService.langChanges$).
Using CSS Logical Properties
Avoid physical-direction properties such as margin-left and use logical properties, and the layout follows just by switching dir.
.card {
margin-inline-start: 16px; /* instead of margin-left */
padding-inline: 12px 8px;
border-inline-end: 1px solid #ccc;
text-align: start; /* instead of left */
}
Angular CDK Bidi
Directionality from @angular/cdk/bidi gets the current document direction and lets you subscribe to changes. CDK and Angular Material components reference it internally.
import { Directionality } from '@angular/cdk/bidi';
@Component({ /* ... */ })
export class CarouselComponent {
private dir = inject(Directionality);
next(): void {
const step = this.dir.value === 'rtl' ? -1 : 1;
this.scrollBy(step);
}
}
Use Directionality for logic that CSS can't solve, such as flipping arrow icons or the direction of sliders and carousels.
7. i18n Strategy for Large-Scale Projects
Designing the Translation Workflow
- Separate responsibilities: developers own the source locale text, translators own the translations.
- Rather than passing files by hand, sync with a translation management service such as Crowdin, Lokalise, or Phrase to prevent missed updates.
- Always write the meaning and description in the
i18nattribute so translators get context.
Translation Key (ID) Naming Conventions
<feature>.<screen or component>.<element>
e.g. order.confirm.submitButton / auth.login.errorInvalid
- Use lowercase English separated by dots, with a hierarchy by feature.
- Name by role, not by the wording (so you don't need to change the ID when the wording changes).
- Use separate IDs when the same translation has different contexts (for example, a "Save" button and a "Save" menu item).
- Collect common vocabulary (OK, Cancel, etc.) under
common.*.
Missing Translation Detection
- With build-time i18n, the
i18nMissingTranslationoption inangular.jsoncontrols the behavior.
"build": {
"options": {
"i18nMissingTranslation": "error"
}
}
| Value | Behavior |
|---|---|
warning | Default. Emits a warning and uses the source text |
error | Fails the build |
ignore | Reports nothing |
- With runtime libraries, detect untranslated keys with Transloco's
transloco-keys-manager(extraction, missing, and unused key detection) or themissingHandlersetting. - In production, set it to
erroror send missing keys to your monitoring.
CI/CD Integration
- Run
ng extract-i18non every pull request and fail if there is a diff (that is, the translation source isn't up to date). - Build all locales with
i18nMissingTranslation: errorto detect missing translations. - Lint the translation file format (such as XLIFF syntax).
- Deploy per-locale output to separate paths and include the locale in the CDN cache key.
- run: npm ci
- run: npx ng extract-i18n --format xlf2 --output-path src/locale
- run: git diff --exit-code src/locale/messages.xlf
- run: npx ng build --localize
Build time grows as locales increase, so also consider parallelizing jobs per locale.