Que fait i18n Web ?
L'internationalisation est une architecture, pas un passage de traduction en fin de projet. L'objectif est une interface qui s'affiche correctement pour n'importe quelle locale sans cas particuliers : pas de phrases concaténées, pas de formats codés en dur, pas de disposition qui casse quand l'allemand est 35 % plus long ou que l'arabe change le sens d'écriture.
Quand utiliser cette compétence
À utiliser lorsque l'utilisateur demande de :
- Ajouter une langue, un sélecteur de langue ou un routage localisé
- Traduire une application, extraire des chaînes ou mettre en place un flux de traduction
- Corriger la plurilisation, le genre ou les bogues du type "1 élément / 2 éléments"
- Formater les dates, nombres, monnaies, temps relatifs ou listes par locale
- Prendre en charge le RTL (arabe, hébreu, persan, ourdou)
- Ajouter hreflang, métadonnées localisées ou sitemaps multilingues
- Réviser une configuration i18n existante, des clés manquantes ou une dérive de traduction
- Choisir entre next-intl, react-i18next, FormatJS, Lingui ou un TMS
Les références associées se trouvent dans references/ et les scripts dans scripts/. Ne lisez que ce dont la tâche a besoin. Les chemins sont relatifs au répertoire de cette compétence (l'environnement d'exécution de l'agent l'affiche lorsque la compétence est chargée).
Non-négociables
- Ne jamais construire une phrase en concaténant des fragments traduits. Traduisez le message entier avec des espaces réservés :
"{count} nouveaux messages", past('nouveau') + ' ' + count + ' ' + t('messages'). - Ne jamais utiliser un texte anglais comme clé. Les clés sont des identifiants stables (
inbox.unreadCount), afin que les modifications de copie ne cassent pas les recherches ou n'invalident pas toutes les traductions. - Chaque chaîne destinée à l'utilisateur est externalisée, y compris
aria-label,alt,title,placeholder, les erreurs de validation, les états vides, les toasts, les e-mails, les notifications push et la sortie PDF. - Ne jamais coder les formats en dur. Pas de
toFixed(2) + '$', pas deMM/DD/YYYYmanuel, pas de noms de mois en anglais, pas de supposition24h. UtilisezIntl. - Une locale est une balise BCP 47, pas une langue.
pt-BRetpt-PT,zh-Hansetzh-Hant,es-MXetes-ESsont des produits différents. Stockez la balise complète. - L'état de la locale doit être limité à la requête côté serveur. Ne placez jamais la locale courante dans une variable mutable au niveau du module pendant le SSR ; les requêtes concurrentes fuiraient les langues les unes dans les autres.
- Les traductions sont des entrées non fiables si elles proviennent d'un TMS, d'une communauté ou d'un CMS. Ne les passez jamais à
innerHTML/dangerouslySetInnerHTMLsans les assainir.
Flux de travail
1. Auditer avant de modifier le code
- Détectez la pile, le routeur et la bibliothèque i18n existante avant de proposer quoi que ce soit.
- Trouvez les chaînes codées en dur :
Considérez la sortie comme des candidats, pas comme une vérité absolue : ignorez les fixtures de test, les constantes, les URLs et les logs destinés aux développeurs.node scripts/scan-hardcoded.mjs src - Inventoriez chaque locale actuellement livrée et où se trouvent les catalogues.
- Identifiez d'abord les zones à plus haut risque : authentification, paiement, erreurs, e-mails et mentions légales.
2. Décider de l'architecture (l'écrire)
Répondez à ces questions avant d'écrire du code :
| Décision | Valeur par défaut | Remarques |
|---|---|---|
| Stratégie de locale | Préfixe de chemin /es/ | Meilleur SEO et mise en cache. Sous-domaines/ccTLD seulement avec une raison. |
| Locale par défaut | Langue source | Ne pas rediriger automatiquement les nouveaux visiteurs vers une locale devinée sans possibilité de la remplacer. |
| Détection de la locale | URL → cookie → Accept-Language → défaut | L'URL gagne afin que les liens partagés ne changent jamais de langue. |
| Format du catalogue | JSON par locale et par espace de noms | Chargez les espaces de noms à la demande ; n'expédiez jamais toutes les locales au client. |
| Syntaxe des messages | ICU MessageFormat | Portable entre bibliothèques et TMS ; prend en charge pluriel/select/ordinal. |
| Style de clé | fonctionnalité.composant.élément | Stable, recherchable, pas de texte anglais. |
| Source de vérité des traductions | TMS ou dépôt, pas les deux | Choisissez-en un ; synchronisez dans un sens en CI. |
| Repli | Chaîne de locales es-MX → es → en | Journalisez les replis en développement pour détecter les lacunes. |
3. Externaliser et structurer les messages
- Remplacez les chaînes par des clés sémantiques et déplacez la copie vers le catalogue de la locale de base.
- Utilisez une clé par phrase complète. Si une phrase contient un lien ou un mot en gras, utilisez l'interpolation de texte enrichi au lieu de la diviser.
- Gardez les espaces réservés nommés (
{nomUtilisateur}), jamais positionnels ({0}). - Ne mettez pas de HTML dans les catalogues sauf si la bibliothèque prend explicitement en charge les balises typées.
- Regroupez par surface de produit, pas par page :
paiement.json,authentification.json,erreurs.json.
Voir references/icu.md pour les règles relatives au pluriel, select, ordinal et texte enrichi.
4. Câbler le routage et le rendu localisés
- Dérivez la locale de la route, validez-la par rapport à la liste prise en charge et renvoyez une erreur 404 pour les locales inconnues plutôt que de faire un repli silencieux.
- Fournissez un sélecteur de langue qui préserve le chemin et la chaîne de requête courants.
- Définissez
<html lang="...">et<html dir="...">sur le document rendu côté serveur. - Chargez uniquement le catalogue de la locale active ainsi que le repli par défaut.
- Rendez le même catalogue disponible pour le rendu côté serveur et côté client ; évitez une logique de traduction dupliquée ou divergente.
Configuration spécifique au framework et pièges : references/frameworks.md.
Routage, détection, hreflang et sitemaps : references/routing-seo.md.
5. Gérer les valeurs sensibles à la locale
Utilisez Intl (ou l'enveloppe du framework), jamais de formatage manuel :
new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount)
new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeZone }).format(date)
new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(-3, 'day')
new Intl.ListFormat(locale, { type: 'conjunction' }).format(items)
new Intl.PluralRules(locale).select(count)
new Intl.Collator(locale).compare(a, b)
- Stockez les horodatages en UTC ; effectuez le rendu dans le fuseau horaire de l'utilisateur, pas celui du serveur.
- La monnaie n'est pas la même chose que la locale : un utilisateur français peut payer en USD.
- Triez/recherchez avec
Intl.Collator, pas avec<oulocaleCompare()sans locale. - Conversion de casse :
toLocaleUpperCase(locale), car leiturc devientİ.
6. Concevoir pour l'expansion et le RTL
- Supposez une augmentation de +30 à 40 % de la longueur du texte (allemand, finnois, russe) et évitez les boutons à largeur fixe ou les étiquettes sur une seule ligne.
- Laissez les conteneurs s'agrandir, autorisez le retour à la ligne et testez avec la pseudo-localisation :
node scripts/pseudo-localize.mjs locales/en.json --out locales/en-XA.json - Utilisez les propriétés CSS logiques (
margin-inline-start,padding-inline,inset-inline) au lieu deleft/right. - Miroirez les icônes directionnelles (flèches, chevrons, progression), pas les logos, horloges, contrôles média ou coches.
- Encadrez le contenu en ligne généré par l'utilisateur avec
<bdi>oudir="auto".
Liste de contrôle RTL complète : references/rtl.md.
7. Mettre en place une QA de traduction dans le CI
Au minimum, échouez la build lorsque les catalogues sont structurellement invalides :
node scripts/check-locales.mjs locales --base en
Il vérifie les clés manquantes/supplémentaires, les valeurs vides, les incohérences d'espaces réservés et la syntaxe ICU invalide. Ajoutez la compilation ICU via l'interface en ligne de commande de votre bibliothèque lorsqu'elle est disponible :
npx formatjs compile-folder --ast locales/en locales/compiled/en
Ajoutez des tests pour les catégories de pluriel, la sortie de date/numéro avec un fuseau horaire fixe et le rendu RTL. Voir references/testing.md.
Décisions clés et pièges
Sélection de la bibliothèque
| Besoin | Choisir |
|---|---|
| Next.js App Router, composants serveur | next-intl |
| React SPA, écosystème mature, nombreux plugins | react-i18next |
| Priorité à ICU, indépendant du framework, extraction en CLI | FormatJS (react-intl) |
| Catalogues compilés, petite empreinte runtime, sécurité de type | Lingui |
| Vue | vue-i18n |
| Svelte | svelte-i18n ou paraglide |
| Astro | routage i18n intégré + votre bibliothèque de messages |
| Vanilla / minimal | Intl + un chargeur de catalogue léger |
N'introduisez pas une deuxième bibliothèque i18n dans une application qui en a déjà une. Étendez la configuration existante.
Plurilisation
Les catégories de pluriel sont spécifiques à la langue. L'anglais a one/other ; l'arabe a zero/one/two/few/many/other ; le japonais n'a que other. N'écrivez jamais count === 1 ? ... : ....
"inbox.unread": "{count, plural, =0 {Aucun message non lu} one {# message non lu} other {# messages non lus}}"
Genre et accord grammatical
Utilisez select sur une variable de genre et gardez la valeur de la variable neutre en termes de langue (female, male, neutral, unknown), pas des mots localisés :
"profile.greeting": "{gender, select, female {Bienvenue, {name}} male {Bienvenue, {name}} other {Bienvenue, {name}}}"
Si une langue a besoin d'un accord sur les adjectifs, les noms ou les articles, exposez le genre/cas grammatical comme argument au lieu de modifier la chaîne.
Texte enrichi
Ne divisez pas "Lisez notre " + lien + " conditions" en trois clés. Utilisez l'interpolation de balises :
"signup.terms": "J'accepte les <link>Conditions d'utilisation</link>"
t.rich('signup.terms', { link: (chunks) => <a href="/terms">{chunks}</a> })
SEO
- Une URL indexable par locale ; ne servez jamais de contenu traduit automatiquement derrière la même URL.
hreflangauto-référencé plusx-default; URLs absolues ; incluez chaque locale.- Localisez
<title>, la méta-description, les balises OG et les données structurées. - Ne combinez pas
hreflangavecnoindexsur les pages localisées.
Détails dans references/routing-seo.md.
Mise en cache
- La locale dans le chemin simplifie la mise en cache CDN. Si vous négociez par en-tête, envoyez
Vary: Accept-Languageet attendez-vous à une fragmentation du cache. - Ne mettez pas en cache une préférence linguistique personnalisée dans une réponse CDN partagée.
- Maintenez
Content-Languagecohérent avec la locale rendue.
Liste de contrôle de révision
Avant de déclarer l'i18n terminée :
- Aucune chaîne codée en dur destinée à l'utilisateur dans les composants, les e-mails ou les erreurs
- Aucune concaténation de phrases ; chaque message est une unité traduisible complète
- Les clés sont sémantiques et stables ; aucun texte anglais comme clé
- Chaque espace réservé existe dans chaque locale ; la syntaxe ICU compile
- Les pluriels utilisent
Intl.PluralRules/ICU, pascount === 1 - Le genre/l'accord est géré via
select, pas de suffixes"/a" - Les dates, nombres, monnaies, temps relatifs utilisent
Intlavec une locale/fuseau horaire explicite -
<html lang>etdirsont corrects dès le premier affichage (SSR), pas définis dans un effet - Le sélecteur de locale préserve le chemin/la requête et fonctionne avec les liens profonds
- Une locale inconnue renvoie 404, pas un repli silencieux
- La disposition survit à une expansion de +35 % et au RTL
-
hreflang, canonical, sitemap et métadonnées localisées sont corrects - La validation du catalogue s'exécute dans le CI
- Le comportement de repli est intentionnel et observable
Anti-patrons
t('bonjour') + ', ' + nom— phrases fragmentéesclé={texteAnglais}— texte traduit comme clécount === 1 ? 'élément' : 'éléments'— logique de pluriel uniquement en anglaisnew Date().toLocaleDateString()sans locale ni fuseau horairevalue.toFixed(2)pour de l'argentlocale === 'ar' && { direction: 'rtl' }dispersé dans les composants au lieu d'undirau niveau du document- Définir
<html lang>dansuseEffect— provoque un flash et casse les robots d'indexation - Charger chaque locale dans le bundle initial
- Redirection automatique par IP sans possibilité de la remplacer
- Traduire des noms propres, des noms de produits, des identifiants de code ou des URLs
- Traduire automatiquement des chaînes et les expédier sans révision
- Supposer qu'un seul fichier de catalogue passe à l'échelle au-delà de quelques centaines de clés