Use Cases (Mis à jour: 02/06/2026)

Implémenter l'i18n avec Claude Code : guide Next.js pratique

Méthode concrète pour ajouter l'i18n à Next.js avec Claude Code, next-intl, contrôles de traduction et pièges courants.

Implémenter l'i18n avec Claude Code : guide Next.js pratique

L’i18n commence par une décision d’architecture

i18n signifie internationalisation : préparer une application pour plusieurs langues et régions. En production, ce n’est pas seulement une liste de fichiers JSON traduits. Il faut définir les URL, la détection de langue, les metadata SEO, les formats de dates et de devises, les règles de pluriel, les liens internes et le contrôle des traductions manquantes. Claude Code est utile parce qu’il peut lire le dépôt, modifier plusieurs fichiers, lancer des commandes et expliquer les risques restants. Mais il faut lui donner une cible claire.

L’erreur que Masa a souvent vue sur des sites de contenu consiste à demander trop vite « traduis tout ». Le résultat a l’air complet, puis on découvre que les CTA, les prix, les descriptions et les URL ne suivent pas la même règle. Une approche plus robuste consiste à demander à Claude Code le flux complet : base de routage, fichiers de messages, migration des pages, script de vérification et notes de revue.

Ce guide utilise Next.js App Router avec next-intl. Pendant la revue, gardez les références officielles ouvertes : documentation Claude Code, configuration du routage next-intl, guide d’internationalisation Next.js et référence Intl de MDN. Elles évitent de copier un exemple ancien, notamment autour de proxy.ts et middleware.ts.

flowchart LR
  A[Besoins] --> B[Routage]
  B --> C[Messages JSON]
  C --> D[Pages et metadata]
  D --> E[Contrôles]
  E --> F[SEO et publication]

Base Next.js App Router avec next-intl

L’exemple utilise ja comme langue par défaut et ajoute en et de. Avec Next.js 16, le fichier de proxy est proxy.ts. Dans un projet plus ancien, le même rôle peut être joué par middleware.ts; Claude Code doit vérifier la version installée avant de modifier le dépôt.

src/
  app/
    [locale]/
      layout.tsx
      page.tsx
  i18n/
    navigation.ts
    request.ts
    routing.ts
  messages/
    ja.json
    en.json
    de.json
  proxy.ts

Le fichier routing.ts centralise les langues et les chemins. Cela rend les changements d’URL lisibles en pull request.

// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['ja', 'en', 'de'],
  defaultLocale: 'ja',
  pathnames: {
    '/': '/',
    '/pricing': {
      ja: '/pricing',
      en: '/pricing',
      de: '/preise',
    },
    '/docs': {
      ja: '/docs',
      en: '/docs',
      de: '/dokumentation',
    },
  },
});

export type Locale = (typeof routing.locales)[number];

Les wrappers de navigation évitent de mélanger des liens conscients de la locale avec des liens bruts.

// src/i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation';
import { routing } from './routing';

export const { Link, redirect, usePathname, useRouter, getPathname } =
  createNavigation(routing);

La configuration de requête lit la locale demandée, vérifie qu’elle est supportée et charge les messages correspondants.

// src/i18n/request.ts
import { hasLocale } from 'next-intl';
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';

export default getRequestConfig(async ({ requestLocale }) => {
  const requested = await requestLocale;
  const locale = hasLocale(routing.locales, requested)
    ? requested
    : routing.defaultLocale;

  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default,
  };
});
// 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|.*\\..*).*)',
};

Relier les clés aux vraies pages

Ne mettez pas tous les textes dans common. Ce fichier doit rester réservé aux éléments réellement partagés : navigation, sélecteur de langue, boutons génériques. Les textes d’une page doivent aller dans un namespace comme HomePage, PricingPage ou DocsPage. Un namespace est simplement un groupe de clés liées au même écran.

{
  "common": {
    "language": {
      "label": "Langue d'affichage",
      "ja": "日本語",
      "en": "English",
      "de": "Deutsch"
    },
    "nav": {
      "docs": "Documentation",
      "pricing": "Tarifs"
    }
  },
  "HomePage": {
    "title": "Diffusez les connaissances de l'équipe en plusieurs langues",
    "lead": "Affichez {count} articles dans la langue du lecteur.",
    "cta": "Réserver un échange"
  }
}

Dans une page App Router, getTranslations fonctionne bien dans un Server Component et prépare aussi la traduction de generateMetadata.

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

type Props = {
  params: Promise<{ locale: string }>;
};

