Skip to content

国际化机制

AutoRouter 的管理面板基于 next-intl 实现 zh-CN 与 en 双语切换。所有面向用户的页面、组件文案、错误提示都通过翻译键引用 src/messages/ 下的 JSON 资源,URL 中始终携带 locale 前缀,切换语言走 Cookie + URL 改写两条通道。

文档站本身(即正在阅读的 VitePress 站点)有独立的 i18n 体系,与应用层 next-intl 完全解耦。本文档先讲应用层,再交代两者的边界。

依赖与版本

package.json:52

json
"next-intl": "^4.9.2"

配置文件分层

src/i18n/ 下四个文件按职责拆分:

文件职责
config.tslocale 常量(locales、defaultLocale、cookie 名)和 Locale 类型
routing.tsnext-intl 路由配置(前缀策略、cookie 参数)
request.ts服务端 getRequestConfig,按 locale 动态 import 翻译文件
navigation.ts导出 i18n-aware 的 Link / useRouter / usePathname 等导航工具

config.ts

ts
export const locales = ["zh-CN", "en"] as const;
export const defaultLocale: Locale = "zh-CN";
export const localeCookieName = "NEXT_LOCALE";
export const localeCookieMaxAge = 60 * 60 * 24 * 365;

export const localeNames: Record<Locale, string> = {
  "zh-CN": "简体中文",
  en: "English",
};

支持两种语言,默认中文,Cookie 名沿用 Next.js 约定的 NEXT_LOCALE,有效期一年。

routing.ts

ts
export const routing = defineRouting({
  locales,
  defaultLocale,
  localePrefix: "always",
  localeCookie: {
    name: localeCookieName,
    maxAge: localeCookieMaxAge,
    sameSite: "lax",
  },
});

localePrefix: "always" 表示所有路径都强制带 locale 段,即 /zh-CN/dashboard 而非 /dashboard。默认 locale 不享受省略特权。这种策略下 URL 总是显式表达语言,对外分享链接和后端日志的归类都更直接。

sameSite: "lax" 允许顶级导航跨站携带 Cookie,符合「用户从外站点开链接进来」的常见场景。

request.ts

ts
export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;

  if (!locale || !routing.locales.includes(locale as Locale)) {
    locale = routing.defaultLocale;
  }

  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default,
  };
});

服务端入口。requestLocale 由 next-intl 从当前请求的 URL 段解析;非法或缺失时回退到 defaultLocale,避免抛错。翻译资源走动态 import(),被 Next.js 打包时按语言切分 chunk。

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

封装出 i18n-aware 的导航 API。组件里用这套替代 next/link / next/navigation,跳转时自动维护 locale 前缀。

路由层

src/app/
├── layout.tsx          # 根 layout:HTML 结构、字体、不引入 next-intl
└── [locale]/
    ├── layout.tsx      # locale-aware layout:注入 NextIntlClientProvider
    ├── page.tsx        # 首页(locale 根)
    ├── (auth)/
    │   └── login/page.tsx
    └── (dashboard)/
        ├── layout.tsx
        ├── dashboard/
        ├── keys/
        ├── logs/
        ├── settings/
        ├── system/
        └── upstreams/

根 vs locale 分工

src/app/layout.tsx 只负责 HTML 骨架、字体变量、<html> 标签的 lang 属性,不引入 next-intl。所有 i18n 上下文集中在 src/app/[locale]/layout.tsx

ts
export function generateStaticParams() {
  return routing.locales.map((locale) => ({ locale }));
}

export default async function LocaleLayout({ children, params }: LocaleLayoutProps) {
  const { locale } = await params;

  if (!routing.locales.includes(locale as Locale)) {
    notFound();
  }

  setRequestLocale(locale);
  const messages = await getMessages();

  return (
    <ThemeProvider>
      <NextIntlClientProvider messages={messages}>
        <QueryProvider>
          <TooltipProvider>
            <AuthProvider>
              <div className="min-h-dvh bg-background text-foreground">{children}</div>
              <Toaster />
            </AuthProvider>
          </TooltipProvider>
        </QueryProvider>
      </NextIntlClientProvider>
    </ThemeProvider>
  );
}

关键点:

  • generateStaticParams 遍历 routing.locales,让 Next.js 为每种语言预生成静态参数
  • locale 不在白名单时调 notFound(),触发 404 页面而非静默回退
  • setRequestLocale(locale) 启用静态渲染优化,让下层 server component 在打包阶段就能确定 locale
  • getMessages() 间接调到 request.tsgetRequestConfig,把完整翻译资源批量注入给 NextIntlClientProvider,下层任何 client component 都能直接读取
  • (auth)(dashboard) 是路由分组(route group),不出现在 URL 中,仅在文件系统层组织代码

