Docs
/
Angular
Chapter 19
19 — Internationalization (i18n)
Two Approaches
@angular/localize (Built-in) | ngx-translate | |
|---|---|---|
| Type | Build-time i18n | Runtime i18n |
| Bundle | One build per locale | Single build, load JSON at runtime |
| Performance | Faster (static) | Slightly slower (dynamic) |
| Switch language | Requires page reload | Instant switch |
| Best for | Large apps, SEO, static content | Dynamic content, single deployment |
Option 1: @angular/localize (Built-Time)
ng add @angular/localize
Mark Strings for Translation
<!-- i18n attribute -->
<h1 i18n="Page title|Welcome heading">Welcome to our app</h1>
<!-- With description and meaning -->
<p i18n="user greeting|Greeting shown after login">Hello, {{ user.name }}!</p>
<!-- i18n on attributes -->
<img [src]="logo" i18n-alt="Logo alt text" alt="Company logo">
<input i18n-placeholder placeholder="Search...">
<!-- ICU expressions for plurals -->
<p i18n>
{ count, plural,
=0 { No items }
=1 { 1 item }
other { {{ count }} items }
}
</p>
<!-- ICU for select (gender, role, etc.) -->
<p i18n>
{ role, select,
admin { Administrator }
editor { Editor }
other { User }
}
</p>
Extract and Translate
# Extract all i18n strings to XLIFF file
ng extract-i18n --output-path src/locale
# Creates: src/locale/messages.xlf
<!-- src/locale/messages.es.xlf (Spanish translation) -->
<trans-unit id="..." datatype="html">
<source>Welcome to our app</source>
<target>Bienvenido a nuestra aplicación</target>
</trans-unit>
Build Per Locale
// angular.json
"i18n": {
"sourceLocale": "en",
"locales": {
"es": "src/locale/messages.es.xlf",
"fr": "src/locale/messages.fr.xlf"
}
},
"build": {
"configurations": {
"es": { "localize": ["es"] },
"fr": { "localize": ["fr"] },
"production": { "localize": true } // Build ALL locales
}
}
ng build --configuration=es # Spanish build only
ng build --localize # All locale builds
Option 2: ngx-translate (Runtime)
npm install @ngx-translate/core @ngx-translate/http-loader
Setup
// app.config.ts
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
export const appConfig: ApplicationConfig = {
providers: [
importProvidersFrom(
TranslateModule.forRoot({
defaultLanguage: 'en',
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient],
},
}),
),
],
};
Translation Files
// assets/i18n/en.json
{
"WELCOME": "Welcome to our app",
"GREETING": "Hello, {{ name }}!",
"NAV": {
"HOME": "Home",
"ABOUT": "About",
"CONTACT": "Contact"
},
"ITEMS": {
"NONE": "No items",
"ONE": "1 item",
"MANY": "{{ count }} items"
}
}
// assets/i18n/es.json
{
"WELCOME": "Bienvenido a nuestra aplicación",
"GREETING": "¡Hola, {{ name }}!",
"NAV": {
"HOME": "Inicio",
"ABOUT": "Acerca de",
"CONTACT": "Contacto"
}
}
Using Translations
import { TranslateModule, TranslateService } from '@ngx-translate/core';
@Component({
imports: [TranslateModule],
template: `
<!-- Pipe (most common) -->
<h1>{{ 'WELCOME' | translate }}</h1>
<p>{{ 'GREETING' | translate:{ name: user.name } }}</p>
<!-- Directive -->
<span [translate]="'NAV.HOME'"></span>
<!-- Nested keys -->
<a>{{ 'NAV.ABOUT' | translate }}</a>
<!-- Language switcher -->
<select (change)="switchLang($any($event.target).value)">
@for (lang of langs; track lang) {
<option [value]="lang">{{ lang | uppercase }}</option>
}
</select>
`,
})
export class AppComponent {
private translate = inject(TranslateService);
langs = ['en', 'es', 'fr'];
constructor() {
this.translate.setDefaultLang('en');
const browserLang = this.translate.getBrowserLang();
this.translate.use(browserLang?.match(/en|es|fr/) ? browserLang : 'en');
}
switchLang(lang: string) {
this.translate.use(lang); // Instant switch — no reload
}
}
In TypeScript (Service)
private translate = inject(TranslateService);
showMessage() {
this.translate.get('GREETING', { name: 'Alice' }).subscribe(msg => {
alert(msg); // "Hello, Alice!" or "¡Hola, Alice!"
});
// Synchronous (if translation is already loaded)
const msg = this.translate.instant('WELCOME');
}
RTL (Right-to-Left) Support
// Detect and apply direction
@Component({
template: `<div [dir]="direction()">...</div>`,
})
export class AppComponent {
private translate = inject(TranslateService);
direction = computed(() => {
const rtlLangs = ['ar', 'he', 'fa', 'ur'];
return rtlLangs.includes(this.translate.currentLang) ? 'rtl' : 'ltr';
});
}
// Use logical properties for RTL support
.card {
margin-inline-start: 1rem; // instead of margin-left
padding-inline-end: 1rem; // instead of padding-right
text-align: start; // instead of text-align: left
border-inline-start: 2px solid blue;
}
// RTL-specific overrides
[dir='rtl'] {
.icon-arrow { transform: scaleX(-1); }
}
Date/Number/Currency Localization
import { registerLocaleData } from '@angular/common';
import localeEs from '@angular/common/locales/es';
import localeFr from '@angular/common/locales/fr';
registerLocaleData(localeEs);
registerLocaleData(localeFr);
// Provide locale
providers: [
{ provide: LOCALE_ID, useValue: 'es' },
]
<!-- Automatically uses the registered locale -->
{{ today | date:'longDate' }} <!-- 15 de enero de 2024 -->
{{ price | currency:'EUR' }} <!-- 1.499,99 € -->
{{ ratio | percent }} <!-- 85 % -->
Key Takeaways
- Use
@angular/localizefor build-time i18n — best performance and SEO - Use
ngx-translatefor runtime language switching without reloading - Use ICU expressions for plurals and gender-based text
- Use CSS logical properties (
margin-inline-start) for RTL support - Register locale data for proper date/number/currency formatting
- Store user's language preference in localStorage or user profile
- Autodetect browser language on first visit:
navigator.language