书译
书译

如何翻译 JSON 语言文件而不破坏键或占位符

了解 en.json 文件中哪些值会被翻译,文件翻译器会重写什么(注释、键顺序、大型 ID),以及如何用免费脚本和测试文件检查占位符、ICU 复数和键。

BookTranslator

BookTranslator Team

19 分钟阅读

要翻译 en.json 这样的 JSON 语言文件,请将整个文件交给能理解 JSON 的翻译器。将原始文本粘贴到聊天窗口或网页翻译器中是有风险的做法。能识别 JSON 的翻译器会读取文件,只翻译文本值,并将相同的键写回,因此你的应用仍然可以加载该文件。复制粘贴可能会翻译键、更改引号或丢失大括号。其中任何一种都会破坏文件。

保留键是容易的部分。大部分麻烦来自之后:一个 {name} 占位符返回成了 {nom},一个 ICU 复数丢失了 other 分支,一个 URL 像其他文本一样被送去翻译,以及翻译器对文件本身做出的一些更改。这些就是本指南涵盖的内容。

我们构建了一个测试文件,其中包含了所有这些情况,并编写了一个免费检查器,用于比较源文件与其翻译。我们还将测试文件通过 BookTranslator 使用的 JSON 解析和写入代码的本地副本运行,用标记代替模型,这样你可以确切看到哪些值被发送以及文件如何返回。

快速回答:文件的每个部分会发生什么

文件部分我们测试中的示例BookTranslator 的做法需要检查的内容
键"greeting", "error"保持原样无
纯文本值"Reading list"已翻译含义、屏幕上的长度
含占位符的值"Welcome back, {name}!", "Saved %s of %d chapters"已翻译,占位符包含在发送的文本中占位符名称和数量(检查器会做这个)
ICU 复数消息"{count, plural, one {# book} other {# books}}"作为单个字符串发送other 分支,以及目标语言的复数类别
内联 HTML"accept the <b>Terms</b>."已翻译;翻译后检查标签数量标签名称和数量(检查器会处理)
以文本形式存储的 URL"https://example.com/help"发送翻译返回时保持不变
不含字母的值"2.4.1", "—", ""保持原样无
数字、true/false、null20, true保持原样超大 ID(见下文)
字符串数组["Open", "Save", "Export as EPUB"]在翻译文件中翻译;在双语文件中不翻译双语文件(如果使用)
注释(JSONC)// Shown on the home screen已移除。如果值中也包含 //,文件可能无法加载上传前移除注释

本指南的其余部分将说明每一行的来源以及如何处理。

第 1 步:上传前检查源文件

对计划翻译的文件运行检查器。它只需要 Python 3,无需其他依赖:

python3 check_i18n_json.py en.json

在我们的测试文件上,它输出:

WARN  limits.projectId: 9007199254740993 is above 2^53 and can change in a JavaScript round trip; store IDs as strings
WARN  error.network: a flat key with dots and a nested key resolve to the same dot path; tools that address strings by dot path can mix them up
INFO  (root): 3 keys look like integers; JavaScript sorts those first and in numeric order when it rewrites the object
INFO  source: 17 strings, 2 numbers

两条警告和关于类整数键的提示,各自对应我们在测试文件经过翻译器时看到的一项变化。如果你的文件没有出现任何一项,可以跳到第 3 步。

检查器还会拒绝非严格 JSON 的文件,例如包含注释或尾随逗号的文件,因为这是下一个问题。

测试文件的返回结果

测试于 2026 年 11 月 2 日运行,针对 BookTranslator 后端当前的 JSON 处理。测试没有调用模型,而是将每个待翻译的值替换为 [ZH] 加上原始文本。这样结果可预测:前面有 [ZH] 的都被发送了,没有的则未发送。两个输出均已发布:翻译文件 和 双语文件。

翻译后的文件

每个键都原样返回,文件也仍然可以解析。包含字母的值都被发送了,包括 {name}、%s、ICU 消息、<b> 标签以及帮助 URL。版本字符串 "2.4.1"、短横线和空字符串没有被发送,因为它们不包含字母。数字和布尔值则保持原样。

