开源中的翻译、模组与模组自由
Unciv 本质上是对《文明5》的复刻,这意味着在机制层面几乎天然没有多少创新空间。 在 UI 方面,这里没有任何东西是没被做过几十次、而且做得精致得多的。 然而,有一个领域 Unciv 是开创性的:翻译的可及性、模组的可能性空间,以及两者之间的关系。
翻译
翻译流程
让我们从翻译说起。这肯定是已经解决的问题,对吧?源文本 + 语言 = 翻译文本,这些信息需要放进文件里让游戏读取。我们和 Firaxis 之类公司有什么不同?
有几件事,但最重要的是:这是开源游戏,因此翻译也是开源的。 这意味着翻译者都是业余爱好者,且没有义务翻译——如果翻译很困难,他们干脆就不翻了。
业余者会犯错,所以错误必须容易被发现,这一点至关重要。这意味着像 "translation key" 这样的格式——例如 DIPLOMACY_GREETING = Siamo lieti di fare la vostra conoscenza.——远不如 A pleasure to meet you. = Siamo lieti di fare la vostra conoscenza. 有效。这种格式既便于翻译(一眼就知道要翻译什么),也便于真正的协作。
我们经常收到的一个建议(来自对项目不太熟悉的人)是"用网站做翻译"。对小型开源游戏来说这不是坏建议,但有多个缺点,而(目前)没有任何翻译网站的优势足以抵消:
- 测试。目前翻译要经过一系列测试验证——后面细说!这让某些语言改动被接受、另一些被拒绝,而且都在同一个平台用同一套测试。外部翻译工具做不到。
- 历史与修订。这正是 Git 的用武之地,世界上没有比它更好的了。单凭这一点就……
- 发布周期。我们大约每半周发布一个版本,如果每次游戏内改动都要上传到翻译网站、每次发布都要下载,那就是额外的工作。有些网站可以自动化——大多数不行。
- 讨论。大多数众包翻译网站不允许对翻译进行讨论和修正。Github 让每条翻译都成为协作成果。
- 批量改动。如果我们要改翻译源文本但保留各种译文(比如把 "Gold from trade routes +[amount]%" 改成 "+[amount]% Gold from trade routes"),只要所有翻译文件都在 Git 里,一分钟就能搞定。如果在外部的,就难说了。
以下是我们过去踩过的一些坑:
- 把所有语言放进同一个文件("一个巨大的翻译字典")——多人同时为不同语言编辑这个文件时会互相冲突。拆成不同文件便于管理。
- 使用 json——json 对机器很好,但对人就没那么友好,人很容易犯错。json 格式出奇地挑剔,少一个结尾引号整个文件就读不了了。
我们最终选择的格式是每种语言一个文件,用 " = " 分隔以利视觉分离,使用 .properties 格式。以 # 开头的行视为注释,所以我们可以为翻译者添加注释。
构建翻译文件
如前所述,Unciv 大约每半周发布一个版本,而这些改动经常包含新对象或新 UI 元素。我们如何保持所有翻译文件最新?
在 Unciv 中,所有对象数据都以 json 格式存储。这让我们可以遍历所有对象(无论类型),提取各种文本字段(字符串或字符串列表)。我们通过保存所有已添加的翻译文本来避免重复,并用现有翻译为在 json 文件中找到的每条翻译 "key" 填充 "value"。
由于我们每次都会重建整个翻译文件,目前翻译者无法为后来的翻译者保留自己的注释。 但另一方面,由于对每行我们添加的内容都已经知道它是否已翻译,我们可以为每条未翻译的行前加 # Requires translation 标记,这能帮助那些几乎全翻译完的语言的翻译者用 ctrl+f 轻松定位需要翻译的新词条(当然下次重建文件时标记会消失)。
由于有些 UI 文本不属于任何特定对象(比如 "Start new game"),我们有一个独立的 template.properties 文件来放那些不在 json 文件里的待翻译文本。与添加对象不同(开发者完全不需要碰翻译文件,因为都是自动关联的),添加带新文本的 UI 元素时,开发者需要记得把文本加到 template.properties 里。
翻译占位符
这对具体的文本到文本翻译很有效,但翻译 "A Temple has been built in Rome" 怎么办?同一个模板可能对应任何建筑名、任何城市名!
我们用占位符解决,看起来像这样:[construction] has been built in [cityName] = [cityName] ha costruito [construction]。 如你所见,参数的位置在不同语言之间可以不同,所以我们必须给所有参数命名。
这也意味着可能存在明确错误的翻译——如果源文本中出现的某个参数没有出现在译文中,游戏里就显示不出来!这就是我们前面提到的翻译测试之一:翻译者开 PR 时,游戏会通过 Github Actions 构建并测试,有失败会通知。目前从 action 输出中找到警告失败的那段文本主要由开发者来做,但我希望有一天也能自动化。
因此,要翻译 "[Temple] has been built in [Rome]" 这样的文本,我们需要:
- 找到相关翻译(做法是擦除输入中方括号之间的所有文本,找到匹配的翻译文本)
- 把占位符名映射到输入文本(construction = Temple, cityName = Rome)
- 把译文中的占位符替换为翻译后的输入文本(在
[cityName] ha costruito [construction]中,把 "[cityName]" 替换为 "Rome" 的翻译,把 "[construction]" 替换为 "Temple" 的翻译)
翻译模组数据
翻译生成从"一个规则集"读取信息,即定义游戏对象的那组 json。 每个模组也是一个规则集,要么替换要么添加游戏定义的基础规则集。 这意味着我们为基础游戏做的同一套翻译生成也可以应用到模组上,因此每个模组制作者都可以(在游戏内)决定为自己的模组生成翻译文件;由于模组作为发布方法论的一部分上传到 Github 以广泛分发,翻译者就能用翻译 Unciv 基础规则集完全相同的方式来翻译这些文件。