中间件

src/proxy.ts 是 Next.js 的 middleware 入口(文件名不是惯用的 middleware.ts,但 Next.js 同时支持 proxy.ts):

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

export default createMiddleware(routing);

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

matcher 排除三类路径:

模式含义
_nextNext.js 内部资源
api所有 API 路由
.*\\..*任何包含 . 的路径(静态文件,如 favicon.ico*.png

其余请求统一交给 next-intl 中间件处理:补 locale 前缀、读写 NEXT_LOCALE Cookie、按 Accept-Language 头协商默认语言。

API 路由完全不走 i18n 中间件,对外不暴露语言概念——API 返回的错误码统一英文,由前端按当前 locale 翻译展示。

翻译文件组织

src/messages/ 下两个 JSON:

文件行数
zh-CN.json1551
en.json1546

按功能 / 页面分 19 个顶层 namespace(按 en.json 出现顺序):

common · nav · repository · auth · dashboard · keys · logs · upstreams ·
circuitBreaker · errors · language · theme · system · billing ·
backgroundSync · trafficRecording · upstreamFailureRules · compensation · cliproxy

两份文件顶层 namespace 完全对齐,子树结构基本一致(5 行差异来自 zh-CN 部分键值有额外的注释字符串)。

namespace 命名约定

namespace 以「页面或功能模块」而非「组件」为粒度。例如 keys 容纳 API Key 管理整个页面的全部文案,upstreams 容纳上游管理页面,circuitBreaker 容纳熔断器子页面,跨页面共用的字段统一放 common。新增页面时同步在两份 JSON 加新 namespace。

客户端与服务端的使用模式

客户端

next-intl 的两个核心 hook:

ts
// src/app/[locale]/(auth)/login/page.tsx:118-119
const t = useTranslations("auth");
const tCommon = useTranslations("common");
ts
// src/components/dashboard/time-range-selector.tsx:36
const locale = useLocale();

useTranslations 接受 namespace 字符串,返回的 t 函数按相对路径取值;useLocale 用于需要按当前语言切换展示逻辑的场景(比如日期格式化)。

服务端

项目中服务端翻译统一走 [locale]/layout.tsx 注入的 messages,没有直接调用 getTranslations。如有 server component 需要单独取翻译,可按 next-intl 文档使用 getTranslations / getLocale

语言切换组件

src/components/language-switcher.tsx

ts
const handleLocaleChange = (nextLocale: Locale) => {
  if (nextLocale === locale) return;
  const queryString = searchParams.toString();
  const targetPath = queryString ? `${pathname}?${queryString}` : pathname;
  router.replace(targetPath, { locale: nextLocale });
};

关键点:

  • pathnameuseRouter 均来自 @/i18n/navigation,即 i18n-aware 版本,自动处理 locale 前缀替换
  • router.replace(targetPath, { locale: nextLocale }) 触发跳转的同时,next-intl 中间件会更新 NEXT_LOCALE Cookie,下次访问任意路径都按新语言渲染
  • 当前 query string 被保留,避免切语言把分页 / 筛选条件清空

UI 用 DropdownMenuRadioGroup 把两种语言渲染为单选项,当前 locale 旁加 ✓ 图标。

文档站 i18n 与应用 i18n 的边界

VitePress 文档站(即你正在阅读的站点)在 docs/.vitepress/config.ts:60-107 单独配置了 locales

ts
locales: {
  root: { label: "简体中文", lang: "zh-CN", themeConfig: { /* 完整侧边栏 */ } },
  en:   { label: "English",  lang: "en-US", link: "/en/", themeConfig: { /* WIP 占位 */ } },
}

这是 VitePress 原生机制,与应用层 next-intl 完全独立

维度应用 i18n文档站 i18n
引擎next-intl 4.xVitePress 原生
翻译源src/messages/*.json各页面 docs/ / docs/en/ 下的 Markdown 文件
路由策略/zh-CN/... /en/... 强制前缀/...(zh-CN root)/ /en/...
部署Next.js 应用主体GitHub Pages 静态文档站

两套体系各自维护翻译,互不共享。这种分离是有意的:应用面文案与产品交互绑定,迭代节奏快;文档面内容偏稳定,迭代节奏慢,分别交给两套合适的工具维护。当前英文文档仅有「Overview (WIP)」占位页,完整英文化由 Issue #167 的后续阶段跟进。

与其他架构文档的衔接

  • 路由分组((auth) / (dashboard))的整体结构以及鉴权流程见 安全模型
  • API 路由不走 i18n 中间件的整体路由结构见 请求生命周期

Released under the MIT License.