next-intl 国际化权威指南及使用说明

适用范围:Next.js 15 App Router + next-intl v4 · 更新于 2026-09-19

结论先行

next-intl 是 Next.js App Router 下官方推荐的国际化(i18n)方案,核心思路是「以 URL 为载体、以中间件为入口、以 React Context 为消息分发通道」。

语言切换失效(t("index") 不生效、根路径 404)几乎都源于两个缺位:

  1. 缺少 middleware.ts —— 路由层的语言检测与重写没有跑起来;
  2. layout.tsx 没有注入 NextIntlClientProvider / setRequestLocale —— 组件层拿不到消息上下文。

官方标准配置是固定五步(缺一不可),详见 next-intl 官方文档:

routing.ts → request.ts → navigation.ts → middleware.ts → [locale]/layout.tsx

只要这五步齐全,useTranslations 与 useLocale 即可在服务端/客户端组件中无缝工作。

问题背景

国际化最朴素的需求是:同一个页面,用户看中文,另一个用户看英文,URL 还要能区分、可分享。这就引出三个问题:

  1. 语言信息放在哪? URL 前缀(/en、/zh)、Cookie、请求头 Accept-Language 都可以。next-intl 默认以 URL 前缀 为准,最直观、可收藏、利于 SEO。
  2. 谁来解析这个前缀? 页面是 app/[locale]/page.tsx 这种带动态段的结构,[locale] 必须有个值。把 / 映射成「默认语言」、把 /en 映射成「English」,这个翻译动作由 middleware 完成。
  3. 文案怎么下发到组件? 几十个命名空间(Header、GlobalForm、KVS…)的 JSON 文案,通过 React Context(NextIntlClientProvider)注入,组件用 useTranslations("Header") 按命名空间取值。

很多项目「翻译文件写好了、t("index") 也写了,但就是不切」,往往卡在第 2、3 点——数据在,管道不通。

对小白友好的一句话:t() 只是「要一杯水」,但得有水管(middleware 决定走哪条路)和水龙头(layout 的 Provider 把水接到组件)。水桶(JSON 文案)是必要的,但不是充分的。

详细内容

一、整体架构:一条请求的完整旅程

浏览器请求 /en/xxx
        │
        ▼
┌─────────────────┐  检测 locale、按需重写 URL、必要时重定向
│   middleware.ts │  (createMiddleware(routing))
└────────┬────────┘
         ▼  重写为 /[locale]/xxx,locale = "en"
┌─────────────────┐  校验 locale、setRequestLocale、注入 Provider
│ [locale]/layout │  (NextIntlClientProvider 包住 children)
└────────┬────────┘
         ▼
┌─────────────────┐  读取消息 JSON,按 locale 返回 messages
│    request.ts   │  (getRequestConfig:requestLocale → messages)
└────────┬────────┘
         ▼
┌─────────────────┐  useTranslations("Header") / useLocale()
│   page.tsx 等   │  (服务端组件直接用;客户端组件经 Provider 用)
└─────────────────┘

五个文件各司其职,任何一个缺位,链路都会断。

二、标准配置五步

第 1 步:src/i18n/routing.ts —— 定义语言与 URL 策略

import {defineRouting} from "next-intl/routing";

export const routing = defineRouting({
    locales: ["en", "zh"],        // 支持的语言
    defaultLocale: "zh",          // 默认语言
    localePrefix: "as-needed",    // URL 前缀策略(关键配置)
});

localePrefix 决定「默认语言是否出现在 URL 里」,三种模式对比:

localePrefix 默认语言 zh 的 URL 非默认语言 en 的 URL 说明
always /zh /en 所有语言都带前缀,最统一,利于 SEO
as-needed /(不带前缀) /en 默认语言省前缀,其余带,本项目采用
never / / 完全靠 Cookie/请求头区分,URL 不可区分语言
⚠️ as-needed 下默认语言在 URL 中没有前缀,这要求 middleware 必须把裸路径 / 重写到 [locale] 段,否则根路径 404。这是最常见的翻车点。

第 2 步:src/i18n/request.ts —— 按 locale 加载消息

import {hasLocale} from "next-intl";
import {getRequestConfig} from "next-intl/server";
import {routing} from "@/i18n/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,
    };
});

第 3 步:src/i18n/navigation.ts —— 语言感知的路由 API

import {createNavigation} from "next-intl/navigation";
import {routing} from "@/i18n/routing";

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

