为什么改一句台词会把本地化搞错位,以及稳定的字符串 ID 怎么防住它
几乎每一次本地化错位,根源都是 ID 从某个会变的东西推导出来。修法很朴素,但必须在第一份字符串表发出去之前就定下来。
发布于
这个 bug 长这样。你把字符串表发给译者。两周后你改写了一段对话、重新导出、发版。 德语版里,一个角色回答了一个没人问过的问题;三句之后,有人在场景中间道了别。 英文版一切正常。
好一阵子没人发现,因为要发现它,你得用一门自己读不懂的语言把游戏玩一遍。
原因永远是同一个
本地化会错位,是因为一个字符串的身份是从会变的东西推导出来的—— 它在文件里的位置、它文本的哈希、或者它在数组里的下标。改写一句, 它后面每一个 ID 都跟着挪,于是译文仍然挂在那些 ID 上, 而那些 ID 现在指向的是别的原文。修法是:在一句台词第一次被写下来的那一刻 分配一个 ID,此后再也不从任何东西推导它——之后这句台词可以移动、可以改写、 可以删除,都不会惊动任何别的台词的身份。
那三种推导法,写的时候看起来都挺合理:
- 按位置(
dialogue_042、line_17)—— 直观、好读,而且在有人往中间插一句的 第一时间就错。 - 按内容哈希 —— 因为确定性而显得诱人,而它恰好是反的:改一个错别字就会改掉 ID, 而那正是你最不想丢掉译文的时刻。
- 按结构路径(
quest.scene.node.3)—— 熬得过改错别字, 熬不过重组场景,而重组比改错别字发生得更频繁。
分配出来的 ID 一样毛病都没有,因为它没有任何性质。它只是一个名字。
分配的代价
得有人负责分配,而且 ID 必须跨重生成留存。这意味着要有一个权威—— 一份被写入的字符串表,而不是被推导出来的—— 以及“绝不把某个 ID 回收给另一句台词”的纪律,哪怕原来那句已经删了。
你还会失去可读的 ID,这在读 diff 的时候是实打实的损失。但它值。 一个会说谎的可读 ID,比一个不说谎的乱码 ID 更糟。
真正救你的那道检查
光有分配还不够,因为伤人的失败不是“某个 ID 变了”,而是“某个 ID 变了、没人发现”。 所以导出必须与上一次产出的字符串表比对,并在 ID 会发生意外变动时拒绝写出。
是拒绝,不是警告。构建期的警告是一条成功命令末尾没人看的文字, 而这个 bug 的全部特征就是“在变贵之前一直隐形”。我们的导出会中止, 并说出哪些 ID 本来会变。
如果你是中途补这一套
已经没记录下来的身份是找不回来的,所以选一个点冻住它: 把当前的字符串表拿来,给里面每一句分配一个稳定 ID,并把这份映射当作此后的权威。 冻结点之前的东西手工对一次,之后的都安全。
要赶在第一份字符串表送去翻译之前做,理由是一道算术: 冻结的代价正比于你做这件事时有多少句台词, 而不做的代价正比于你已经发了多少种语言。