Communitygithub.com

mwasobaddy/skills

为 Next.js App Router 应用架构并实现国际化——文件夹结构、语言前缀路由、类型化消息文件、ICU 复数以及带有 hreflang 的本地化 SEO。在添加或修复 Next.js 网站或应用的多语言或翻译时使用,在 next-intl、Lingui 和 next-i18next 之间选择,配置带有代理或中间件的 [locale] 段,或将纯英文应用迁移到本地化 URL。触发词:i18n、本地化、多语言、翻译、语言路由、lang 属性、hreflang、next-intl。

skills 是什么?

这是一个针对 Next.js App Router 的 i18n (国际化) 技能,提供了从零开始搭建或改造现有应用支持多语言的完整指南。技能默认推荐使用 **next-intl** 库,因为它无需构建插件、宏步骤或每个语言实例的维护,消息文件是纯 JSON 格式,基于源语言类型化。对于编译时提取和更小的运行时,可以选择 @lingui/react;而对于已有 i18next 经验的团队,则可以选择 next-i18next。技能详细说明了规范的文件夹架构:将所有静态字典放在 `messages/` 目录下(不在 `src/` 内),locale 逻辑集中在 `src/i18n/` 文件夹中,并使用 `[locale]` 作为根动态段来包裹所有本地化内容。它还强调了关键的陷阱,例如 `setRequestLocale` 在 Next.js 16.3+ 上已过时,Next 15+ 中 `params` 是 Promise,以及必须在代理中验证 locale 以避免渲染未定义的内容。对于已有应用的改造,技能提供了循序渐进的迁移计划,包括字符串盘点、路由策略选择、URL 重定向以及 hreflang 元数据的添加,确保不丢失现有的 SEO 排名。该技能支持 Claude Code、Cursor 等主流 AI 代理平台,并提供了详细的参考文献和测试脚本以确保文档的准确触发。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/mwasobaddy/skills/tree/HEAD/nextjs-i18n

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Next.js i18n

为 Next.js 应用提供多语言支持的结构性指导:文件夹布局必须如何,出错时会出现什么问题,以及应该避免什么。

首先选择库

对于 App Router 上的新项目,默认选择 next-intl。它不需要构建插件、宏步骤或每个语言实例的管理;消息是纯 JSON 格式,并根据你的源语言进行类型化。

库使用场景成本
next-intl (默认)新的 App Router 项目,任何团队规模无 —— 纯 JSON,无需代码生成
@lingui/react你想要编译时提取和微小的运行时,或者希望跨 React/Vue/Astro 使用同一个 i18n 层SWC 或 Babel 宏插件,需要精确锁定版本
next-i18next团队已经熟悉 i18next,或者你需要迁移 Pages Router 的代码每次请求都需要实例,getT/useT 分离
Next.js 内置的 i18n仅限 Pages Router。 绝不用于 App Router不适用

Next.js 的 i18n: { locales, defaultLocale } 配置仅适用于 Pages Router。在 App Router 中它不生效 —— 那里的语言路由意味着需要一个 [locale] 段加上一个代理。不要让 AI 代理为 App Router 项目"启用"内置的 i18n。

完整的比较,包括打包成本和每个库的权衡:references/library-comparison.md。

标准架构

将静态字典放在 src/ 之外,这样打包和导入路径保持清晰,并将所有语言机制放在一个文件夹中。

messages/                      # 纯静态字典 —— 放在 /src 之外
├── en.json                    # 源文件;其他每种语言都镜像它
├── de.json
└── ja.json
src/
├── i18n/
│   ├── routing.ts             # defineRouting: 语言 + 默认语言
│   ├── request.ts             # getRequestConfig: 解析语言,加载消息
│   └── navigation.ts          # createNavigation: 识别语言的 Link/redirect
├── proxy.ts                   # 语言检测 + 重定向 (Next 16;之前是 middleware.ts)
└── app/
    ├── [locale]/              # 包裹整个本地化应用
    │   ├── layout.tsx         # 验证语言,<html lang>,generateStaticParams
    │   └── page.tsx
    └── not-found.tsx          # 全局 404 放在 [locale] 之外