另外还有四处变化:

  1. 一个字符串被放进了错误的键。 在我们的测试中,文件里有一个扁平键 "error.network",其文本为“Check your connection.”,同时还有一个嵌套的 "error": { "network": "Network error" }。翻译器通过用点连接各部分的路径来标识每个字符串,因此两个字符串拥有相同的地址 error.network。在输出中,两个键都保存了“Network error”的翻译。“Check your connection.” 消失了。这需要同一文件中同时存在两种风格,虽然罕见,但会静默失败。检查器会在你上传之前发出警告。
  2. 类似整数的键被重新排序。 "steps": { "10": ..., "2": ..., "1": ... } 返回后变成了 "1"、"2"、"10"。JavaScript 在重建对象时,总是先按数字顺序列出类似整数的键。键和值仍然匹配,所以你的应用没问题,但与源文件的差异对比会变得嘈杂。
  3. 一个大 ID 丢失了精度。 9007199254740993 返回后变成了 9007199254740992。JavaScript 将数字存储为 64 位浮点数,而大于 2^53 的整数无法全部精确表示(MDN 关于 Number.MAX_SAFE_INTEGER)。请将长 ID 存储为字符串,或将它们排除在语言文件之外。
  4. 格式被重写了。 输出使用两个空格缩进,并以换行符结尾。如果你的源文件使用制表符或四个空格,差异对比中会出现空白变化。

我们的测试文件没有重复键,但值得了解:JavaScript 的 JSON 解析器对于重复键只保留最后一个值,因此较早的文本永远不会被翻译。你的应用会加载相同的最后一个值,所以可见部分不会出错,但检查器会列出重复键,以便你清理。

双语文件

双语文件保留每个原始值,并添加一个末尾带有 _translated 的兄弟键:

"greeting": "Welcome back, {name}!",
"greeting_translated": "[ZH] Welcome back, {name}!",

这对于审阅很有用,因为源文本和翻译并排放在一起。但这不是你的应用可以加载的文件,因为现在每个键都有一个孪生键。请在应用中使用翻译后的文件。

一个缺口:数组内的字符串不会获得 _translated 兄弟键,因为数组项没有键可供添加。在我们的测试中,"menu": ["Open", "Save", "Export as EPUB"] 在双语文件中返回英文,但在翻译后的文件中已翻译。如果你从双语文件进行审阅,请检查翻译后文件中的数组。

带注释的文件

许多项目在语言文件中保留注释(JSONC),而严格 JSON 不允许这样做。翻译器首先尝试严格 JSON。如果失败,它会移除注释和尾随逗号,然后重试。移除注释意味着删除一行中 // 之后的所有内容,这包括 https:// 中的 //。

我们的带注释和 URL 的文件完全无法加载;解析在第一个注释处失败。一个带注释但任何值内都没有 // 的文件加载正常,并且注释从输出中被丢弃。

所以:如果你的文件有注释,请在上传前移除它们,或者保留一份带注释的副本,并接受翻译后不会包含它们。

第 2 步:准备文件

你可以在翻译前解决大部分问题:

  • 使其成为严格 JSON。 移除注释和尾随逗号。如果你的构建工具读取 JSONC,请将带注释的文件保留为源文件,并导出一份干净的副本用于翻译。
  • 使用一种键风格。 要么嵌套键,要么用点扁平化键,不要在同一文件中同时使用两者。如果你使用 i18next,其 keySeparator 选项控制如何读取键中的点,因此在扁平化任何内容之前请检查该设置。
  • 将非文本数据移出。 ID、URL、功能标志和限制属于配置,而不是语言文件。如果 URL 必须保留,请保留它,但预计在翻译后需要检查。我们的不翻译指南涵盖了如何决定还有哪些内容应保留原语言。
  • 以相同方式编写占位符。 一种风格,例如 {name},比混合使用 {name}、{{name}} 和 %s 更容易检查。

第 3 步:翻译整个文件

将清理后的文件上传到 BookTranslator 的文档翻译器,该工具将 .json 与其他接受的本地化和结构化文本格式一起列出。价格在结账前显示。任务完成后,你的历史页面会提供翻译后的文件和双语文件作为单独下载。

文本的翻译方式有两点会影响最终结果:

  • 模型看不到你的键名。 每个值都作为带编号的片段发送,不包含键名。如果 "button.open" 和 "status.open" 都写着“Open”,模型无法知道其中一个是动词,另一个是形容词。简短且含义模糊的字符串正是需要手动检查的对象。
  • 每个值都是独立翻译的。 没有任何机制将两个键关联起来,因此同一个词出现在两个地方可能返回两种不同的译法,而一个需要两种不同译法的词也可能返回相同的译文。BookTranslator 的可选自动术语表会收集人名、地名和专业术语并保持其一致性,但你无法为它提供自己的固定译法。对于必须采用唯一确切译法的术语,请维护一份术语列表,并在翻译后检查这些字符串;我们的术语表指南说明了如何规划一份术语表。