组件里不要用 next/link 和 next/navigation 的原生 API,改用这里导出的 Link / useRouter,它们会自动补上正确的 locale 前缀。

第 4 步:src/middleware.ts —— 入口闸门(最容易漏)

import createMiddleware from "next-intl/middleware";
import {routing} from "@/i18n/routing";

export default createMiddleware(routing);

export const config = {
    matcher: "/((?!api|trpc|_next|_vercel|.*\\..*).*)",
};

它的三件事:

  1. 检测:根据 URL / Cookie / Accept-Language 判定当前 locale;
  2. 重写:把 URL 映射到 [locale] 动态段(/ → /[locale],/en/xxx → [locale]=en);
  3. 重定向:URL 缺失语言信息时补齐。

matcher 用来排除 _next 静态资源、api 路由、带扩展名的文件,避免误伤。

第 5 步:src/app/[locale]/layout.tsx —— 注入上下文(第二容易漏)

import {NextIntlClientProvider, hasLocale} from "next-intl";
import {setRequestLocale} from "next-intl/server";
import {notFound} from "next/navigation";
import {routing} from "@/i18n/routing";

export default async function RootLayout({children, params}) {
    const {locale} = await params;                 // Next 15 中 params 是 Promise
    if (!hasLocale(routing.locales, locale)) {
        notFound();                                 // 非法 locale 直接 404
    }
    setRequestLocale(locale);                       // 供 request.ts 使用

    return (
        <html lang={locale}>
        <body>
        <NextIntlClientProvider>{children}</NextIntlClientProvider>
        </body>
        </html>
    );
}

NextIntlClientProvider 不传 messages 时,会自动把 request 配置里的全部消息提供给客户端组件;服务端组件则直接经 context 取值。

三、本项目落地(multi-request 多消息分组)

本项目没有用标准单文件 request.ts,而是用 multi-request.ts 支持多消息分组,见 next.config.ts:

const withNextIntl = createNextIntlPlugin({
    requestConfig: "./src/common/i18n/multi-request.ts",
});

其核心逻辑:读 messages/list.json 声明的分组列表,逐个加载并合并:

// messages/list.json
["default"]

// multi-request.ts 核心
const i18nList = (await import(`../../../messages/list.json`)).default;
let messages = {};
for (const key in i18nList) {
    const path = i18nList[key];
    const message = await import(`../../../messages/${path}/${locale}.json`);
    if (path === "default") {
        messages = {...message.default};   // 默认分组:打平到顶层
        continue;
    }
    messages[path] = message.default;       // 非默认分组:按分组名命名空间化
}
注意:src/common/i18n/request.ts 是历史遗留的单文件版本(加载 messages/${locale}.json,路径已失效),当前未被 next.config.ts 引用,属废弃代码,可清理。

四、常见问题与排查表

现象 根因 修复
根路径 / 返回 404 缺少 middleware,as-needed 下默认语言无前缀无法映射 [locale] 补 src/middleware.ts
报错 Unable to find next-intl context layout 没包 NextIntlClientProvider 在 [locale]/layout.tsx 包 Provider
切换语言不生效 / 文案始终是默认语言 未 setRequestLocale(locale),requestLocale 取不到值 layout 里调用 setRequestLocale
非默认语言能访问、默认语言 404 前缀策略与 middleware 不匹配 确认 localePrefix 与 matcher 一致
t() 返回 key 本身或空 消息 JSON 路径/命名空间对不上 检查 messages/{分组}/{locale}.json 与 useTranslations 命名空间
客户端组件拿不到文案 Provider 未覆盖到该组件 确认组件在 NextIntlClientProvider 树内

五、总结归纳

  1. next-intl 是「URL 前缀 + 中间件 + Context」三位一体的方案,五个文件(routing / request / navigation / middleware / layout)缺一不可。
  2. 语言切换失效,优先怀疑 middleware 和 layout——这两处是「管道」,翻译文件只是「水桶」。
  3. localePrefix: "as-needed" 让默认语言省掉前缀,代价是必须依赖 middleware 重写裸路径。
  4. 本项目通过 multi-request.ts + messages/list.json 实现了多消息分组加载,默认分组打平、其余分组命名空间化,可平滑扩展。
  5. 排错按「路由 → 上下文 → 消息」三层逐段排查,几乎能覆盖所有 i18n 失效场景。

参考:next-intl 官方文档