使这种结构生效的规则:

  • [locale] 是一个根动态段。 所有本地化的内容都位于其下。根级路由(not-found.tsx、error.tsx、api/)放在外部,并且不能无条件调用 i18n 钩子,否则会在运行时崩溃。
  • 一个源语言。 messages/en.json 是主文件。每个其他文件必须完全镜像其结构。
  • 所有语言知识放在 src/i18n/ 中。 组件从 @/i18n/navigation 导入 Link,绝不从 next/link 导入。添加一种语言只需修改一处。

搭建(新项目)

按此顺序运行。顺序很重要 —— proxy.ts 依赖于 routing.ts,布局依赖于 request.ts。

  1. 安装 next-intl,用 createNextIntlPlugin() 包裹 next.config.ts。
  2. 使用 defineRouting 创建 src/i18n/routing.ts。
  3. 创建 src/proxy.ts,导出 createMiddleware(routing) 和默认匹配器。
  4. 使用 getRequestConfig 创建 src/i18n/request.ts,解析语言。
  5. 将 app/** 移动到 app/[locale]/** 下;添加 generateStaticParams 并验证参数。
  6. 使用 createNavigation(routing) 创建 src/i18n/navigation.ts。
  7. 创建 messages/en.json,并用 useTranslations 替换硬编码的字符串。
  8. 设置 <html lang> 并添加本地化元数据和 hreflang。

每个步骤的精确文件内容:references/next-intl-setup.md。

陷阱

在编写任何 i18n 代码之前阅读本节。这些是会导致浪费数小时的问题。

  • setRequestLocale 是旧的。 在 Next.js 16.3+ 上,应该在 getRequestConfig 中使用 next/root-params 读取语言。大多数指南、教程和 LLM 训练数据仍然教授 setRequestLocale(params.locale)。它仍然有效且被记录为向后兼容 —— 但它是旧 API。只在 Next < 16.3 上使用它。无论如何,generateStaticParams 仍然需要。
  • 在 Next 15+ 上 params 是一个 Promise。 使用 const {locale} = await params。将 params.locale 视为类型错误,而不是运行时错误。
  • 验证语言,否则未知值会渲染。 使用 hasLocale(routing.locales, locale) 和 notFound()。如果没有,/xx/about 会在一个虚假的 URL 上渲染你的英文页面。
  • 在 Next 16 上是 proxy.ts,而不是 middleware.ts。 middleware.ts 已弃用。代理仅在 Node.js 上运行 —— 不支持 Edge 运行时。Lingui 和 next-i18next 宣传 Edge 安全的代理;如果你需要 Edge,你面临的是一个真正的权衡,而不是免费的胜利。
  • 绝不拼接翻译后的字符串。 "You have " + count + " items" 在日语和阿拉伯语中是不合语法的。使用 ICU:"{count, plural, =1 {One item} other {# items}}"。
  • 绝不使用 + 或消息内的模板字面量进行插值。 使用命名占位符 —— {name} —— 以便词序可以调整。
  • 日期和数字会导致水合不匹配。 服务器上的 new Date() 和浏览器中的会因时区和时钟而不同。在 getRequestConfig 中设置 timeZone,或者对于相对时间仅作客户端渲染。症状:"Text content does not match server-rendered HTML"。
  • 格式化依赖于语言环境。 没有显式语言环境的 toLocaleString() 会使用运行时语言环境进行格式化,而不是请求的语言环境。使用 useFormatter() / getFormatter()。
  • getRequestConfig 每次请求运行一次,并被 React 缓存。 不要将请求变化的 state(购物车、用户偏好)放在里面。只放语言变化的配置。
  • 服务器操作(Server Actions)和路由处理程序(Route Handlers)没有根参数。 await locale() 仅适用于服务器组件。在操作和处理程序中显式传递语言作为参数。

改造现有应用

将其视为一个有 URL 影响的迁移,而不是配置更改。当你添加 [locale] 前缀时,每个现有 URL 都会改变形状。

按此顺序计划:

  1. 盘点字符串 —— 运行 scripts/find-untranslated.sh 来查找硬编码的 JSX 文本。
  2. 在编码之前选择路由策略。 始终使用语言前缀(/en/about、/de/about)是默认的;它没有歧义且 hreflang 清晰。localePrefix: 'as-needed' 在裸路径上提供默认语言 —— 更好的 URL,但 SEO 信号较弱。现在做出决定;稍后切换是完全重新迁移。
  3. 在接触任何组件之前搭建路由。 先让 proxy.ts + [locale] 配合仅英文的消息工作。
  4. 将路由移动到 [locale]/ 下 —— 这是一个大型的机械性 git mv。在一个提交中完成,这样 git log --follow 保持可用。
  5. 按命名空间替换字符串,而不是按文件。端到端地翻译一个命名空间,以便在键形状复制之前得到验证。
  6. 为更改的每个 URL 添加重定向。 /about → /en/about(或 307 到检测到的语言)。如果没有这个,你会丢失所有现有的排名。不可商量。
  7. 只有在每个语言都有该路由后才添加 hreflang + 本地化元数据,否则你会发出指向 404 的替代链接。

带有重定向和回滚说明的完整迁移序列:references/migration.md。

应该避免什么

  • 不要将 messages/ 放在 src/ 内。 源代码树中的仅服务器字典模糊了哪些内容被发送到客户端。
  • 不要在本地化路由中从 next/link 导入 Link。 它会去掉语言前缀并静默地链接到错误的语言。
  • 不要在 href 中手动编写语言前缀(href={/${locale}/about})。使用 @/i18n/navigation 中的 Link,它可以处理带前缀的、按需的和域路由。
  • 不要使用一个扁平的键命名空间。 按功能/页面分组。扁平的键会冲突且难以搜索。
  • 不要将构建时的自动翻译作为源文件。 机器输出是草稿;未经审查就提交会使错误永久化且无法追踪。
  • 不要只是将一种语言添加到 routing.locales 就认为完成了。 没有翻译的 messages/xx.json,每个页面都会渲染键或静默回退。
  • 不要混合使用路由策略。 as-needed 加上指向带前缀 URL 的 hreflang 是一个自找的 SEO 错误。

参考文献

按需阅读,无需提前阅读:

  • references/library-comparison.md —— 在 next-intl、Lingui、next-i18next 之间选择;打包成本、迁移触发条件、每个库的设置结构。
  • references/next-intl-setup.md —— 标准设置中的每个文件,可直接复制使用。
  • references/messages-and-keys.md —— 命名空间结构、键命名、ICU 语法、日期/数字/列表格式化、类型化消息。
  • references/seo.md —— hreflang、<html lang>、本地化的 generateMetadata、规范、站点地图替代、语言切换器 SEO 规则。
  • references/migration.md —— 改造现有应用、重定向、回滚。

脚本

  • scripts/find-untranslated.sh —— 标记项目中可能硬编码的用户面向字符串,以便你在开始之前评估工作量。作为改造的第一步运行。
  • scripts/run-trigger-eval.sh —— 衡量这个技能的 description 是否在正确的提示上触发。更改描述时使用它。

测试描述

description 是 AI 代理在决定加载此技能之前唯一看到的东西。如果它不能触发,下面的所有内容都不重要。

bash scripts/run-trigger-eval.sh --dry-run          # 显示计划,不进行 API 调用
bash scripts/run-trigger-eval.sh --split train     # 12 个查询 x 3 次运行
bash scripts/run-trigger-eval.sh --split validation --out report.json

查询集:evals/trigger-queries.json —— 12 个应该触发,8 个不应该触发,按 60/40 分成固定的训练和验证集。

编辑描述时:

  • 仅使用训练集修复失败。在最后使用验证集进行一次检查,看更改是否泛化 —— 如果训练集改善而验证集下降,说明你过拟合了。
  • 应该触发但失败意味着描述太窄:添加查询所代表的概念,而不是查询的关键词。
  • 不应该触发但失败意味着描述太宽泛:说明这个技能不涵盖什么。
  • 在编辑之前重新运行。触发是非确定性的,一次运行不能证明什么。

不要从失败的查询中添加关键词 —— 这就是描述最终看起来像关键词列表并停止匹配真实提示的方式。

相关技能