当每条用户可见的字符串都放在仓库里的同一个目录文件中,通过键引用,并且只能通过经过评审的 diff 修改,界面文案就能保持一致。这和你对待逻辑代码的纪律一样,只是用在了文案上。本页其余内容,都是让这套纪律真正落地的实践。

本文是操作指南。背后的架构,以及代码库为什么应当成为产品文案唯一可信来源的论证,见代码原生的产品文案。想了解原因,请先读那篇;想知道怎么做,继续往下看。

界面文案应该放在哪里?

每种源语言一个目录文件,放在仓库中,紧挨着渲染它的代码。 不要放在电子表格里,不要放在设计文件里,也不要放在三月份还准确的 wiki 页面里。开发者、评审者、翻译人员和编码 Agent 想知道某个界面写了什么,都应该来这里看。

由此得出的实操规则:

  • 一个目录文件,一种源语言区域。 一份 en.json(或 .po、.ftl、.strings,取决于你的 i18n 库读取哪种)就是文案的权威版本。monorepo 里有多个应用时,每个应用各有一份,共享字符串则放进双方共同引用的共享目录文件。要避免的是出现两个都能存放同一条消息的文件。
  • 按界面或功能分组,不要按组件分组。 像 checkout.summary.total 这样的键比 SummaryCard.totalLabel 更经得起时间考验,因为组件会被重命名、拆分,而界面和消息本身不会变。
  • 键名要对应消息本身,而不是具体措辞。 checkout.submit 在文案从“下单”改成“立即购买”后依然成立。checkout.placeOrderButton 就不行了,键和文本对不上,评审者之后就会误以为它们是两条不同的字符串。
  • 目录文件的改动要和使用它的代码放在同一个 PR 里。 字符串在一个 PR 里加,组件在另一个 PR 里加,这样的字符串迟早会被遗漏、遗忘或重复。

如果你的字符串还是组件里的字面量,第一步就是创建目录文件。手动完成这件事本身就是一个项目,这正是连接仓库、交给自动转换来完成的理由,后文会详细介绍。

为什么按键引用比搜索字面量更好?

因为一个键只会对应一条字符串,而同一个字面量有多少个开发者就有多少种写法。 对搜索工具来说,“Sign in”“Sign In”“Log in”“Login”是四条字符串,对用户来说却是同一个意图。当所有组件都调用 t("auth.signIn"),哪种措辞正确这个问题只需在一处回答一次,答案就适用于所有用到这个键的地方。

两个习惯能让这套做法持续有效:

  • 先复用,再新增。 创建键之前,先在目录文件中搜索这条消息。如果 common.cancel 已存在,再建一个文本相同的 dialog.cancelButton 就是文案漂移的开端,而不是无害的重复。只要有人改了其中一个,两者就会各改各的。
  • 不要拼接字符串。 t("cart.items") + " " + count 生成的文本,没有任何目录条目能描述。请使用 i18n 库提供的插值和复数形式,让整句话成为一个可评审的完整单元。

人们常用的借口是“这个字符串只出现一次,写成字面量没问题”。它今天只出现一次。但组件会被复制,界面会长出兄弟页面,字面量会跟着一起走,然后各自分叉。

如何在 PR 中评审文案改动?

像对待函数签名变更一样对待目录文件中的改动:评审者要阅读、质疑并批准。 这个实践把一致性从记忆问题变成了评审问题,而评审恰恰是人们最可靠地保持专注的时刻。

让目录评审行之有效的做法:

  • 措辞变更在 diff 中独占一行。 前提是字符串在目录文件中。组件内部的措辞变更同样是 diff 里的一行,但它混在逻辑改动之中,很容易被当作“只是文本”一扫而过。
  • 把目录文件的改动分配给文案负责人。 在目录路径上配置一条 CODEOWNERS 条目,意味着无论谁负责文案——产品经理、内容设计师,还是最在意措辞的开发者——每次改动都会请求他评审。PR 里的其他内容照常走普通评审流程。
  • 每行改动问三个问题。 这是该事物的标准术语吗?语气和大小写与周围的字符串一致吗?键名还能准确描述这条消息吗?评审者问完这三个问题,风格指南想要预防的大部分问题就都能拦住。
  • 拒绝悄悄重命名。 一个 PR 把 auth.signIn 从“Sign in”改成“Log in”却没有任何说明,这就是由碰巧在编辑的人做出的文案决策。要么在 PR 描述里写一句改动原因,要么回滚。

这种失败模式有个名字:文案漂移,指用户可见文本随着时间推移、跨多个界面偏离其认可的源文本,而且根本没有唯一可信来源可供对照。什么是文案漂移讲了成因;在 diff 中评审目录文件,则是能阻止其中大部分成因的实践。

如何防止新的硬编码字符串混进来?

让新增的字面量直接导致构建失败。 规范和评审只能拦住人们记得去检查的问题。其余的交给 lint 规则,每个 PR 都会检查,不需要任何人记得。

配置很简单:

  • 启用禁止字面量字符串的规则。 eslint-plugin-i18next 为 JavaScript 和 TypeScript 项目提供了这类规则。把范围限定在 JSX 文本和面向用户的属性上,比如输入提示、title、aria-label 和 alt,并将测试文件和 Storybook story 设为豁免,这样规则才有公信力。
  • 在合并发生的地方运行它。 pre-commit 钩子很方便,但真正起作用的是 CI 步骤,因为无论作者的编辑器有没有配置,它都会在 PR 上运行。
  • 增加未使用键和缺失键的检查。 大多数 i18n 库都有配套工具,可以列出代码中引用但目录文件里没有的键,以及目录文件里有但没有任何地方引用的键。同样放进 CI 里运行。过时的措辞往往就藏在未使用的键里。
  • 先从严开始,再通过注释放行例外。 对整个 components/ 文件夹关闭的规则就不算规则。只在某一行上附带理由关闭的规则,则是一种文档。

