在 Linux 环境下, 主流的软件翻译方法是通过工具抓取源代码中的需翻译字符串到外部文本文件, 形成一个字典, 翻译之后在运行期加载. 更专业一点就是, 从软件本身要能国际化 (internationalization, 一般简写为 i18n, 因为 i 和 n 之间有 18 个字符), 支持从源代码中提取字符串来翻译; 而翻译这个步骤则成为本地化 (localization, 简称 l10n).
要让软件具备国际化属性, 这必须程序员们亲自努力, 为需翻译字符串附加一些修饰. 如下面这个程序
int main(int argc, char* argv[])
{
if (1 == argc || 0 == strcmp("hello", argv[1]))
{
puts("Hello");
}
else
{
puts("World");
}
return 0;
}
要加上区分信息很简单, 比如
#define TRANSLATE_THIS(x) x
int main(int argc, char* argv[])
{
if (1 == argc || 0 == strcmp("hello", argv[1]))
{
puts(TRANSLATE_THIS("Hello"));
}
else
{
puts(TRANSLATE_THIS("World"));
}
return 0;
}
$ xgettext hello.c --keyword=TRANSLATE_THIS -o hello.po
那么 xgettext 将提取被宏
TRANSLATE_THIS
所包括的所有字符串到 hello.po 纯文本文件中.在 po 文件中, 每个被提取的字符串是一个 msgid, 而翻译则是接下来一行中的 msgstr, 现在所有的 msgstr 都空着, 在 po 文件中直接写入翻译结果并保存即可. 此外还要指定 po 文件头部有文件编码等信息.
翻译完成后, 在本地创建 locale 相关目录, 再使用 msgfmt 工具编译 po 文件为二进制文件, 并放入 locale 目录中
$ mkdir -p $LANG/LC_MESSAGES && msgfmt hello.po -o $LANG/LC_MESSAGES/hello.mo
这个目录的意义待会儿再解释.
使用纯文本编辑器盯着 po 文件翻译条目是很累很无趣的事情, 好在有许多工具可以选取. 比较流行的有 poEdit 和 Qt Linguist. 接下来继续讨论程序的事情.
到此为止, 程序本身还不具备本地化的能力, 因为宏
TRANSLATE_THIS
什么也没干, 程序本身的逻辑并没有变化, 也不知道如何导入翻译目标字符串替换原有字符串. 现在将宏定义换掉, 使用 locale 中的组件来完成本地化工作#include <stdio.h>
#include <string.h>
#include <libintl.h>
#include <locale.h>
#define TRANSLATE_THIS(x) gettext(x)
int main(int argc, char* argv[])
{
setlocale(LC_ALL, "");
bindtextdomain("hello", ".");
textdomain("hello");
if (1 == argc || 0 == strcmp("hello", argv[1]))
{
puts(TRANSLATE_THIS("Hello"));
}
else
{
puts(TRANSLATE_THIS("World"));
}
return 0;
}
函数
bindtextdomain
的第二个参数确实是搜索路径, 但这并不表示 bindtextdomain
在当前路径下搜索 hello.mo 文件. 查查 man 3 bindtextdomain
, 其实路径还隐含了两个参数 locale 和 category, 这也就是刚才在编译 po 文件时, 输出目录需要指定为 $LANG/LC_MESSAGES 的原因.如果提示 libintl, gettext 等函数无法链接, 则需要添加 lib 引用
$ cc hello.c -o hello.out -lintl
程序修改后, 可能需要追加翻译项目, 比如上面例子修改后
int main(int argc, char* argv[])
{
setlocale(LC_ALL, "");
bindtextdomain("hello", ".");
textdomain("hello");
if (1 == argc || 0 == strcmp("hello", argv[1]))
{
puts(TRANSLATE_THIS("Hello"));
}
else
{
puts(TRANSLATE_THIS("world")); // to lowercase
}
return 0;
}
$ xgettext hello.c --keyword=TRANSLATE_THIS -o hello.po
会发生一件悲催的事情: 原有的翻译被清空了; 准确地说, 整个 hello.po 文件被重新生成了. 在进行翻译时, 要使用 xgettext 的 -j 参数来追加条目
$ xgettext hello.c --keyword=TRANSLATE_THIS -o hello.po -j
如果要清理掉已经不用的项目, 那么可以使用 msgmerge 工具:
$ xgettext hello.c --keyword=TRANSLATE_THIS -o hello_tmp.po && msgmerge hello.po hello_tmp.po -o hello.po && rm hello_tmp.po
现在 hello.po 文件中, msgid "World" 被放在了文件最后, 翻译仍然保留, 不过被注释了.
小心 msgmerge 的输入文件顺序不要弄错, 一定是旧文件在前, 新文件在后, 并且合并后的文件头部工程信息和翻译人员信息以旧文件为准.