下面这段 i18n 配置就是要粘贴到 AGENTS.md 的内容:十条简短规则,要求 coding agent 把每个用户可见的字符串都交给翻译函数处理,按功能命名键,绝不拼接片段,绝不用三元表达式模拟复数,也绝不改动生成的语言文件。globalize.now 是 AI 驱动的本地化基础设施,我们读过大量 Agent 写的 i18n 代码,几乎所有问题都出自同样的几类错误,每条规则对应其中一类。

本文后半部分讲这个文件在哪些场景下会失效:开发者最常用的两类代码编写界面根本不读它,再怎么调整措辞也无济于事。

AGENTS.md 的 i18n 部分该写什么?

就是下面这一段。把其中三个文件路径和 lint 命令改成你项目里的,其余保持不变。

## Internationalization

- Every user-visible string goes through the translation function. That
  includes JSX text, input hint text, `title`, `aria-label`, `alt`, toast and
  error messages, empty states and the document `<title>`.
- The source locale is `en`. The catalog is `locales/en.json`. Add the key in
  the same change that uses it.
- Keys are namespaced by feature: `checkout.summary.total`, not `total3`.
  Search the catalog for an existing key before creating one.
- Never concatenate translated fragments. Use interpolation:
  `t('cart.items', { count })`, not `t('cart.you_have') + count`.
- Plurals use the library's plural forms (`_one` / `_other` keys, or ICU
  `{count, plural, ...}`). Never `count === 1 ? 'item' : 'items'`.
- Dates, numbers and currency go through `Intl.DateTimeFormat` and
  `Intl.NumberFormat` with the active locale. Never a bare
  `toLocaleString()`, never a currency symbol glued to a number.
- Do not hardcode the locale list, language names or text direction in
  components. Read them from `i18n/config.ts`.
- Do not translate brand names, code identifiers, URLs or environment values.
- Do not edit locale files other than `en`. They are generated. If a
  translation looks wrong, fix the source key or the glossary, not the file.
- Before finishing, run `npm run lint`. The `i18next/no-literal-string`
  rule must pass.

十条是有意为之。Cursor 的规则文档建议把规则聚焦在常用模式上,用 linter 而不是粘贴整份风格指南;Claude Code 的建议也一样:指令要具体到可验证,文件要简短,因为内容越长,遵循度越低。一个写了四十条的 i18n 段落,只会被一扫而过。

为什么是这十条,而不是一句“使用 i18n”?

因为 Agent 在写 <Button>Save changes</Button> 的时候,本来就认为自己在“使用 i18n”。含糊的指令会被 Agent 按自己理解的“合规”糊弄过去。下面每条规则都点名一种具体的输出,并明令禁止。

属性中的字面量。 Agent 学会了 JSX 子节点需要翻译,却会把 aria-label="Close" 和 alt="Company logo" 写成普通字符串,因为属性看起来像配置而不是文案。把这些属性逐一列出能堵上漏洞,笼统的“翻译所有内容”则不行。

拼接。 t('greeting') + ' ' + name + '!' 在英文里渲染没问题,但无法翻译成名字在前或名字会变形的语言。用插值可以让译者拿到一个只含一个变量的完整字符串,调整语序即可。

三元复数。 count === 1 ? 'item' : 'items' 是我们在 i18n 配置完成后最常见的 Agent 写法,对任何复数形式多于两种的语言都是错的。规则直接点名这个写法,Agent 才能对号入座。如果你的语言目录使用 ICU,ICU MessageFormat 是什么,以及它在 AI 生成的应用中哪里会出问题一文介绍了语法。

生成的语言文件。 这条规则的存在,正是因为 Agent 太热心。让它修一个德语错别字,它会打开 locales/de.json 直接改,直到下一次翻译任务从源文件重新生成该文件,修改就丢了。告诉它非源文件都是生成的,并说明真正的修改位置,就能避免一类悄无声息的回归。

在同一次改动中编辑 en。 没有这条规则,Agent 添加完 t('checkout.summary.total') 就走人,键会一直以自己的名字渲染到界面上,直到有人发现。语言目录条目和它的首次使用应该出现在同一个 diff 里。

各个工具到底从哪里读取这个文件?

同样的内容,放哪里取决于用的是哪个 Agent,而放置位置决定了规则是常驻上下文,还是只在需要时才加载。

仓库根目录的 AGENTS.md 是通用位置。它是纯 Markdown 格式,没有必填字段,由 Linux 基金会旗下的 Agentic AI Foundation 维护,Cursor、Codex、Copilot coding agent 等一长串工具都会读取。规范也支持嵌套文件,离被编辑文件最近的那个优先生效。

Cursor 会直接读取 AGENTS.md(包括嵌套的),也有自己的范围限定格式。限定在 UI 文件的规则,在匹配的文件被打开之前不会进入上下文:

---
description: i18n rules for user-visible strings
globs: src/**/*.tsx, src/**/*.jsx
alwaysApply: false
---
(paste the Internationalization section here)

文件必须以 .mdc 结尾并放在 .cursor/rules/ 中;该文件夹里不带 frontmatter 的普通 .md 会被忽略。

