多语言中的 .POT .PO .MO 和 xgettext

如果你了解过 WordPress 的多语言,你就会发现关于这块的知识点中,会时不时的出现, .po .pot .mo 这类的文件。

Google 上面有一张图,比较直观的解释了这三者的关系:

xgettext

在介绍图中的三种文件之前,先要了解一下xgettext这个工具,它是在 Linux 上的一个程序,Ubuntu 下可以直接使用(命令行中),其功能是抽取给定的文件中可供翻译的字符串。

支持下面下列的这些语言,不同语言的标识略微有差异。
C, C++, ObjectiveC, PO, Shell, Python, Lisp, EmacsLisp, librep, Scheme, Smalltalk, Java, JavaProperties, C#, awk, YCP, Tcl, Perl, PHP, GCC-source, NXStringTable, RST, RSJ, Glade, Lua, JavaScript, Vala, GSettings, Desktop

以 JavaScript 为例:

var translateMap = {
    'oh my god': '你好'
}

function gettext(text) {
    return translateMap[text];
}

gettext('oh my god');

xgettext会抽取gettext里的字符串作为是可以被翻译的内容,我们也可以将 gettext() 替换为 _(),同样是可以被提取的,这个上面所谓的标识了,更多细节可以在参考链接中找到对于这个程序的参数解释。

运行下面的这个命令即可生成 .pot 文件

xgettext --keyword=_ --language=javascript --add-comments --from-code=utf-8 --sort-output -o ./hello.pot hello.js

po 和 pot 文件都是文本文件

.POT

pot 是 Portable Object Template。现在我们知道 .pot 文件是可以通过 xgettext 从代码文件中提取出来,根据英文全称我们知道这类文件是模板的概念,根据查阅资料得知,POT 文件是 PO 文件的模板文件。模板文件中所有翻译字符串留空,一个 POT 文件本质上是一个没有翻译的空 PO 文件,只有原始字符串。

.PO

po 是  portable object file 的缩写,.po 文件可以看做是 .pot 文件的子集,因为 pot 文件和 po 文件从语法上来看是一致的,只不过填充了翻译的内容。

.MO

mo 代表 Machine Object,这是一个二进制数据文件,是 po 文件编译后的产物,通常我们汉化程序或者主题时,如果没有 pot 或者 po 文件的话,可以用 mo 文件反编译出 po 文件进行多语言的翻译。

反编译的工具是 msgfmt ,这个在 Ubuntu 上也是现有的。
最后再看一下 mo 文件二进制格式解析定义:

   byte
             +------------------------------------------+
          0  | magic number = 0x950412de                |
             |                                          |
          4  | file format revision = 0                 |
             |                                          |
          8  | number of strings                        |  == N
             |                                          |
         12  | offset of table with original strings    |  == O
             |                                          |
         16  | offset of table with translation strings |  == T
             |                                          |
         20  | size of hashing table                    |  == S
             |                                          |
         24  | offset of hashing table                  |  == H
             |                                          |
             .                                          .
             .    (possibly more entries later)         .
             .                                          .
             |                                          |
          O  | length & offset 0th string  ----------------.
      O + 8  | length & offset 1st string  ------------------.
              ...                                    ...   | |
O + ((N-1)*8)| length & offset (N-1)th string           |  | |
             |                                          |  | |
          T  | length & offset 0th translation  ---------------.
      T + 8  | length & offset 1st translation  -----------------.
              ...                                    ...   | | | |
T + ((N-1)*8)| length & offset (N-1)th translation      |  | | | |
             |                                          |  | | | |
          H  | start hash table                         |  | | | |
              ...                                    ...   | | | |
  H + S * 4  | end hash table                           |  | | | |
             |                                          |  | | | |
             | NUL terminated 0th string  <----------------' | | |
             |                                          |    | | |
             | NUL terminated 1st string  <------------------' | |
             |                                          |      | |
              ...                                    ...       | |
             |                                          |      | |
             | NUL terminated 0th translation  <---------------' |
             |                                          |        |
             | NUL terminated 1st translation  <-----------------'
             |                                          |
              ...                                    ...
             |                                          |
             +------------------------------------------+

多语言翻译的流程

下图是 GNU gettext 程序在多语言中的协作关系。从中我们其实可以看到如何从源代码提取待翻译字符然后交付给翻译人员,最终实现整个多语言流程,这是一个可以借鉴的流程。

Original C Sources ───> Preparation ───> Marked C Sources ───╮
                                                             │
              ╭─────────<─── GNU gettext Library             │
