把对话做成舒服的双语版本:一场从“能用”到“好用”的工程化折腾

最近我做了一件看起来很小、但实际上很有工程味的事:

把一款 Ren’Py 游戏《Eternum》,从已有中文补丁,改造成一个“主英辅中、体验舒服”的双语版本。

一开始我以为这件事会很简单: 不就是“英文 + 中文”一起显示出来吗?

后来真正开始动手,我才发现,问题根本不是“能不能双语”,而是:

怎么做出一个看着舒服、读起来顺、尽量完整,而且不把游戏原本 UI 毁掉的双语版本。

这篇文章,我就完整复盘一下这次折腾: 从最初的 naïve 方案,到中途踩坑,再到最后做出一个基本满意的版本,以及我最后得出的真正工程结论。


一、最开始我以为:这只是个“文本替换”问题

《Eternum》本身是英文,社区有人做了中文补丁。 我的目标不是简单玩中文版,而是想做一个:

英文主显示,中文辅助显示的双语版本。

也就是说,我想要的不是:

  • 纯英文
  • 纯中文
  • 粗暴堆两行字的双语

而是一个更接近字幕体验的版本:

  • 英文保留原版观感
  • 中文作为辅助提示
  • UI 尽量保持原汁原味
  • 阅读时不累

最开始我以为,思路非常直接:

  1. 提取中文补丁里的翻译文件

  2. 找到英文原句和中文译句

  3. 拼成:

    English
    中文
    
  4. 再放回游戏里显示

听上去挺简单,实际上这只是第一层。


二、第一版方案:正则替换,快速做出能跑的 MVP

第一版,我走的是最典型的工程起步路线:

先不追求完美,先做出一个能跑的 MVP。

我写了一个 Python 脚本,大致逻辑是:

  • 读取中文翻译脚本
  • 用正则识别这种结构:
# e "Hello."
e "你好。"
  • 然后自动改成:
e "Hello.\n你好。"

这一步的价值非常大。

因为它让我在很短时间内验证了三件事:

  1. 双语方案在 Ren’Py 里是可行的
  2. 文本层改造比想象中更容易落地
  3. 真正的难点根本不在“能不能做出来”,而在“体验怎么做对”

MVP 一跑通,我就立刻看到了成果—— 游戏里确实开始出现双语对白了。

但问题也立刻跟着来了。


三、真正的坑,不在“有没有双语”,而在“为什么双语这么丑”

第一版做出来之后,我很快就发现:

做出双语,不代表做出了“舒服的双语”。

问题一上来就很明显:

1. 中英像两个平级字幕,在抢主视觉

英文和中文都是完整一行,大小、权重差不多,读起来非常累。

2. 历史记录也跟着双语了,乱成一团

因为当时是直接把文本改成:

English
中文

所以历史记录读到的也是这整段,结果就是整个 history 页面也双语化了,而且排版很糟。

3. 有些句子双语,有些句子只有中文

最开始我以为是 UI 的问题,后来才发现不是。 这是 builder 数据生成层 的问题。

4. 对话框会“时上时下”

因为有的句子是双语,两行;有的句子只有单语,一行。 如果文本框高度动态变化,视觉上就会跳。

这时候我才真正意识到:

这不是一个简单的文本替换问题,而是一个“文本层 + UI 层 + 构建流程”共同协作的问题。


四、第二阶段:从“能用”转向“体验好”

这一步其实是这次折腾里最关键的转折点。

我开始不再把重点放在“怎么继续替换更多文本”,而是先回头去处理 UI 层的观感问题

我最后确定的显示原则是:

英文主显示,中文辅助显示。

不是等权双语,也不是主中辅英,而是:

  • 英文保留原版风格
  • 中文略小一点
  • 中文放在英文下方
  • 中英之间有一点呼吸感
  • 对话框高度固定,避免跳动

这个原则一确定,后面的改造就清晰了。


五、最重要的一步:把“样式”从文本里剥离出来

我中途走过一个弯路: 试图把 {size}{color}{font} 这些 Ren’Py 文本标签直接写进生成器。

也就是说,生成器不仅生成双语文本,还顺手把字体、字号、颜色都写死进去。

这条路一开始看起来很方便,但很快就暴露出问题:

  • 文本内容和样式耦合在一起
  • 后面 UI 想微调会很痛苦
  • 某些字体强制指定后,还可能带来乱码/缺字风险

后来我把这层关系拆开了:

最终的分工是:

1. builder 只负责生成纯文本双语

也就是只输出:

English
中文

2. UI 层负责最终怎么显示

比如:

  • 中文缩放到英文的 0.84~0.86
  • 固定额外高度
  • 统一使用一个 text 控件
  • 保持原版对话框视觉风格

这一拆,整个工程瞬间顺了很多。


六、单 text 渲染,比双控件渲染更稳

这也是我这次很重要的一个判断。

一开始,我试过:

  • 英文一个 text
  • 中文一个 text

这看起来逻辑清楚,但实际很容易出现:

  • 基线不齐
  • 两行像两个独立控件
  • CTC 图标和文本对齐怪异
  • 不同语句类型下表现不一致

