你让 Claude Code 给登录表单加个加载状态。加载状态没问题。可按钮现在写的是“Log in”而不是“Sign in”,错误提示变得更友好了,还有一个标题莫名其妙改成了句首大写、其余小写的格式。这些都没人要求过,测试全部通过,除非有人逐字逐句读完整个 diff,否则这些改动就这样上线了。
本文只讲这一种故障:已有的、已批准的文案被 AI 编程 Agent 悄悄改写,以及防止它的工作流。Agent 新写出的硬编码、未键化字符串是另一个问题,详见AI 编程工具为什么总是添加硬编码字符串。产品文案究竟应该放在哪里,则是架构层面的问题,详见代码原生的产品文案。这里只聚焦事故本身——某个周一早上,措辞变了,却没人决定过要改。globalize.now 是内置本地化能力的代码原生文案管理系统,这类事故正是它存在的原因之一。
AI 编程 Agent 为什么会改写界面文案?
AI 编程 Agent 会改写界面文案,是因为它们看到的是直接写在组件里的文字;一旦文案放进源语言文件、通过键引用,修改就成了明确、可审查的编辑,而不是其他任务的副作用。
在组件里,“Sign in”没有任何特殊地位。它只是 JSX 属性里的一个字符串字面量,和 className、test id 没有区别。Agent 学过数百万条 diff,其中改进周边代码是好行为;对模型来说,把标签改得“更清晰”和重命名变量是同一类操作。没有类型错误等着它,没有失败的测试,文字上也没有负责人标注。改写文案,对 Agent 来说毫无代价。
这种改动能通过评审,原因如出一辙。评审者看的是逻辑。措辞改动夹在一段确实是关于加载状态的 diff 中间,能编译,能渲染;就算快照测试报错,常规做法也是更新快照——这等于把新措辞当成已批准的内容记录了下来。
AGENTS.md 这类规则文件真能阻止吗?
Agent 规则文件只能降低发生频率,并不能让界面文案变成不可写。真正让修改变得可见的是架构。
规则文件还是要配——它是成本最低的一层,而且经常奏效,值得花三十秒。下面这段适用于 AGENTS.md、CLAUDE.md 和 Cursor rules(.cursorrules 或 .cursor/rules/):
## UI copy policy — do not modify user-facing text
- Never change user-visible strings (button labels, headings, empty states,
error messages, tooltips, placeholder text) unless the task explicitly
asks for a copy change.
- User-visible text lives in the source locale file (e.g. locales/en.json).
Reference it by key. Never inline a new user-facing string in a component.
- If a task needs new user-facing text, add a key to the source locale file,
reference it, and list the new key in your summary for copy review.
- Any diff to the source locale file is a copy change. Call it out
separately — it needs its own approval, apart from code review.
说实话,规则只是上下文,而上下文之间会互相竞争。在长任务里,这条指令只是数千 token 代码中的一行,有时就会落败——败给上下文压缩,败给从没见过它的子 Agent,或是败给周边文件里的强势模式。更糟的是,违反规则的 Agent 不会通知你。你收不到“措辞被改了”的提醒,只会拿到一份碰巧改过措辞的 diff。约定不等于检查。
所以规则文件是第一层,但也是不该单独依赖的一层。
如何让文案修改变得可见,而不是悄无声息?
把所有用户可见字符串从组件中移出,放到源语言文件的键后面。此后,Agent 想改你的措辞,就必须去编辑一个专门管措辞的文件。
这是规则文件做不到的一步,机制很简单。当组件引用 t('auth.signIn') 而不再重复写出“Sign in”,两件事同时发生:组件的 diff 里不再有文案,处理加载状态的 Agent 在它工作的地方无词可改;而且每一次文案变更,无论出自 Agent 还是人,都集中落在同一个文件里——在那里,被改动的一行本身就是你关心的事件,而不是淹没在噪声里。扫一遍语言文件的 diff 只要几秒,回滚一次糟糕的改写只需改一行。
需要说明这能换来什么:Agent 仍然可以编辑语言文件。重点不在于让措辞碰不得,而在于让改动措辞不再是副作用,而成为清晰、可审查的动作。这就是从 diff 里发现问题,与从客户那里发现问题之间的差别。
关于为什么应把事实来源放在代码库里,而不是放在同步进代码的设计工具里,完整论述见支柱文章代码原生的产品文案。本文只需要其中一个结论:键化文案能让你的规则文件从一个请求变成可检查的东西。
审查关卡是什么样的?
把对源语言文件的任何 diff 都视为文案变更,并交给负责措辞的人审查。
具体做法:把 locales 目录加入 CODEOWNERS,任何改动都会请求文案负责人审查;在 PR 模板里加一行——“此 PR 是否修改了面向用户的措辞,这是任务要求的吗?”;并保留规则文件中要求 Agent 在总结里列出所改键的条目。Agent 照做时,审查一眼即过;不照做时,CODEOWNERS 钩子也会兜底拦住。
对小团队来说,这比听起来轻量得多。语言文件的 diff 很短,读起来是大白话,“这个按钮现在该写 Log in 吗?”五秒就能决定——前提是这个问题真的摆到了对方面前,而这正是关卡的全部职责。
如果 Agent 已经改写了文案怎么办?
先恢复已批准的措辞,再把字符串键化,让下一次改写以 diff 的形式显现。
找出受损之处本质上是个 git 操作:把受影响的组件与你信任的最近一次提交做 diff,只看字符串字面量,把改动的部分恢复。如果你无法确定哪个措辞才是已批准的——按钮写的是“Log in”,营销页写的是“Sign in”,也没有任何记录说明哪个才对——那你就碰到了这类事故会不断加剧的更深层状态。文案漂移(copy drift)指的是用户可见文本在不同界面之间、随时间推移与其已批准的源文案不一致,而且根本没有唯一的事实来源可供对照。这个概念、成因及其下游影响,另有专文:什么是文案漂移。
然后把恢复好的内容键化,从 Agent 动过的字符串开始。它们显然正处在火线上。
本地化在其中处于什么位置?
悄悄改写在单一语言里只是个麻烦,在十种语言里就是隐患:所有基于旧措辞的翻译都已过期,却没有任何提示让你重新翻译。
Agent 事故的影响正是在这里叠加的。英文改写版上线后,德语、日语和西班牙语仍是旧说法——界面之间的不一致不再只存在于不同页面,而是跨越了不同语言。语言目录互相脱节是另一种故障模式,有各自的解决办法,详见翻译文件不同步。
键化文案同样能补上这个环节:源字符串一旦变更就是一个可见事件,其翻译挂靠在它之下,而不是一个悄悄挪了位置的字面量。这正是 globalize.now 的设计思路:文案放在代码库里,单一来源、通过键引用,本地化是同一套操作而非外挂——连接一个仓库,所有语言都从这唯一的来源派生。使用 AI 编程工具交付的团队最早遇到这个问题,所以我们的内容面向开发者,也面向用 AI 工具构建产品的人。
下一次 Agent 进入你的组件时,被改掉的标签会以一次决策的面貌出现,还是变成一个意外?globalize.now 在你连接的仓库中单一管理用户可见字符串,所有语言都从这唯一的一份文案派生。
globalize.now 把硬编码的应用文案转换为可直接翻译的语言文件,并在你持续发布时保持同步更新。
免费试用 globalize.now