Proxyos Weekly 065
Laurence-042
- 3 minutes read - 491 wordsTL;DR 概览
搞定了基础的 i18n,但具体 UX 尚需优化
本期目标
- 做英语的 i18n
- 用英语通玩一遍
- 开个 itch
- 琢磨下宣传
进展速记
本期假设 / 预期
预期:
copilot 的新定价标准太抢钱了,上一期用之前的使用习惯,一天就烧了 33%,我得调整一下策略了。
i18n 的制作应该只需要几个脚本就行,避免让 llm 直接去读文件,应该还撑得住这一期,我应该也能再这一期内搞定 i18n
结果:
i18n 比预想复杂得多。
虽然最终完成了整套翻译流水线,但其工作量远超“写几个脚本”——主要时间都花在 ProxyOS 特殊内容(html、tres、原始文件)的翻译架构设计上。
本期确定性变化
新增:
- i18n 全套工作流涉及的脚本
- 更新目录结构
- 扫描标准 tres 资源增量生成待翻译文件及里面的条目
- 结合标准 tres 资源和翻译文件生成翻译其他语言 tres 资源
- 扫描 gd 里的 tr 和 tscn 里的文本生成翻译文件(走 godot 自己的翻译流程)
- 扫描并检查缺失翻译的 csv 文件和过时的 translation 文件
- 设置里新增翻译选项,并在游戏启动时自动检测玩家系统语言进行设置
- 更好的日志框架
- 所有翻译(deepseek-v4-flash 和 gemini-3-flash 倾情机翻)
- 调整部分翻译避免布局被挤爆,这中文版虽然已经预留了 50%的空间,但事实证明不太够
变更:
- 全部非日志文本做成可翻译的
- 全部日志文本改成英文
- 优化了在剧情中进行演出的 action 实现,使其成为通用窗口管理 action,让两个演出都可以复用它
- 优化设置的架构,现在它的界面靠 tscn 了(我寻思这游戏这个阶段应该不用考虑 rimworld 那种设置布局全靠代码的灵活性。而且就算需要,也能增量加给 mod 用的增量设置系统,而不是让主系统的设置也这么搞——尤其是 godot 做逻辑 mod 可能没 C#的 Harmony 那么方便的情况下)
- 将设置生效的时间提前到 config 加载,以此避免加载资源时 TranslationServer 还没被设置语言
- 优化一些翻译
修复:
删除:
主要进展内容/本期关键判断点
我做出了哪些「如果错了也要付代价」的判断?
翻译系统架构
原本寻思歇两天看看其他人怎么省 token 的,但是还是不知不觉干了两天活还把配额烧到了只剩 25%(这期结束时只剩 7%了),主要就是在搞这个 i18n……
话说我还是觉得 internationalization 叫 i18n 十分微妙
Godot 最佳实践
首先我研究了下 godot 的翻译机制……
是的,这个本应在我开工前就搞的,但我一开始也没想着做这么大,也没准备翻译,你看这不是就被技术债追上了吗……
可以看出来 godot 的机制大概有三种,我懒,所以我用清言总结了下
| 维度 | TranslationServer.translate() | tr() | tscn 中 text 自动翻译 |
|---|---|---|---|
| 本质 | 底层翻译 API | 对 TranslationServer 的封装 + 语法糖 | 控件内部在 set_text 时自动调用 tr(text) |
| 键的来源 | 你显式传的 message 参数 | 你显式传的 message 参数 | 控件 text 属性里的原始字符串 |
| 调用方式 | 显式调用:TranslationServer.translate(…) | 显式调用:tr(…) | 隐式:设置 label.text = “xxx” 时自动触发 |
| 支持上下文 | 是(context 参数) | 是(context 参数) | 是(内部就是 tr(text, context),但 context 通常为空) |
| 支持复数 | 是(translate_plural) | 是(tr_n / tr_plural,视版本) | 一般不直接用,需要你自己写逻辑再赋值 |
| 场景 | 通用:脚本、非节点逻辑、自定义翻译逻辑 | 通用:所有需要标记/调用翻译的地方 | 仅限支持自动翻译的控件属性(Label.text 等) |
| 是否自动刷新 UI | 否 | 否 | 否(语言切换后,需要重新 set_text 或手动刷新) |
| 提取工具识别 | 不识别,它不是“标记” | 是提取工具识别的标记 | 是提取工具识别的标记 |
官方建议的最佳实践差不多是用 godot 结合 gettext,自动提取 tr 和 tscn 里的键整理成 po 文件,必要时手写 po 文件(有更灵活的要求、必须使用 TranslationServer.translate 之类的)并在项目配置里加载 po 文件作为翻译
但我不准用这个最佳实践——因为它并不是我的项目的最佳实践
ProxyOS 最佳实践
ProxyOS 的特殊性
ProxyOS 的架构十分特殊,它不仅仅是内容驱动的(剧本编排全是。tres),它的大量内容是涉及向玩家展示 html、原始文件的
单单内容驱动本身不是啥太大的问题,因为最终内容还是要在 Label 上显示的,只要把 tres 里的文本作为 key 写翻译文件就行
但 godot 似乎不支持自动提取 tres 内的文本生成 po 文件,我得写个自动提取的脚本。
而且展示的内容通常是诸如“系统正处于%s 状态/ The system is currently %s”这种东西,要依赖 Label 的翻译机制,我就必须照顾各种语序,调整大量 tscn 结构,保证 %s 单独占一个 Node,否则“系统正处于离线状态”可匹配不上“离线状态”这个翻译 key(理论上也可以反过来,把“系统正处于离线状态”作为翻译 key,但那样的话属实给自己找不自在,tres 和翻译很难对得上)
原始文件问题也不大,毕竟我可以在脚本里要求取文件时做个翻译,把path/to/zh/file 翻译成path/to/en/file来将对应文件放用户目录下
但我需要定位相关脚本添加 tr,不过这不是啥大事,总比 tres 那需要改大量 tscn 的好
但 html 就焦巴脆了,因为它的结构和内容是耦合在一起的,不像 tres 的结构最后会被展平到 Label,也不像文件路径本身没有结构,它的内容就是结构
这就导致不仅要翻译文本,还得保持其结构不变。
单单如此我也不会说“焦巴脆”,毕竟 i18n 这名字就是在 web app 流行起来的,html 的 i18n 不是问题对吧?
不完全对。
首先,ProxyOS 里的网页源码是要展示给玩家看的,不该让初学者被其中的 i18n 标记干扰注意力,i18n 标记的存在也会让叙事变得一堆补丁。
而更重要的是,有些信息在网页源码的注释里,这可不是 i18n 能管得到的。
换句话说,问题从来不是“如何翻译 html”,而是:
ProxyOS 里的 html 本身就是游戏内容,
而且部分剧情甚至藏在源码注释里。
因此我需要翻译后的 html 依然能作为游戏道具被玩家阅读,
而不仅仅是正确显示文字。以上两点就导致我必须准备对应语言的网页副本
但因为 html 的结构和内容是耦合在一起的,之后我改个网页结构就必须改所有网页的其他语言副本
ProxyOS 的解法
采用 Overlay(覆盖层)模式:
- 保留一份基准 HTML
- 自动提取文本生成翻译 CSV
- 根据 CSV 生成各语言副本
- 运行时自动加载对应语言版本
- 类似前面提到的“原始文件”的处理方法
- 找不到对应语言的可以直接显示基准的(以后也可以扩展为按照特定 fallback 路径找最近能接受的语言,但应该不至于需要这种机制)
这时候就会发现,html 和原始文件实际上都可以使用这个机制做翻译,那么 tres 是不是也可以呢?
当然可以!
Overlay 模式把**“运行时拼接”变成了“构建时填充”**。因为生成了独立语言的 tres 副本,我可以直接在 CSV/翻译端把整个带占位符的句子作为 Key(系统正处于%s 状态 -> The system is currently %s),%s的来自 en 版本 tres 的offline可以被直接填进去,直接向 Label 写入整个翻译结果,而不用去拆分 UI 节点。
而且代码修改也很简单,只需要无脑 regex replace 把非 log 的中文字符串加个tr()就可以了
于是就产生了下面这套工作流
| gd | tscn | tres | 原始文件 | html | |
|---|---|---|---|---|---|
| 生成翻译文件 | 自定义原生资源扫描脚本 | 自定义原生资源扫描脚本 | 自定义扩展资源扫描脚本 | 无 | 自定义扩展资源扫描脚本 |
| 生成翻译其他语言 tres 资源 | 无 | 无 | 自定义替换脚本 | 直接写对应版本的文件 | 自定义替换脚本 |
| 翻译机制 | godot 的 tr 翻译 | godot 的 Label 内置 tr 翻译 | gd 加载对应语言版本的 tres | gd 加载对应语言版本的 原始文件 | gd 加载对应语言版本的 html |
你可能会问,为啥再 gd/tscn 上都搞自定义扫描脚本,毕竟前面也说了 gd 和 tscn 是支持通过扫描提取待翻译文本,而这里确实也在用 godot 内置的翻译机制?
说实话,这俩用 godot 机制扫描从信息角度来说确实没啥问题,但是 godot 扫出来的是 po 文件,而我比较喜欢 csv 翻译。因为用 csv 可以快速确认哪个格子是空的,但用 po 就只能再搞个工具进行检查了,而且似乎还需要 gettext 之类的第三方工具。反正也不复杂,而且用自定义扫描脚本可以自动将不同模块的翻译分散到不同翻译文件里,这样更方便进行对照,以后维护会更方便
所以这就是结果了:ProxyOS 没有采用 Godot 推荐的 i18n 流程,而是建立了一套以 CSV 为中心、同时覆盖 HTML、原始文件和内容资源的 Overlay 翻译流水线。
当然,坑永远存在
- claude 把行号当 html 的定位标记了,我给改成 xpath 了
- claude 用了 utf-8-sig(带 BOM)读写翻译 csv,估计是为了方便我用 excel 填内容,但是 godot 不能用带 bom 的(The CSV files must be saved with UTF-8 encoding without a byte order mark.)
- 即使在开发阶段使用中文时留了 50%的余量,翻译成英文时还是超长了,优化了界面
- agent 总是无法处理所有文件,但看 copilot 传的 prompt 似乎也没啥明显问题……所以让 agent 写了个脚本用于检查遗漏文件,顺便检查过时翻译并自动删除以触发自动重新导入
瓶颈与问题清单
哪些问题还没解,但也许我已经知道“它们不是什么”?
下期计划
- 用英语通玩一遍
- 开个 itch
- 琢磨下宣传
试玩版
暂缓,第一次上传需要做好准备,等进入 beta 阶段再说