Posez votre question et obtenez un résumé du document en referencant cette page et le Provider AI de votre choix
En intégrant le serveur MCP Intlayer à votre assistant AI, vous pouvez récupérer tous les documents directement depuis ChatGPT, DeepSeek, Cursor, VSCode, etc.
Voir la documentation du serveur MCPLe contenu de cette page a été traduit à l'aide d'une IA.
Voir la dernière version du contenu original en anglaisSi vous avez une idée d’amélioration pour améliorer cette documentation, n’hésitez pas à contribuer en submitant une pull request sur GitHub.
Lien GitHub de la documentationCopier le Markdown du doc dans le presse-papiers
Traduisez votre site Next.js 15 utilisant next-i18next avec Intlayer | Internationalisation (i18n)
À qui s'adresse ce guide
- Junior : Suivez les étapes exactes et copiez les blocs de code. Vous obtiendrez une application multilingue fonctionnelle.
- Intermédiaire : Utilisez les checklists et les conseils de bonnes pratiques pour éviter les pièges courants.
- Senior : Parcourez la structure générale, les sections SEO et automatisation ; vous y trouverez des valeurs par défaut pertinentes et des points d'extension.
Ce que vous allez construire
- Projet App Router avec des routes localisées (ex. : /, /fr/...)
- Configuration i18n avec locales, locale par défaut, support RTL
- Initialisation i18n côté serveur et un provider côté client
- Traductions avec namespaces chargées à la demande
- SEO avec hreflang, sitemap localisé, robots
- Middleware pour le routage selon la locale
- Intégration Intlayer pour automatiser les workflows de traduction (tests, remplissage IA, synchronisation JSON)
Note : next-i18next est construit sur i18next. Ce guide utilise les primitives i18next compatibles avec next-i18next dans l’App Router, tout en gardant une architecture simple et prête pour la production. Pour une comparaison plus large, voir next-i18next vs next-i18next vs Intlayer.
1) Structure du projet
Installez les dépendances next-i18next :
npm install next-i18next i18next react-i18next i18next-resources-to-backendCommencez avec une structure claire. Gardez les messages séparés par locale et namespace.
.├── i18n.config.ts└── src ├── locales │ ├── en │ │ ├── common.json │ │ └── about.json │ └── fr │ ├── common.json │ └── about.json ├── app │ ├── i18n │ │ └── server.ts │ └── [locale] │ ├── layout.tsx │ └── about.tsx └── components ├── I18nProvider.tsx ├── ClientComponent.tsx └── ServerComponent.tsxChecklist (intermédiaire/senior) :
- Gardez un JSON par namespace et par locale
- Ne centralisez pas trop les messages ; utilisez des namespaces petits, spécifiques à une page ou une fonctionnalité
- Évitez d’importer toutes les locales en même temps ; chargez uniquement ce dont vous avez besoin
2) Installer les dépendances
bashpnpm add i18next react-i18next i18next-resources-to-backendSi vous prévoyez d'utiliser les APIs ou la configuration interop de next-i18next, ajoutez également :
pnpm add next-i18next3) Configuration i18n principale
Définissez les locales, la locale par défaut, les langues RTL, et les helpers pour les chemins/URLs localisés.
Copier le code dans le presse-papiers
export const locales = ["en", "fr"] as const;export type Locale = (typeof locales)[number];export const defaultLocale: Locale = "en";export const rtlLocales = ["ar", "he", "fa", "ur"] as const;export const isRtl = (locale: string) => (rtlLocales as readonly string[]).includes(locale);export function localizedPath(locale: string, path: string) { return locale === defaultLocale ? path : "/" + locale + path;}const ORIGIN = "https://example.com";export function abs(locale: string, path: string) { return ORIGIN + localizedPath(locale, path);}Note importante : Si vous utilisez next-i18next.config.js, assurez-vous qu'il soit aligné avec i18n.config.ts pour éviter toute dérive.
4) Initialisation i18n côté serveur
Initialisez i18next sur le serveur avec un backend dynamique qui importe uniquement le JSON de locale/espace de noms requis.
Copier le code dans le presse-papiers
import { createInstance } from "i18next";import { initReactI18next } from "react-i18next/initReactI18next";import resourcesToBackend from "i18next-resources-to-backend";import { defaultLocale } from "@/i18n.config";// Charger les ressources JSON depuis src/locales/<locale>/<namespace>.jsonconst backend = resourcesToBackend( (locale: string, namespace: string) => import(`../../locales/${locale}/${namespace}.json`));export async function initI18next( locale: string, namespaces: string[] = ["common"]) { const i18n = createInstance(); await i18n .use(initReactI18next) .use(backend) .init({ lng: locale, fallbackLng: defaultLocale, ns: namespaces, defaultNS: "common", interpolation: { escapeValue: false }, react: { useSuspense: false }, }); return i18n;}Note intermédiaire : Gardez la liste des namespaces courte par page pour limiter la charge. Évitez les bundles globaux « attrape-tout ».
5) Fournisseur client pour les composants React
Encapsulez les composants client avec un provider qui reflète la configuration serveur et charge uniquement les namespaces demandés.
Copier le code dans le presse-papiers
"use client";import * as React from "react";import { I18nextProvider } from "react-i18next";import { createInstance } from "i18next";import { initReactI18next } from "react-i18next/initReactI18next";import resourcesToBackend from "i18next-resources-to-backend";import { defaultLocale } from "@/i18n.config";const backend = resourcesToBackend( (locale: string, namespace: string) => import(`../../locales/${locale}/${namespace}.json`));type Props = { locale: string; namespaces?: string[]; resources?: Record<string, any>; // { ns: bundle } children: React.ReactNode;};export default function I18nProvider({ locale, namespaces = ["common"], resources, children,}: Props) { const [i18n] = React.useState(() => { const i = createInstance(); i.use(initReactI18next) .use(backend) .init({ lng: locale, fallbackLng: defaultLocale, ns: namespaces, resources: resources ? { [locale]: resources } : undefined, defaultNS: "common", interpolation: { escapeValue: false }, react: { useSuspense: false }, }); return i; }); return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;}Astuce pour débutant : Vous n'avez pas besoin de transmettre tous les messages au client. Commencez uniquement avec les namespaces de la page.
6) Mise en page et routes localisées
Définissez la langue et la direction, et pré-générez les routes par locale pour favoriser le rendu statique.
Copier le code dans le presse-papiers
import type { ReactNode } from "react";import { locales, defaultLocale, isRtl, type Locale } from "@/i18n.config";export const dynamicParams = false;export function generateStaticParams() { return locales.map((locale) => ({ locale }));}export default function LocaleLayout({ children, params,}: { children: ReactNode; params: { locale: string };}) { const locale: Locale = (locales as readonly string[]).includes(params.locale) ? (params.locale as any) : defaultLocale; const dir = isRtl(locale) ? "rtl" : "ltr"; return ( <html lang={locale} dir={dir}> <body>{children}</body> </html> );}7) Exemple de page avec utilisation serveur + client
Copier le code dans le presse-papiers
import I18nProvider from "@/components/I18nProvider";import { initI18next } from "@/app/i18n/server";import type { Locale } from "@/i18n.config";import ClientComponent from "@/components/ClientComponent";import ServerComponent from "@/components/ServerComponent";// Forcer le rendu statique pour la pageexport const dynamic = "force-static";export default async function AboutPage({ params: { locale },}: { params: { locale: Locale };}) { const namespaces = ["common", "about"] as const; const i18n = await initI18next(locale, [...namespaces]); const tAbout = i18n.getFixedT(locale, "about"); return ( <I18nProvider locale={locale} namespaces={[...namespaces]}> <main> <h1>{tAbout("title")}</h1> <ClientComponent /> <ServerComponent t={tAbout} locale={locale} count={0} /> </main> </I18nProvider> );}Traductions (un JSON par namespace sous src/locales/...):
Copier le code dans le presse-papiers
{ "title": "À propos", "description": "Description de la page À propos", "counter": { "label": "Compteur", "increment": "Incrémenter" }}Copier le code dans le presse-papiers
{ "title": "À propos", "description": "Description de la page À propos", "counter": { "label": "Compteur", "increment": "Incrémenter" }}Composant client (charge uniquement le namespace requis) :
Copier le code dans le presse-papiers
"use client";import React, { useState } from "react";import { useTranslation } from "react-i18next";const ClientComponent = () => { const { t, i18n } = useTranslation("about"); const [count, setCount] = useState(0); const numberFormat = new Intl.NumberFormat(i18n.language); return ( <div> <p>{numberFormat.format(count)}</p> <button aria-label={t("counter.label")} onClick={() => setCount((c) => c + 1)} > {t("counter.increment")} </button> </div> );};export default ClientComponent;Assurez-vous que la page/le provider inclut uniquement les namespaces dont vous avez besoin (par exemple, about). Si vous utilisez React < 19, mémoïsez les formateurs lourds comme Intl.NumberFormat.
Composant serveur synchrone intégré sous une frontière client :
Copier le code dans le presse-papiers
type ServerComponentProps = { t: (key: string) => string; locale: string; count: number;};const ServerComponent = ({ t, locale, count }: ServerComponentProps) => { const formatted = new Intl.NumberFormat(locale).format(count); return ( <div> <p>{formatted}</p> <button aria-label={t("counter.label")}>{t("counter.increment")}</button> </div> );};export default ServerComponent;8) SEO : Métadonnées, Hreflang, Sitemap, Robots
La traduction du contenu est un moyen d'améliorer la portée. Configurez soigneusement le SEO multilingue.
Bonnes pratiques :
- Définir lang et dir à la racine
- Ajouter alternates.languages pour chaque locale (+ x-default)
- Lister les URLs traduites dans sitemap.xml et utiliser hreflang
- Exclure les zones privées localisées (ex. /fr/admin) dans robots.txt
Copier le code dans le presse-papiers
import type { Metadata } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n.config";export async function generateMetadata({ params,}: { params: { locale: string };}): Promise<Metadata> { const { locale } = params; // Importer le bon bundle JSON depuis src/locales const messages = (await import("@/locales/" + locale + "/about.json")) .default; const languages = Object.fromEntries( locales.map((locale) => [locale, localizedPath(locale, "/about")]) ); return { title: messages.title, description: messages.description, alternates: { canonical: localizedPath(locale, "/about"), languages: { ...languages, "x-default": "/about" }, }, };}export default async function AboutPage() { return <h1>À propos</h1>;}Copier le code dans le presse-papiers
import type { MetadataRoute } from "next";import { locales, defaultLocale, abs } from "@/i18n.config";export default function sitemap(): MetadataRoute.Sitemap { const languages = Object.fromEntries( locales.map((locale) => [locale, abs(locale, "/about")]) ); return [ { url: abs(defaultLocale, "/about"), lastModified: new Date(), changeFrequency: "monthly", priority: 0.7, alternates: { languages }, }, ];}Copier le code dans le presse-papiers
import type { MetadataRoute } from "next";import { locales, defaultLocale, localizedPath } from "@/i18n.config";const ORIGIN = "https://example.com";const expandAllLocales = (path: string) => [ localizedPath(defaultLocale, path), ...locales .filter((locale) => locale !== defaultLocale) .map((locale) => localizedPath(locale, path)),];export default function robots(): MetadataRoute.Robots { const disallow = [ ...expandAllLocales("/dashboard"), ...expandAllLocales("/admin"), ]; return { rules: { userAgent: "*", allow: ["/"], disallow }, host: ORIGIN, sitemap: ORIGIN + "/sitemap.xml", };}9) Middleware pour le routage des locales
Détecte la locale et redirige vers une route localisée si elle est manquante.
Copier le code dans le presse-papiers
import { NextResponse, type NextRequest } from "next/server";import { defaultLocale, locales } from "@/i18n.config";const PUBLIC_FILE = /\.[^/]+$/; // exclure les fichiers avec extensionsexport function middleware(request: NextRequest) { const { pathname } = request.nextUrl; if ( pathname.startsWith("/_next") || pathname.startsWith("/api") || pathname.startsWith("/static") || PUBLIC_FILE.test(pathname) ) { return; } const hasLocale = locales.some( (locale) => pathname === "/" + locale || pathname.startsWith("/" + locale + "/") ); if (!hasLocale) { const locale = defaultLocale; const url = request.nextUrl.clone(); url.pathname = "/" + locale + (pathname === "/" ? "" : pathname); return NextResponse.redirect(url); }}export const config = { matcher: [ // Correspond à tous les chemins sauf ceux commençant par ces préfixes et les fichiers avec une extension "/((?!api|_next|static|.*\\..*).*)", ],};10) Performance et bonnes pratiques DX
- Définir les attributs html lang et dir : Fait dans src/app/[locale]/layout.tsx.
- Diviser les messages par namespace : Garder les bundles petits (common.json, about.json, etc.).
- Minimiser la charge côté client : Sur les pages, passer uniquement les namespaces nécessaires au provider.
- Préférer les pages statiques : Utiliser export const dynamic = 'force-static' et generateStaticParams par locale.
- Synchroniser les composants serveur : Passer des chaînes/formatages pré-calculés au lieu d'appels asynchrones au moment du rendu.
- Mémoriser les opérations lourdes : Surtout dans le code client pour les anciennes versions de React.
- Cache et headers : Préférer le statique ou revalidate plutôt que le rendu dynamique quand c'est possible.
11) Tests et CI
- Ajouter des tests unitaires pour les composants utilisant t afin de garantir que les clés existent.
- Valider que chaque namespace possède les mêmes clés dans toutes les locales.
- Remonter les clés manquantes lors du CI avant le déploiement.
Intlayer automatisera une grande partie de cela (voir section suivante).
12) Ajouter Intlayer par-dessus (automatisation)
Intlayer vous aide à garder les traductions JSON synchronisées, à tester les clés manquantes, et à les compléter avec l'IA si désiré.
Installez les dépendances intlayer :
npm install intlayer @intlayer/sync-json-plugin -DCopier le code dans le presse-papiers
import { type IntlayerConfig, Locales } from "intlayer";import { locales, defaultLocale } from "@/i18n";import { syncJSON } from "@intlayer/sync-json";export const locales = [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH];const config: IntlayerConfig = { internationalization: { locales, defaultLocale, }, ai: { apiKey: process.env.OPENAI_API_KEY, }, plugins: [ syncJSON({ source: ({ locale }) => `./locales/${locale}.json`, }), ],};export default config;Ajouter des scripts dans le package :
Copier le code dans le presse-papiers
{ "scripts": { "i18n:fill": "intlayer fill", "i18n:test": "intlayer test" }}Flux courants :
- pnpm i18n:test en CI pour échouer la build en cas de clés manquantes
- pnpm i18n:fill localement pour proposer des traductions AI pour les clés nouvellement ajoutées
Vous pouvez fournir des arguments CLI ; consultez la documentation CLI d'Intlayer.
13) Dépannage
- Clés introuvables : Assurez-vous que la page/le provider liste les bons namespaces et que le fichier JSON existe sous src/locales/<locale>/<namespace>.json.
- Mauvaise langue/flash d’anglais : Vérifiez la détection de la locale dans middleware.ts et la propriété lng du provider.
- Problèmes de mise en page RTL : Vérifiez que dir est dérivé de isRtl(locale) et que votre CSS respecte [dir="rtl"].
- Alternatives SEO manquantes : Confirmez que alternates.languages inclut toutes les locales ainsi que x-default.
- Bundles trop volumineux : Scindez davantage les namespaces et évitez d’importer l’arborescence complète des locales côté client.
14) Et ensuite
- Ajouter plus de locales et de namespaces à mesure que les fonctionnalités évoluent
- Localiser les pages d’erreur, les emails et le contenu généré par API
- Étendre les workflows Intlayer pour ouvrir automatiquement des PRs pour les mises à jour de traduction
Si vous préférez un starter, essayez le template : https://github.com/aymericzip/intlayer-next-i18next-template.