Si votre app Next.js est traduite mais que l’onglet du navigateur, la page 404, les erreurs de formulaire et l’aperçu de lien restent en anglais, la cause est presque toujours la même : ces chaînes sont produites hors d’un composant, donc aucun appel useTranslations ne les a jamais touchées. globalize.now est une infrastructure de localisation propulsée par l’IA, et c’est cette couche que nous voyons rester à moitié faite quand un agent IA ajoute l’i18n à un projet App Router : la page visible est convertie, le reste de la surface ne l’est pas.

Voici où vit chacune de ces chaînes et l’appel next-intl qui la prend en charge. Chaque exemple suit la documentation actuelle de next-intl, consultée le 28/10/2026.

Où se cachent les chaînes non traduites dans une app App Router ?

Dans six endroits qui ne sont pas des composants React affichant du texte. Un agent de code qui ajoute l’i18n en parcourant vos composants convertit l’UI visible et passe tout droit devant ceux-ci :

  1. Les titres et descriptions de page dans generateMetadata.
  2. Les erreurs de validation et les messages de résultat des Server Actions.
  3. not-found.tsx, la page 404.
  4. error.tsx, la page d’erreur d’exécution.
  5. Les fichiers générés comme opengraph-image.tsx, manifest.ts et sitemap.ts.
  6. Les Route Handlers qui renvoient du texte.

Point commun : aucun n’est rendu normalement dans l’arbre de layouts [locale], donc la locale doit leur être transmise ou lue autrement. L’article sur le routing Next.js 16 couvre le câblage du routing ; celui-ci traite de ce que ce câblage n’atteint pas.

Comment traduire les titres et descriptions de page ?

Appelez getTranslations depuis next-intl/server dans generateMetadata. La fonction peut être exportée depuis une page ou un layout, et elle est asynchrone : l’API avec await est donc la bonne :

// app/[locale]/page.tsx
import {getTranslations} from 'next-intl/server';

export async function generateMetadata() {
  const t = await getTranslations('Metadata');
  return {title: t('title'), description: t('description')};
}

Le piège, c’est la forme statique. Les agents écrivent souvent export const metadata = {title: 'Dashboard'} parce que c’est plus court, et une constante ne peut pas appeler une fonction de traduction. Convertissez-la en fonction, puis ajoutez les mêmes clés au catalogue pour chaque locale. C’est aussi le texte vu par les moteurs de recherche, d’où le fait que traduire l’app sans traduire ses métadonnées ne fait pas grand-chose pour le classement.

Comment traduire les messages des Server Actions et de validation ?

Passez la locale à l’action et appelez getTranslations avec un namespace et cette locale. Une Server Action ne sait pas dans quelle langue est la page ; la documentation de next-intl vous demande donc de la fournir, par exemple en argument :

'use server';
import {getTranslations} from 'next-intl/server';

export async function subscribe(locale: string, formData: FormData) {
  const t = await getTranslations({locale, namespace: 'Subscribe'});
  // ...validate, then return t('success') or t('invalidEmail')
}

Une réserve de la même documentation : pensez à ce qui se passe si un visiteur peut changer de langue pendant qu’un message d’action est affiché. Avec un segment [locale] dans l’URL, c’est géré automatiquement, puisque la route change. Sans lui, vous voudrez peut-être effacer le message vous-même, par exemple en donnant au composant key={locale} pour réinitialiser son état.

Pour la validation de formulaires avec Zod, l’approche documentée est une error map. Zod en accepte une par appel safeParse, et la map peut appeler getTranslations. Mettez cela dans un helper asynchrone et passez son résultat dans l’option error de chaque parse. Vos schémas contiennent alors des règles, pas des phrases en anglais, et les formulaires générés par IA arrêtent d’envoyer "Invalid email" sur une page en letton.

Comment traduire les pages 404 et d’erreur ?

Placez not-found.tsx dans app/[locale]/, puis ajoutez un catch-all pour qu’elle se déclenche vraiment. Next.js rend ce fichier quand un segment appelle notFound(), et une URL inconnue ne l’appelle pas d’elle-même :

// app/[locale]/[...rest]/page.tsx
import {notFound} from 'next/navigation';

export default function CatchAllPage() {
  notFound();
}

Les requêtes que le middleware next-intl ne capture pas n’ont aucune locale, donc la 404 [locale] ne s’applique jamais à elles. La réponse de la documentation est app/global-not-found.tsx, que Next.js documente comme expérimental et qui rend ses propres html et body. Pour une valeur de [locale] inconnue, la documentation indique d’appeler notFound(), typiquement dans i18n/request.ts.

error.tsx est différente. Next.js impose que ce soit un Client Component, et elle est rendue sous votre layout : il lui faut donc un NextIntlClientProvider portant au moins le namespace Error. Ne passez que ces messages, pas tout le catalogue :

