你的流水线会检查 JavaScript 语法、做类型检查、跑测试、查格式、审计依赖、扫描密钥。然后它把一个没人读过的 de.json 直接发到了生产环境——这个文件由某个没人审计过的模型生成。

这不是假设。我在测试语料里埋了一个 bug,它真实反映了这类问题:

打开网易新闻 查看精彩图片

  • 英文原文:"Your changes were saved successfully."(你的更改已成功保存。)
  • 西班牙语译文:"No se pudieron guardar tus cambios."(你的更改无法保存。)

西班牙语说的是“无法保存”。不是近似错误——而是英文的完全相反。同一个 key,两个字符串里都没有占位符,所以没有可丢失的东西。JSON 合法,key 集合逐字节一致。

我见过的每一个 i18n 检查工具都会放过这个文件。

标准工具在检查什么

常规工具比较的是语言文件的结构。取源文件树、取目标文件树、分别展开、对比 key 集合。你会得到一份差异报告。

这确实有用,你应该保留它。它能抓住最常见的 i18n 缺陷——周五有人加了 key 但没人翻译。它毫秒级运行、确定性输出、零成本。

但看看它实际检查的是什么:它读 key,不读 value。上面列出的每一项检查都是关于结构的陈述,而上面那个 bug 不是结构问题。结构完美,语义颠倒。

第二层:占位符漂移

结构工具能触及的第二层是占位符漂移,值得做好:

  • 英文原文:"Hello {{name}}, you have {{count}} new messages"
  • 西班牙语译文:"Hola, tienes mensajes nuevos"

两个占位符都消失了。取决于你的 i18n 库和框架,这要么渲染成一句丢失了主语和数字的句子,要么在运行时抛错,要么——我最喜欢的——在生产环境把字面量 {{name}} 静默渲染给用户。

这件事比看起来难,因为“占位符”不是单一语法。在一个混合代码库里,你会遇到多种写法。只认识 {{...}} 的检查器,面对一堆 %1$s 的文件会报告“干净”。而位置型 printf 有它自己的失败模式:翻译者可能为了符合目标语言语法而合法地调换 %1$s 和 %2$s 的顺序,所以“占位符顺序不同”是正确的、不应被标记,而“%2$s 完全消失”才是 bug。

方向也很重要。源里有、译文里没了的占位符是错误。译文里凭空出现、源里没有对应物的占位符是另一个更奇怪的问题——通常是幻觉——它应该得到警告而不是硬失败,因为偶尔这是有意为之。

所以:做结构检查,覆盖所有语法,把方向和位置型情况搞对。这能覆盖大多数缺陷。但它覆盖不了语义颠倒的句子。

显然的办法是让模型读每一对源/译文,然后判断是否一致。