内联 HTML 标签会在翻译后计数,标签不匹配的片段会被重试。花括号占位符、%s 风格的占位符以及 ICU 语法会作为文本的一部分发送,但不会以同样的方式检查。请对每一份译文执行第 4 步。

如果你更愿意使用聊天模型或本地模型进行翻译,请只发送值,绝不要发送原始文件,然后用脚本按键将其放回。下面的检查在两种情况下都适用。

第 4 步:检查翻译后的文件

将译文与源文件进行对比:

python3 check_i18n_json.py en.json fr.json

为了展示它能捕获哪些问题,我们制作了一个植入了错误的法语文件。它是手工制作的,并非模型输出。在出现与第 1 步相同的源文件警告之后,检查器打印出:

ERROR app.greeting: placeholders differ; lost ['name'] extra ['nom']
ERROR app.help_url: urls differ; lost ['https://example.com/help'] extra ['https://example.com/aide']
ERROR app.items: ICU message has no 'other' case
ERROR app.legacy: placeholders differ; lost ['%d'] extra []
ERROR limits.maxFiles: type changed from int to str
ERROR limits.projectId: non-text value changed: 9007199254740993 -> 9007199254740992
INFO  menu[1]: identical to source: 'Save'
INFO  compared 20 source values, 1 unchanged strings

上述每一项都会在应用中表现为缺陷:名称为空、链接损坏、渲染复数时崩溃或显示原始消息、缺少章节数、将限制值作为文本进行比较。INFO 行列出了返回后未发生变化的字符串。有些是正常的,比如品牌名称;但法语菜单中的“Save”则不正常。

检查器会比较键、值类型、非文本值、占位符({name}、{{name}}、%s、%1$s、%@)、ICU 参数名、HTML 标签和 URL。它不会评判翻译本身的质量。

复数类别本就应当变化

ICU 复数会为每个复数类别列出一条消息。英语有两个类别:one 和 other,但许多语言有不同的类别集合。我们 items 消息的一个好的俄语翻译通常会使用 one、few、many 和 other,而日语翻译只使用 other。因此,这里的类别不匹配并不总是错误。

这些类别来自 Unicode CLDR 复数规则,而 other 是每种语言都有的类别。以下是 Node.js 中 Intl.PluralRules 在 CLDR 48 下报告的基数类别:

语言复数类别
英语、德语one、other
法语、西班牙语、葡萄牙语one、many、other
俄语、波兰语one、few、many、other
阿拉伯语zero、one、two、few、many、other
日语、中文、韩语其他

法语、西班牙语和葡萄牙语仅在表示百万等非常大的整数时才使用 many,因此小数量对应的消息通常无需该类别。检查器会将缺失 other 报告为错误,而其他类别变化则作为警告,供与此表对照。你可以自行打印任意语言的列表:

node -e 'console.log(new Intl.PluralRules("ru").resolvedOptions().pluralCategories)'

翻译器会收到英语的复数类别,因此请检查返回结果。如果俄语或阿拉伯语消息仍然只有 one 和 other,请手动添加缺失的类别,或让译者完成。

然后在应用中加载它

检查器证明文件可以安全加载,而非读起来通顺。将应用切换到新语言并查看界面:德语或芬兰语中的长单词是否会溢出按钮,阿拉伯语或希伯来语的从右到左文本,以及代码与语言文件分开格式化的日期或数字。我们的翻译与本地化指南涵盖了除文字之外,不同市场还会发生哪些变化。

常见问题

翻译后的文件无法在我的应用中加载。 对其运行检查器。如果检查器无法解析,说明文件在翻译后被编辑过,或原本就无效。如果解析成功,请查找关于键或类型的 ERROR 行。

字符串显示为原始 ICU 消息,如 {count, plural, ...}。 ICU 语法在翻译中损坏,通常是缺少大括号或缺少 other 分支。从源文件复制结构,仅翻译每个分支内的文字。

我的文件是 YAML、.po 文件或 Markdown。 BookTranslator 也接受这些格式,但每种格式有其规则。文档方面,请参阅我们的 Markdown 翻译指南;网页方面,请参阅 HTML 文件指南。

我可以只翻译新增的键吗? 单次上传不行;整个文件都会被翻译。将新增键放入单独文件,翻译后按键合并结果。检查器将确认合并后的文件包含所有键。

我应该翻译键吗? 不。键是代码查找的标识符。只有值是供人阅读的文本。

相关文章