13.5 — Internationalisation
🎯 Objectif : préparer une app à servir plusieurs langues et cultures sans tout réécrire à chaque ajout. i18n n’est pas qu’un dictionnaire de strings : c’est de la pluralisation, des dates, des devises, du RTL et de l’organisation.
À l'issue de cet axe, tu sauras :
- Distinguer i18n (préparer le code) et l10n (traduire pour un marché)
- Choisir entre i18next, next-intl, FormatJS / react-intl, vue-i18n
- Écrire des messages ICU MessageFormat avec pluralisation et genre
- Formater dates, nombres et devises avec Intl.* natif
- Gérer une application en RTL (arabe, hébreu) sans dupliquer les composants
Confirmé
i18n vs l10n — deux disciplines liées
Section intitulée « i18n vs l10n — deux disciplines liées »| Terme | Sens | Acteurs |
|---|---|---|
| i18n (internationalization) | Préparer le code à supporter plusieurs langues / cultures | Développeurs |
| l10n (localization) | Traduire et adapter à un marché précis | Traducteurs, marketing |
| g11n (globalization) | i18n + l10n + go-to-market | Direction produit |
Tu codes l’i18n une fois ; la l10n se rajoute marché par marché.
« 18 lettres entre i et n » → i18n. Idem pour l10n et a11y. Convention universelle.
Le périmètre i18n — bien plus que des strings
Section intitulée « Le périmètre i18n — bien plus que des strings »| Domaine | Exemple |
|---|---|
| Strings UI | « Add to cart » → « Ajouter au panier » |
| Pluriels | 1 article / 2 articles / 0 article (FR ≠ EN ≠ AR) |
| Genre grammatical | « Bonjour Marie » vs « Bonjour Pierre » |
| Dates | 30/04/2026 (FR) vs 04/30/2026 (EN-US) vs 2026-04-30 (ISO) |
| Heures | 14:30 (FR) vs 2:30 PM (EN-US) |
| Nombres | 1 234,56 (FR) vs 1,234.56 (EN-US) vs 1.234,56 (DE) |
| Devises | 1 234,56 € vs $1,234.56 vs ¥1,235 (pas de décimales en JPY) |
| Téléphones, adresses | Format pays |
| RTL | Arabe, hébreu : direction inversée + miroir UI |
| Pluralisation | EN : 2 formes (singular/plural). FR : 2-3. AR : 6 formes ! |
| Tri / ordre alphabétique | É vient avec E en FR, après Z en SV |
L’API native Intl — souvent suffisante
Section intitulée « L’API native Intl — souvent suffisante »Avant d’embarquer une lib, regarde ce que Intl fait nativement (universel en 2026).
// Datesnew Intl.DateTimeFormat('fr-FR', { dateStyle: 'long' }) .format(new Date()); // 30 avril 2026
new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }) .format(new Date()); // April 30, 2026
// Relative time — « il y a 2 jours »new Intl.RelativeTimeFormat('fr', { numeric: 'auto' }) .format(-2, 'day'); // avant-hier
// Nombresnew Intl.NumberFormat('fr-FR').format(1234.5); // "1 234,5"
// Devisesnew Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }) .format(1234.56); // "1 234,56 €"
// Pluriels (sans message — pour brancher manuellement)new Intl.PluralRules('fr').select(2); // "other" (2 = pluriel)new Intl.PluralRules('ar').select(11); // "many" (l'arabe en a 6)
// Listesnew Intl.ListFormat('fr').format(['a', 'b', 'c']); // "a, b et c"
// Segments (graphèmes, mots, phrases)[...new Intl.Segmenter('fr', { granularity: 'word' }).segment('Bonjour le monde')];Intl est déjà 80 % du job. Reste à gérer les strings (le dictionnaire) et la pluralisation des messages.
Choisir une bibliothèque i18n
Section intitulée « Choisir une bibliothèque i18n »| Lib | Stack | Force | Faiblesse |
|---|---|---|---|
| next-intl | Next.js (App Router) | Excellente DX, ICU natif, SSR-friendly, RSC | Couplé à Next |
| i18next | Universel (React, Vue, Svelte, vanilla) | Écosystème énorme (plugins, backends, ICU) | Moins typé par défaut |
| FormatJS / react-intl | React | ICU first, format natif, types stricts | Boilerplate plus lourd |
| vue-i18n | Vue 3 | Officiel Vue, ICU support | Vue uniquement |
| Lingui | React | Macros à la compilation, bundle minuscule | Setup initial plus complexe |
| Paraglide (Inlang) | Universel | Type-safe à 100 %, tree-shakable, message par fonction | Plus jeune, écosystème moindre |
Repères 2026
Section intitulée « Repères 2026 »- Next.js 16 App Router + RSC →
next-intlest le défaut. - Astro →
astro-i18nextou natif via[lang]/...+i18next. - Vite + React standalone →
i18nextou Paraglide (montant). - Vue 3 →
vue-i18n@9+(Composition API).
ICU MessageFormat — le format universel
Section intitulée « ICU MessageFormat — le format universel »Le standard ICU (par Unicode) couvre pluralisation, sélection, genre, nombres :
{count, plural, =0 {Aucun message} one {# message} other {# messages}}{gender, select, female {Bienvenue, {name} ! Elle est connectée.} male {Bienvenue, {name} ! Il est connecté.} other {Bienvenue, {name} ! Connexion réussie.}}Combiné :
{count, plural, =0 {Aucune commande} one {1 commande, livrée le {date, date, ::yMMMMd}} other {# commandes, livrées le {date, date, ::yMMMMd}}}Le formatteur ICU choisit la bonne forme selon la langue (l’arabe a 6 formes : zero, one, two, few, many, other).
Exemple complet — next-intl (Next.js 16)
Section intitulée « Exemple complet — next-intl (Next.js 16) »Structure
Section intitulée « Structure »messages/ fr.json en.json ar.jsonsrc/app/[locale]/ layout.tsx page.tsxsrc/i18n.tsmiddleware.tsmessages/fr.json
Section intitulée « messages/fr.json »{ "Home": { "title": "Bienvenue {name}", "cart": "{count, plural, =0 {Panier vide} one {1 article} other {# articles}}", "lastLogin": "Dernière connexion : {date, date, long}" }}Component
Section intitulée « Component »import { useTranslations, useFormatter } from 'next-intl';
export default function Home() { const t = useTranslations('Home'); const format = useFormatter(); return ( <main> <h1>{t('title', { name: 'Marie' })}</h1> <p>{t('cart', { count: 3 })}</p> <p>{t('lastLogin', { date: new Date() })}</p> <p>{format.dateTime(new Date(), { dateStyle: 'long' })}</p> </main> );}next-intl est type-safe : si tu utilises une clé qui n’existe pas en fr.json, TypeScript t’engueule à la compilation.
Routing localisé
Section intitulée « Routing localisé »export const locales = ['fr', 'en', 'ar'] as const;export const defaultLocale = 'fr';Tes URLs deviennent /fr/products, /en/products, /ar/products. Le middleware redirige /products → /fr/products selon Accept-Language.
Exemple complet — i18next (universel)
Section intitulée « Exemple complet — i18next (universel) »import i18next from 'i18next';import HttpBackend from 'i18next-http-backend';import LanguageDetector from 'i18next-browser-languagedetector';import ICU from 'i18next-icu';
await i18next .use(HttpBackend) .use(LanguageDetector) .use(ICU) .init({ fallbackLng: 'fr', supportedLngs: ['fr', 'en', 'ar'], backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' }, interpolation: { escapeValue: false }, });i18next.t('cart', { count: 0 });// → "Aucun article" (selon /locales/fr/translation.json)Plugins utiles :
i18next-http-backend— charge les traductions à la demande.i18next-icu— active la syntaxe ICU.i18next-resources-to-backend— bundle local sans HTTP.i18next-browser-languagedetector— détecte la langue navigateur.
Organisation des fichiers de traduction
Section intitulée « Organisation des fichiers de traduction »Par feature, pas par locale
Section intitulée « Par feature, pas par locale »✅ Bonfeatures/ cart/ locales/ fr.json en.json products/ locales/...
❌ Mauvais (dépendances croisées)locales/ fr/ cart.json products.json profile.jsonNamespaces
Section intitulée « Namespaces »{ "cart.title": "Panier", "cart.empty": "Votre panier est vide"}Charger uniquement les namespaces de la page courante = bundle JSON plus petit.
Source de vérité
Section intitulée « Source de vérité »| Stratégie | Pour qui |
|---|---|
| JSON dans le repo | Petits projets, peu de traductions |
| TMS (Crowdin, Lokalise, Phrase, Tolgee) | Équipes avec traducteurs externes |
| Tolgee (OSS, possible self-host) | Compromis devs / traducteurs |
| i18nexus, Locize | i18next + cloud sync |
Workflow standard : devs poussent les clés en EN (source), traducteurs traduisent dans le TMS, sync vers le repo via CI.
RTL — droite-à-gauche
Section intitulée « RTL — droite-à-gauche »L’arabe, l’hébreu, l’ourdou s’écrivent de droite à gauche. Bonne nouvelle : le navigateur fait l’essentiel.
<html dir="rtl" lang="ar">CSS moderne 2026 utilise les propriétés logiques qui s’inversent automatiquement :
/* ❌ Cassé en RTL */.card { margin-left: 1rem; padding-right: 2rem; }
/* ✅ Inverse automatique */.card { margin-inline-start: 1rem; padding-inline-end: 2rem; }| Propriété physique | Propriété logique |
|---|---|
margin-left | margin-inline-start |
padding-right | padding-inline-end |
border-bottom | border-block-end |
text-align: left | text-align: start |
left: 0 | inset-inline-start: 0 |
width | inline-size |
height | block-size |
Tailwind v4 propose les utilitaires logiques par défaut : ms-4, me-4, ps-4, pe-4…
Icônes directionnelles
Section intitulée « Icônes directionnelles »Les flèches (→, ←, breadcrumbs, sliders) doivent se mirorer en RTL :
[dir="rtl"] .icon-arrow { transform: scaleX(-1);}Mais attention : un check ✓ ou un avatar ne doivent PAS se miroirer. Réfléchis pour chaque icône.
Tester le RTL
Section intitulée « Tester le RTL »DevTools → Settings → Devices → ajoute un device avec dir="rtl". Ou ajoute manuellement <html dir="rtl"> et observe les régressions.
Pièges classiques
Section intitulée « Pièges classiques »1. Concaténation de strings
Section intitulée « 1. Concaténation de strings »// ❌ Pas tous les langues mettent le verbe à la finconst msg = t('hello') + ' ' + name + '!';
// ✅ Variable dans le message — l'ordre est langue-dépendantt('hello', { name }); // "Bonjour {name} !" → "{name}さん、こんにちは!"2. Pluriel par if
Section intitulée « 2. Pluriel par if »// ❌ Marche en EN/FR, faux en AR/RU/PLconst msg = count === 1 ? '1 item' : `${count} items`;
// ✅ ICU pluralt('cart', { count });3. Strings inlinées dans le code
Section intitulée « 3. Strings inlinées dans le code »Une string codée en dur ne sera jamais traduite. Outils :
eslint-plugin-i18next— alerte sur les strings non extraites.- Lingui macros — extraction automatique au build.
4. Emails / PDFs / push
Section intitulée « 4. Emails / PDFs / push »L’email de confirmation, le PDF de facture, la notif push doivent aussi être traduits — ils ont leur propre tunnel. Centralise les templates par locale.
5. Numéros de téléphone, codes postaux
Section intitulée « 5. Numéros de téléphone, codes postaux »import parsePhoneNumber from 'libphonenumber-js';const num = parsePhoneNumber('+33612345678');num.formatInternational(); // "+33 6 12 34 56 78"num.formatNational(); // "06 12 34 56 78"6. Dates qui voyagent
Section intitulée « 6. Dates qui voyagent »Stocke en UTC (ISO 8601) en base, formate au rendu. Évite à tout prix de stocker 30/04/2026 (ambigu : 30 avril ou 4 mars ?).
SEO multilingue
Section intitulée « SEO multilingue »<!-- Indique à Google que cette page a des équivalents --><link rel="alternate" hreflang="fr" href="https://example.com/fr/products" /><link rel="alternate" hreflang="en" href="https://example.com/en/products" /><link rel="alternate" hreflang="ar" href="https://example.com/ar/products" /><link rel="alternate" hreflang="x-default" href="https://example.com/products" />Ajoute aussi le Content-Language header HTTP et lang sur <html>.
Auto-évaluation
Section intitulée « Auto-évaluation »Pour aller plus loin
Section intitulée « Pour aller plus loin »- next-intl — next-intl-docs.vercel.app
- i18next — i18next.com
- ICU MessageFormat — unicode.org/…/messageFormat
- MDN Intl — developer.mozilla.org/…/Intl
- RTL Styling 101 — Ahmad Shadeed
- Inlang / Paraglide — i18n type-safe nouvelle génération
Retour : Index axe 13 — applique tout ça via le projet de l’axe.