// app/[locale]/layout.tsx (excerpt)
const messages = await getMessages();
// ...
<NextIntlClientProvider locale={locale} messages={pick(messages, 'Error')}>

useTranslations('Error') fonctionne ensuite dans error.tsx. Cette page ne couvre pas global-error : vérifiez ce fichier à la main si vous en avez un.

Et les images de partage, le manifest et le sitemap ?

Chacun a besoin que la locale lui soit fournie, car ils se trouvent hors de l’arbre de layouts. Pour opengraph-image.tsx, lisez locale depuis params et appelez getTranslations({locale, namespace}). Avec un localePrefix personnalisé, la documentation avertit que la route générée peut devenir inaccessible, car Next.js ne voit pas les rewrites du proxy (middleware) ; le correctif consiste à faire ignorer au matcher les chemins se terminant par opengraph-image.

app/manifest.ts se situe hors de [locale], donc next-intl ne peut pas déduire une locale du chemin. Choisissez-en une représentative, généralement celle par défaut, et passez-la explicitement. Pour sitemap.ts, ajoutez des entrées alternates.languages et construisez chaque URL avec le getPathname({locale, href}) asynchrone de votre module de navigation, préfixé par l’hôte. next-intl envoie aussi par défaut un en-tête de réponse link qui annonce les versions linguistiques ; un sitemap est l’option quand il vous faut du contrôle sur des slugs pilotés par un CMS.

Comment gérer les Route Handlers ?

Prenez la locale depuis un search param, un segment ou l’en-tête accept-language, validez-la avec hasLocale(routing.locales, locale), et renvoyez une erreur 400 en cas d’échec. Appelez ensuite getTranslations({locale, namespace}). Sans validation, une chaîne arbitraire atteint votre recherche dans le catalogue.

Comment retrouver l’anglais laissé par votre agent ?

Lancez ceci à la racine du projet. Ces commandes sont volontairement approximatives : le but est d’obtenir une liste à lire, pas un score.

# 1. Static metadata that cannot be translated
grep -rn "export const metadata" app

# 2. Titles and descriptions already in generateMetadata
grep -rn "title:" app --include=page.tsx --include=layout.tsx

# 3. Server Actions with literal return messages
grep -rln "use server" app src 2>/dev/null | xargs grep -n "message:\|error:"

# 4. Zod schemas with inline messages
grep -rn "message:\s*['\"]" app src 2>/dev/null

# 5. 404 and error files, and whether a catch-all exists
ls app/\[locale\]/not-found.tsx app/\[locale\]/error.tsx "app/[locale]/[...rest]/page.tsx" 2>&1

Tout ce qui, dans les quatre premiers résultats, est un littéral anglais brut a sa place dans le catalogue. Pourquoi les agents ajoutent sans cesse des chaînes codées en dur explique pourquoi cela se reproduit : l’agent suit le motif visible, et le motif visible, c’est le composant. Si vous utilisez Copilot, les limites des fichiers d’instructions de Copilot s’appliquent aussi ici.

Qu’est-ce que cela change pour votre catalogue ?

Cela ajoute des clés, et c’est là que la dérive commence. Chaque élément ci-dessus fait entrer de nouvelles entrées dans le catalogue, comme Metadata, Error, Subscribe et les messages de validation, et chacune demande une traduction dans chaque locale livrée. Un projet qui traduit ses composants une fois puis ajoute plus tard une Server Action ou un nouveau titre de page, c’est le projet décrit dans pourquoi les fichiers de traduction se désynchronisent.

next-intl sert les traductions à l’exécution et nous ne le remplaçons pas. globalize.now se place une couche au-dessus : vous connectez le dépôt dans l’app, la base de code est convertie une fois en catalogue, puis des jobs de push traduisent les nouvelles unités du catalogue au fur et à mesure, livrées sous forme de changements que vous relisez. L’intégration Next.js décrit la mise en place, et ce que nous touchons ou non dans une configuration i18n existante couvre le cas où next-intl est déjà câblé à la main.

Par où commencer

Lancez les cinq grep, convertissez tout export metadata statique, ajoutez la route catch-all et donnez son provider à error.tsx. Vérifiez ensuite que le catalogue contient les nouveaux namespaces dans chaque locale. Si vous avez livré une app construite par IA et préférez que la conversion et la traduction soient prises en charge plutôt que maintenues, la page vibe coders en donne l’aperçu, la référence développeur couvre les configurations d’agent, et la tarification a sa propre page.

globalize.now transforme le texte codé en dur de votre application en fichiers de traduction prêts à l'emploi et les maintient à jour au fur et à mesure de vos déploiements.

Essayer globalize.now gratuitement