Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

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é 11 min prérequis : axes 5-9 lus

TermeSensActeurs
i18n (internationalization)Préparer le code à supporter plusieurs langues / culturesDéveloppeurs
l10n (localization)Traduire et adapter à un marché précisTraducteurs, marketing
g11n (globalization)i18n + l10n + go-to-marketDirection 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.


DomaineExemple
Strings UI« Add to cart » → « Ajouter au panier »
Pluriels1 article / 2 articles / 0 article (FR ≠ EN ≠ AR)
Genre grammatical« Bonjour Marie » vs « Bonjour Pierre »
Dates30/04/2026 (FR) vs 04/30/2026 (EN-US) vs 2026-04-30 (ISO)
Heures14:30 (FR) vs 2:30 PM (EN-US)
Nombres1 234,56 (FR) vs 1,234.56 (EN-US) vs 1.234,56 (DE)
Devises1 234,56 € vs $1,234.56 vs ¥1,235 (pas de décimales en JPY)
Téléphones, adressesFormat pays
RTLArabe, hébreu : direction inversée + miroir UI
PluralisationEN : 2 formes (singular/plural). FR : 2-3. AR : 6 formes !
Tri / ordre alphabétiqueÉ vient avec E en FR, après Z en SV

Avant d’embarquer une lib, regarde ce que Intl fait nativement (universel en 2026).

// Dates
new 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
// Nombres
new Intl.NumberFormat('fr-FR').format(1234.5); // "1 234,5"
// Devises
new 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)
// Listes
new 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.


LibStackForceFaiblesse
next-intlNext.js (App Router)Excellente DX, ICU natif, SSR-friendly, RSCCouplé à Next
i18nextUniversel (React, Vue, Svelte, vanilla)Écosystème énorme (plugins, backends, ICU)Moins typé par défaut
FormatJS / react-intlReactICU first, format natif, types strictsBoilerplate plus lourd
vue-i18nVue 3Officiel Vue, ICU supportVue uniquement
LinguiReactMacros à la compilation, bundle minusculeSetup initial plus complexe
Paraglide (Inlang)UniverselType-safe à 100 %, tree-shakable, message par fonctionPlus jeune, écosystème moindre
  • Next.js 16 App Router + RSCnext-intl est le défaut.
  • Astroastro-i18next ou natif via [lang]/... + i18next.
  • Vite + React standalonei18next ou Paraglide (montant).
  • Vue 3vue-i18n@9+ (Composition API).

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).


messages/
fr.json
en.json
ar.json
src/app/[locale]/
layout.tsx
page.tsx
src/i18n.ts
middleware.ts
{
"Home": {
"title": "Bienvenue {name}",
"cart": "{count, plural, =0 {Panier vide} one {1 article} other {# articles}}",
"lastLogin": "Dernière connexion : {date, date, long}"
}
}
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.

src/i18n.ts
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.


i18n.ts
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.

✅ Bon
features/
cart/
locales/
fr.json
en.json
products/
locales/...
❌ Mauvais (dépendances croisées)
locales/
fr/
cart.json
products.json
profile.json
{
"cart.title": "Panier",
"cart.empty": "Votre panier est vide"
}

Charger uniquement les namespaces de la page courante = bundle JSON plus petit.

StratégiePour qui
JSON dans le repoPetits projets, peu de traductions
TMS (Crowdin, Lokalise, Phrase, Tolgee)Équipes avec traducteurs externes
Tolgee (OSS, possible self-host)Compromis devs / traducteurs
i18nexus, Locizei18next + cloud sync

Workflow standard : devs poussent les clés en EN (source), traducteurs traduisent dans le TMS, sync vers le repo via CI.


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é physiquePropriété logique
margin-leftmargin-inline-start
padding-rightpadding-inline-end
border-bottomborder-block-end
text-align: lefttext-align: start
left: 0inset-inline-start: 0
widthinline-size
heightblock-size

Tailwind v4 propose les utilitaires logiques par défaut : ms-4, me-4, ps-4, pe-4

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.

DevTools → Settings → Devices → ajoute un device avec dir="rtl". Ou ajoute manuellement <html dir="rtl"> et observe les régressions.


// ❌ Pas tous les langues mettent le verbe à la fin
const msg = t('hello') + ' ' + name + '!';
// ✅ Variable dans le message — l'ordre est langue-dépendant
t('hello', { name }); // "Bonjour {name} !" → "{name}さん、こんにちは!"
// ❌ Marche en EN/FR, faux en AR/RU/PL
const msg = count === 1 ? '1 item' : `${count} items`;
// ✅ ICU plural
t('cart', { count });

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.

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.

import parsePhoneNumber from 'libphonenumber-js';
const num = parsePhoneNumber('+33612345678');
num.formatInternational(); // "+33 6 12 34 56 78"
num.formatNational(); // "06 12 34 56 78"

Stocke en UTC (ISO 8601) en base, formate au rendu. Évite à tout prix de stocker 30/04/2026 (ambigu : 30 avril ou 4 mars ?).


<!-- 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>.


Tu écris : `count === 1 ? '1 article' : `${count} articles``. Pourquoi est-ce un anti-pattern i18n ?
Pour formater une date avec « il y a 3 jours » en français et son équivalent espagnol / arabe, quelle solution est recommandée en 2026 ?
Tu déploies une version arabe et l'UI est cassée : marges décalées, icônes flèches dans le mauvais sens. Quelle est la cause la plus probable ?
Sur un projet Next.js 16 App Router multilingue, quelle solution est la plus adaptée en 2026 ?


Retour : Index axe 13 — applique tout ça via le projet de l’axe.