如果你的应用由 Codex、Claude Code 或 Cursor 用一条提示词翻译完成,上线前请检查七件事:长字符串是否放得下,每种语言的复数形式是否匹配,变量是否完好保留,术语是否一致,语气在母语者读来是否自然,每个 key 是否在所有 locale 中都存在,以及下一个功能究竟会不会被翻译。globalize.now 是 AI 驱动的本地化基础设施,我们见到的一次性翻译后的问题,大多是机械性的,这反而是好消息:机械性问题可以检查出来。

首轮翻译很少是问题所在。现代 Agent 能快速产出通顺的译文。单条提示词缺少的,是校验输出的环节,而这些检查正是这个环节。

翻译后文本还放得下吗?

往往放不下,而且最先出问题的是按钮。W3C 关于翻译中文本尺寸的指南指出,源文本越短,翻译后膨胀的幅度往往越大,所以一个两词的按钮标签,膨胀比例比一整段文字还高。指南还提到,德语等语言中的复合名词可能无法换行,某些文字系统比拉丁文字需要更多的水平或垂直空间。

Agent 在 JSON 文件里翻译字符串时看不到界面,自然不知道“Save changes”现在已经把按钮撑满了。请用你发布的最长语言运行应用,走一遍主要流程,留意被裁切的文字、换行的按钮和被截断的表头。如果你支持从右到左的语言,也用其中一种再走一遍;Lovable 应用的阿拉伯语与 RTL 指南 讲的是文字方向问题,这是单靠溢出检查发现不了的。

治本的办法在布局,而不是缩短译文:让容器可以随内容伸展,避免给文本设置固定宽度,也别为了塞进去而缩写。这部分可以交给你的 Agent,因为它是代码问题。

复数形式与目标语言匹配吗?

请尽早检查,因为它出错时没有任何提示。Unicode CLDR 定义了六个复数类别:zero、one、two、few、many 和 other。英语只用 one 和 other,俄语用 one、few、many 和 other,威尔士语六个全用,i18next 的文档里阿拉伯语同样有全部六个复数键。

如果 Agent 把英语的那一对键原样复制到俄语或阿拉伯语文件里,得到的语言包可以正常解析和渲染,但凡是需要文件中缺失类别的数字,显示的都是错误的形式,而且不会报错。使用 i18next 时,变量必须严格命名为 count,文档明确说明,缺少它就不会回退到不带后缀的普通键。同一个 Agent 在组件里写 count === 1 ? 'item' : 'items',翻译开始之前就已经有同样的 bug;语法层面的问题可参阅什么是 ICU MessageFormat,以及它为何总让 AI 生成的应用栽跟头。

这项检查是机械性的:对每个 locale,把其中的复数键与该语言所需的类别逐一比对。

插值变量还在吗?

不一定,用脚本一查便知。{{name}}、{count} 之类的插值变量必须在每条译文中原样出现。Agent 翻译长文件时,有时会改写变量名,有时觉得去掉变量句子更通顺就直接删掉,有时还会把标签里的内容也翻译了。结果就是渲染出空白、裸露的大括号,或者直接崩溃。

把每条译文中的变量集合与源字符串逐一比对。下面这段简短的 Node 脚本可以完成比对,同时还会报告英语里有、其他 locale 里缺失的 key:

import fs from "node:fs";

const [src, ...targets] = process.argv.slice(2);
const read = (f) => JSON.parse(fs.readFileSync(f, "utf8"));
const vars = (s) =>
  [...String(s).matchAll(/\{\{?\s*[\w.]+\s*\}?\}/g)].map((m) => m[0]).sort().join(",");

const base = read(src);
let problems = 0;
for (const file of targets) {
  const loc = read(file);
  for (const [key, text] of Object.entries(base)) {
    if (!(key in loc)) { console.log(file, "missing", key); problems++; continue; }
    if (typeof text === "string" && vars(text) !== vars(loc[key])) {
      console.log(file, "variable mismatch", key); problems++;
    }
  }
}
process.exit(problems ? 1 : 0);

