Front-end angularforall.com

- Angular i18n : localisation multi-langue

Angular I18N Localisation Traduction Frontend
Angular i18n : localisation multi-langue

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 sizeOptimal (traductions inlinées)+16kb (lib) + JSON chargés runtime
Changement de langueRechargement de pageInstantané (hot swap)
Performances (compile time) (runtime parsing)
FlexibilitéBuild séparé par localeUn seul build, N langues
ICU expressionsNatif (pluriel, genre, select)Manuel dans les JSON
Intégration CIng build --localizePas de build supplémentaire
Cas idéalSites multilingues statiques, SEO fortSaaS, dashboards, langue user-defined
Règle de décision : Si les utilisateurs changent rarement de langue et que le SEO multilingue compte → i18n natif. Si l'utilisateur peut changer sa langue à n'importe quel moment sans rechargement → @ngx-translate.

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>
Jamais de concaténation ! Au lieu de '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.element dès le premier commit — jamais d'IDs auto-générés
  • registerLocaleData pour toutes les locales cibles dans app.config.ts
  • ICU expressions pour tous les pluriels et sélects — jamais de concaténation
  • ng extract-i18n automatisé 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 localStorage et attribut lang sur <html>
  • baseHref par locale (/en/, /es/) pour le SEO multilingue — pas de query string ?lang=en
  • Utiliser Intl.RelativeTimeFormat, Intl.ListFormat pour les formatages avancés non couverts par les pipes

Partager