Angular intègre l’internationalisation grâce au paquet @angular/localize. Vous marquez les textes dans les templates avec l’attribut i18n, et dans TypeScript avec $localize. La CLI les extrait dans un fichier de traduction (XLIFF, JSON…), et au moment du build elle produit une variante de l’application par langue, avec les textes déjà remplacés. Le résultat est aussi rapide que l’application non traduite : aucune recherche de clé à l’exécution.
Au-delà des textes, l’i18n couvre aussi les pluriels et le formatage. « 2 files », mais « 20 files » en roumain donne « 20 de fișiere » ; 1,499.50 en anglais et 1.499,50 en roumain ; la date avec le nom du jour dans la langue de l’utilisateur. C’est là qu’interviennent les expressions ICU et les données de locale.
Marquer les textes
ng add @angular/localize installe le paquet et charge @angular/localize/init comme polyfill. L’attribut i18n marque un élément ; sa valeur optionnelle a la forme meaning|description@@id. Le sens (meaning) et la description aident le traducteur. Un id personnalisé (@@cart.title) évite de perdre la traduction quand vous modifiez le texte source. Pour les attributs, vous utilisez i18n-alt, i18n-title, etc. Dans le code, $localize est un tagged template avec la même syntaxe, et les interpolations reçoivent des noms (${count}:count:).
ng add @angular/localize
# ajoute @angular/localize/init aux polyfills et les types à tsconfig — sans cela, i18n et $localize ne démarrent pasICU : plural et select
Les expressions ICU choisissent le texte selon une valeur. plural utilise les catégories CLDR de la langue (LOCALE_ID), plus les correspondances exactes (=0). select choisit selon une chaîne. Le roumain a trois catégories :
onepour 1 ;fewpour 0, 2–19 et 101–119 ;otherpour le reste (20, 100, 120…), où apparaît « de » : « 20 de fișiere » (20 fichiers).
La règle se trouve dans les données de locale d’Angular (@angular/common/locales/ro). Les expressions ICU fonctionnent dans n’importe quel template, même sans traductions.
Vous avez téléversé 2 fichiers en tant que éditeur.
Catégorie CLDR pour 2 en roumain :few
Extraction et fichiers de traduction
ng extract-i18n rassemble tous les messages marqués. Par défaut, il écrit messages.xlf (XLIFF 1.2) ; avec --format, vous pouvez aussi choisir XLIFF 2, XMB, JSON ou ARB. Vous copiez le fichier pour chaque langue (messages.en.xlf), les traducteurs remplissent les éléments <target>, et les ids relient la source à sa traduction.
ng extract-i18n --output-path src/locale # par défaut : messages.xlf (XLIFF 1.2)
ng extract-i18n --format=xlf2 --output-path src/locale
ng extract-i18n --format=json --output-path src/localeUn build par langue ou des traductions à l’exécution
L’approche standard se fait au moment du build : dans angular.json, vous déclarez sourceLocale et locales, et avec "localize": true la CLI produit un dossier par langue (/ro/, /en/…). Le serveur choisit le dossier selon l’URL ou la préférence de l’utilisateur. Changer de langue revient à naviguer vers un autre dossier, donc à recharger la page.
L’alternative se fait à l’exécution : un seul build, et dans main.ts vous chargez les traductions avec loadTranslations() avant le bootstrap. Là aussi, changer de langue implique un rechargement, car les textes marqués avec i18n dans les templates sont évalués à la création des composants. Si vous devez changer de langue sans rechargement, utilisez une bibliothèque de traduction à l’exécution.
{
"projects": {
"shop": {
"i18n": {
"sourceLocale": "ro",
"locales": {
"en": "src/locale/messages.en.xlf",
"de": "src/locale/messages.de.xlf"
}
},
"architect": {
"build": {
"options": { "localize": true }
}
}
}
}
}Votre panier
Vous avez 3 articles dans votre panier.
Les messages sont des appels $localize avec des ids (@@cart.title), traduits avec loadTranslations(). Cette démo a ses propres boutons et ne suit pas la langue du site.
La démo réévalue les appels $localize dans le code. Les textes marqués avec i18n dans les templates ne changent pas de cette façon, c’est pourquoi les vraies applications rechargent la page quand la langue change.
Formatage selon la locale
Les pipes date, number, currency et percent (et les fonctions formatDate, formatNumber…) utilisent LOCALE_ID. Pour toute langue autre que en-US, vous devez enregistrer les données avec registerLocaleData. Ce site le fait pour ro, avec RON comme devise par défaut (DEFAULT_CURRENCY_CODE). Toutes les pipes acceptent aussi un paramètre locale explicite.
import { registerLocaleData } from '@angular/common';
import localeRo from '@angular/common/locales/ro';
import { DEFAULT_CURRENCY_CODE, LOCALE_ID } from '@angular/core';
registerLocaleData(localeRo); // obligatoire pour toute locale autre que en-US
export const appConfig: ApplicationConfig = {
providers: [
{ provide: LOCALE_ID, useValue: 'ro' }, // utilisé par date, number, currency, percent, ICU
{ provide: DEFAULT_CURRENCY_CODE, useValue: 'RON' },
],
};| Locale | Date | Nombre | Devise (EUR) | Pourcentage |
|---|---|---|---|---|
ro | vineri, 9 octombrie 2026 | 1.234.567,89 | 1.499,50 € | 18,8 % |
en | Friday, October 9, 2026 | 1,234,567.89 | €1,499.50 | 18.8% |
de | Freitag, 9. Oktober 2026 | 1.234.567,89 | 1.499,50 € | 18,8 % |
ja | 2026年10月9日金曜日 | 1,234,567.89 | €1,499.50 | 18.8% |
Comment ce site change de langue
Le site n’utilise pas l’i18n au moment du build pour ses propres textes. La langue de l’interface est un signal : le service LangStore (dans core/i18n), avec lang() et les valeurs 'en' ou 'ro'. Le bouton de la barre du haut appelle toggle(), qui enregistre le choix dans localStorage sous la clé atlas-lang. Au démarrage, la clé est lue ; si elle est absente, la langue est l’anglais. Un effect maintient l’attribut lang de l’élément <html> synchronisé.
Le changement ne recharge pas la page : le texte est réévalué quand le signal change. Il existe deux types de texte bilingue :
- L’interface (barre du haut, menus, recherche) a des clés dans
messages.ts, typées avecMessageKey. La pipetles traduit :{{ 'topic.minRead' | t: { n: 4 } }}. TypeScript exige que chaque clé existe dans les deux langues. - Les textes de ce site vivent dans des dictionnaires, un fichier JSON par langue (
i18n/en/…,i18n/ro/…,i18n/de/…). Chaque texte a une clé et se lit avec la pipet:{{ 'signals:title' | t }}. La pipe est impure, elle est donc réévaluée quand la langue change.
La prose d’une page est écrite dans son template dans les deux langues (@if (lang.lang() === 'en')), et son code est dans deux fichiers .snippets. La démo $localize ci-dessus est un mécanisme différent avec ses propres boutons : elle ne suit pas la langue du site.
Quand OUI, quand NON
- Applications publiques, SEO, performance : chaque langue est un build optimisé, sans clés à l’exécution.
- Langues connues au moment du build et rarement modifiées (la préférence dans le compte, l’URL
/en/). - Équipes avec des traducteurs professionnels qui travaillent en XLIFF.
- Fonctionne bien avec le SSR / prerender : une variante par langue (voir SSR).
- Changer de langue sans rechargement est une exigence produit.
- Les traductions viennent d’un CMS et changent sans déploiement.
- Des dizaines de langues, et vous ne voulez pas des dizaines de builds.
- Petites applications internes dans une seule langue : juste
LOCALE_ID+ pipes + ICU, sans extraction.
Pièges fréquents
- Concaténer des textes (
'You have ' + n + ' items') : l’ordre des mots diffère selon les langues. Utilisez un seul message avec un placeholder, ou une expression ICU. - Pluriels avec
n === 1 ? … : …: en roumain, vous obtenez « 20 fișiere » au lieu de « 20 de fișiere ». Utilisez leplurald’ICU. - Messages sans id personnalisé : l’id généré change à la moindre correction du texte, et la traduction est perdue.
- Formats écrits en dur (
'dd/MM/yyyy',toFixed(2) + ' lei') : ils contournent la locale. Utilisez les pipes. - Locale non enregistrée :
LOCALE_IDdéfini surrosansregisterLocaleDatalève une erreur à la première pipe.
Aide-mémoire
MARQUAGE i18n="sens|description@@id" · i18n-title="@@id" · $localize`:@@id:texte ${x}:nom:`
ICU {n, plural, =0 {…} one {…} few {…} other {…}} · {x, select, a {…} other {…}}
Roumain : one = 1 · few = 0, 2–19, 101–119 · other = 20+, 120+ (« 20 de fișiere » = 20 fichiers)
EXTRACTION ng extract-i18n [--format=xlf|xlf2|xmb|json|arb] [--output-path src/locale]
BUILD angular.json: i18n.sourceLocale + i18n.locales · "localize": true → un bundle par langue
EXÉCUTION loadTranslations() avant le bootstrap · clearTranslations() · nouvelle langue = rechargement
FORMATAGE registerLocaleData + LOCALE_ID + DEFAULT_CURRENCY_CODE · pipes avec le paramètre locale
ALTERNATIVE bibliothèques de traduction à l’exécution (p. ex. Transloco, ngx-translate) — changement de langue sans rechargement