export default async function HomePage({ params }: Props) {
  const { locale } = await params;
  setRequestLocale(locale);
  const t = await getTranslations({ locale, namespace: 'HomePage' });

  return (
    <main>
      <h1>{t('title')}</h1>
      <p>{t('lead', { count: 42 })}</p>
      <a href={`/${locale}/pricing`}>{t('cta')}</a>
    </main>
  );
}

Le sélecteur de langue doit conserver le chemin courant et changer uniquement la locale. Une substitution manuelle comme remplacer /en par /fr peut casser une URL qui contient ces lettres ailleurs.

Bloquer les traductions manquantes en CI

Quand Claude Code génère des messages, les humains repèrent souvent le style maladroit mais pas toujours une clé absente. Le script suivant compare la langue de référence avec les autres fichiers.

// scripts/check-translations.mjs
import { readdir, readFile } from 'node:fs/promises';

const messagesDir = new URL('../src/messages/', import.meta.url);
const baseLocale = 'ja';

function flattenKeys(value, prefix = '') {
  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
    return [prefix];
  }

  return Object.entries(value).flatMap(([key, child]) => {
    const nextPrefix = prefix ? `${prefix}.${key}` : key;
    return flattenKeys(child, nextPrefix);
  });
}

async function readMessages(locale) {
  const file = new URL(`${locale}.json`, messagesDir);
  return JSON.parse(await readFile(file, 'utf8'));
}

const files = await readdir(messagesDir);
const locales = files.filter((file) => file.endsWith('.json')).map((file) => file.replace(/\.json$/, ''));
const baseKeys = new Set(flattenKeys(await readMessages(baseLocale)));
let hasError = false;

for (const locale of locales.filter((item) => item !== baseLocale)) {
  const targetKeys = new Set(flattenKeys(await readMessages(locale)));
  const missing = [...baseKeys].filter((key) => !targetKeys.has(key));
  const extra = [...targetKeys].filter((key) => !baseKeys.has(key));

  if (missing.length || extra.length) {
    hasError = true;
    console.error(`\n${locale}.json has translation key drift`);
    if (missing.length) console.error('Missing:', missing.join(', '));
    if (extra.length) console.error('Extra:', extra.join(', '));
  }
}

if (hasError) process.exit(1);
console.log(`Translation keys are aligned for ${locales.length} locales.`);

Ce contrôle ne juge pas la qualité littéraire. Il garantit simplement que le contrat de clés est complet. C’est suffisant pour éviter qu’un bouton ou un titre disparaisse dans une locale au moment de publier.

Trois cas d’usage concrets

Le premier cas est une page de tarifs SaaS. Elle mélange devise, taxes, période de facturation et noms de plans. Une traduction littérale ne suffit pas. Demandez à Claude Code d’utiliser Intl.NumberFormat ou les formatters de next-intl, puis relisez l’ordre naturel de la phrase pour chaque marché.

Le deuxième cas est une documentation technique. Les termes middleware, proxy, locale et namespace ne doivent pas tous être traduits. Expliquez-les simplement à la première occurrence, puis conservez le terme original pour que le lecteur puisse chercher dans la documentation officielle.

Le troisième cas est une interface d’administration. Le risque principal est l’action irréversible. Les libellés “supprimer”, “désactiver” ou “révoquer une invitation” doivent être précis. Incluez boutons, modales de confirmation, notifications et journaux d’audit dans le même système de clés.

Pour un blog multilingue, traduire le corps de l’article ne suffit pas. Title, description, canonical, liens alternatifs, image OGP et liens internes doivent être vérifiés ensemble. Documentez cette règle dans CLAUDE.md best practices pour que Claude Code applique le même processus à chaque mise à jour.

Pièges courants et résultat testé

PiègeEffetSolution
Décider les URL trop tardRedirections confuses après publicationChoisir prefix, sous-domaine ou domaine au départ
Clés trop vaguestitle2 et text3 deviennent impossibles à relireNommer par écran et par sens
Trop compter sur le fallbackLes traductions absentes sont masquéesFaire échouer la CI en cas de clé manquante
Concaténer dates et montantsOrdre et séparateurs incorrectsUtiliser Intl ou les formatters
Oublier metadataLes résultats de recherche restent dans la langue par défautTraduire generateMetadata

J’ai vérifié le script sur un petit jeu de messages en supprimant HomePage.cta du fichier anglais. Le script a échoué et a indiqué la clé manquante. Il ne peut pas décider si “Réserver un échange” convient culturellement à tous les marchés. La bonne répartition est donc claire : Claude Code et la CI détectent les oublis mécaniques, les humains relisent CTA, prix, juridique et nuances culturelles. Pour aller plus loin, combinez ce flux avec la checklist de revue Claude Code ou la page de consultation ClaudeCodeLab.

#Claude Code #i18n #internationalisation #Next.js #next-intl
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.