把对话做成舒服的双语版本:一场从“能用”到“好用”的工程化折腾
最近我做了一件看起来很小、但实际上很有工程味的事:
把一款 Ren’Py 游戏《Eternum》,从已有中文补丁,改造成一个“主英辅中、体验舒服”的双语版本。
一开始我以为这件事会很简单: 不就是“英文 + 中文”一起显示出来吗?
后来真正开始动手,我才发现,问题根本不是“能不能双语”,而是:
怎么做出一个看着舒服、读起来顺、尽量完整,而且不把游戏原本 UI 毁掉的双语版本。
这篇文章,我就完整复盘一下这次折腾: 从最初的 naïve 方案,到中途踩坑,再到最后做出一个基本满意的版本,以及我最后得出的真正工程结论。
一、最开始我以为:这只是个“文本替换”问题
《Eternum》本身是英文,社区有人做了中文补丁。 我的目标不是简单玩中文版,而是想做一个:
英文主显示,中文辅助显示的双语版本。
也就是说,我想要的不是:
- 纯英文
- 纯中文
- 粗暴堆两行字的双语
而是一个更接近字幕体验的版本:
- 英文保留原版观感
- 中文作为辅助提示
- UI 尽量保持原汁原味
- 阅读时不累
最开始我以为,思路非常直接:
提取中文补丁里的翻译文件
找到英文原句和中文译句
拼成:
English 中文再放回游戏里显示
听上去挺简单,实际上这只是第一层。
二、第一版方案:正则替换,快速做出能跑的 MVP
第一版,我走的是最典型的工程起步路线:
先不追求完美,先做出一个能跑的 MVP。
我写了一个 Python 脚本,大致逻辑是:
- 读取中文翻译脚本
- 用正则识别这种结构:
# e "Hello."
e "你好。"
- 然后自动改成:
e "Hello.\n你好。"
这一步的价值非常大。
因为它让我在很短时间内验证了三件事:
- 双语方案在 Ren’Py 里是可行的
- 文本层改造比想象中更容易落地
- 真正的难点根本不在“能不能做出来”,而在“体验怎么做对”
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 核心逻辑,本质上还是:
- 抓“注释英文 + 下一行中文”
- 成功匹配就拼成双语
- 匹配不上就原样保留
这意味着它天然有盲区:
- 没有标准英文注释的句子
- 非普通对白结构
extendcentered- 旁白
- 某些字符串型文本
- 结构不规整的翻译块
也就是说:
我已经把 UI 做到七八成了,但 builder 还停留在 MVP 阶段。
八、这次最重要的认知:真正限制完整双语的,不是 UI,而是 builder
这是我这次折腾里最有价值的工程结论。
如果只看最后体验,最容易让人误判的是:
- 看见双语不全,就继续改字体
- 看见有些句子不顺眼,就继续调间距
- 看见某些地方没双语,就继续改
screens.rpy
但真正的问题其实不是这些。
真正的问题是:
双语数据本身没有生成出来。
也就是说,你前端已经有一个不错的播放器了, 但后端送来的内容还不完整。
这时候再卷 UI,收益已经很低了。
九、我最后得出的正确路线
如果我现在重新做一遍,我会直接把整个项目拆成两层:
第一层:UI 层
目标是:
- 主英辅中
- 对话框固定高度
- 单
text渲染 - 尽量保留原版风格
这一层我现在基本满意了。
第二层:builder 层
下一步真正值得做的,不是更多 UI 抛光,而是做一个 builder v2:
- 不再只靠邻行正则匹配
- 改成按
translate chinese xxx:的翻译块解析 - 在块内逐条识别普通对白、旁白、
extend、centered - 最好再引入原版英文脚本作为英文真源
- 最后重新生成更完整的双语翻译文件
这才是从“一个可玩的双语版本”走向“一个完整工程项目”的关键一步。
十、这个项目真正让我学到的,不是 Ren’Py,而是工程思维
这次做《Eternum》双语版本,表面上是在折腾一个视觉小说补丁。 但更底层的收获,其实是这些:
1. 先做 MVP,别一开始就追终局
如果我一开始就想“完整双语、完美 UI、零 bug”,那大概率只会把自己拖死。 先做出能跑的版本,非常重要。
2. 问题要分层
很多时候“体验差”并不等于“前端差”。 可能是数据层、构建层、显示层混在一起导致的。
3. 样式和内容要分离
把样式写死进文本里,看起来省事,后面一定痛苦。
4. 真正影响体验的,往往不是你最开始以为的地方
最开始我以为重点是“如何显示双语”,最后才发现重点其实是“如何生成更完整的双语”。
十一、如果把这件事继续做下去,它已经不只是一个补丁了
做到这里,我已经越来越强烈地感觉到:
这件事完全可以做成一个小型开源项目。
因为它已经不再只是“我自己想舒服地玩一个游戏”,而开始具备了工具化的潜力:
- 从 Ren’Py 汉化补丁中提取翻译数据
- 生成双语版本
- 配套一个舒服的 UI 补丁
- 最终形成一套可复用的构建流程
如果继续往下做,下一步就不该再是“帮一个游戏修补丁”,而是:
把它抽象成一个 Ren’Py 双语补丁构建器。
十二、这次折腾的最终状态
如果现在给这个阶段下一个结论,我会这么说:
现在已经做到的
- 做出了一个主英辅中的双语版本
- UI 体验基本可长期游玩
- 构建流程已经跑通
- 问题已经从“能不能做”变成“如何做完整”
还没完全做到的
- 普通对白双语覆盖率还不够完整
- builder 仍然偏 MVP
- 某些结构化文本还没有系统性处理
接下来真正值得做的
- 重写 builder v2
- 从邻行匹配升级到翻译块解析
- 让普通对白的双语覆盖率真正上去
结语
这次做《Eternum》双语版本,最开始只是一个很朴素的想法:
我想要一个看着舒服的双语体验。
但做到后面,我越来越觉得,这其实是一个很典型的工程过程:
- 从需求出发
- 先做最小可行版本
- 不断暴露真正的问题
- 最后逐渐把一个“临时脚本”逼成一个“工具原型”
这也是我最近越来越喜欢的一种感觉:
不是停留在“我想要一个东西”, 而是逼自己走到 “我能不能把这个东西做成一个可复用的系统”。
而这,可能比玩到一个舒服的双语版本,更有意思。