i18n-extract:用户能看见的话,一句都不许写死
这是 Harness 系列的 skill 拆解篇,拆的这一份叫 i18n-extract。它不在澄清链里,也不绑某个阶段,属于通用组:加国际化支持时用、发布前审计文案时用、把文案交译者之前用。母篇《从能跑到可交付:一套前端 AI 协作系统的设计笔记
》讲整套系统的形状,这个系列逐份拆 skill:为什么有它,具体怎么干。我用的系统是开源的 frontend-harness-cli
,文中所有规则都能在仓库里找到对应实现。
英文版上线了,页面上躺着三句中文
先看一个具体的场景。
一个产品做了英文版,上线前大家都点过:首页是英文,列表页是英文,设置页也是英文。发出去第二天,用户截图来了——英文界面里躺着三句中文:一句在空状态里(“暂无数据”),一句在删除确认弹窗里,一句是表单校验失败时的提示。
还有更难看的:某个按钮上印着的不是文字,是 checkout.submit.label——一个 key 的名字。这个按钮的代码早就改成调用翻译函数了,但资源文件里从来没人给它建过这条记录,运行时取不到值,只好把 key 本人显示出来。
这两类事故的共同点:没有一处是"翻错了"。 三句中文是没人提取过——开发时顺手写死在组件里,测试时大家的注意力都在主流程上,那几句藏在空状态和弹窗里的文案,谁也没"注意到"。按钮那条相反:提取做了,资源却没跟上。
skill 原文第一句就点破了这种故障的性质:国际化是悄悄坏的。 一句硬编码文案以一种语言上线,一个组件调用了从没加进资源文件的 key,没人发现,直到用户截图发出来。硬编码不报错,缺 key 也不报错——运行时顶多回退一下。整个系统里没有一盏灯会为它亮。
这一份 skill 是什么
i18n-extract 干两件事,原文说得很明确:提取,和完整性校验,两半一起做——只做一半,故障模式就还在。
- 只提取不校验,就是上面那个按钮:代码里全是翻译调用,资源文件缺了几条,界面上印 key 名。
- 只校验不提取,就是那三句中文:资源文件再完整也救不了它们,因为它们根本不在资源系统里,校验扫的是 key 的对应关系,扫不到一句写死在组件里的中文。
所以它的产出不是"翻译好了",而是两个可数的结论:审计范围内硬编码的用户可见字符串为零;每个翻译调用,在每个已发布语言里都有对应的 key。数得出来,才算做完。
它怎么干:六步,从定约定到逐语言校验
流程在 skill 里是固定的六步,每一步都有明确产出。
第一步,先定约定。 动文件之前,先从代码库里确认三件事:资源文件怎么组织、key 的命名模式(比如 namespace.section.key)、代码里怎么调用翻译。产出是写下来的约定;如果项目里还没有,先提议、取得一致,再动文件。为什么这一步在最前面?因为提取是批量动作,约定错了,后面几百个 key 全错。
第二步,扫硬编码字符串。 在目标代码里系统地搜用户可见的字面量:JSX 文本、按钮标签、placeholder、aria-label、toast 和错误信息。产出是一份 file:line 的命中清单。注意排除项:类名、测试 ID、日志信息这类不可见字符串不算——它们不是文案(第三个设计决策会细说)。
第三步,用稳定 key 提取。 每条文案放进资源文件,key 要能说清它的位置和含义;代码里的字面量替换成翻译调用。产出是资源条目与调用点一一对应——一条文案一个 key,一个 key 一处含义。
第四步,双向校验完整性。 正着查:代码里每个翻译调用,key 在资源文件中存在吗?反着查:资源文件里每个 key,有没有人调用?产出两份清单——缺失 key,必须为零;未使用 key,只报告、不自动删(可能是别的分支在用,也可能是真废弃了,让人来判断)。
第五步,每个语言都查,不只默认语言。 对每个已发布的语言文件重复存在性检查,产出按语言分开的缺失计数。这一条的口径很硬:一个 key 在某个语言里缺失,是缺陷,不是待办。因为运行时它不会报错,只会回退——回退到默认语言,或者回退到显示 key 名——用户先于你发现。
第六步,实际跑一遍界面。 条件允许时,用非默认语言打开受影响的页面,亲眼看有没有原始 key 或回退文本漏网。产出是查过的页面清单;实在跑不了,就明写一条"未运行",不许假装查过。
六步走完,对照完成判据:范围内零硬编码残留、每个语言缺失计数为零、未使用 key 列在报告里、key 命名一致遵循约定。
设计一:提取是清扫,不是点修
为什么第二步必须是"扫",而不是"看到一句提一句"?
因为漏掉的那一句,按定义就是没人注意到的那一句。
点修的逻辑是:谁发现一句没翻译的,就顺手提掉。这个逻辑依赖"发现"——而硬编码文案藏身的地方,恰好都是注意力到不了的地方:空状态、错误 toast、校验信息、无权限提示。主流程上的标题和按钮人人看得见,早就提取了;剩下的全在支路上。skill 的反模式清单专门点名这一条:只提取可见的顺利路径,把错误 toast、空状态、校验信息留成硬编码——那些恰恰是用户会截图的字符串。 出事的时候用户截的图,永远是出错的那一屏。
所以提取的单位不是"句",是"范围":一个功能区、一个页面组,整片扫一遍,命中清单归零才算完。扫这个动作不性感,但它把"有没有漏"从运气问题变成了计数问题:清单还剩几条,看得见。
设计二:key 和它的译文,必须同生同死
提取这一步里藏着两条纪律,违反了,后患比不提取还大。
第一条:key 命名含义和位置,永远不命名措辞。
反模式是拿英文句子当 key——"Click here to continue" 直接当键名。用起来很顺手,看 key 知道意思。但第一次改文案就出事:文案从"Click here to continue"改成"Continue",key 要么跟着改——所有调用点全部变孤儿;要么不改——key 和显示文字从此对不上,下一个人读代码时被误导。原文的话:key 命名的是含义和位置,永远不是措辞。 措辞是会变的,含义和位置不变;key 必须绑在不变的那个上面。
第二条:新 key 和它在每个语言的条目,同一次改动里一起落地。
这就是第五步的前置版。先在代码里写下调用、资源文件"回头再补",等于亲手制造那个印着 key 名的按钮。原子性的要求是:调用点、默认语言条目、其他语言条目,在同一次变更里齐活。某个语言暂时没译文,也要显式占位并记成缺陷,不许靠"文件里没有"来表达"还没翻"——运行时分不清这两种状态,只会回退给你看。
还有一条相关的:不许拼凑翻译片段。 "You have " + n + " items" 这种把句子拆成碎片、中间塞变量的写法,默认了所有语言都是"主语-动词-数量-名词"的语序。换个语言,语序一变,拼出来的就是病句。正规做法是参数化消息:整句作为一条文案,变量用占位符嵌进去,让译者能挪动占位符的位置。
设计三:什么不许提取
扫的时候如果无差别提取,会把另一类东西污染进来:根本不是文案的字符串。
skill 在第二步就划了线:类名、测试 ID、日志信息,按项目约定排除在外。想想为什么。类名和测试 ID 是给机器读的标识符,把它们塞进资源文件,译者会一脸茫然地"翻译"一个 submit-button,而真正的风险是有人改了译文,测试和样式跟着崩。日志是给开发者看的,写进日志的那一刻就该定死一种语言,跟着用户语言切换的日志没法搜、没法聚合。
这条边界的本质是:i18n 管的是"用户能看见的话",不是"代码里的所有字符串"。 提取范围划错,两边都受伤——该提的漏掉是事故,不该提的提进来是噪声,噪声多了,资源文件没人敢信,校验报告没人愿意看。
机器怎么兜底
六步流程靠人(或 AI)执行,总有扫漏的时候。系统里还有一层机器兜底:harness scan 的数据维度有一条 i18n-missing-key 检查——代码里被引用、却在某个语言文件里不存在的 key,按缺失计扣分(每个 6 分)。
它和流程的关系是分工:流程里的第四、第五步是交付前的主动校验,目标是为零;扫描是随时能跑的被动体检,防止的是"上次校验之后又有人新写了调用、没建条目"。一个是出门前的检查清单,一个是烟雾报警器——报警器不能代替检查清单,但没有报警器,清单上次查完之后的变化就没人管了。
它的边界
把边界说清。
第一,它只管存在性,不管翻译质量。key 齐全、文案蹩脚,它判全对——译文好不好是译者的活,它最多保证译者拿到的清单是完整的。
第二,它管不了不在代码里的文案:后端返回的错误信息、数据库里存的运营文案、图片里烘焙进去的文字,都不在扫描范围内。那些得在各自的系统里另设规矩。
第三,它的结论只对审计范围成立。扫了结算页,没扫设置页,设置页里的硬编码不会自己消失——范围是人划的,划界时漏掉的区域,和漏掉的那句文案一样危险。
说到底,这份 skill 处理的是一种很特别的故障:不报错、不崩溃、不留日志,只在用户截图里现形。对付这种故障没有取巧的办法,只能把"每一句用户能看见的话"变成可清点的资产,再定期点数。下一篇拆 bug-root-cause:bug 已经发生了,怎么逼自己先找到根因、再动手修,而不是先把症状按下去。