~9 min de lecture
i18n en Angular : @angular/localize, ngx-translate ou Transloco ?
Tu démarres un nouveau projet Angular qui doit supporter plusieurs langues. Trois options s’offrent à toi : la solution native @angular/localize, le vétéran ngx-translate, ou le moderne @jsverse/transloco.
Spoiler : il n’y a pas de “meilleure” solution. Il y a celle qui correspond à ton contexte. Ce guide te donne les critères pour trancher.
Les trois approches en un coup d’œil
Avant d’entrer dans le détail, il faut comprendre une distinction fondamentale qui change tout :
| Solution | Stratégie | Conséquence |
|---|---|---|
@angular/localize |
Compile-time (un build par langue) | Performance maximale, mais switch de langue impossible sans reload |
ngx-translate |
Runtime (chargement dynamique) | Switch instantané, un seul build pour toutes les langues |
@jsverse/transloco |
Runtime (chargement dynamique) | Switch instantané + API moderne (Signals, lazy loading natif) |
Cette distinction compile-time vs runtime est le premier critère de choix. On y revient en détail plus bas.
Option 1 : @angular/localize (la solution native)
C’est la solution officielle, maintenue par l’équipe Angular. Elle s’intègre directement dans le compilateur.
Setup
ng add @angular/localize
Tu marques tes textes avec l’attribut i18n dans tes templates :
<h1 i18n="@@welcome.title">Welcome to our app</h1>
<button i18n="@@welcome.cta">Sign up</button>
<!-- Sur des attributs -->
<input i18n-placeholder placeholder="Enter your email" />
<!-- Avec interpolation -->
<p i18n>Hello {{ userName }}, you have {{ count }} messages</p>
Dans le code TypeScript :
import { Component, inject, LOCALE_ID } from '@angular/core';
@Component({
selector: 'app-greeting',
template: `<p>{{ message }}</p>`,
})
export class Greeting {
private readonly locale = inject(LOCALE_ID);
message = $localize`:@@greeting:Hello world`;
}
Tu extrais ensuite les chaînes dans un fichier XLIFF :
ng extract-i18n --output-path src/locale
Et tu configures angular.json pour générer un build par langue :
{
"projects": {
"my-app": {
"i18n": {
"sourceLocale": "en-US",
"locales": {
"fr": "src/locale/messages.fr.xlf",
"es": "src/locale/messages.es.xlf"
}
},
"architect": {
"build": {
"options": {
"localize": true
}
}
}
}
}
}
Le vrai trade-off
Le compile-time, c’est génial pour les performances : l’AoT compiler convertit le code Angular en JavaScript optimisé pendant le build, donc zéro overhead runtime, traductions inlinées dans le bundle final.
Mais ça veut aussi dire :
- Un build par langue (3 langues = 3 dossiers
dist/) - Un déploiement par langue (sous-domaines, sous-dossiers, ou serveur qui route)
- Pas de switch de langue côté client sans recharger une autre version de l’app
Pour les formats : @angular/localize supporte uniquement XLIFF ou XMB (formats XML), là où ngx-translate utilise du JSON par défaut. C’est plus rigide mais c’est aussi le standard de l’industrie de la traduction (les agences pro travaillent en XLIFF).
Option 2 : ngx-translate (le vétéran)
Lancée en 2014, c’est la lib historique. Elle a longtemps été le défaut de facto pour les apps qui avaient besoin de switch dynamique.
Setup
npm install @ngx-translate/core @ngx-translate/http-loader
Configuration moderne avec providers :
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideTranslateService, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient } from '@angular/common/http';
export function httpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideTranslateService({
loader: {
provide: TranslateLoader,
useFactory: httpLoaderFactory,
deps: [HttpClient],
},
defaultLanguage: 'en',
}),
],
};
Fichiers de traduction en JSON dans assets/i18n/en.json :
{
"welcome": {
"title": "Welcome",
"cta": "Sign up"
},
"greeting": "Hello {{ name }}, you have {{ count }} messages"
}
Usage dans les templates :
<h1>{{ 'welcome.title' | translate }}</h1>
<button>{{ 'welcome.cta' | translate }}</button>
<p>{{ 'greeting' | translate: { name: userName, count } }}</p>
Et dans le code :
import { Component, inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
@Component({ /* ... */ })
export class LangSwitcher {
private readonly translate = inject(TranslateService);
switchLang(lang: string): void {
this.translate.use(lang);
}
}
Pourquoi tu pourrais l’éviter aujourd’hui
ngx-translate est toujours maintenu — l’équipe ne supporte que la version courante avec les nouveaux updates et fixes, et un partenariat avec HeroDevs offre du support commercial pour les versions EOL. Mais l’API reste essentiellement basée sur RxJS et les pipes, sans intégration native Signals.
Le lazy loading existe mais demande de la configuration manuelle. Pour un projet moderne (Angular 17+), Transloco offre tout ce que ngx-translate propose, en plus moderne.
Option 3 : @jsverse/transloco (le moderne)
Initialement développé sous le nom @ngneat/transloco, le projet est désormais publié sous le scope @jsverse. C’est l’alternative moderne à ngx-translate, pensée pour les patterns Angular récents.
Setup
ng add @jsverse/transloco
Le schematic configure tout automatiquement. Voici la config générée :
import { ApplicationConfig, isDevMode } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideTransloco } from '@jsverse/transloco';
import { TranslocoHttpLoader } from './transloco-loader';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideTransloco({
config: {
availableLangs: ['en', 'fr', 'es'],
defaultLang: 'en',
reRenderOnLangChange: true,
prodMode: !isDevMode(),
},
loader: TranslocoHttpLoader,
}),
],
};
Le loader par défaut :
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Translation, TranslocoLoader } from '@jsverse/transloco';
@Injectable({ providedIn: 'root' })
export class TranslocoHttpLoader implements TranslocoLoader {
private readonly http = inject(HttpClient);
getTranslation(lang: string) {
return this.http.get<Translation>(`/assets/i18n/${lang}.json`);
}
}
Quatre façons d’utiliser les traductions
1. Le pipe (basique) :
<h1>{{ ‘welcome.title’ | transloco }}</h1>
<p>{{ ‘greeting’ | transloco: { name: userName } }}</p>
Simple, mais il crée une subscription par utilisation. À éviter si tu as beaucoup de clés dans le même template.
2. La directive structurelle (la plus efficace dans les templates) :
<ng-container *transloco="let t">
<h1>{{ t(‘welcome.title’) }}</h1>
<button>{{ t(‘welcome.cta’) }}</button>
<p>{{ t(‘greeting’, { name: userName }) }}</p>
</ng-container>
La fonction t est mémoïsée, donc les résultats de traduction sont mis en cache. La directive crée une seule subscription par template.
3. La directive avec prefix (gros gain de DRY) :
C’est l’astuce que beaucoup de devs Transloco découvrent tard. Tu peux spécifier un préfixe de clé valable pour tout le bloc :
<ng-container *transloco="let t; prefix: ‘welcome’">
<h1>{{ t(‘title’) }}</h1>
<button>{{ t(‘cta’) }}</button>
<p>{{ t(‘subtitle’) }}</p>
</ng-container>
Au lieu de répéter welcome.title, welcome.cta, welcome.subtitle, tu ne tapes que la clé feuille. Pratique sur les composants type formulaire avec 15-20 clés sous le même namespace.
À noter : l’input read a été renommé prefix à partir de v7.1.0. L’ancien read est déprécié et sera supprimé en v8.
<!-- Avant v7.1.0 (déprécié) -->
<ng-container *transloco="let t; read: ‘welcome’">
<!-- À partir de v7.1.0 -->
<ng-container *transloco="let t; prefix: ‘welcome’">
Tu peux aussi changer la langue spécifiquement pour un bloc :
<ng-container *transloco="let t; lang: ‘fr’; prefix: ‘welcome’">
<h1>{{ t(‘title’) }}</h1>
</ng-container>
4. L’API Signals (la vraie révolution v7.5+) :
import { Component, signal } from ‘@angular/core’;
import { translateSignal } from ‘@jsverse/transloco’;
@Component({
selector: ‘app-greeting’,
template: `
<p>{{ message() }}</p>
<p>{{ greeting() }}</p>
`,
})
export class Greeting {
private readonly userName = signal(‘Gaetan’);
private readonly count = signal(5);
// Réactif au changement de langue
message = translateSignal(‘welcome.title’);
// Réactif au changement de langue ET aux signals passés en params
greeting = translateSignal(‘greeting’, {
name: this.userName,
count: this.count,
});
}
Là où translateSignal change la donne : tu peux passer des Signals comme paramètres dynamiques. Le résultat se recalcule automatiquement quand n’importe quelle dépendance change (langue ou paramètre).
La clé peut elle-même être un Signal :
import { Component, computed, signal } from ‘@angular/core’;
import { translateSignal } from ‘@jsverse/transloco’;
@Component({ /* ... */ })
export class Status {
private readonly status = signal<’pending’ | ‘success’ | ‘error’>(‘pending’);
// La clé change selon le statut
private readonly statusKey = computed(() => `status.${this.status()}`);
message = translateSignal(this.statusKey);
}
Pour traduire un objet entier (utile pour les listes ou structures complexes) :
import { translateObjectSignal } from ‘@jsverse/transloco’;
@Component({ /* ... */ })
export class Menu {
// Récupère tout le sous-arbre ‘navigation’ comme objet
navigation = translateObjectSignal(‘navigation’);
// Usage dans le template : navigation().home, navigation().settings, etc.
}
L’API service pour les cas hors template
Quand tu as besoin de traduire dans un service, un guard, ou de la logique métier :
import { inject, Injectable } from ‘@angular/core’;
import { TranslocoService } from ‘@jsverse/transloco’;
@Injectable({ providedIn: ‘root’ })
export class NotificationService {
private readonly transloco = inject(TranslocoService);
notifySuccess(): void {
// Synchrone (suppose que les traductions sont chargées)
const message = this.transloco.translate(‘notifications.success’);
this.show(message);
}
notifyAsync(): void {
// Observable (s’adapte aux changements de langue)
this.transloco.selectTranslate(‘notifications.success’).subscribe(message => {
this.show(message);
});
}
changeLang(lang: string): void {
this.transloco.setActiveLang(lang);
}
private show(message: string): void {
// ...
}
}
Pour les cas vraiment standalone (sans injection), il existe aussi des fonctions utilitaires :
import { translate, translateObject, getBrowserLang } from ‘@jsverse/transloco’;
// Proxy vers TranslocoService.translate
const text = translate(‘hello’);
// Détection de la langue navigateur
const userLang = getBrowserLang(); // ‘fr’, ‘en’, etc.
Attention : translate() ne fonctionne que si le service a été initialisé et que les traductions sont chargées.
Les scopes : lazy loading par feature
Le pattern le plus puissant de Transloco. Tu déclares un scope sur une lazy route :
import { Routes } from ‘@angular/router’;
import { provideTranslocoScope } from ‘@jsverse/transloco’;
export const adminRoutes: Routes = [
{
path: ‘’,
loadComponent: () => import(‘./admin-dashboard’),
providers: [
provideTranslocoScope(‘admin’),
],
},
];
Tu places les fichiers dans assets/i18n/admin/en.json et assets/i18n/admin/fr.json. Transloco les charge uniquement quand l’utilisateur navigue vers cette route, et les merge sous le namespace admin :
<ng-container *transloco="let t; prefix: ‘admin’">
<h1>{{ t(‘dashboard.title’) }}</h1>
<p>{{ t(‘dashboard.welcome’) }}</p>
</ng-container>
Tu peux aussi customiser le namespace ou l’alias :
provideTranslocoScope({
scope: ‘admin’,
alias: ‘a’, // Accessible via ‘a.dashboard.title’
})
C’est ce qui permet à Transloco d’être performant même sur des apps avec des milliers de clés : tu ne charges que ce dont tu as besoin, quand tu en as besoin.
Comparatif détaillé
Performance & bundle size
| Solution | Bundle ajouté | Runtime overhead |
|---|---|---|
@angular/localize |
~0 KB (inliné au build) | Aucun |
ngx-translate |
~15-20 KB gzippé | Pipe + subscriptions RxJS |
Transloco core |
~8 KB gzippé | Directive optimisée + Signals |
| Transloco + plugins | jusqu’à 40 KB gzippé | Selon plugins (MessageFormat, L10n) |
@angular/localize reste imbattable côté performance brute : les chaînes sont littéralement remplacées dans le bundle au build. Pas de pipe, pas de subscription, pas de cache à gérer. Si tes Web Vitals sont critiques (e-commerce, SEO premium), c’est un argument lourd.
Lazy loading des traductions
C’est ici que les solutions runtime brillent — et que la native montre ses limites.
Avec @angular/localize : impossible. Toutes les traductions de la langue active sont dans le bundle initial. Si tu as 5000 clés, elles sont toutes chargées dès le premier paint.
Avec ngx-translate : possible mais demande de la config manuelle (TranslateModule.forChild, custom loader par module).
Avec Transloco : c’est natif via le système de scopes.
// Dans une lazy route
import { provideTranslocoScope } from '@jsverse/transloco';
export const adminRoutes: Routes = [
{
path: '',
component: AdminDashboard,
providers: [
provideTranslocoScope('admin'),
],
},
];
Tu places ensuite tes traductions dans assets/i18n/admin/en.json, et elles ne sont chargées que quand l’utilisateur navigue vers cette route. Pour une grosse app, c’est une économie significative sur le First Load.
SSR / Angular Universal
@angular/localize : compatible nativement, mais avec une contrainte forte. Comme c’est compile-time, ton serveur Node doit servir le bon build selon la langue. En pratique, ça veut dire :
- Soit un sous-domaine par langue (
fr.app.com,en.app.com) - Soit un sous-dossier (
app.com/fr,app.com/en) - Soit un serveur Express qui route vers le bon build
ngx-translate : SSR fonctionnel mais demande de gérer manuellement le pré-chargement des traductions côté serveur pour éviter le flash de clés non traduites (ex: welcome.title qui apparaît brièvement avant la traduction).
Transloco : SSR (Server-Side Rendering) compatibility est une feature listée officiellement, et le système de loader fonctionne côté serveur sans config supplémentaire dans la plupart des cas.
Switch de langue à chaud
| Solution | Switch sans reload | UX |
|---|---|---|
@angular/localize |
❌ Non | Reload complet (ou redirection) |
ngx-translate |
✅ Oui | Instantané |
Transloco |
✅ Oui | Instantané + réactif via Signals |
Si tes utilisateurs changent souvent de langue (apps multilingues B2B, plateformes internationales), oublie @angular/localize.
Format des fichiers de traduction
| Solution | Formats supportés |
|---|---|
@angular/localize |
XLIFF 1.2, XLIFF 2.0, XMB/XTB |
ngx-translate |
JSON (loader par défaut), extensible |
Transloco |
JSON (par défaut), extensible |
Le XLIFF est le standard pour les agences de traduction professionnelles. Si tu travailles avec des traducteurs externes via des outils comme Lokalise, Crowdin ou Phrase, ils gèrent tous les trois formats — mais le JSON est plus dev-friendly.
Le guide de décision
Voici comment trancher selon ton contexte :
Choisis @angular/localize si…
- Ton app a un nombre fixe et limité de langues (2-3 max)
- Les performances sont critiques (Lighthouse 100, Core Web Vitals)
- Tu peux te permettre un déploiement par langue (sous-domaines/sous-dossiers)
- Tu travailles déjà avec des traducteurs pro en XLIFF
- Le switch de langue est rare ou inexistant côté utilisateur
Cas typique : site marketing ou e-commerce avec déploiement par région.
Choisis ngx-translate si…
- Tu maintiens un projet existant qui l’utilise déjà
- Tu n’as pas de besoin Signals ou de patterns Angular très récents
- Tu privilégies une lib mature et stable sans surprises
Pour un nouveau projet en 2026, c’est rarement le bon choix. Transloco fait tout ce que ngx-translate fait, en mieux.
Choisis Transloco si…
- C’est un nouveau projet Angular 17+
- Tu veux switch de langue à chaud sans reload
- Tu utilises massivement les Signals
- Tu as besoin de lazy loading de traductions par feature/module
- Ton app a beaucoup de langues ou de traductions volumineuses
- Tu veux un seul build pour toutes les langues
Cas typique : SaaS B2B multilingue, app de gestion, dashboard admin.
Le piège du “j’utiliserai la solution native”
Beaucoup de devs choisissent @angular/localize “parce que c’est officiel”. C’est un raisonnement risqué.
La solution native a été conçue pour un cas d’usage précis : les apps déployées par locale. Si ton besoin réel c’est “les utilisateurs peuvent changer de langue dans les settings”, tu vas te battre contre l’outil pendant tout le projet.
Les contraintes architecturales (un build par langue, pas de switch dynamique) ne se voient pas au début. Elles te frappent au moment du déploiement, quand tu réalises que tu dois configurer un reverse proxy pour router selon la locale, ou quand le PO demande “et on peut pas juste mettre un dropdown de langues ?”.
Choisis selon ton besoin métier réel, pas selon le label “officiel”.
En résumé
| Critère | @angular/localize |
ngx-translate |
Transloco |
|---|---|---|---|
| Stratégie | Compile-time | Runtime | Runtime |
| Bundle size | Optimal | Moyen | Petit (core) |
| Switch dynamique | ❌ | ✅ | ✅ |
| Lazy loading | ❌ | Manuel | Natif (scopes) |
| API Signals | ❌ | ❌ | ✅ |
| SSR | ✅ (compile) | ✅ (config) | ✅ (natif) |
| Format | XLIFF/XMB | JSON | JSON |
| Maintenance | Équipe Angular | Communauté | jsverse (actif) |
| Cas idéal | Sites multi-régions | Legacy | SaaS moderne |
Ma reco pour un nouveau projet Angular 17+ : Transloco par défaut. Tu gardes toutes les options ouvertes (switch dynamique, lazy loading, Signals) sans pénalité significative sur le bundle. Et si un jour tu veux migrer vers @angular/localize pour des raisons de perf, le travail de marquage des chaînes est déjà fait — il “suffira” d’adapter la syntaxe.
L’inverse — partir de @angular/localize puis vouloir du switch dynamique — est beaucoup plus douloureux.
📚 Envie de creuser Angular ?
👉🏼 ➡️ Découvre EasyAngularKit