在 CI 中以 node check-locales.mjs locales/en.json locales/de.json locales/ru.json 运行它。脚本假定语言文件的 key 是单层结构,如果你的语言包是嵌套的,请相应调整循环。

术语一致吗?

同一次响应内部是一致的,跨多次响应就会漂移。大型应用的翻译若被拆分到多个文件、会话或批次,就没有任何机制保证“workspace”“project”和你的产品名每次译法相同。最后可能出现按钮写一种说法、帮助页又是另一种说法。

把术语一次性写下来:整理一份术语表,涵盖产品名、功能名和不可翻译的词,并让翻译环节读取它。globalize.now 在转换代码库时会生成共享术语表,因此术语在翻译开始前就已作为数据存在。如果不借助工具,至少也要准备一个简短的术语表文件,并在 Agent 指令中引用;AGENTS.md 规则一文 介绍了这些指令该放在哪里。

母语者读来自然吗?

这是脚本无法完成的一项检查。语域、正式程度和大小写,是一次性翻译最容易露出机器味的地方:敬称的正式程度不对,祈使句按钮读起来像在下命令,在某些语言里全大写的标题读来像在吼叫。

不必逐条审阅。从你发布的每个语系里各选一种语言,请母语者读一读首次使用界面、结账或注册流程,以及错误提示。这些界面的语气直接影响用户去留。把他们的修改记入术语表,下一轮就能从这些修正出发。

每个 key 都存在吗?每条字符串都进了语言包吗?

两种不同的缺口在界面上看起来一模一样。一种是 key 在英语里有、在某个 locale 里缺失,上面的脚本能查出来;另一种是字符串直接硬编码在组件里,根本没进语言包,自然没有任何译文能覆盖它。Agent 在属性、toast 和错误消息里留下的第二种问题,比在可见文本里更常见;Agent 为什么总是新增硬编码字符串 讲了这种模式,以及能抓住它的 lint 规则。

把应用切到非英语 locale,逐页点一遍。剩下的任何英文,要么是缺失的 key,要么是从未进入系统的字符串,两者的修复方式不同。

下一个功能会被翻译吗?

只有当翻译成为代码发布流程的一部分,下一个功能才会被翻译;而这正是一条提示词跑完的方案过不了的一关。那条提示词翻译的是当天下午的应用状态。Agent 接下来构建的功能仍会以英文出现在同样的文件里,各个 locale 就会一个功能接一个功能地落后。翻译文件为什么会逐渐失去同步 描述了这种落差是如何累积的。

一次性方案和流水线的分野就在这里。本地化前移到开发循环中 阐述了这一立场;落到实践上就是:转换只做一次,之后的改动在产生的地方随即翻译。使用 globalize.now,你只需在应用中连接仓库,代码库会一次性完成转换,此后由推送任务翻译新增的语言包条目,结果以变更的形式交付,由你审阅并合并。运行时库和语言包格式始终归你所有。已有人工译员的团队可以保留审校环节,只是它不再是英文代码与发布之间唯一的一道关卡。

如果你更想用自己的脚本来做,最低要求是在 CI 中运行上面的脚本,再加一条规则:新增字符串必须走语言包。如何本地化 AI 生成的应用 带你走完整条路径,Claude Code 指南 则专门讲这个 Agent。

从哪里开始?

今天就拿现有的语言文件跑一遍脚本,只要一分钟,就能知道结构是否健全。然后用你最长的语言打开应用,把首次使用流程点一遍。如果你发布了一个 AI 构建的应用,希望转换、翻译和后续更新都有人代劳,而不是自己手动维护,vibe coder 页面 是总览,开发者文档 介绍 Agent 的配置方式,定价 则有单独的页面。

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

免费试用 globalize.now