Implémentez un système i18n robuste dans Angular : extraction des messages, traduction, gestion des dates et devises, et workflow d'équipe pour le.
i18n natif vs @ngx-translate — quel choix ?
| Critère | i18n natif Angular | @ngx-translate |
|---|---|---|
| Bundle size | Optimal (traductions inlinées) | +16kb (lib) + JSON chargés runtime |
| Changement de langue | Rechargement de page | Instantané (hot swap) |
| Performances | (compile time) | (runtime parsing) |
| Flexibilité | Build séparé par locale | Un seul build, N langues |
| ICU expressions | Natif (pluriel, genre, select) | Manuel dans les JSON |
| Intégration CI | ng build --localize | Pas de build supplémentaire |
| Cas idéal | Sites multilingues statiques, SEO fort | SaaS, dashboards, langue user-defined |
Configuration i18n natif Angular
Angular intègre un système i18n natif basé sur les fichiers XLIFF 2.0. Il produit un bundle distinct par locale au build — les traductions sont inlinées, zéro overhead runtime.
# angular.json — configuration multi-locale
{
"projects": {
"mon-app": {
"i18n": {
"sourceLocale": "fr",
"locales": {
"en": {
"translation": "src/locale/messages.en.xlf",
"baseHref": "/en/"
},
"es": {
"translation": "src/locale/messages.es.xlf",
"baseHref": "/es/"
},
"ar": {
"translation": "src/locale/messages.ar.xlf",
"baseHref": "/ar/"
}
}
},
"architect": {
"build": {
"options": {
"localize": true // ou ["en", "es"] pour un sous-ensemble
}
},
"serve": {
"configurations": {
"en": { "browserTarget": "mon-app:build:en" },
"fr": { "browserTarget": "mon-app:build:fr" }
}
}
}
}
}
}
Enregistrer les données de locale dans app.config.ts :
// app.config.ts — locale et pipes de formatage
import { ApplicationConfig, LOCALE_ID } from '@angular/core';
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import localeEn from '@angular/common/locales/en';
import localeAr from '@angular/common/locales/ar';
registerLocaleData(localeFr, 'fr');
registerLocaleData(localeEn, 'en');
registerLocaleData(localeAr, 'ar');
export const appConfig: ApplicationConfig = {
providers: [
// Angular injecte automatiquement LOCALE_ID depuis le build --localize
// Pour le serveur dev : forcer la locale explicitement
{ provide: LOCALE_ID, useValue: 'fr' },
]
};
Marquage des messages dans les templates
Angular utilise l'attribut i18n pour marquer les textes dans les templates HTML, et la fonction $localize tagged template pour le code TypeScript.
<!-- Texte simple — ID explicite avec @@ -->
<h1 i18n="Titre de la page d'accueil@@page.home.title">
Bienvenue sur notre application
</h1>
<!-- Texte avec interpolations -->
<p i18n="@@user.greeting">
Bonjour {{ userName }}, vous avez {{ count }} messages.
</p>
<!-- Attribut HTML traduit -->
<input
i18n-placeholder="@@search.placeholder"
placeholder="Rechercher..."
i18n-aria-label="@@search.aria"
aria-label="Champ de recherche"
>
<!-- Image avec alt traduit -->
<img src="logo.svg" i18n-alt="@@logo.alt" alt="Logo de l'application">
$localize dans le code TypeScript
// Textes dans les classes TypeScript — $localize tagged template
import { Component, inject } from '@angular/core';
@Component({ ... })
export class UserFormComponent {
// Textes statiques traduits à la compilation
readonly titre = $localize`:Titre formulaire@@user.form.title:Profil utilisateur`;
readonly erreurRequis = $localize`:@@validation.required:Ce champ est obligatoire`;
readonly erreurEmail = $localize`:@@validation.email:Adresse email invalide`;
// Texte dynamique — interpolation dans $localize
getWelcome(name: string): string {
return $localize`:@@user.welcome:Bienvenue ${name}:name: !`;
}
}
// Validateur avec message i18n
import { AbstractControl, ValidationErrors } from '@angular/forms';
function emailValidator(control: AbstractControl): ValidationErrors | null {
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(control.value);
return isValid ? null : {
email: $localize`:@@validation.email.format:Format d'email invalide`
};
}
Convention de nommage des IDs
Utilise des IDs explicites (@@feature.element.context) — les IDs auto-générés changent si le texte source est modifié, ce qui casse la synchronisation avec les traductions existantes.
@@auth.login.title,@@auth.logout.confirm@@dashboard.stats.total,@@dashboard.error.load@@form.validation.required,@@form.submit.label
ICU expressions : pluriel, genre, select
Les expressions ICU (International Components for Unicode) gèrent les cas complexes de pluriel et de genre directement dans les templates, sans logique TypeScript.
<!-- Pluriel — adapte le texte selon la quantité -->
<span i18n="@@inbox.count">
{count, plural,
=0 { Aucun message }
=1 { 1 message non lu }
other { {{ count }} messages non lus }
}
</span>
<!-- Select — adapte selon une valeur discrète -->
<p i18n="@@user.role.label">
Votre rôle : {role, select,
admin { Administrateur }
editor { Éditeur de contenu }
viewer { Lecteur seul }
other { Utilisateur }
}
</p>
<!-- Genre grammatical (fr/es/de) -->
<p i18n="@@product.added">
{gender, select,
male { Le produit a été ajouté }
female { La catégorie a été ajoutée }
other { L'élément a été ajouté }
}
</p>
<!-- ICU imbriqué : pluriel + select -->
<p i18n="@@cart.summary">
{itemCount, plural,
=0 { Votre panier est vide }
=1 { {item, select,
product { 1 produit dans votre panier }
service { 1 service dans votre panier }
other { 1 article dans votre panier }
}
}
other { {{ itemCount }} articles dans votre panier }
}
</p>
'Vous avez ' + count + ' messages', toujours utiliser les ICU expressions. La concaténation ne fonctionne pas correctement pour les langues avec des ordres de mots différents (japonais, arabe).
Extraction et workflow CI/CD
La commande ng extract-i18n scanne tous les templates et $localize pour générer le fichier source des messages.
# Extraire en XLIFF 2.0 (recommandé — meilleur support des ICU)
ng extract-i18n \
--format xliff2 \
--out-file src/locale/messages.xlf \
--output-path src/locale/
# Structure du XLIFF 2.0 généré
# <xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0">
# <file original="ng://app" id="ngi18n">
# <unit id="page.home.title">
# <notes>
# <note category="location">src/app/home/home.component.html:5</note>
# </notes>
# <segment>
# <source>Bienvenue sur notre application</source>
# <target></target> <-- Rempli par le traducteur
# </segment>
# </unit>
# </file>
# </xliff>
Automatisation CI/CD
# .github/workflows/i18n-check.yml — Vérifier les traductions manquantes
name: i18n Check
on: [push, pull_request]
jobs:
check-translations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
# Extraire les messages depuis le code
- run: ng extract-i18n --format xliff2 --out-file /tmp/messages.xlf
# Comparer avec le fichier versionné — détecter les nouveaux messages
- name: Detect untranslated messages
run: |
diff src/locale/messages.xlf /tmp/messages.xlf || \
echo "::warning::Nouveaux messages i18n détectés — pensez à mettre à jour les traductions"
# Build multi-locale
- run: ng build --localize --configuration=production
Outils de gestion des traductions :
- Lokalise — Plateforme collaborative, import/export XLIFF automatisé
- Phrase — CI/CD intégré, détection des messages manquants
- Crowdin — Open source friendly, GitHub integration native
- POEditor — Simple et économique pour les petits projets
Locale data : dates, nombres, devises
Les pipes Angular (date, number, currency, percent) utilisent les données de locale CLDR pour formater selon les conventions régionales.
<!-- Date : formats localisés -->
<p>{{ today | date:'fullDate' }}</p>
<!-- fr: lundi 13 avril 2026 | en: Monday, April 13, 2026 | ar: الاثنين، 13 أبريل 2026 -->
<p>{{ today | date:'shortDate' }}</p>
<!-- fr: 13/04/2026 | en: 4/13/26 | de: 13.04.26 -->
<!-- Nombre : séparateurs locaux -->
<p>{{ 1234567.89 | number:'1.2-2' }}</p>
<!-- fr: 1 234 567,89 | en: 1,234,567.89 | de: 1.234.567,89 -->
<!-- Devise -->
<p>{{ 29.99 | currency:'EUR':'symbol':'1.2-2' }}</p>
<!-- fr: 29,99 € | en: €29.99 | de: 29,99 € -->
<p>{{ 29.99 | currency:'USD':'symbol-narrow' }}</p>
<!-- $29.99 (symbole court) -->
<!-- Pourcentage -->
<p>{{ 0.875 | percent:'1.0-1' }}</p>
<!-- fr: 87,5 % | en: 87.5% -->
LOCALE_ID dans le code TypeScript
// Lire la locale active dans les services
import { Injectable, inject, LOCALE_ID } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class FormatterService {
private locale = inject(LOCALE_ID);
// Formatage manuel avec Intl (hors pipes)
formatCurrency(amount: number, currency: string): string {
return new Intl.NumberFormat(this.locale, {
style: 'currency',
currency,
minimumFractionDigits: 2
}).format(amount);
}
formatRelativeTime(date: Date): string {
const rtf = new Intl.RelativeTimeFormat(this.locale, { numeric: 'auto' });
const diff = (date.getTime() - Date.now()) / 1000;
if (Math.abs(diff) < 60) return rtf.format(Math.round(diff), 'second');
if (Math.abs(diff) < 3600) return rtf.format(Math.round(diff / 60), 'minute');
return rtf.format(Math.round(diff / 3600), 'hour');
}
}
@ngx-translate : changement dynamique de langue
Pour les apps SaaS ou les dashboards où l'utilisateur doit pouvoir changer de langue sans rechargement, @ngx-translate/core est la solution standard.
# Installation
npm install @ngx-translate/core @ngx-translate/http-loader
// app.config.ts — Configuration @ngx-translate avec HTTP loader
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { HttpClient } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
// Charger les fichiers assets/i18n/fr.json, assets/i18n/en.json...
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
importProvidersFrom(
TranslateModule.forRoot({
defaultLanguage: 'fr',
loader: {
provide: TranslateLoader,
useFactory: createTranslateLoader,
deps: [HttpClient]
}
})
)
]
};
Structure des fichiers de traduction JSON
// assets/i18n/fr.json — Traductions françaises
{
"COMMON": {
"SAVE": "Enregistrer",
"CANCEL": "Annuler",
"DELETE": "Supprimer",
"LOADING": "Chargement..."
},
"AUTH": {
"LOGIN": {
"TITLE": "Connexion",
"EMAIL": "Adresse email",
"PASSWORD": "Mot de passe",
"SUBMIT": "Se connecter",
"ERROR": "Identifiants incorrects"
}
},
"USER": {
"WELCOME": "Bienvenue {{ name }} !",
"MESSAGES_COUNT": {
"ZERO": "Aucun message",
"ONE": "1 message non lu",
"OTHER": "{{ count }} messages non lus"
}
}
}
// Utilisation dans les composants
import { Component, inject, OnInit } from '@angular/core';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
@Component({
standalone: true,
imports: [TranslateModule],
template: `
<!-- Pipe translate -->
<h1>{{ 'AUTH.LOGIN.TITLE' | translate }}</h1>
<!-- Avec paramètre dynamique -->
<p>{{ 'USER.WELCOME' | translate:{ name: user.name } }}</p>
<!-- Directive translate -->
<button [translate]="'COMMON.SAVE'"></button>
<!-- Sélecteur de langue -->
<select (change)="onLangChange($event)">
<option value="fr">Français</option>
<option value="en">English</option>
<option value="es">Español</option>
</select>
`
})
export class LoginComponent implements OnInit {
private translate = inject(TranslateService);
ngOnInit(): void {
// Détecter la langue du navigateur
const browserLang = this.translate.getBrowserLang();
this.translate.use(browserLang?.match(/fr|en|es/) ? browserLang : 'fr');
}
onLangChange(event: Event): void {
const lang = (event.target as HTMLSelectElement).value;
// Changement instantané sans rechargement
this.translate.use(lang);
localStorage.setItem('lang', lang); // persister
}
// Dans les classes — pas de pipe possible
getErrorMessage(): void {
this.translate.get('AUTH.LOGIN.ERROR').subscribe(msg => {
console.error(msg);
});
}
}
Support RTL et langues bidirectionnelles
Les langues RTL (arabe, hébreu, persan) nécessitent d'inverser le sens de lecture. Angular Material et Bootstrap ont un support RTL natif via l'attribut dir.
// Appliquer dir="rtl" dynamiquement selon la locale
import { Component, inject, OnInit, Renderer2 } from '@angular/core';
import { LOCALE_ID } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
const RTL_LOCALES = ['ar', 'he', 'fa', 'ur'];
@Component({...})
export class AppComponent implements OnInit {
private locale = inject(LOCALE_ID);
private renderer = inject(Renderer2);
private translate = inject(TranslateService);
ngOnInit(): void {
this.applyDirection(this.translate.currentLang ?? this.locale);
// Réagir aux changements de langue
this.translate.onLangChange.subscribe(({ lang }) => {
this.applyDirection(lang);
});
}
private applyDirection(lang: string): void {
const dir = RTL_LOCALES.includes(lang) ? 'rtl' : 'ltr';
this.renderer.setAttribute(document.documentElement, 'dir', dir);
this.renderer.setAttribute(document.documentElement, 'lang', lang);
}
}
/* CSS — utiliser les propriétés logiques pour RTL automatique */
.card {
/* Propriétés physiques (NE PAS utiliser en app multilingue) */
/* margin-left: 1rem; ← inversé en RTL */
/* text-align: left; ← inversé en RTL */
/* Propriétés logiques (RTL aware) */
margin-inline-start: 1rem; /* left en LTR, right en RTL */
text-align: start; /* left en LTR, right en RTL */
padding-inline: 1rem 2rem; /* left/right selon la direction */
border-inline-start: 3px solid var(--primary); /* left/right selon dir */
}
/* Bootstrap 5 RTL : charger le CSS RTL */
/* <link rel="stylesheet" href="bootstrap.rtl.min.css"> quand dir="rtl" */
Checklist i18n complète
- IDs explicites
@@feature.elementdès le premier commit — jamais d'IDs auto-générés registerLocaleDatapour toutes les locales cibles dansapp.config.ts- ICU expressions pour tous les pluriels et sélects — jamais de concaténation
ng extract-i18nautomatisé dans le pipeline CI avant chaque release- Tests UI dans au moins 2 locales avec des textes longs (allemand +30%, russe +15%) pour détecter les débordements CSS
- Propriétés CSS logiques (
margin-inline-start,text-align: start) pour le support RTL automatique - Persistance de la langue choisie dans
localStorageet attributlangsur<html> baseHrefpar locale (/en/,/es/) pour le SEO multilingue — pas de query string?lang=en- Utiliser
Intl.RelativeTimeFormat,Intl.ListFormatpour les formatages avancés non couverts par les pipes