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
Internationalizing (i18n) with next-intl to make your Next.js 15 app multilingual
This guide walks you through next-intl best practices in a Next.js 15 (App Router) app, and shows how to layer Intlayer on top for robust translation management and automation.
See the comparison in next-i18next vs next-intl vs Intlayer.
- For juniors: follow step-by-step sections to get a working multilingual app.
- For mid-level devs: pay attention to payload optimization and server/client separation.
- For seniors: note static generation, middleware, SEO integration, and automation hooks.
What we’ll cover:
- Setup and file structure
- Optimizing how messages are loaded
- Client and server component usage
- Metadata, sitemap, robots for SEO
- Middleware for locale routing
- Adding Intlayer on top (CLI and automation)
Set up your application using next-intl
Install the next-intl dependencies:
npm install next-intl
.├── locales│ ├── en│ │ ├── common.json│ │ └── about.json│ ├── fr│ │ ├── common.json│ │ └── about.json│ └── es│ ├── common.json│ └── about.json└── src ├── i18n.ts ├── middleware.ts ├── app │ └── [locale] │ ├── layout.tsx │ └── about │ └── page.tsx └── components ├── ClientComponentExample.tsx └── ServerComponent.tsx
Setup and Loading Content
Load only the namespaces your routes need and validate locales early. Keep server components synchronous when possible and push only the required messages to the client.
Copier le code dans le presse-papiers
import { getRequestConfig } from "next-intl/server";import { notFound } from "next/navigation";export const locales = ["en", "fr", "es"] as const;export const defaultLocale = "en" as const;async function loadMessages(locale: string) { // Load only the namespaces your layout/pages need const [common, about] = await Promise.all([ import(`../locales/${locale}/common.json`).then((m) => m.default), import(`../locales/${locale}/about.json`).then((m) => m.default), ]); return { common, about } as const;}export default getRequestConfig(async ({ locale }) => { if (!locales.includes(locale as any)) notFound(); return { messages: await loadMessages(locale), };});
Copier le code dans le presse-papiers
import type { ReactNode } from "react";import { locales } from "@/i18n";import { getLocaleDirection, unstable_setRequestLocale,} from "next-intl/server";export const dynamic = "force-static";export function generateStaticParams() { return locales.map((locale) => ({ locale }));}export default async function LocaleLayout({ children, params,}: { children: ReactNode; params: { locale: string };}) { const { locale } = params; // Set the active request locale for this server render (RSC) unstable_setRequestLocale(locale); const dir = getLocaleDirection(locale); return ( <html lang={locale} dir={dir}> <body>{children}</body> </html> );}
Copier le code dans le presse-papiers
import { getTranslations, getMessages, getFormatter } from "next-intl/server";import { NextIntlClientProvider } from "next-intl";import pick from "lodash/pick";import ServerComponent from "@/components/ServerComponent";import ClientComponentExample from "@/components/ClientComponentExample";export const dynamic = "force-static";export default async function AboutPage({ params,}: { params: { locale: string };}) { const { locale } = params; // Messages are loaded server-side. Push only what's needed to the client. const messages = await getMessages(); const clientMessages = pick(messages, ["common", "about"]); // Strictly server-side translations/formatting const tAbout = await getTranslations("about"); const tCounter = await getTranslations("about.counter"); const format = await getFormatter(); const initialFormattedCount = format.number(0); return ( <NextIntlClientProvider locale={locale} messages={clientMessages}> <main> <h1>{tAbout("title")}</h1> <ClientComponentExample /> <ServerComponent formattedCount={initialFormattedCount} label={tCounter("label")} increment={tCounter("increment")} /> </main> </NextIntlClientProvider> );}
Usage in a client component
Let's take an example of a client component rendering a counter.
Translations (shape reused; load them into next-intl messages as you prefer)
Copier le code dans le presse-papiers
{ "counter": { "label": "Counter", "increment": "Increment" }}
Copier le code dans le presse-papiers
{ "counter": { "label": "Compteur", "increment": "Incrémenter" }}
Client component
Copier le code dans le presse-papiers
"use client";import React, { useState } from "react";import { useTranslations, useFormatter } from "next-intl";const ClientComponentExample = () => { // Scope directly to the nested object const t = useTranslations("about.counter"); const format = useFormatter(); const [count, setCount] = useState(0); return ( <div> <p>{format.number(count)}</p> <button aria-label={t("label")} onClick={() => setCount((count) => count + 1)} > {t("increment")} </button> </div> );};
Don't forget to add "about" message on the page client message (only include the namespaces your client actually needs).
Usage in a server component
This UI component is a server component and can be rendered under a client component (page → client → server). Keep it synchronous by passing precomputed strings.
Copier le code dans le presse-papiers
type ServerComponentProps = { formattedCount: string; label: string; increment: string;};const ServerComponent = ({ formattedCount, label, increment,}: ServerComponentProps) => { return ( <div> <p>{formattedCount}</p> <button aria-label={label}>{increment}</button> </div> );};
Notes:
- Compute formattedCount server-side (e.g., const initialFormattedCount = format.number(0)).
- Avoid passing functions or non-serializable objects into server components.
Copier le code dans le presse-papiers
import type { Metadata } from "next";import { locales, defaultLocale } from "@/i18n";import { getTranslations } from "next-intl/server";function localizedPath(locale: string, path: string) { return locale === defaultLocale ? path : "/" + locale + path;}export async function generateMetadata({ params,}: { params: { locale: string };}): Promise<Metadata> { const { locale } = params; const t = await getTranslations({ locale, namespace: "about" }); const url = "/about"; const languages = Object.fromEntries( locales.map((locale) => [locale, localizedPath(locale, url)]) ); return { title: t("title"), description: t("description"), alternates: { canonical: localizedPath(locale, url), languages: { ...languages, "x-default": url }, }, };}// ... Rest of the page code
Copier le code dans le presse-papiers
import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const formatterLocalizedPath = (locale: string, path: string) => locale === defaultLocale ? origin + path : origin + "/" + locale + path;export default function sitemap(): MetadataRoute.Sitemap { const aboutLanguages = Object.fromEntries( locales.map((l) => [l, formatterLocalizedPath(l, "/about")]) ); return [ { url: formatterLocalizedPath(defaultLocale, "/about"), lastModified: new Date(), changeFrequency: "monthly", priority: 0.7, alternates: { languages: aboutLanguages }, }, ];}
Copier le code dans le presse-papiers
import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const withAllLocales = (path: string) => [ path, ...locales .filter((locale) => locale !== defaultLocale) .map((locale) => "/" + locale + path),];export default function robots(): MetadataRoute.Robots { const disallow = [ ...withAllLocales("/dashboard"), ...withAllLocales("/admin"), ]; return { rules: { userAgent: "*", allow: ["/"], disallow }, host: origin, sitemap: origin + "/sitemap.xml", };}
Middleware for locale routing
Add a middleware to handle locale detection and routing:
Copier le code dans le presse-papiers
import createMiddleware from "next-intl/middleware";import { locales, defaultLocale } from "@/i18n";export default createMiddleware({ locales: [...locales], defaultLocale, localeDetection: true,});export const config = { // Skip API, Next internals and static assets matcher: ["/((?!api|_next|.*\\..*).*)"],};
Best practices
- Set html lang and dir: In src/app/[locale]/layout.tsx, compute dir via getLocaleDirection(locale) and set <html lang={locale} dir={dir}>.
- Split messages by namespace: Organize JSON per locale and namespace (e.g., common.json, about.json).
- Minimize client payload: On pages, send only required namespaces to NextIntlClientProvider (e.g., pick(messages, ['common', 'about'])).
- Prefer static pages: Export export const dynamic = 'force-static' and generate static params for all locales.
- Synchronous server components: Pass precomputed strings (translated labels, formatted numbers) rather than async calls or non-serializable functions.
Implement Intlayer on top of next-intl
Install the intlayer dependencies:
npm install intlayer @intlayer/sync-json-plugin -D
Create the intlayer configuration file:
Copier le code dans le presse-papiers
import { type IntlayerConfig, Locales } from "intlayer";import { syncJSON } from "@intlayer/sync-json-plugin";const config: IntlayerConfig = { internationalization: { locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH], defaultLocale: Locales.ENGLISH, }, ai: { apiKey: process.env.OPENAI_API_KEY, }, plugins: [ // Keep your per-namespace folder structure in sync with Intlayer syncJSON({ source: ({ key, locale }) => `./locales/${locale}/${key}.json`, }), ],};export default config;
Add package.json scripts:
Copier le code dans le presse-papiers
{ "scripts": { "i18n:fill": "intlayer fill", "i18n:test": "intlayer test" }}
Notes:
- intlayer fill: uses your AI provider to fill missing translations based on your configured locales.
- intlayer test: checks for missing/invalid translations (use it in CI).
You can configure arguments and providers; see Intlayer CLI.