How to translate a game’s .po and .mo files (gettext)
The .po is the editable source; the .mo is the compiled catalog the game actually loads — which is why editing the .po alone changes nothing. RuneTranslate reads both, protects %s and {count} placeholders, refits plural forms to your language, and writes a working .mo back.
如果你打开一个游戏文件夹,发现里面有个装满 .po 与 .mo 文件的 locale 目录,那么这个游戏是用 gettext 做的本地化 —— 而 RuneTranslate 现在可以把它从头到尾翻译完,从创建项目一直到导出可游玩的版本。
本文说明这两种文件到底是什么、为什么改动你能读懂的那一个通常在游戏里毫无变化,以及该如何正确地翻译它们。
.po 与 .mo —— 几乎人人都会踩的区别
它们是同一份语言包的两种形态:
- `.po` —— Portable Object。可以用任意编辑器打开的纯文本文件。每条记录包含
msgid(原文)与msgstr(译文)。译者实际工作的正是这个文件。 - `.mo` —— Machine Object。同样内容的已编译二进制版本,其结构让游戏可以瞬间查到某条字符串。运行中的游戏读取的就是它。
最后这一点会让人白白搭进一个下午。你可以把 .po 的每一行都译完、保存、启动游戏 —— 却看不到任何变化,因为游戏根本不会打开 .po。它加载的是旁边那个 .mo,里面仍是旧文本。通常你需要用 GNU gettext 工具里的 msgfmt 把前者编译成后者。RuneTranslate 两个都会写出,因此没有额外的编译步骤。
gettext 游戏的文件夹长什么样
标准结构是一个按语言代码划分的 locale 目录:
MyGame/game.exeMyGame/locale/en/LC_MESSAGES/mygame.moMyGame/locale/fr/LC_MESSAGES/mygame.moMyGame/locale/de/LC_MESSAGES/mygame.mo
LC_MESSAGES 文件夹是约定的一部分,并非笔误;mygame 则是文本域,也就是游戏加载字符串时所请求的名称。不少游戏改用更扁平的形式,例如 lang/fr.po,同样可以工作。
哪些游戏在用 gettext?
gettext 源自 Unix 世界,后来成为 C、Python 以及一切基于 GTK 的软件做本地化的默认方式。落到游戏上,就是:
- Python 与 pygame 游戏
- Solarus、SDL 及其他 C/C++ 引擎
- 通过 Poedit、Weblate 或 Crowdin 做本地化的游戏 —— 它们都原生支持 PO
- 部分 Godot 项目,其支持以 PO 作为翻译来源
它在欧美独立游戏中远比日式游戏常见 —— Kirikiri 和 RPG Maker 这类日系引擎都把文本存放在各自的格式里。
用 RuneTranslate 翻译 gettext 游戏
- 把 RuneTranslate 指向游戏文件夹。 也就是含有
locale/、lang/或i18n/的那个。识别依据是文件的实际字节而不是扩展名,因此实际上是 3D 场景的.mo会被忽略,而不会生成一个坏掉的项目。 - 查看概要。 在你确认创建项目之前,会看到语言包数量、已编译
.mo与源文件.po的比例、游戏已自带哪些语言,以及字符串总数。 - 选择语言与翻译服务。 免费的 Google 或 DeepL,或你自己的 DeepL、OpenAI、Claude、DeepSeek 密钥 —— 参见服务对比。
- 校对后导出。 你会得到一份译文已就位的游戏完整副本。
悄悄毁掉 gettext 翻译的两件事
占位符。 msgid 是格式字符串,而不是写好的成品文本。Hello, %s! 会在运行时把玩家名字代入。机器翻译常常会删掉 %s、给它加上空格,或者遇到 Python 的具名形式 %(player)s 时把里面的单词也译了。后果不是外观问题:少一个 %s 会让后面所有参数整体错位,多数引擎会直接崩溃。RuneTranslate 会在文本送往翻译服务之前,把它找到的每个占位符(%s、%d、%(player)s、{count}、{0})遮蔽起来,之后再还原。
复数形式。 英语有两种(“1 coin” / “5 coins”),日语和土耳其语只有一种,俄语和波兰语有三种。语言包会在文件头里声明自己用几种;一旦这个数字与语言对不上,gettext 就会越过列表末尾,然后悄无声息地退回未翻译的原文。RuneTranslate 会按你的目标语言重新适配这些形式,并相应改写文件头。
为什么导出会把语言包写两遍
导出既会生成一个规范的新语言 —— locale/<你的语言>/LC_MESSAGES/ —— 也会生成游戏原本就在加载的那个语言包的更新版本。
真正让译文显示出来的是后者。游戏决定加载哪种语言的方式,从文件夹里看不出来:它可能读取操作系统语言、某个配置文件、启动器参数,或者干脆写死在代码里。给一个只会请求英文的游戏加上中文语言包,你面对的将是一个看似毫无变化的版本。覆盖它确实会加载的那个语言包,就免去了这层猜测。两者都只发生在导出的副本中 —— 你原本的游戏文件夹不会被修改。
你尚未翻译的条目会保留游戏原有的译文,因此一个做到一半的项目绝不会清空开发者自己的本地化。
它做不到的事
只有已经存在于语言包中的文本才能被翻译。如果开发者把某条字符串直接写死在代码里,而没有经由 gettext,那么它既不在 .po 也不在 .mo 中,任何 PO 工具都无法从中找到它。这是格式的限制,而不是工具的限制。
RuneTranslate 还会把 msgid 视为原文,这正是 gettext 自身的约定。少数项目让 msgid 保持英文,而真正的源语言放在 msgstr 里;这类项目会从英文开始翻译。
最后,如果游戏运行在 RuneTranslate 直接支持的引擎上 —— Godot、Unity、Ren’Py —— 那就交由该引擎处理,这样游戏其余的文本也能被取到,而不仅仅是语言包。
分享之前
启动导出的版本,读上几屏。菜单和按钮是最快的检查点,因为语言包里的文本大多在那里。如果哪里不对 —— 游戏识别不到、导出的版本打不开,或文本仍是原语言 —— 请通过应用的帮助菜单或 Discord 反馈。下载 RuneTranslate,打开你的第一个语言包。
