使用您最喜欢的AI助手总结文档,并引用此页面和AI提供商
通过将 Intlayer MCP 服务器集成到您的 AI 助手,您可以直接从 ChatGPT、DeepSeek、Cursor、VSCode 等获取所有文档。
查看 MCP 服务器文档此页面的内容已使用 AI 翻译。
查看英文原文的最新版本如果您有改善此文档的想法,请随时通过在GitHub上提交拉取请求来贡献。
文档的 GitHub 链接复制文档 Markdown 到剪贴板
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.
复制代码到剪贴板
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), };});
复制代码到剪贴板
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> );}
复制代码到剪贴板
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)
复制代码到剪贴板
{ "counter": { "label": "Counter", "increment": "Increment" }}
复制代码到剪贴板
{ "counter": { "label": "Compteur", "increment": "Incrémenter" }}
Client component
复制代码到剪贴板
"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.
复制代码到剪贴板
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.
复制代码到剪贴板
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
复制代码到剪贴板
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 }, }, ];}
复制代码到剪贴板
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:
复制代码到剪贴板
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:
复制代码到剪贴板
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:
复制代码到剪贴板
{ "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.