Next.js i18n
Directives structurelles pour mettre une application Next.js en plusieurs langues : comment l'organisation des dossiers doit être, ce qui casse quand elle est incorrecte, et ce qu'il faut éviter.
Choisir d'abord la bibliothèque
Par défaut, utilisez next-intl pour tout nouveau projet sur App Router. Il ne nécessite pas de plugin de build, ni d'étape macro, ni de gestion d'instances par locale ; les messages sont en JSON simple typés à partir de votre locale source.
| Bibliothèque | Quand l'utiliser | Coût |
|---|---|---|
| next-intl (défaut) | Nouveau projet App Router, équipe de toute taille | Aucun — JSON simple, pas de génération de code |
| @lingui/react | Vous voulez une extraction à la compilation et un runtime minimal, ou une couche i18n commune à React/Vue/Astro | Plugin macro SWC ou Babel, épinglé exactement |
| next-i18next | L'équipe connaît déjà i18next, ou vous migrez du code Pages Router | Instance par requête, séparation getT/useT |
i18n intégré à Next.js | Pages Router uniquement. Jamais App Router | Non applicable |
La configuration i18n: { locales, defaultLocale } de Next.js est réservée au Pages Router. Dans App Router, elle ne fait rien — le routage des locales y nécessite un segment [locale] plus un proxy. Ne laissez pas l'agent « activer » l'i18n intégré pour un projet App Router.
Comparaison complète, y compris le coût en bundle et les compromis par bibliothèque :
references/library-comparison.md.
Architecture canonique
Gardez les dictionnaires statiques en dehors de src/ pour que le bundling et les chemins d'import restent évidents, et regroupez toute la machinerie des locales dans un seul dossier.
messages/ # dictionnaires statiques purs — hors de /src
├── en.json # source de vérité ; chaque autre locale le reflète
├── de.json
└── ja.json
src/
├── i18n/
│ ├── routing.ts # defineRouting : locales + defaultLocale
│ ├── request.ts # getRequestConfig : résoudre la locale, charger les messages
│ └── navigation.ts # createNavigation : Link/redirect tenant compte de la locale
├── proxy.ts # détection de locale + redirection (Next 16 ; était middleware.ts)
└── app/
├── [locale]/ # englobe toute l'application localisée
│ ├── layout.tsx # valider la locale, <html lang>, generateStaticParams
│ └── page.tsx
└── not-found.tsx # 404 global vit EN DEHORS de [locale]
Règles qui font fonctionner cette structure :
[locale]est un segment racine dynamique. Tout ce qui est localisé se trouve sous lui. Les routes racines (not-found.tsx,error.tsx,api/) restent à l'extérieur et ne doivent pas appeler les hooks i18n de manière inconditionnelle, sinon elles planteront à l'exécution.- Une seule locale source.
messages/en.jsonest le maître. Chaque autre fichier doit refléter exactement sa structure. - Toute la connaissance des locales dans
src/i18n/. Les composants importentLinkdepuis@/i18n/navigation, jamais depuisnext/link. Un seul endroit pour ajouter une langue.
Configuration (projet neuf)
Exécutez dans cet ordre. L'ordre est important — proxy.ts dépend de routing.ts, et le layout dépend de request.ts.
- Installez
next-intl, enveloppeznext.config.tsaveccreateNextIntlPlugin(). - Créez
src/i18n/routing.tsavecdefineRouting. - Créez
src/proxy.tsexportantcreateMiddleware(routing)plus le matcher par défaut. - Créez
src/i18n/request.tsavecgetRequestConfigrésolvant la locale. - Déplacez
app/**sousapp/[locale]/**; ajoutezgenerateStaticParamset validez le paramètre. - Créez
src/i18n/navigation.tsaveccreateNavigation(routing). - Créez
messages/en.jsonet remplacez les chaînes codées en dur paruseTranslations. - Définissez
<html lang>et ajoutez les métadonnées localisées + hreflang.
Contenu exact des fichiers pour chaque étape : references/next-intl-setup.md.
Pièges à éviter
Lisez cette section avant d'écrire du code i18n. Ce sont les échecs qui coûtent des heures.
setRequestLocaleest obsolète. Sur Next.js 16.3+, lisez la locale avecnext/root-paramsdansgetRequestConfigà la place. La plupart des guides, tutoriels et données d'entraînement LLM enseignent encoresetRequestLocale(params.locale). Cela fonctionne encore et est documenté comme rétrocompatible — mais c'est l'ancienne API. Utilisez-la uniquement sur Next < 16.3.generateStaticParamsreste obligatoire dans tous les cas.paramsest une Promise sur Next 15+.const {locale} = await params. Considérezparams.localecomme une erreur de type, pas d'exécution.- Validez la locale ou les valeurs inconnues s'affichent. Utilisez
hasLocale(routing.locales, locale)etnotFound(). Sans cela,/xx/aboutaffichera votre page anglaise à une URL factice. proxy.ts, pasmiddleware.ts, sur Next 16.middleware.tsest déprécié. Le proxy s'exécute uniquement sur Node.js — le runtime Edge n'est pas supporté. Lingui et next-i18next annoncent des proxies compatibles Edge ; si vous avez besoin d'Edge, vous avez un véritable compromis, pas un avantage gratuit.- Ne concaténez jamais les chaînes traduites.
"Vous avez " + count + " éléments"est grammaticalement incorrect en japonais et en arabe. Utilisez ICU :"{count, plural, =1 {Un élément} other {# éléments}}". - N'interpolez jamais avec
+ou des littéraux de gabarit dans un message. Utilisez des espaces réservés nommés —{nom}— pour que l'ordre des mots puisse changer. - Les dates et les nombres provoquent des décalages d'hydratation.
new Date()sur le serveur et dans le navigateur diffèrent par le fuseau horaire et l'horloge. DéfinisseztimeZonedansgetRequestConfig, ou rendez côté client uniquement pour les heures relatives. Symptôme : « Text content does not match server-rendered HTML ». - Le formatage dépend de la locale.
toLocaleString()sans locale explicite formate selon la locale d'exécution, pas la locale demandée. UtilisezuseFormatter()/getFormatter(). getRequestConfigs'exécute par requête et est mis en cache par React. N'y mettez pas d'état variable par requête (panier, préférences utilisateur). Seulement une configuration variant selon la locale.- Les Server Actions et les Route Handlers n'ont pas de paramètres racine.
await locale()fonctionne uniquement dans les Server Components. Passez la locale explicitement comme argument dans les actions et les handlers.
Mise à niveau d'une application existante
Considérez cela comme une migration avec un impact sur les URL, pas un changement de configuration. Chaque URL existante change de forme lorsque vous ajoutez un préfixe [locale].
Planifiez dans cet ordre :
- Inventaire des chaînes — exécutez
scripts/find-untranslated.shpour trouver le texte JSX codé en dur. - Choisissez la stratégie de routage avant de coder. Le préfixe de locale systématique (
/en/about,/de/about) est le défaut ; il est sans ambiguïté et propre pour hreflang.localePrefix: 'as-needed'sert la locale par défaut au chemin nu — meilleures URL, signal SEO plus faible. Décidez maintenant ; changer plus tard est une migration complète. - Mettez en place le routage avant de toucher à un composant. Faites fonctionner
proxy.ts+[locale]avec des messages en anglais uniquement. - Déplacez les routes sous
[locale]/— c'est un grandgit mvmécanique. Faites-le en un seul commit pour quegit log --followreste utile. - Remplacez les chaînes espace de noms par espace de noms, pas fichier par fichier. Traduisez un espace de noms de bout en bout pour que la forme des clés soit validée avant de se multiplier.
- Ajoutez des redirections pour chaque URL qui a changé.
/about→/en/about(ou 307 vers la locale détectée). Sans cela, vous perdez tous les classements existants. Non négociable. - Ajoutez hreflang + métadonnées localisées uniquement après que chaque locale a la route, sinon vous émettez des alternatives pointant vers des 404.
Séquence complète de migration avec les notes de redirection et de retour arrière : references/migration.md.
Ce qu'il faut éviter
- Ne mettez pas
messages/danssrc/. Les dictionnaires réservés au serveur dans l'arborescence source brouillent ce qui est envoyé au client. - N'importez pas
Linkdepuisnext/linkdans les routes localisées. Cela supprime le préfixe de locale et crée silencieusement des liens vers la mauvaise langue. - N'écrivez pas manuellement les préfixes de locale dans les hrefs (
href={/${locale}/about}). UtilisezLinkdepuis@/i18n/navigation, qui gère les préfixés, as-needed et le routage par domaine. - N'utilisez pas un seul espace de noms plat pour les clés. Regroupez par fonctionnalité/page. Les clés plates entrent en collision et sont impossibles à rechercher.
- Ne prenez pas la traduction automatique au moment du build comme source de vérité. La sortie machine est un brouillon ; la valider sans relecture rend les erreurs permanentes et introuvables.
- N'ajoutez pas une locale à
routing.localessans faire le reste. Sans unmessages/xx.jsontraduit, chaque page affiche les clés ou fait un repli silencieux. - Ne mélangez pas les stratégies de routage.
as-neededplus unhreflangpointant vers des URL préfixées est un bug SEO auto-infligé.
Références
Lisez-les à la demande, pas à l'avance :
references/library-comparison.md— choisir entre next-intl, Lingui, next-i18next ; coût en bundle, déclencheurs de migration, forme de configuration par bibliothèque.references/next-intl-setup.md— chaque fichier de la configuration canonique, prêt à copier-coller.references/messages-and-keys.md— structure des espaces de noms, nommage des clés, syntaxe ICU, formatage des dates/nombres/listes, messages typés.references/seo.md— hreflang,<html lang>,generateMetadatalocalisé, canonique, alternatives de sitemap, règles SEO du sélecteur de locale.references/migration.md— mise à niveau d'une application existante, redirections, retour arrière.
Scripts
scripts/find-untranslated.sh— signale les chaînes probablement codées en dur destinées aux utilisateurs dans un projet, afin que vous puissiez évaluer le travail avant de commencer. Exécutez-le comme étape 1 d'une mise à niveau.scripts/run-trigger-eval.sh— mesure si ladescriptionde cette compétence se déclenche sur les bonnes invites. Utilisez-le lors de la modification de la description.
Tester la description
La description est la seule chose que l'agent voit avant de décider de charger cette compétence. Si elle ne se déclenche pas, rien de ce qui suit n'a d'importance.
bash scripts/run-trigger-eval.sh --dry-run # afficher le plan, pas d'appels API
bash scripts/run-trigger-eval.sh --split train # 12 requêtes x 3 exécutions
bash scripts/run-trigger-eval.sh --split validation --out report.json
Ensemble de requêtes : evals/trigger-queries.json — 12 doivent se déclencher, 8 ne doivent pas se déclencher, répartis 60/40 en un ensemble train et un ensemble validation fixes.
Lors de l'édition de la description :
- Corrigez les échecs en utilisant uniquement l'ensemble train. Touchez l'ensemble validation une seule fois à la fin pour vérifier que le changement se généralise — si l'ensemble train s'améliore et que l'ensemble validation se dégrade, vous avez surajusté.
- Les échecs de type « doit se déclencher » signifient que la description est trop étroite : ajoutez le concept que la requête représente, jamais les mots-clés de la requête.
- Les échecs de type « ne doit pas se déclencher » signifient qu'elle est trop large : dites ce que la compétence ne couvre pas.
- Réexécutez avant d'éditer. Le déclenchement est non déterministe, et une seule exécution ne prouve rien.
N'ajoutez pas de mots-clés provenant d'une requête qui échoue — c'est ainsi que les descriptions finissent par ressembler à une liste de mots-clés et cessent de correspondre aux vraies invites.