VS Code 中的 GitHub Copilot 有三种方式。.github/copilot-instructions.md 对每次聊天请求都生效。frontmatter 中带 applyTo: "**/*.tsx,**/*.jsx" 的 .github/instructions/i18n.instructions.md 文件,只在创建或修改这些文件时生效。仓库根目录的 AGENTS.md 会在开启 chat.useAgentsMdFile 设置后由 Local agent 读取;子文件夹中的嵌套 AGENTS.md 文件则由另一个默认关闭的实验性设置 chat.useNestedAgentsMdFiles 控制。Copilot 的具体行为见如何为用 GitHub Copilot 构建的应用添加 i18n。

Claude Code 读取 CLAUDE.md;自 v2.1.277 起,如果工作目录往上都没有 CLAUDE.md,它也会自行读取根目录的 AGENTS.md。如果两者并存,文档推荐的做法是在 CLAUDE.md 开头用 @AGENTS.md 导入,让文件只加载一次。需要限定范围时,在 frontmatter 中带 paths: 列表的 .claude/rules/i18n.md 只会在 Claude 读取匹配文件时加载。完整的 Claude Code 工作流见如何本地化 Claude Code 生成的应用。

对于多个 Agent 共用的仓库,比较实际的做法是:把十条规则放进团队用得最多的那个 Agent 的限定范围文件里,再在 AGENTS.md 里加两行指引(“UI 字符串:参见 .cursor/rules/i18n.mdc 中的 i18n 规则,并严格遵守”),这样不支持范围限定的 Agent 也能拿到指令。

哪些代码编写方式从不读取 AGENTS.md?

行内补全。这一点两家厂商都有文档说明,也是规则文件不可能是完整答案的原因。

Cursor 规则 FAQ 里有一问:“规则会影响 Cursor Tab 或其他 AI 功能吗?”回答是干脆的“不会”。规则服务于 Agent;Tab,也就是补全你正在输入那一行的自动补全,看不到这些规则。VS Code 的自定义指令页面对 Copilot 也有同样的说明:你在编辑器中输入时,行内建议不会参考这些指令。

所以,开发者敲下 <p>No results found</p> 并接受灰色补全文本的那个场景,恰恰是规则够不到的地方。聊天和 agent 模式会给字符串建键;而你在想别的事时顺手补全的那一行则不会。一周正常开发下来,代码库里会不断积累字面量,它们都来自指令文件覆盖不到的这条路径,而认真写了规则文件的开发者,反而会以为是 Agent 无视了规则。

Claude Code 没有行内补全,但它的文档从另一个角度说了同一件事:CLAUDE.md 的内容只是上下文,不是强制配置;想不管模型怎么决定都要拦住某个操作,就用 hook。这是理解这里所有工具的正确思路:指令文件只是改变概率,并不强制执行。

到底靠什么强制执行规则?

CI 里的 lint 规则。既然规则文件已经要求 Agent 运行它,再加一行配置就够了。eslint-plugin-i18next 自带 no-literal-string;在组件目录启用后,JSX 里出现字面量时构建会直接失败,而不是礼貌地提醒一下。配置方法、规则选项,以及为什么指令和强制执行是两个不同的层面,都写在为什么 Cursor 在 i18n 配置完成后仍不断添加硬编码字符串里,本文不再重复。

lint 层提供了两样规则文件做不到的东西。其一,它能拦住行内补全产出的代码,因为它检查的是文件,而不是对话。其二,它把 Agent 自己的循环变成修复流程:上面那段规则里的最后一条要求 Agent 在收尾前运行 lint,而一个看到 no-literal-string 失败的 Agent 会在同一个会话里自己把字符串建好键。

对 Claude Code 来说,在刚写完的文件上运行 linter 的 PreToolUse 或编辑后 hook,就是其文档指向的强制执行机制。hook 以 shell 命令的形式在固定节点运行,无论模型有没有选择遵循规则,都会生效。

仍然漏网的字符串怎么办?

它们需要提取、对照现有命名空间建键,再翻译,这部分才是值得自动化的,而不是反复重写提示词。构建变红只能告诉你有字面量存在,还得有人把它变成键,加到 en 里,再同步到其他所有语言区域。

globalize.now 处理的就是这一层。转换是一次性的,直接在应用内完成:连接仓库,代码库只转换一次,语言目录以 pull request 的形式交给你审查。之后,推送任务会在新的目录条目合入时翻译它们,周二 PR 里新增的键,周三就有了翻译。运行时库、目录格式和上面的规则文件都归你所有;开发者参考文档介绍了 Cursor、Claude Code、Codex 和 Copilot 里的 Agent 如何承担配置完成后的角色,Cursor 集成则是最短的上手教程。

另外两类相关问题各有专文。如果问题是 Agent 改写已经建好键的文案,参见如何阻止 AI Agent 改写你的 UI 文案,解决办法与上面第九条规则一样,都是坚守源语言规范。如果各语言区域的文件总是逐渐不一致,翻译文件为什么会不同步解释了背后的路由问题。

从哪里开始?

粘贴这段规则,限定到你的 UI 目录,当天下午就在 CI 里启用 no-literal-string。然后观察一周 lint 规则拦住了什么,这就能衡量规则文件单靠自己究竟能起多大作用。如果你在发布 AI 构建的应用,希望漏网字符串的提取和翻译由专人接手,而不是自己维护,vibe coder 页面是整体介绍,定价单独成页。

globalize.now 把硬编码的应用文案转换为可直接翻译的语言文件,并在你持续发布时保持同步更新。

免费试用 globalize.now