Réponse rapide
Si vos traductions Next.js ne fonctionnent pas, vérifiez dans cet ordre : (1) La clé de traduction existe dans le fichier JSON avec la bonne imbrication, (2) Le fichier JSON est valide (pas de virgule finale), (3) La locale est correctement détectée dans le middleware, (4) Le Provider/contexte englobe vos composants, (5) Vous utilisez le bon hook (Composant Serveur vs Client). 90% des problèmes sont des fautes de frappe dans les clés ou des entrées JSON manquantes.
J'ai débogué des centaines de configurations i18n sur des projets React et Next.js. La bonne nouvelle ? Les bugs de traduction correspondent presque toujours à une dizaine de problèmes prévisibles. La mauvaise nouvelle ? Ils peuvent être incroyablement frustrants à diagnostiquer quand on ne sait pas où chercher.
Ce guide couvre tous les problèmes de traduction que j'ai rencontrés en production, organisés du plus fréquent au plus rare. Je me concentre sur Next.js 14/15 avec App Router, mais la plupart s'appliquent aussi au Pages Router.
Problème #1 : La clé de traduction s'affiche au lieu de la traduction
Symptôme : Vous voyez common.buttons.submit à l'écran au lieu de "Envoyer"
C'est le problème numéro 1, et ça signifie généralement l'une de ces choses :
La clé n'existe pas dans votre JSON
JSON1// ❌ Votre code utilise : t('common.buttons.submit') 2// Mais votre en.json contient : 3{ 4 "common": { 5 "button": { // Notez : "button" pas "buttons" 6 "submit": "Envoyer" 7 } 8 } 9}
Solution : Vérifiez soigneusement la structure de votre JSON. Utilisez un IDE avec aperçu du chemin JSON ou un outil comme IntlPull qui valide les clés.
Vous utilisez le mauvais namespace
TSX1// ❌ Incorrect : cherche dans le namespace par défaut 2const t = useTranslations(); 3t('checkout.title'); 4 5// ✅ Correct : spécifiez le namespace 6const t = useTranslations('checkout'); 7t('title');
Le fichier JSON n'est pas chargé
Vérifiez votre configuration i18n :
TypeScript1// i18n/request.ts pour next-intl 2import { getRequestConfig } from 'next-intl/server'; 3 4export default getRequestConfig(async ({ locale }) => ({ 5 messages: (await import(`../messages/${locale}.json`)).default 6}));
Vérifiez que le chemin du fichier est correct. Une erreur fréquente est de mettre les messages dans /public/locales alors que votre config attend /messages.
Problème #2 : Syntaxe JSON invalide
Symptôme : L'app plante avec "Unexpected token" ou les traductions échouent silencieusement
JSON est strict. Ces erreurs casseront tout :
JSON1// ❌ Virgule finale (le plus courant) 2{ 3 "welcome": "Bonjour", 4 "goodbye": "Au revoir", // <-- Cette virgule casse tout 5} 6 7// ❌ Guillemets simples 8{ 9 'welcome': 'Bonjour' // Doit utiliser des guillemets doubles 10} 11 12// ❌ Guillemets non échappés dans les valeurs 13{ 14 "message": "Cliquez "ici" pour continuer" // Nécessite échappement 15} 16 17// ✅ Correct 18{ 19 "welcome": "Bonjour", 20 "goodbye": "Au revoir", 21 "message": "Cliquez \"ici\" pour continuer" 22}
Solution : Utilisez un validateur JSON. VS Code surligne les erreurs de syntaxe. Exécutez cat en.json | python -m json.tool pour valider en ligne de commande.
Problème #3 : Traduction manquante pour une locale spécifique
Symptôme : L'anglais fonctionne, mais l'espagnol/allemand/etc. affiche les clés
Vous avez ajouté la clé dans en.json mais oublié les autres locales :
/messages
en.json ✅ Contient "checkout.newFeature": "Try our new feature"
es.json ❌ Cette clé manque complètement
de.json ❌ Cette clé manque complètement
Solution : Utilisez un outil de gestion de traduction comme IntlPull qui suit les traductions manquantes par locale. Ou configurez un fallback :
TypeScript1// Configuration next-intl avec fallback 2export default getRequestConfig(async ({ locale }) => ({ 3 messages: { 4 ...(await import(`../messages/en.json`)).default, // Fallback 5 ...(await import(`../messages/${locale}.json`)).default 6 } 7}));
Problème #4 : Le middleware ne détecte pas la locale
Symptôme : Affiche toujours la langue par défaut, la locale dans l'URL est ignorée
TypeScript1// ❌ Le middleware ne s'exécute pas sur vos routes 2export const config = { 3 matcher: ['/api/:path*'] // Ne matche que les routes API ! 4}; 5 6// ✅ Matcher correct pour i18n 7export const config = { 8 matcher: ['/((?!api|_next|.*\\..*).*)'] 9};
Vérifiez aussi l'emplacement de votre fichier middleware - il doit être à middleware.ts à la racine du projet, pas dans /app ou /src/app.
Vérifier la logique de détection de locale
TypeScript1// middleware.ts 2import createMiddleware from 'next-intl/middleware'; 3 4export default createMiddleware({ 5 locales: ['en', 'es', 'de', 'fr'], 6 defaultLocale: 'en', 7 localePrefix: 'always' // ou 'as-needed' 8});
Astuce debug : Ajoutez un console.log dans le middleware pour voir quelle locale est détectée :
TypeScript1export default function middleware(request: NextRequest) { 2 console.log('Locale détectée :', request.nextUrl.pathname); 3 // ... reste du middleware 4}
Problème #5 : Erreurs de désynchronisation d'hydratation
Symptôme : La console affiche "Text content does not match server-rendered HTML"
Ça arrive quand le serveur et le client rendent des traductions différentes :
Cause 1 : Utiliser un hook client dans un Composant Serveur
TSX1// ❌ Composant Serveur utilisant un hook client 2// app/[locale]/page.tsx (Composant Serveur par défaut) 3import { useTranslations } from 'next-intl'; // Ça marche en fait 4 5export default function Page() { 6 const t = useTranslations('home'); 7 return <h1>{t('title')}</h1>; // Fonctionne avec next-intl ! 8}
Attendez, ça fonctionne en fait avec next-intl parce qu'il détecte le contexte. Mais avec react-i18next :
TSX1// ❌ Avec react-i18next dans un Composant Serveur 2'use server'; 3import { useTranslation } from 'react-i18next'; // Ne marchera pas 4 5// ✅ Utilisez la fonction côté serveur 6import { getTranslations } from 'next-intl/server'; 7 8export default async function Page() { 9 const t = await getTranslations('home'); 10 return <h1>{t('title')}</h1>; 11}
Cause 2 : Formatage date/heure sans fuseau horaire
TSX1// ❌ Le serveur peut être en UTC, le client en fuseau local 2{formatDate(new Date())} 3 4// ✅ Spécifiez toujours le fuseau horaire 5import { format } from 'date-fns-tz'; 6{format(new Date(), 'PPP', { timeZone: userTimezone })}
Problème #6 : Le Provider n'englobe pas les composants
Symptôme : "Could not find IntlProvider" ou erreurs de contexte similaires
TSX1// ❌ Provider manquant dans le layout 2// app/[locale]/layout.tsx 3export default function Layout({ children }) { 4 return <html><body>{children}</body></html>; 5} 6 7// ✅ Avec le provider 8import { NextIntlClientProvider } from 'next-intl'; 9import { getMessages } from 'next-intl/server'; 10 11export default async function Layout({ 12 children, 13 params: { locale } 14}: { 15 children: React.ReactNode; 16 params: { locale: string }; 17}) { 18 const messages = await getMessages(); 19 20 return ( 21 <html lang={locale}> 22 <body> 23 <NextIntlClientProvider messages={messages}> 24 {children} 25 </NextIntlClientProvider> 26 </body> 27 </html> 28 ); 29}
Problème #7 : Les clés dynamiques ne fonctionnent pas
Symptôme : t(variableName) retourne la clé, pas la traduction
TSX1// ❌ Variable comme clé directement 2const key = `status.${order.status}`; 3t(key); // Certains bundlers ne peuvent pas optimiser ça 4 5// ✅ Utilisez un mapping explicite 6const statusMessages = { 7 pending: t('status.pending'), 8 shipped: t('status.shipped'), 9 delivered: t('status.delivered') 10}; 11return statusMessages[order.status]; 12 13// ✅ Ou utilisez t.raw() pour les clés dynamiques avec next-intl 14t(`status.${order.status}`); // Ça marche en fait avec next-intl
Problème #8 : La pluralisation ne fonctionne pas
Symptôme : Affiche "{count, plural, one {# article} other {# articles}}" littéralement
Vous utilisez le format ICU mais la librairie ne le parse pas :
JSON1// Votre JSON 2{ 3 "items": "{count, plural, one {# article} other {# articles}}" 4}
TSX1// ❌ Variable non passée 2t('items'); 3 4// ✅ Passez la variable count 5t('items', { count: 5 }); // "5 articles"
Vérifiez que votre librairie supporte ICU
- next-intl : Support ICU complet ✅
- react-i18next : Nécessite le plugin
i18next-icu - next-translate : Pluralisation basique uniquement
Problème #9 : Problèmes d'environnement/build
Les clés marchent en dev, cassent en production
TypeScript1// ❌ Les imports dynamiques peuvent échouer au build 2const messages = await import(`@/messages/${locale}.json`); 3 4// ✅ Assurez-vous que toutes les locales sont connues statiquement 5import en from '@/messages/en.json'; 6import es from '@/messages/es.json'; 7 8const messages = { en, es }; 9export const getMessages = (locale: string) => messages[locale];
"Module not found" après ajout d'une nouvelle locale
Après avoir ajouté un nouveau fichier de locale, redémarrez votre serveur dev. Next.js met en cache la résolution des modules.
Terminalrm -rf .next && npm run dev
Problème #10 : Les liens/navigation perdent la locale
Symptôme : Cliquer sur des liens internes remet la langue par défaut
TSX1// ❌ Le Link standard perd la locale 2import Link from 'next/link'; 3<Link href="/about">À propos</Link> 4 5// ✅ Utilisez la navigation de next-intl 6import { Link } from '@/i18n/navigation'; // Votre navigation configurée 7<Link href="/about">À propos</Link> // Préserve la locale automatiquement 8 9// ✅ Ou incluez manuellement la locale 10import { useLocale } from 'next-intl'; 11const locale = useLocale(); 12<Link href={`/${locale}/about`}>À propos</Link>
Checklist de débogage
Quand les traductions cassent, passez cette liste en revue :
| Vérification | Commande/Action |
|---|---|
| Syntaxe JSON valide | `cat messages/en.json |
| La clé existe | Recherchez la clé exacte dans le fichier JSON |
| Le fichier de locale existe | ls messages/ |
| Le middleware s'exécute | Ajoutez console.log, vérifiez la sortie serveur |
| Provider en place | Vérifiez layout.tsx pour IntlProvider |
| Import correct | Serveur : getTranslations, Client : useTranslations |
| Cache vidé | rm -rf .next && npm run dev |
Messages d'erreur courants décryptés
| Erreur | Signification | Solution |
|---|---|---|
Missing message: "key" | Clé absente du JSON | Ajoutez la clé à tous les fichiers de locale |
Unable to find next-intl locale | Le middleware ne définit pas la locale | Vérifiez l'emplacement et la config de middleware.ts |
Hydration failed | Désynchronisation serveur/client | Utilisez le bon hook selon le type de composant |
Cannot read property 't' of undefined | Provider manquant | Englobez avec IntlClientProvider |
ENOENT: no such file | Chemin de fichier incorrect | Vérifiez le chemin du dossier messages dans la config |
Prévention : Évitez ces problèmes dès le départ
1. Utilisez TypeScript pour des clés type-safe
TypeScript1// Générez des types depuis votre JSON 2// Avec next-intl, créez un fichier de types : 3type Messages = typeof import('./messages/en.json'); 4declare global { 5 interface IntlMessages extends Messages {} 6}
Maintenant TypeScript signalera les clés invalides.
2. Utilisez un système de gestion des traductions
Des outils comme IntlPull font automatiquement :
- Validation de la syntaxe JSON
- Suivi des traductions manquantes par locale
- Prévention des fautes de frappe avec l'autocomplétion
- Synchronisation des clés sur toutes les locales
3. Ajoutez des vérifications CI
YAML1# .github/workflows/i18n-check.yml 2- name: Valider les fichiers JSON 3 run: | 4 for f in messages/*.json; do 5 python -m json.tool "$f" > /dev/null || exit 1 6 done 7 8- name: Vérifier les clés manquantes 9 run: npx intlpull check --config intlpull.config.json
4. Tests d'intégration pour les parcours critiques
TypeScript1// e2e/i18n.spec.ts 2test('la page checkout se rend dans toutes les locales', async ({ page }) => { 3 for (const locale of ['en', 'es', 'de']) { 4 await page.goto(`/${locale}/checkout`); 5 // Ne doit pas contenir de clés de traduction brutes 6 await expect(page.locator('body')).not.toContainText('checkout.'); 7 } 8});
Quand utiliser la gestion des traductions
Si vous rencontrez ces problèmes régulièrement, envisagez un TMS :
| Scénario | Fichiers JSON DIY | Gestion des traductions |
|---|---|---|
| < 50 clés, 1-2 devs | ✅ Ça marche | Excessif |
| > 200 clés, plusieurs devs | ❌ Conflits de merge | ✅ Source unique de vérité |
| > 3 langues | ❌ Difficile à synchroniser | ✅ Détection des clés manquantes |
| Traducteurs externes | ❌ Cauchemar d'échange JSON | ✅ Workflow intégré |
IntlPull (pub assumée) est conçu spécifiquement pour ça. Il s'intègre à votre workflow Git, détecte les traductions manquantes en CI, et supporte les mises à jour OTA pour ne pas avoir à redéployer pour des corrections de traduction.
Questions fréquentes
Pourquoi ma fonction t() retourne-t-elle la clé au lieu de la traduction ?
Votre clé de traduction n'existe pas dans le fichier JSON ou le namespace. Vérifiez les fautes de frappe dans la clé, vérifiez que la structure JSON correspond au chemin attendu par votre code, assurez-vous d'utiliser le bon namespace, et validez que le fichier JSON est bien chargé. C'est le problème i18n le plus courant.
Pourquoi mes traductions fonctionnent en anglais mais pas dans les autres langues ?
La clé de traduction manque dans les autres fichiers de locale. Quand vous ajoutez une nouvelle clé à en.json, vous devez aussi l'ajouter à es.json, de.json, etc. Utilisez un TMS comme IntlPull pour détecter automatiquement les traductions manquantes, ou configurez des locales de fallback dans votre config.
Comment corriger les erreurs d'hydratation i18n Next.js ?
Le serveur et le client rendent un contenu différent. Ça arrive généralement avec le formatage date/heure (le serveur est en UTC, le client en local), l'utilisation de hooks client dans des Composants Serveur, ou une désynchronisation de locale entre serveur et client. Spécifiez les fuseaux horaires explicitement, utilisez les bons hooks selon le type de composant, et assurez-vous que la locale est passée correctement.
Pourquoi mon middleware Next.js ne détecte pas la locale ?
Le pattern matcher de votre middleware ne matche pas vos routes. Assurez-vous que middleware.ts est à la racine du projet (pas dans /app), que votre matcher inclut les routes nécessaires, et que le tableau locales correspond à vos langues supportées. Ajoutez un console.log dans le middleware pour débugger ce qui est détecté.
Comment débugger les erreurs "Could not find IntlProvider" ?
Votre composant n'est pas englobé par le provider i18n. Vérifiez que NextIntlClientProvider (ou le provider de votre librairie) est dans votre app/[locale]/layout.tsx et englobe tous les composants enfants. Le provider doit recevoir les messages et la locale courante.
Pourquoi mon fichier JSON provoque des erreurs "Unexpected token" ?
Votre syntaxe JSON est invalide. Problèmes courants : virgules finales, guillemets simples au lieu de doubles, guillemets non échappés dans les valeurs, ou virgules manquantes. Passez votre JSON dans un validateur comme python -m json.tool pour trouver l'emplacement exact de l'erreur.
Comment faire fonctionner les traductions dans les Composants Serveur Next.js ?
Utilisez les fonctions de traduction côté serveur. Avec next-intl, utilisez getTranslations() de next-intl/server dans les Composants Serveur. Le hook standard useTranslations() fonctionne avec next-intl grâce à la détection de contexte, mais pour react-i18next vous avez besoin de fonctions explicites côté serveur.
Pourquoi mes traductions cassent en production mais marchent en développement ?
Les imports dynamiques peuvent échouer au build. Assurez-vous que tous les fichiers de locale existent avant le build, utilisez des imports statiques ou des imports dynamiques vérifiés, et videz le cache .next avant de builder. Les builds production optimisent les imports différemment du mode dev.
Comment éviter que la locale se perde lors de la navigation ?
Utilisez le composant Link de votre librairie i18n au lieu du Link Next.js. Avec next-intl, configurez les exports de navigation dans i18n/navigation.ts et importez Link depuis là. Sinon, incluez manuellement la locale dans le href : /${locale}/about.
Comment tester que toutes les traductions existent ?
Ajoutez des vérifications CI qui valident le JSON et détectent les clés manquantes. Validez la syntaxe JSON avec un linter, comparez les clés entre les fichiers de locale, et lancez des tests d'intégration qui vérifient que les pages n'affichent pas de clés de traduction brutes. La CLI IntlPull fournit une détection automatique des traductions manquantes.
Résumé
La plupart des problèmes i18n Next.js entrent dans des catégories prévisibles :
- Fautes de frappe dans les clés → Utilisez les types TypeScript et l'autocomplétion
- JSON invalide → Validez en CI
- Données de locale manquantes → Utilisez un TMS avec vérification de synchronisation
- Mauvais hook/contexte → Suivez les règles Composant Serveur vs Client
- Problèmes de middleware → Vérifiez l'emplacement du fichier et la config du matcher
L'écosystème s'est beaucoup amélioré. Avec App Router et des librairies modernes comme next-intl, l'i18n est significativement plus fiable qu'avec Pages Router. Mais ça reste du code, et le code a des bugs.
Construisez défensivement : clés type-safe, validation CI, et bons outils vous épargneront des heures de débogage.
Besoin d'aide pour gérer les traductions à l'échelle ? Commencez gratuitement avec IntlPull — détection automatique des traductions manquantes, traduction IA, et intégration next-intl fluide.