使用 AI 编码工具的团队受影响更大,因为这些工具会根据局部上下文重新生成组件,字面量也跟着回来了。为什么 Cursor AI 总是添加硬编码字符串详细讲解了这条 lint 规则,包括由规则文件、linter 和 CI 组成的三层配置。

面向开发者的文案风格指南应该包含什么?

仓库里的一个短文件,一屏能看完,说明语气、统一术语,并写明字符串放在哪里。 长篇风格指南放在 wiki 里,读一遍就没人再看了。简短的指南则放在代码旁边,每次添加字符串时,人和工具都会读到它。

一份可用的 COPY.md(或现有贡献指南中的一个章节)应涵盖:

  1. 语气,三句话说清。 产品听起来像谁、有多正式、称呼用户是用“你”还是“用户”。足以平息大多数争论。
  2. 绝不能变的术语。 产品对每个功能的称呼、核心操作的动词(是“保存”还是“更新”?是“删除”还是“移除”?),以及你决定不再使用的词。
  3. 大小写和标点规则。 按钮和标题的大小写风格、提示文字末尾是否加句号、数字和日期的写法。
  4. 字符串放在哪里、键怎么命名。 目录文件路径、分组规则、键命名规则,以及“新增前先搜索”。
  5. 拿不准时怎么办。 该问谁,或者暂时先复用哪个键。

把它放在仓库里,这样规则文件可以指向它,评审者可以在评论中链接它,编码 Agent 也能在写标签之前先读它。

如何避免 AI 编码 Agent 破坏一致性?

给 Agent 一个可读取的可信来源,再加一道在它偏离时拦住它的关卡。 Agent 生成表单时,完全可能在一个界面写“提交”,下一个界面写“发送”,因为每次都是当时局部上最可能的词。这不是它粗心,而是它没有可以保持一致的参照。

两个动作就能解决大部分问题。第一,写一个规则文件(AGENTS.md、.cursor/rules、CLAUDE.md,取决于你的工具读取哪个),规定:用户可见文本放进目录文件,复用已有的键,写标签之前先读 COPY.md。第二,上一节的 lint 规则,因为规则文件能减少猜测,而 lint 规则能拦住漏过去的猜测。

关于 Agent,本文要说的就这些。Agent 改写已有字符串这个具体问题,以及如何让这种改动变得可见而不是悄无声息,是另一个话题:如何阻止 AI 编码 Agent 改写你的界面文案。

本地化在其中处于什么位置?

一致性和本地化依赖同一个前提——一份公认的源文本,所以能带来一致性的目录文件,同样也能带来本地化。 一个团队如果说不清三种写法中哪一种是认可的,也就没法把其中任何一种翻译对。先把源文本定下来,翻译就成了目录文件的衍生产物,而不是拥有另一份“真相”的独立项目。

实际上,这意味着你为一致性建立的目录文件,已经是翻译人员或翻译工具的工作基础。每个键在每种目标语言中都有对应译文,键名通用,源目录文件中的措辞变更,会作为一次翻译需要跟进的改动清晰可见。如果任由译文落后于源文本,就会出现为什么翻译文件会不同步中描述的那类问题,而解法依然是同一套纪律:一个目录文件、用键引用、改动体现在 diff 中。

globalize.now 的定位

globalize.now 是内置本地化能力的代码原生文案管理系统,本页的每一项实践,都是它对你的仓库所做的前提假设。 你在应用中连接 GitHub 或 GitLab 仓库,扫描会梳理出现有内容。如果字符串是组件里的字面量,一次性转换会把它们移到键后面的目录文件里,没有 i18n 配置的话还会补上。如果你已经在用 next-intl、react-i18next、i18next 或 Lingui,它会直接适配现有方案。结果会以独立分支上的 PR 形式交付,所以第一份目录文件会经过上文所述的评审。之后,你新增的源字符串由推送任务翻译,并以 PR 形式提交结果。仓库始终是唯一可信来源。

我们正在朝着让同一套纪律贯穿营销站点、落地页和产品的方向努力,让一个功能在用户接触到的每个地方都以相同的方式描述。这是我们的发展方向,不是现在就能购买的功能。定价请见定价页面。

本周就能落地的检查清单

  1. 在仓库里为每种源语言创建一个目录文件,并确定键命名规则。
  2. 把你最常改动的界面里的字符串迁移进去,并通过键引用。
  3. 启用禁止字面量字符串的 lint 规则,范围限定在面向用户的文本,并在 CI 中运行。
  4. 在目录文件路径上添加一行 CODEOWNERS。
  5. 写好 COPY.md:语气、统一术语、大小写规则、字符串放在哪里。控制在一屏之内。
  6. 让 Agent 的规则文件指向目录文件和 COPY.md。
  7. 评审时,拒绝任何在 PR 描述中没有说明理由的措辞变更。

如果第二步看起来要花上一个月的工作量,那就连接仓库,阅读它提出的方案,再评审它提交的 PR。

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

免费试用 globalize.now