╭─── make <───┤                                              │
│             ╰─────────<────────────────────┬───────────────╯
│                                            │
│   ╭─────<─── PACKAGE.pot <─── xgettext <───╯   ╭───<─── PO Compendium
│   │                                            │              ↑
│   │                                            ╰───╮          │
│   ╰───╮                                            ├───> PO editor ───╮
│       ├────> msgmerge ──────> LANG.po ────>────────╯                  │
│   ╭───╯                                                               │
│   │                                                                   │
│   ╰─────────────<───────────────╮                                     │
│                                 ├─── New LANG.po <────────────────────╯
│   ╭─── LANG.gmo <─── msgfmt <───╯
│   │
│   ╰───> install ───> /.../LANG/PACKAGE.mo ───╮
│                                              ├───> "Hello world!"
╰───────> install ───> /.../bin/PROGRAM ───────╯

.POT 和 .PO

由于 PO 文件是 POT 的 “子集”,.POT 只是一个模板,有的人可能会好奇是不是直接有 .PO 文件就可以了?

由于我们大概率会碰到源代码被频繁修改的情况,这个时候就需要重新解析一份 .POT 文件来提供给翻译人员,翻译人员通过 poeditor 这类工具来实现具体的翻译。如果没有 .POT 文件充当一个模板,原先翻译好的 .po 文件就会被改动,新的待翻译内容和已被翻译的内容掺杂在一个文件中,如何解析拆分?从工程层面上就会变的复杂,因此一个简单的翻译流程就是,先生成模板文件,通过模板文件产出翻译内容,程序则直接使用翻译内容,即 .po 文件,或将他们编译成二进制使用。

文件格式

.po 文件虽然是文本文件,可以使用任何文本编辑器打开,但里面的内容也是按照一定的格式来编写的,对于一些基本的语法需要了解一下。

注释

注释都是非必填的,一共有五种。

# translator-comments 翻译者写的注释就会在这里显示

#. extracted-comments xgettext 从源代码中提取后给出的注释,通常是程序生成

#: reference…  表明翻译的内容在源代码中的位置,比如 hello.js:11 代表 hello.js 的第 11 行

#, flag…

#| 一般会放原先翻译的内容,比如下面的这种方式
#| msgctxt previous-context
#| msgid previous-untranslated-string

msgctxt "上下文内容,用来辅助翻译"

msgid: '未翻译语言'
msgstr: '翻译语言'

上面还漏了一个注释类型 #, flag… flag 有两种类型:

#, fuzzy 表示这个翻译内容可能不是正确的,当使用这个标记后,poeditor 里会高亮这条内容,提示翻译者需要注意这块内容的是否需要修改。

另一种 flag 是类似下面这种,代表着原始内容中包含 php 风格的字符串。

  • php-format
  • no-php-format

比如说下面的这段代码中 %d %s 是 php 中的语法,当原始的内容中有些是动态生成的的情况下,就像下面的这种语法,你需要明确告诉编辑器这是「代码语法」不是内容,而 poeditor 则会给出提示。

<?php
$num = 5;
$location = 'tree';

$format = 'There are %d monkeys in the %s';
echo sprintf($format, $num, $location);
?>

除了上面的「注释」「flag」两类外,还有一种语法是表示单复数的,在翻译中可能需要对这类情况需要进行处理。

#: hello.js:11
msgid "found %d fatal error"
msgid_plural "found %d fatal errors"
msgstr[0] "s'ha trobat %d error fatal"
msgstr[1] "s'han trobat %d errors fatals"

msgid_plural 代表复数时候的翻译内容,msgstr[] 这种语法则可以对具体的个数做区分,有点类似 switch... case ... 的意思。

如何开始

最简单的方式就是直接新建一个 .po 的文件,然后复制黏贴下面这段字符,即可使用 poedit 进行编辑,如果没有 msgidmsgstr 的话,将这个文件拖拽到 poeditor 是会报错的。

#: hello.js:11
msgid "oh my god"
msgstr ""

Poeditor 这款软件是目前多语言翻译工具中比较突出的,但是收费,可以看到右侧还提供了人工翻译。

Localise 提供的是在线版的翻译工具,效果也还可以,工具本身都是很容易上手的。

参考链接:
https://www.drupal.org/node/1814954
https://zh.wikipedia.org/wiki/Gettext
https://www.gnu.org/software/gettext/manual/html_node/xgettext-Invocation.html
https://www.labri.fr/perso/fleury/posts/programming/a-quick-gettext-tutorial.html