跳到主要内容
AZ Tools

gettext .mo 目录文件检查器

.mo 文件是 msgfmt 编译 .po 之后留下的产物,也是真正随程序发布到 locale/<lang>/LC_MESSAGES/ 的那个文件。一旦 .po 丢失,它就成了没人能读的二进制,尽管结构本身很小:一个魔数、一个修订号、两张字符串表的条目数与偏移,外加一张读取器可以忽略的可选哈希表。本工具把这些全部走一遍,显示里面究竟有什么。魔数是 0x950412de,同时也是它的字节交换值 0xde120495,这正是文件声明其余各个字的字节序的唯一方式,所以为大端机器编译的目录文件在这里读起来和小端的完全一样。.mo 里藏着的两样东西都是内嵌的 NUL 字节,在文本编辑器里看不见,也最容易弄错:带上下文的条目把 msgctxt、一个 0x04 字节和 msgid 拼成一个字符串;复数条目在原文一侧用 NUL 分隔 msgid 与 msgid_plural,在译文一侧用 NUL 连接各个形式。本工具把两者都拆开,上下文单独成列,复数形式编号列出。msgid 为空的元数据条目会被解析成 Project-Id-Version、Language、Content-Type、Plural-Forms 和 POT-Creation-Date,文件中其余字符串一律按该文件头声明的编码解码,而不是靠猜。当文件头缺失或撒谎时,页面会明说,而不是默默产出乱码。复数表达式会被解析并对 n = 0 到 20 求值,于是你能看到每个数量选中哪一种形式;条目携带的形式数量还会与 nplurals 核对,两者不符是真实存在的缺陷,只在某些数量上返回错误的字符串。此外还会指出未翻译条目、与 msgid 完全相同的译文、译文中丢失的 %s 或 {}、未排序的原文表(C 语言读取器的二分查找依赖该顺序),以及槽位少于条目数的哈希表。它不会把目录文件还原成 .po,也无法判断翻译好坏,只回答文件结构是否可靠、自身是否一致。

使用方法

  1. 把 .mo 文件拖到框里:可以是 locale/<lang>/LC_MESSAGES/ 里的编译产物,也可以是 msgfmt 生成的任何文件。
  2. 先看文件头一行:字节序、修订号、条目数,以及两张字符串表和哈希表在文件中的实际位置。
  3. 在元数据条目中确认字符编码。下面所有字符串都用它解码,因此 Content-Type 有误应当最先修正。
  4. 查看 n = 0 到 20 的复数规则表,看每个数量选中哪种形式,并把复数条目的形式数量与 nplurals 对照。
  5. 浏览发现的问题列表,再用过滤框在条目表中定位某个 msgid、译文或上下文。

常见问题

为什么魔数会出现两个不同的值?
魔数只有一个,就是 0x950412de。但 .mo 文件把每个 32 位字都按编译它的机器的字节序存放,魔数也一样。读取器把前四个字节按两种方式各读一遍:若按小端读得到 0x950412de,整个文件就是小端;若按大端读得到该值,整个文件就是大端。这就是全部的字节序声明,别处没有任何标志位,所以在大端机器上编译的目录文件在小端机器上照样能用。本页面把原始的四个字节和推断出的字节序并排显示,你可以自己核对,而不必只是相信结论。
有些 msgid 里的 0x04 字节是什么?
那是 msgctxt 的存放方式。gettext 没有单独的上下文字段,于是 msgfmt 把上下文、一个 0x04 字节(ASCII 的 EOT)和 msgid 拼成一个字符串,pgettext 查找的正是这个拼接后的字符串。所以两个条目可以共用 msgid “Open” 而仍然是不同条目:一个是 "menu\x04Open",另一个是 "state\x04Open"。在编辑器或简单的转储里分隔符是看不见的,两者看起来就像重复项。本工具在该字节处切分,并让上下文单独成列,共用同一 msgid 的条目就会并排出现。
复数形式如何存放?nplurals 又要与什么一致?
原文一侧是 msgid、一个 NUL 字节,然后是 msgid_plural;译文一侧是各个形式用 NUL 连接起来。应该有几种形式由文件头决定:Plural-Forms 里有 nplurals,以及一个关于 n 的表达式,返回要使用的形式下标。如果某条目的形式数少于 nplurals 所承诺的,选中缺失下标的那些数量将取不到任何内容,gettext 会退回未翻译的 msgid 或 msgid_plural,而且只在部分数字上发生,这正是该缺陷能通过测试的原因。本页面统计每个复数条目的形式数量,并标出与文件头不符的条目。
Content-Type 文件头缺失或有误会怎样?
文件内部除了这个头之外没有任何东西标明自身编码,读取器只能相信它。Python 的 gettext 会直接拒绝该目录文件:未填写的 charset=CHARSET 触发未知编码错误,按声明编码无法解释的字节触发解码错误。本页面刻意更宽容:退回 UTF-8,尽可能解码,并告诉你发生的是哪一种,因为你打开这个文件往往就是为了查清应用为何显示乱码。完全没有元数据条目的目录文件则既无编码,也无语言和复数规则;gettext 这时把字符串当作 ASCII,并套用日耳曼语系的 n != 1 规则。
条目顺序重要吗?
重要,而且这是手写 .mo 生成器最常忽略的要求。gettext 规范要求原文字符串按字节顺序排序,因为读取器可以在原文表上用二分查找定位条目。C 实现在没有可用哈希表时正是这么做的,于是未排序的目录文件会悄悄找不到某些条目;而把整个文件读成字典的 Python 却能完全正常读取。这是一类很讨厌的缺陷:在你的测试脚本里正常,在应用里却不行。本页面会检查排序,并报告有多少对条目次序颠倒。
哈希表有什么用,可以忽略吗?
它是可选的查找加速结构:msgfmt 写入一张 hashpjw 表,槽位比条目数多约三分之一,采用开放定址,这样读取器不用二分查找也能找到 msgid。读取器完全可以忽略它,Python 就是这么做的,大小为 0 的目录文件也完全合法。不合法的是槽位不多于条目数的表,因为开放定址至少要留一个空位;出现这种组合说明写文件的程序并不理解该格式,因此这里会报告出来。页面显示的大小与偏移直接取自文件头中的字。

相关工具