Demandez aujourd'hui à Cursor ou Claude Code d'ajouter l'i18n à une application Next.js, et vous obtiendrez très probablement middleware.ts, setRequestLocale dans chaque layout, et useTranslations appelé depuis une page async — une configuration correcte pour Next.js 15, mais qui a une génération de retard sur Next.js 16. globalize.now est une infrastructure de localisation propulsée par l'IA, donc c'est exactement cette couche que nous surveillons : le câblage du routage a changé, le catalogue qu'il sert non. Voici ce qui a bougé, comment reconnaître de quelle époque provient votre code généré, et les trois correctifs qui prennent un après-midi plutôt qu'une réécriture complète.
Rien de tout cela n'est un changement cassant. C'est justement pour ça que c'est facile à manquer.
Qu'est-ce qui a changé dans Next.js 16 pour l'i18n ?
Le fichier qui négocie votre locale a changé de nom. Next.js documente la convention de fichier middleware comme dépréciée et renommée proxy, apparue dans la v16.0.0, et son explication est d'ordre lexical — « middleware » était constamment confondu avec le middleware Express, la fonctionnalité a donc été renommée pour décrire ce qu'elle est réellement : une frontière réseau.
Deux changements de comportement accompagnent ce renommage dans la v16. Proxy s'exécute par défaut sur le runtime Node.js, et l'option de configuration par fichier runtime n'y est plus disponible : la définir fait échouer Next.js. Proxy n'est pas non plus pris en charge en export statique, ce qui compte si votre négociation de locale est la seule raison qui vous empêche d'exporter.
Pour le routage de locale en particulier, le guide de configuration de next-intl indique désormais le fichier sous le nom src/proxy.ts et précise clairement qu'il s'appelait middleware.ts jusqu'à Next.js 16. L'import qu'il contient reste inchangé :
// src/proxy.ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};
Notez l'asymétrie, car c'est un piège classique : le fichier s'appelle désormais proxy.ts, tandis que le chemin d'import reste next-intl/middleware. Renommer l'import est une sur-correction fréquente.
Pourquoi le code i18n généré par IA sort-il avec une version de retard ?
Parce qu'un agent de codage prédit le motif le plus représenté, et le motif le plus représenté est celui qui a eu des années pour s'accumuler. Chaque article de blog, réponse Stack Overflow et exemple GitHub sur le routage de locale de l'App Router écrit avant fin 2026 parle de middleware.ts. Un modèle qui pèse ce corpus face à quelques semaines de documentation sur la v16 optera pour l'ancienne forme, avec assurance, sans aucun signal indiquant qu'un renommage a eu lieu.
C'est le même mécanisme derrière un problème dont nous avons déjà parlé, où Cursor continue d'ajouter des chaînes codées en dur après la configuration i18n — l'agent reproduit le code statistiquement normal, pas le vôtre. C'est aussi pourquoi les fichiers d'instructions ne corrigent que partiellement le problème pour GitHub Copilot : les règles lues par l'interface de chat ne sont pas forcément lues par la complétion en ligne.
La conséquence pratique est limitée mais bien réelle. Votre configuration générée fonctionne, donc rien n'échoue de façon visible. Puis vous tombez sur un bug, vous le cherchez, et chaque réponse actuelle décrit des fichiers que vous n'avez pas.
Comment savoir de quelle époque provient ma configuration générée ?
Quatre grep. Lancez-les depuis la racine du projet et vous saurez en une minute environ.
# 1. Pre-16 locale negotiation file
ls middleware.ts src/middleware.ts 2>/dev/null
# 2. Legacy static-rendering API
grep -rn "setRequestLocale" app src 2>/dev/null
# 3. Hooks called inside async components
grep -rn -B3 "useTranslations" app | grep -n "async function"
# 4. Which next-intl era the config reads from
grep -rn "root-params\|await params" src/i18n/request.ts 2>/dev/null
Des résultats sur 1 et 2 signalent une structure pré-16. Un résultat sur 3 est une véritable erreur d'exécution qui attend la première requête affichant ce composant. Aucun résultat sur 4 signifie que votre configuration de requête lit la locale à l'ancienne.
Dois-je renommer middleware.ts en proxy.ts ?
Pas dans l'urgence, et vous ne devriez pas le faire à la main. Next.js fournit un codemod qui renomme à la fois le fichier et la fonction exportée :
npx @next/codemod@canary middleware-to-proxy .
Le renommage est une dépréciation, pas une suppression, donc un middleware.ts existant continue de fonctionner. La raison de le faire quand même relève du coût de maintenance : une fois vos noms de fichiers alignés sur la documentation actuelle, chaque résultat de recherche futur s'applique à votre dépôt. Le laisser obsolète, c'est payer une petite taxe à chaque session de débogage, indéfiniment.
setRequestLocale est-il déprécié dans next-intl ?
Il est marqué comme historique, une affirmation plus nuancée que « déprécié » et qu'il vaut la peine de lire précisément. La documentation de next-intl décrit setRequestLocale comme une API qui existait avant l'introduction de next/root-params, indique qu'elle reste prise en charge pour des raisons de rétrocompatibilité, et recommande next/root-params à la place.
La forme la plus récente lit la locale correspondante directement dans votre configuration de requête, plutôt que de la faire transiter manuellement à travers chaque layout et chaque page :
// src/i18n/request.ts
import * as rootParams from 'next/root-params';
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';
export default getRequestConfig(async ({locale}) => {
if (!locale) {
const paramValue = await rootParams.locale();
if (hasLocale(routing.locales, paramValue)) {
locale = paramValue;
} else {
notFound();
}
}
return {locale};
});
next/root-params est disponible par défaut à partir de Next.js 16.3 ; sur des versions antérieures, il faut l'activer via experimental.rootParams. Suivez la configuration de cette façon et le rendu statique devient gratuit, tant que vous continuez d'exporter generateStaticParams pour le segment [locale].
L'ancienne approche imposait d'appeler setRequestLocale dans chaque page et layout que vous vouliez rendre statiquement, avant tout autre appel next-intl, car Next.js rend layouts et pages indépendamment. C'est une règle qu'un agent IA oublie dès le cinquième fichier qu'il écrit. Supprimer cette exigence supprime toute une catégorie de bugs.
Pourquoi useTranslations plante-t-il dans mon composant serveur asynchrone ?
Parce que les hooks ne peuvent pas être appelés depuis des composants async, et useTranslations en est un. C'est une contrainte des React Server Components, pas une bizarrerie de next-intl, et la réponse de next-intl est un ensemble parallèle de fonctions pouvant être attendues :
// Async component — await the server API
import {getTranslations} from 'next-intl/server';
export default async function ProfilePage() {
const user = await fetchUser();
const t = await getTranslations('ProfilePage');
return <h1>{t('title', {username: user.name})}</h1>;
}
// Non-async component — the hook is correct here
import {useTranslations} from 'next-intl';
export default function UserDetails({user}) {
const t = useTranslations('UserProfile');
return <h2>{t('title')}</h2>;
}
getFormatter, getNow, getTimeZone, getMessages et getLocale suivent le même principe. Le deuxième exemple mérite qu'on s'y attarde : un composant non asynchrone sans fonctionnalité interactive est un composant partagé, et next-intl résout la bonne implémentation selon qu'il s'affiche côté serveur ou côté client. Donc useTranslations dans un composant serveur n'est pas une erreur — l'appeler depuis un composant async en est une.
Pourquoi est-ce que j'obtiens l'erreur de contexte NextIntlClientProvider ?
Les notes de dépannage de next-intl donnent deux causes, et elles appellent des correctifs opposés. Soit le composant s'exécute réellement côté client sans provider au-dessus de lui, auquel cas vous l'enveloppez et lui passez les messages nécessaires, soit il a dérivé dans un graphe de modules client alors que vous attendiez un rendu serveur, auquel cas transmettez-le via children depuis un composant serveur plutôt que de l'importer à l'intérieur d'un tel composant.
Le second cas est celui que rencontrent les applications générées par IA, car les agents appliquent 'use client' généreusement pour faire fonctionner l'interactivité, et la directive est contagieuse tout au long du graphe d'import. Le motif préféré est de traduire côté serveur et de transmettre des chaînes déjà finalisées de l'autre côté de la frontière :
import {useTranslations} from 'next-intl';
import Expandable from './Expandable'; // 'use client'
export default function FAQEntry() {
const t = useTranslations('FAQEntry');
return <Expandable title={t('title')}>{t('description')}</Expandable>;
}
Si un composant a réellement besoin des messages côté client, vous pouvez limiter la portée d'un provider à ce seul sous-arbre plutôt que d'envoyer tous les messages au navigateur — messages={null} sur le provider racine n'en transmet aucun.
Qu'est-ce que rien de tout cela ne change ?
Vos fichiers de locale. Chaque correctif ci-dessus relève du câblage de routage et de rendu ; le JSON que charge votre application n'est touché par rien de tout cela. C'est justement le point à retenir : le câblage est un travail d'un après-midi qui change une fois par an, tandis que le catalogue est ce qui se dégrade chaque semaine à mesure que de nouvelles interfaces sont livrées.
C'est exactement la frontière pour laquelle nous construisons. next-intl et ses voisins servent les traductions à l'exécution — nous ne les concurrençons pas, et si vous hésitez encore entre eux, next-intl contre react-i18next contre Lingui expose les compromis. globalize.now se situe une couche au-dessus, en produisant les clés et les fichiers de locale que lisent ces runtimes, ce qui explique pourquoi l'intégration Next.js est indifférente au fait que votre négociation vive dans middleware.ts ou proxy.ts.
La conversion est ponctuelle et se déroule dans l'application : vous connectez le dépôt, globalize.now convertit le code une fois et ouvre une pull request avec le catalogue. Ensuite, des jobs de push traduisent les nouvelles unités de catalogue à mesure qu'elles apparaissent — c'est exactement le mode de défaillance décrit dans pourquoi les fichiers de traduction n'arrêtent pas de dériver, et la raison pour laquelle il vaut mieux combler cet écart avant votre troisième locale, pas après.
Si votre projet dispose déjà d'une configuration next-intl écrite à la main, nous avons détaillé exactement ce que nous touchons et ce que nous ne touchons pas. Sinon, le guide pas à pas Cursor est le chemin le plus court — avec cette réserve que son fichier généré porte le nom de l'ancienne convention, donc lancez le codemod après.
Par où commencer
Lancez les quatre commandes grep. Si vous obtenez des résultats sur les deux premiers, exécutez le codemod, déplacez votre configuration de requête vers next/root-params, et vous êtes à jour. Regardez ensuite les fichiers de locale, car c'est la partie qui continuera de dériver le mois prochain. Si vous livrez une application construite par IA et voulez que le catalogue soit géré plutôt que maintenu à la main, la page vibe coders en est l'aperçu et la tarification figure sur 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