6. Internationalisation
6.1. Exemple 21 : une application en deux langues
L'application [RdvMedecins] existe en français et en anglais. Dans le cours [NestJS], c'est le serveur qui traduisait les pages, avant de les envoyer. Désormais, les pages sont fabriquées dans le navigateur : c'est donc le client qui les traduit, et changer de langue ne demande plus rien au serveur.
[Angular] a son propre système de traduction, [@angular/localize], qui fabrique une application compilée par langue : on ne change alors de langue qu'en chargeant une autre application. Nous utiliserons plutôt la bibliothèque [ngx-translate], la plus utilisée avec [Angular], qui traduit pendant l'exécution : un clic suffit à retraduire toute la page.
| 21-i18n/src/app/
├── i18n/
│ ├── fr.json le dictionnaire français
│ ├── en.json le dictionnaire anglais
│ ├── i18n.ts la configuration de ngx-translate
│ └── pluriel.pipe.ts le choix du pluriel
├── app.ts
└── app.html
|
La bibliothèque est déclarée dans le fichier [package.json] de l'espace de travail :
| "@ngx-translate/core": "^18.0.0",
|
6.1.1. Les dictionnaires
| {
"APP": {
"TITRE": "Exemple 21 - une application en deux langues",
"LANGUE": "Langue"
},
"ACCUEIL": {
"BONJOUR": "Bonjour {{nom}} !",
"AUJOURDHUI": "Nous sommes le {{date}}.",
"PRIX": "Une consultation coûte {{prix}}.",
"RENDEZ_VOUS": {
"zero": "aucun rendez-vous",
"one": "un rendez-vous",
"other": "{{n}} rendez-vous"
},
"AJOUTER": "Ajouter un rendez-vous",
"RETIRER": "En retirer un"
},
"ERRORS": {
"LOGIN_TAKEN": "Ce login est déjà utilisé par un autre compte.",
"TOO_MANY_ATTEMPTS": "Trop de tentatives infructueuses. Réessayez dans {{minutes}} minute(s)."
},
"SERVEUR": {
"TITRE": "Traduire une réponse du serveur",
"EXPLICATION": "Le serveur RdvMedecins ne renvoie que des clés. C'est le client qui les traduit :"
}
}
|
| {
"APP": {
"TITRE": "Example 21 - a two-language application",
"LANGUE": "Language"
},
"ACCUEIL": {
"BONJOUR": "Hello {{nom}}!",
"AUJOURDHUI": "Today is {{date}}.",
"PRIX": "A consultation costs {{prix}}.",
"RENDEZ_VOUS": {
"zero": "no appointment",
"one": "one appointment",
"other": "{{n}} appointments"
},
"AJOUTER": "Add an appointment",
"RETIRER": "Remove one"
},
"ERRORS": {
"LOGIN_TAKEN": "This login is already used by another account.",
"TOO_MANY_ATTEMPTS": "Too many failed attempts. Try again in {{minutes}} minute(s)."
},
"SERVEUR": {
"TITRE": "Translating a server response",
"EXPLICATION": "The RdvMedecins server only sends keys. The client translates them:"
}
}
|
- chaque texte a une clé, formée des noms des niveaux successifs : [ACCUEIL.BONJOUR], [ERRORS.LOGIN_TAKEN]... Le code et les templates ne contiennent plus aucun texte, seulement des clés ;
- ligne 7 : les paramètres s'écrivent entre doubles accolades : [{{nom}}] ;
- lignes 10-14 : les formes du pluriel, rangées sous une même clé (voir plus loin) ;
- lignes 18-21 : les clés [ERRORS.*] sont celles que renvoie le serveur [RdvMedecins] dans ses réponses d'erreur (chapitre suivant). Le serveur ne renvoie pas de texte, mais une clé ; le client la traduit dans la langue de l'utilisateur.
6.1.2. La configuration
| // =========================================================================
// 21-i18n/src/app/i18n/i18n.ts - la configuration de ngx-translate
// -------------------------------------------------------------------------
// ngx-translate est la bibliothèque de traduction la plus utilisée avec
// Angular. On lui fournit :
// - un DICTIONNAIRE par langue (fr.json, en.json), où chaque texte a une
// CLÉ ("ACCUEIL.BONJOUR") ; les paramètres s'écrivent {{nom}} ;
// - un CHARGEUR (TranslateLoader), qui renvoie le dictionnaire d'une
// langue. Le nôtre les prend dans le code de l'application (import de
// fichiers JSON) ; un autre pourrait les télécharger ;
// - la langue de départ (celle mémorisée dans localStorage, sinon celle
// du navigateur, sinon le français) et la langue de repli.
// Dans les templates, {{ 'ACCUEIL.BONJOUR' | translate: { nom: 'Marie' } }}
// affiche le texte de la langue COURANTE ; changer de langue met toute la
// page à jour.
//
// C'est, côté client, le même principe que le cours NestJS (dictionnaires
// i18n/*.json + helper {{t}}) - mais la traduction se fait maintenant dans
// le navigateur, et changer de langue ne demande plus rien au serveur.
//
// (Angular a aussi son propre système, @angular/localize, qui fabrique UNE
// application compilée par langue : on ne peut alors changer de langue
// qu'en changeant de page. ngx-translate traduit, lui, pendant l'exécution.)
// =========================================================================
import { Provider } from '@angular/core';
import { provideTranslateLoader, provideTranslateService, TranslateLoader, TranslationObject } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
import fr from './fr.json';
import en from './en.json';
export type Langue = 'fr' | 'en';
export const LANGUES: Langue[] = ['fr', 'en'];
export const CLE_LANGUE = 'exemple21.langue';
// le chargeur des dictionnaires
class DictionnairesLoader implements TranslateLoader {
getTranslation(langue: string): Observable<TranslationObject> {
return of(langue === 'en' ? en : fr);
}
}
function langueDeDepart(): Langue {
const memorisee = localStorage.getItem(CLE_LANGUE);
if (memorisee === 'fr' || memorisee === 'en') {
return memorisee;
}
return navigator.language.startsWith('en') ? 'en' : 'fr';
}
export function provideI18n(): Provider[] {
return provideTranslateService({
loader: provideTranslateLoader(DictionnairesLoader),
fallbackLang: 'fr', // une clé absente en anglais : on prend le français
lang: langueDeDepart(),
});
}
|
- lignes 29-30 : les dictionnaires sont importés comme des modules : ils font partie du code de l'application. C'est possible grâce à l'option [resolveJsonModule] du fichier [tsconfig.json] ;
- ligne 34 : la clé sous laquelle la langue choisie est rangée dans le [localStorage] (exemple 20) ;
- lignes 37-41 : le chargeur des dictionnaires : [ngx-translate] l'appelle pour obtenir le dictionnaire d'une langue, sous forme d'observable. Le nôtre les prend dans le code ; un autre pourrait les télécharger depuis le serveur ;
- lignes 43-49 : la langue de départ : celle que l'utilisateur a choisie la dernière fois, sinon celle du navigateur, sinon le français ;
- lignes 51-57 : [provideTranslateService] rend les services de [ngx-translate] disponibles dans toute l'application. [fallbackLang] : la langue utilisée pour une clé absente du dictionnaire courant.
| // =========================================================================
// 21-i18n/src/main.ts
// -------------------------------------------------------------------------
// provideI18n() : ngx-translate (cf. app/i18n/i18n.ts).
// registerLocaleData(localeFr) : les règles françaises des pipes date,
// currency... (l'anglais est toujours disponible).
// =========================================================================
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import { bootstrapApplication } from '@angular/platform-browser';
import { App } from './app/app';
import { provideI18n } from './app/i18n/i18n';
registerLocaleData(localeFr);
bootstrapApplication(App, { providers: [provideI18n()] }).catch((erreur) => console.error(erreur));
|
- ligne 15 : les pipes [date] et [currency] (exemple 09) connaissent les règles de l'anglais ; celles du français doivent être enregistrées ;
- ligne 17 : les services de [ngx-translate] sont ajoutés à l'application.
6.1.3. Le pluriel
[ngx-translate] ne gère pas les pluriels. Un petit pipe choisit la bonne forme :
| // =========================================================================
// 21-i18n/src/app/i18n/pluriel.pipe.ts - choisir la forme du pluriel
// -------------------------------------------------------------------------
// ngx-translate ne gère pas les pluriels. Le dictionnaire en donne les
// formes sous une même clé ("RENDEZ_VOUS": { "zero", "one", "other" }) et
// ce pipe choisit la bonne, selon le nombre et la langue :
// {{ 'ACCUEIL.RENDEZ_VOUS' | pluriel: n : langue | translate: { n } }}
// Intl.PluralRules connaît les règles de chaque langue ("one" ou "other" en
// français comme en anglais, mais d'autres formes en polonais, en arabe...).
// =========================================================================
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'pluriel' })
export class PlurielPipe implements PipeTransform {
transform(cle: string, n: number, langue: string | null): string {
if (n === 0) {
return `${cle}.zero`;
}
return `${cle}.${new Intl.PluralRules(langue ?? 'fr').select(n)}`;
}
}
|
- lignes 16-21 : le pipe reçoit une clé ([ACCUEIL.RENDEZ_VOUS]), un nombre et une langue ; il rend la clé de la bonne forme ([ACCUEIL.RENDEZ_VOUS.one], par exemple) ;
- ligne 18 : zéro a sa propre forme (« aucun rendez-vous ») ;
- ligne 20 : [Intl.PluralRules], fourni par le navigateur, connaît les règles de chaque langue : [one] ou [other] en français et en anglais, mais d'autres formes en polonais ou en arabe. (Attention : en français, 1,5 est au singulier ; en anglais, au pluriel.)
6.1.4. La page traduite
| // =========================================================================
// 21-i18n/src/app/app.ts - une page traduite
// -------------------------------------------------------------------------
// - 'CLE' | translate un texte ;
// - 'CLE' | translate: { nom: ... } un texte à paramètres ;
// - date / currency avec la langue une date, un montant, au format
// de la langue courante ;
// - translate.use('en') change la langue : toute la
// page se retraduit.
// TranslateService.currentLang est un SIGNAL : la langue courante.
// =========================================================================
import { Component, effect, inject, signal } from '@angular/core';
import { CurrencyPipe, DatePipe } from '@angular/common';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { CLE_LANGUE, Langue, LANGUES } from './i18n/i18n';
import { PlurielPipe } from './i18n/pluriel.pipe';
@Component({
selector: 'app-root',
imports: [CurrencyPipe, DatePipe, TranslatePipe, PlurielPipe],
templateUrl: './app.html',
})
export class App {
private readonly translate = inject(TranslateService);
protected readonly langue = this.translate.currentLang;
protected readonly langues = LANGUES;
protected readonly nombre = signal(2);
protected readonly aujourdhui = new Date();
// ce que renverrait le serveur RdvMedecins (cf. chapitre sur la boîte noire)
protected readonly reponsesServeur = [
{ statusCode: 409, cle: 'ERRORS.LOGIN_TAKEN', params: {} },
{ statusCode: 429, cle: 'ERRORS.TOO_MANY_ATTEMPTS', params: { minutes: '15' } },
];
// translate.translate(clé) : un SIGNAL qui suit la langue courante
private readonly titre = this.translate.translate('APP.TITRE');
constructor() {
// à chaque changement de langue : on la mémorise, et on met à jour
// l'attribut lang de la page (utile aux lecteurs d'écran, aux correcteurs)
effect(() => {
const langue = this.langue();
if (langue) {
localStorage.setItem(CLE_LANGUE, langue);
document.documentElement.lang = langue;
}
document.title = String(this.titre());
});
}
protected choisir(langue: Langue): void {
this.translate.use(langue);
}
}
|
- ligne 21 : [TranslatePipe] : le pipe [translate] ;
- ligne 25 : [TranslateService] : le service principal de [ngx-translate] ;
- ligne 26 : [currentLang] : la langue courante, sous forme de signal ;
- lignes 32-35 : deux réponses d'erreur, telles que les renverrait le serveur : une clé, et des paramètres ;
- ligne 38 : [translate.translate(clé)] rend un signal qui vaut la traduction de la clé, et qui suit la langue courante ;
- lignes 43-50 : un effet, exécuté à chaque changement de langue : la langue est mémorisée (ligne 46), l'attribut [lang] de la page est mis à jour (ligne 47 ; il sert aux lecteurs d'écran et aux correcteurs orthographiques), ainsi que le titre de l'onglet (ligne 49) ;
- ligne 54 : [use] change la langue courante : toute la page se retraduit.
| <div class="container py-4" style="max-width: 720px">
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="h3 mb-0">{{ 'APP.TITRE' | translate }}</h1>
<div class="btn-group btn-group-sm" role="group" [attr.aria-label]="'APP.LANGUE' | translate">
@for (l of langues; track l) {
<button class="btn" [class]="langue() === l ? 'btn-primary' : 'btn-outline-primary'" (click)="choisir(l)">
{{ l.toUpperCase() }}
</button>
}
</div>
</div>
<div class="card p-3 mb-3">
<p>{{ 'ACCUEIL.BONJOUR' | translate: { nom: 'Marie' } }}</p>
<!-- un paramètre calculé par un autre pipe, dans la langue courante -->
<p>{{ 'ACCUEIL.AUJOURDHUI' | translate: { date: (aujourdhui | date: 'fullDate' : '' : langue() ?? 'fr') } }}</p>
<p>{{ 'ACCUEIL.PRIX' | translate: { prix: (1234.5 | currency: 'EUR' : 'symbol' : '1.2-2' : langue() ?? 'fr') } }}</p>
<!-- pluriel choisit la clé (zero, one, other), translate la traduit -->
<p class="fw-bold">{{ 'ACCUEIL.RENDEZ_VOUS' | pluriel: nombre() : langue() | translate: { n: nombre() } }}</p>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-success" (click)="nombre.set(nombre() + 1)">{{ 'ACCUEIL.AJOUTER' | translate }}</button>
<button class="btn btn-sm btn-outline-secondary" [disabled]="nombre() === 0" (click)="nombre.set(nombre() - 1)">
{{ 'ACCUEIL.RETIRER' | translate }}
</button>
</div>
</div>
<div class="card p-3">
<h2 class="h6">{{ 'SERVEUR.TITRE' | translate }}</h2>
<p>{{ 'SERVEUR.EXPLICATION' | translate }}</p>
<ul class="mb-0">
@for (r of reponsesServeur; track r.cle) {
<!-- la clé du serveur EST une clé du dictionnaire ; params fournit ses paramètres -->
<li><code>{{ r.statusCode }} {{ r.cle }}</code><br>→ {{ r.cle | translate: r.params }}</li>
}
</ul>
</div>
</div>
|
- ligne 3 : ['APP.TITRE' | translate] : la traduction de la clé, dans la langue courante ;
- ligne 6 : le bouton de la langue courante est mis en valeur ;
- ligne 14 : un texte à paramètre : le pipe reçoit un objet qui donne la valeur de chaque paramètre ;
- lignes 16-17 : la valeur d'un paramètre peut être calculée par un autre pipe. Le dernier argument de [date] et de [currency] est la langue : la date et le montant sont écrits à la manière française, ou anglaise ;
- ligne 19 : deux pipes enchaînés : [pluriel] choisit la clé, [translate] la traduit ;
- ligne 34 : la clé renvoyée par le serveur est traduite, avec ses paramètres.
[npm start 21-i18n] ; en français :

On clique sur [EN], puis sur « Remove one » :

On recharge la page : elle reste en anglais, la langue a été mémorisée.