Internationalization Overview
Estimated Time: 2 hours | Difficulty: Intermediate | Prerequisites: Components, Routing
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ Angular i18n Approaches │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Built-in i18n (AOT) Runtime i18n (ngx-translate/Transloco)│
│ ───────────────────── ──────────────────────────────────────│
│ │
│ ✓ Best performance ✓ Switch languages at runtime │
│ ✓ AOT compilation ✓ Single bundle for all languages │
│ ✓ ICU message format ✓ Lazy load translations │
│ ✗ Separate build per locale ✓ Easier to manage │
│ ✗ No runtime switching ✓ API/CMS integration │
│ ✗ Larger bundle size │
│ │
│ Best for: Marketing sites, Best for: Apps, dashboards, │
│ SEO-critical pages admin panels, SPAs │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Built-in Angular i18n
Setup
Copy
# Add localize package
ng add @angular/localize
# Generate translation file
ng extract-i18n --output-path src/locale
Marking Text for Translation
Copy
<!-- Using i18n attribute -->
<h1 i18n>Welcome to our application</h1>
<!-- With description and meaning -->
<h1 i18n="site header|Main welcome message">Welcome to our application</h1>
<!-- With custom ID for stable translations -->
<h1 i18n="@@welcomeHeader">Welcome to our application</h1>
<!-- Translate attributes -->
<img [src]="logo" i18n-alt alt="Company logo" i18n-title title="Click to go home" />
<!-- Plural expressions -->
<span i18n>{count, plural,
=0 {No items}
=1 {One item}
other {{{count}} items}
}</span>
<!-- Select expressions -->
<span i18n>{gender, select,
male {He is online}
female {She is online}
other {They are online}
}</span>
<!-- Nested ICU expressions -->
<span i18n>{gender, select,
male {He has {count, plural, =0 {no messages} =1 {one message} other {{{count}} messages}}}
female {She has {count, plural, =0 {no messages} =1 {one message} other {{{count}} messages}}}
other {They have {count, plural, =0 {no messages} =1 {one message} other {{{count}} messages}}}
}</span>
Translation File (messages.xlf)
Copy
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="welcomeHeader" datatype="html">
<source>Welcome to our application</source>
<target>Bienvenido a nuestra aplicación</target>
<note priority="1" from="description">Main welcome message</note>
<note priority="1" from="meaning">site header</note>
</trans-unit>
<trans-unit id="itemCount" datatype="html">
<source>{VAR_PLURAL, plural, =0 {No items} =1 {One item} other {<x id="INTERPOLATION"/> items}}</source>
<target>{VAR_PLURAL, plural, =0 {Sin elementos} =1 {Un elemento} other {<x id="INTERPOLATION"/> elementos}}</target>
</trans-unit>
</body>
</file>
</xliff>
Build Configuration
Copy
// angular.json
{
"projects": {
"my-app": {
"i18n": {
"sourceLocale": "en-US",
"locales": {
"es": {
"translation": "src/locale/messages.es.xlf",
"baseHref": "/es/"
},
"fr": {
"translation": "src/locale/messages.fr.xlf",
"baseHref": "/fr/"
},
"de": "src/locale/messages.de.xlf"
}
},
"architect": {
"build": {
"options": {
"localize": true
},
"configurations": {
"es": {
"localize": ["es"]
},
"fr": {
"localize": ["fr"]
}
}
},
"serve": {
"configurations": {
"es": {
"buildTarget": "my-app:build:development,es"
}
}
}
}
}
}
}
Copy
# Build all locales
ng build --localize
# Build specific locale
ng build --configuration=es
# Serve specific locale
ng serve --configuration=es
Transloco (Recommended Runtime Solution)
Setup
Copy
npm install @jsverse/transloco
ng add @jsverse/transloco
Configuration
Copy
// transloco.config.ts
import { TranslocoGlobalConfig } from '@jsverse/transloco-utils';
const config: TranslocoGlobalConfig = {
rootTranslationsPath: 'src/assets/i18n/',
langs: ['en', 'es', 'fr', 'de'],
defaultLang: 'en'
};
export default config;
// app.config.ts
import { provideTransloco, TranslocoModule } from '@jsverse/transloco';
import { TranslocoHttpLoader } from './transloco-loader';
export const appConfig: ApplicationConfig = {
providers: [
provideTransloco({
config: {
availableLangs: ['en', 'es', 'fr', 'de'],
defaultLang: 'en',
fallbackLang: 'en',
reRenderOnLangChange: true,
prodMode: !isDevMode(),
missingHandler: {
useFallbackTranslation: true,
logMissingKey: true
}
},
loader: TranslocoHttpLoader
})
]
};
// transloco-loader.ts
@Injectable({ providedIn: 'root' })
export class TranslocoHttpLoader implements TranslocoLoader {
private http = inject(HttpClient);
getTranslation(lang: string) {
return this.http.get<Translation>(`/assets/i18n/${lang}.json`);
}
}
Translation Files
Copy
// assets/i18n/en.json
{
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"loading": "Loading...",
"error": "An error occurred"
},
"auth": {
"login": "Log In",
"logout": "Log Out",
"register": "Register",
"email": "Email",
"password": "Password",
"forgotPassword": "Forgot Password?",
"welcomeBack": "Welcome back, {{name}}!"
},
"products": {
"title": "Products",
"addToCart": "Add to Cart",
"outOfStock": "Out of Stock",
"price": "Price: {{price, currency}}",
"itemCount": {
"zero": "No items",
"one": "1 item",
"other": "{{count}} items"
}
},
"validation": {
"required": "This field is required",
"email": "Please enter a valid email",
"minLength": "Minimum {{min}} characters required",
"maxLength": "Maximum {{max}} characters allowed"
}
}
// assets/i18n/es.json
{
"common": {
"save": "Guardar",
"cancel": "Cancelar",
"delete": "Eliminar",
"edit": "Editar",
"loading": "Cargando...",
"error": "Ocurrió un error"
},
"auth": {
"login": "Iniciar Sesión",
"logout": "Cerrar Sesión",
"register": "Registrarse",
"email": "Correo Electrónico",
"password": "Contraseña",
"forgotPassword": "¿Olvidaste tu contraseña?",
"welcomeBack": "¡Bienvenido de nuevo, {{name}}!"
},
"products": {
"title": "Productos",
"addToCart": "Añadir al Carrito",
"outOfStock": "Agotado",
"price": "Precio: {{price, currency}}",
"itemCount": {
"zero": "Sin artículos",
"one": "1 artículo",
"other": "{{count}} artículos"
}
}
}
Using Transloco in Components
Copy
// product-list.component.ts
import { TranslocoModule, TranslocoService } from '@jsverse/transloco';
@Component({
selector: 'app-product-list',
standalone: true,
imports: [TranslocoModule],
template: `
<!-- Structural directive approach (recommended) -->
<ng-container *transloco="let t; read: 'products'">
<h1>{{ t('title') }}</h1>
@for (product of products(); track product.id) {
<div class="product-card">
<h3>{{ product.name }}</h3>
<p>{{ t('price', { price: product.price }) }}</p>
@if (product.inStock) {
<button>{{ t('addToCart') }}</button>
} @else {
<span class="out-of-stock">{{ t('outOfStock') }}</span>
}
</div>
}
<p>{{ t('itemCount', { count: products().length }) }}</p>
</ng-container>
<!-- Pipe approach -->
<button>{{ 'common.save' | transloco }}</button>
<!-- With params -->
<p>{{ 'auth.welcomeBack' | transloco:{ name: userName() } }}</p>
`
})
export class ProductListComponent {
private transloco = inject(TranslocoService);
products = input.required<Product[]>();
userName = signal('John');
// Programmatic translation
showSuccessMessage() {
const message = this.transloco.translate('common.saved');
// Or with params
const greeting = this.transloco.translate('auth.welcomeBack', {
name: this.userName()
});
}
// Observe translations (reactive)
message$ = this.transloco.selectTranslate('products.title');
}
Language Switcher
Copy
// language-switcher.component.ts
@Component({
selector: 'app-language-switcher',
standalone: true,
imports: [TranslocoModule],
template: `
<div class="language-switcher">
@for (lang of availableLangs; track lang.id) {
<button
[class.active]="lang.id === activeLang()"
(click)="switchLang(lang.id)"
>
{{ lang.label }}
</button>
}
</div>
`
})
export class LanguageSwitcherComponent {
private transloco = inject(TranslocoService);
availableLangs = [
{ id: 'en', label: 'English' },
{ id: 'es', label: 'Español' },
{ id: 'fr', label: 'Français' },
{ id: 'de', label: 'Deutsch' }
];
activeLang = signal(this.transloco.getActiveLang());
constructor() {
// Subscribe to language changes
this.transloco.langChanges$.subscribe(lang => {
this.activeLang.set(lang);
// Persist preference
localStorage.setItem('preferredLang', lang);
// Update document lang attribute
document.documentElement.lang = lang;
});
}
switchLang(lang: string) {
this.transloco.setActiveLang(lang);
}
}
Scoped Translations (Lazy Loading)
Copy
// features/admin/admin.routes.ts
export const ADMIN_ROUTES: Routes = [
{
path: '',
loadComponent: () => import('./admin.component'),
providers: [
provideTranslocoScope({
scope: 'admin',
loader: {
en: () => import('./i18n/en.json'),
es: () => import('./i18n/es.json')
}
})
]
}
];
// features/admin/admin.component.ts
@Component({
template: `
<ng-container *transloco="let t; read: 'admin'">
<h1>{{ t('dashboard.title') }}</h1>
<p>{{ t('dashboard.welcome') }}</p>
</ng-container>
`
})
export class AdminComponent {}
Locale-Aware Formatting
Number, Date, and Currency Formatting
Copy
// locale.service.ts
@Injectable({ providedIn: 'root' })
export class LocaleService {
private transloco = inject(TranslocoService);
private localeMap: Record<string, string> = {
'en': 'en-US',
'es': 'es-ES',
'fr': 'fr-FR',
'de': 'de-DE'
};
get currentLocale(): string {
return this.localeMap[this.transloco.getActiveLang()] ?? 'en-US';
}
formatNumber(value: number, options?: Intl.NumberFormatOptions): string {
return new Intl.NumberFormat(this.currentLocale, options).format(value);
}
formatCurrency(value: number, currency = 'USD'): string {
return new Intl.NumberFormat(this.currentLocale, {
style: 'currency',
currency
}).format(value);
}
formatDate(date: Date, options?: Intl.DateTimeFormatOptions): string {
return new Intl.DateTimeFormat(this.currentLocale, options).format(date);
}
formatRelativeTime(value: number, unit: Intl.RelativeTimeFormatUnit): string {
return new Intl.RelativeTimeFormat(this.currentLocale, {
numeric: 'auto'
}).format(value, unit);
}
}
// Custom pipe for locale-aware formatting
@Pipe({
name: 'localeCurrency',
standalone: true,
pure: false
})
export class LocaleCurrencyPipe implements PipeTransform {
private locale = inject(LocaleService);
transform(value: number, currency = 'USD'): string {
return this.locale.formatCurrency(value, currency);
}
}
// Usage
@Component({
imports: [LocaleCurrencyPipe],
template: `
<span>{{ product.price | localeCurrency:'EUR' }}</span>
`
})
export class PriceComponent {}
RTL (Right-to-Left) Support
Copy
// rtl.service.ts
@Injectable({ providedIn: 'root' })
export class RtlService {
private transloco = inject(TranslocoService);
private rtlLanguages = ['ar', 'he', 'fa', 'ur'];
isRtl = signal(false);
constructor() {
this.transloco.langChanges$.subscribe(lang => {
const isRtl = this.rtlLanguages.includes(lang);
this.isRtl.set(isRtl);
document.documentElement.dir = isRtl ? 'rtl' : 'ltr';
document.documentElement.lang = lang;
});
}
}
// app.component.ts
@Component({
template: `
<div [class.rtl]="rtl.isRtl()" [dir]="rtl.isRtl() ? 'rtl' : 'ltr'">
<router-outlet />
</div>
`,
styles: [`
:host {
--spacing-start: 1rem;
--spacing-end: 0;
}
:host-context([dir="rtl"]) {
--spacing-start: 0;
--spacing-end: 1rem;
}
.card {
margin-inline-start: var(--spacing-start);
margin-inline-end: var(--spacing-end);
}
`]
})
export class AppComponent {
rtl = inject(RtlService);
}
Best Practices
Use Translation Keys
Use meaningful, hierarchical keys like
products.details.priceAvoid Concatenation
Never concatenate translated strings - use placeholders instead
Handle Plurals Properly
Use ICU message format for pluralization
Lazy Load Translations
Load feature-specific translations on demand
Copy
// ❌ Bad - String concatenation
const message = translate('hello') + ' ' + userName + '!';
// ✅ Good - Use placeholders
// Translation: "Hello, {{name}}!"
const message = translate('greeting', { name: userName });
// ❌ Bad - Manual plural handling
const items = count === 1 ? 'item' : 'items';
// ✅ Good - ICU plural format
// Translation: "{count, plural, =1 {# item} other {# items}}"
const items = translate('itemCount', { count });
Next: Accessibility (a11y)
Build accessible Angular applications for all users