后来我改成了:

一个 text 控件,文本内部用换行和局部标签控制中文辅助显示。

这个思路明显更稳:

  • 视觉上更像一个完整对白块
  • 原版 UI 保留得更好
  • 副作用更少
  • 维护也更简单

这一步做完之后,整个双语版的“补丁感”才真正弱下来。


七、UI 做到后,我才发现真正的大问题其实是:覆盖率

当界面已经基本舒服之后,一个更本质的问题暴露出来了:

为什么有些地方是完整双语,有些地方还是只有中文?

一开始我怀疑是某些界面没有生效,后来我看了构建报告,才意识到:

问题根本不在显示层,而在生成层。

我当时的 builder 核心逻辑,本质上还是:

  • 抓“注释英文 + 下一行中文”
  • 成功匹配就拼成双语
  • 匹配不上就原样保留

这意味着它天然有盲区:

  • 没有标准英文注释的句子
  • 非普通对白结构
  • extend
  • centered
  • 旁白
  • 某些字符串型文本
  • 结构不规整的翻译块

也就是说:

我已经把 UI 做到七八成了,但 builder 还停留在 MVP 阶段。


八、这次最重要的认知:真正限制完整双语的,不是 UI,而是 builder

这是我这次折腾里最有价值的工程结论。

如果只看最后体验,最容易让人误判的是:

  • 看见双语不全,就继续改字体
  • 看见有些句子不顺眼,就继续调间距
  • 看见某些地方没双语,就继续改 screens.rpy

但真正的问题其实不是这些。

真正的问题是:

双语数据本身没有生成出来。

也就是说,你前端已经有一个不错的播放器了, 但后端送来的内容还不完整。

这时候再卷 UI,收益已经很低了。


九、我最后得出的正确路线

如果我现在重新做一遍,我会直接把整个项目拆成两层:

第一层:UI 层

目标是:

  • 主英辅中
  • 对话框固定高度
  • text 渲染
  • 尽量保留原版风格

这一层我现在基本满意了。

第二层:builder 层

下一步真正值得做的,不是更多 UI 抛光,而是做一个 builder v2

  • 不再只靠邻行正则匹配
  • 改成按 translate chinese xxx: 的翻译块解析
  • 在块内逐条识别普通对白、旁白、extendcentered
  • 最好再引入原版英文脚本作为英文真源
  • 最后重新生成更完整的双语翻译文件

这才是从“一个可玩的双语版本”走向“一个完整工程项目”的关键一步。


十、这个项目真正让我学到的,不是 Ren’Py,而是工程思维

这次做《Eternum》双语版本,表面上是在折腾一个视觉小说补丁。 但更底层的收获,其实是这些:

1. 先做 MVP,别一开始就追终局

如果我一开始就想“完整双语、完美 UI、零 bug”,那大概率只会把自己拖死。 先做出能跑的版本,非常重要。

2. 问题要分层

很多时候“体验差”并不等于“前端差”。 可能是数据层、构建层、显示层混在一起导致的。

3. 样式和内容要分离

把样式写死进文本里,看起来省事,后面一定痛苦。

4. 真正影响体验的,往往不是你最开始以为的地方

最开始我以为重点是“如何显示双语”,最后才发现重点其实是“如何生成更完整的双语”。


十一、如果把这件事继续做下去,它已经不只是一个补丁了

做到这里,我已经越来越强烈地感觉到:

这件事完全可以做成一个小型开源项目。

因为它已经不再只是“我自己想舒服地玩一个游戏”,而开始具备了工具化的潜力:

  • 从 Ren’Py 汉化补丁中提取翻译数据
  • 生成双语版本
  • 配套一个舒服的 UI 补丁
  • 最终形成一套可复用的构建流程

如果继续往下做,下一步就不该再是“帮一个游戏修补丁”,而是:

把它抽象成一个 Ren’Py 双语补丁构建器。


十二、这次折腾的最终状态

如果现在给这个阶段下一个结论,我会这么说:

现在已经做到的

  • 做出了一个主英辅中的双语版本
  • UI 体验基本可长期游玩
  • 构建流程已经跑通
  • 问题已经从“能不能做”变成“如何做完整”

还没完全做到的

  • 普通对白双语覆盖率还不够完整
  • builder 仍然偏 MVP
  • 某些结构化文本还没有系统性处理

接下来真正值得做的

  • 重写 builder v2
  • 从邻行匹配升级到翻译块解析
  • 让普通对白的双语覆盖率真正上去

结语

这次做《Eternum》双语版本,最开始只是一个很朴素的想法:

我想要一个看着舒服的双语体验。

但做到后面,我越来越觉得,这其实是一个很典型的工程过程:

  • 从需求出发
  • 先做最小可行版本
  • 不断暴露真正的问题
  • 最后逐渐把一个“临时脚本”逼成一个“工具原型”

这也是我最近越来越喜欢的一种感觉:

不是停留在“我想要一个东西”, 而是逼自己走到 “我能不能把这个东西做成一个可复用的系统”。

而这,可能比玩到一个舒服的双语版本,更有意思。