Linux 程序中的消息国际化
Linux 每天都变得越来越流行。到目前为止,典型的 Linux 用户一直是系统管理员、学生或 UNIX 黑客。GNOME、KDE 和 GNUStep 等新项目正在为不同的、技术准备较少的用户铺平道路。
对于至少具备中等计算机技能的人来说,运行英文软件通常不是问题,但最终用户需要(并且 想要)使用他们自己的语言的软件,以便提高工作效率或对系统感到舒适。此外,许多程序需要知道诸如日期或金额等本地约定,以便有用和完整。
本文是对 GNU gettext 系统的介绍,它是一组工具和库,供程序员和翻译人员使用,使他们能够生成使用指定语言的文本消息的多语言程序。我们将处理使用 ISO-8859-X 字符集之一的语言,但日语和中文除外,因为它们需要额外的注意。
在讨论程序中对不同语言的支持时,经常出现两个词:国际化 和 本地化。由于一遍又一遍地书写这些单词(没有拼写错误)既烦人又耗时,人们将它们缩写为 I18N 和 L10N。18 和 10 表示每个单词的第一个字母和最后一个字母之间的字母数。
国际化 程序意味着采取必要的步骤使其了解不同的语言和国家标准。
本地化 过程发生在当国际化的程序被赋予正确行为所需的信息,以适应某种语言和一套文化习惯时。
对于程序员和最终用户来说,首先要做的是配置 Linux 机器以使用区域设置。大多数用户只需要按照可从 ftp://sunsite.unc.edu/pub/Linux/docs/ 和镜像站点下载的 Locales mini-HOWTO 进行操作即可。最新的发行版(例如 Red Hat 5.0)包含支持区域设置所需的一切。
一旦系统启用了区域设置支持,您必须指定您希望使用的特定标准和语言。这是通过一组环境变量完成的。每个变量控制区域设置系统的特定方面
LANG 指定全局区域设置,但可以被以下变量覆盖。
LC_COLLATE 指定用于排序和比较的区域设置。
LC_CTYPE 指定正在使用的字符集,以便 isupper('<\#192>') 在意大利区域设置中返回 true。
LC_MONETARY 提供有关在特定区域设置中表示货币的信息。
LC_NUMERIC 提供有关数字的信息:数字如何在组中划分和分隔,小数点是什么等等。
LC_TIME 指定使用哪个区域设置来表示时间:例如,AM/PM 或 24 小时制值。
LC_MESSAGES 指示您首选的程序文本消息的语言。
LC_ALL 覆盖任何先前的指示并设置全局区域设置。
全局区域设置值的示例包括
en_US 表示美国的英语。
it_IT 表示意大利的意大利语。
fr_CA 表示加拿大的法语。
默认情况下使用的区域设置(除非被之前的变量覆盖)称为 C(或 POSIX)区域设置。因此,通过使用 date 可以很容易地说明区域设置感知程序的行为,例如(参见列表 1)。首先,在不设置 LC_ALL 变量的情况下,响应是英文的。接下来,设置 LC_ALL 以获得意大利语响应,法语响应(指定了加拿大法语),然后是英语响应(加拿大英语)。意大利语区域设置的“No such file or directory”未被翻译,这意味着意大利语信息不可用;因此,而是使用了默认值。
让我们先看一下 GNU gettext 包。如果您的系统上没有安装它,您可以从 ftp://prep.ai.mit.edu/pub/gnu/ 或其镜像站点下载它。
当使用此包编写多语言程序时,字符串被“包装”在函数调用中,而不是直接在源代码中编码。该函数称为 gettext,它接受恰好一个字符串参数并返回一个字符串。
尽管 gettext 非常简单,但它非常有效:传递作为参数的字符串在表中查找以找到相应的翻译。如果找到翻译,则 gettext 返回它;否则,返回传递的字符串,程序将继续使用默认语言。
我们的第一个国际化的 Hello, world! 程序可能是
#include <stdio.h> #include <libintl.h> void main(void) { textdomain("hello-world"); printf(gettext("Hello, world!\n")); }
始终记住在每个使用 gettext 包的 C 程序中包含 <libintl.h>。
函数 textdomain 应在使用 gettext 之前调用。它的目的是为程序选择正确的“消息数据库”(更合适的术语是“消息目录”)以供使用。
然后,每个可翻译的字符串都必须用作 gettext 的参数。每次都写 gettext("foobar") 可能会很烦人。这就是为什么许多程序员使用这个宏
#define _(x) gettext(x)
通过这样做,消息国际化引入的开销非常小:与其写 "foobar",不如只写 _("foobar")。每个可翻译的字符串只多三个字符,其优点是此宏完全消除了模块中的 gettext 代码。
一旦程序国际化,本地化过程就可以开始了。首先要做的是从源代码中提取所有需要翻译的字符串。
这个自动过程由 xgettext 执行。结果是一个可编辑的 .po(可移植对象)文件。xgettext 扫描作为参数传递的源文件,并提取程序员用 gettext 或其他标识符标记的每个可翻译的字符串。
在我们的例子中,我们可以这样调用 xgettext
xgettext -a -d hello-world -k_ -s -v hello-world.c
生成的 hello-world.po 如列表 2 所示。
我建议您查看 gettext info 文档,了解其他有用的开关。我在这里使用的开关定义如下
-a 提取所有字符串。
-d 将结果输出到 hello-world.po(默认为 messages.po)。
-k 指示 xgettext 在搜索可翻译的字符串时查找 _ (默认值 gettext 和 gettext_noop 仍然会被查找)。
-s 生成排序后的输出并删除重复项。
-v 告诉 xgettext 在生成消息时显示详细信息。
此时,翻译人员可以简单地用消息填充 hello-world.po,而无需了解源代码。实际上,程序可以在添加新语言之前进行国际化和编译。
可移植对象必须编译成机器对象(.mo 文件)才能有用。这是通过命令完成的
msgfmt -o hello-world.mo -v hello-world.po

图 1
图 1. 表示从 C 源代码获取 .mo 文件所需的所有步骤的框图。最关键的部分是运行 tupdate(见下文)以合并新的、未翻译的字符串和之前的工作,而不会丢失它。
最后一步是将 hello-world.mo 复制到合适的位置,gettext 系统可以在那里找到它。在我的 Linux 机器上,默认位置是 /usr/share/locale/LL/ LC_MESSAGES/ 或 /usr/share/locale/LL_CC/LC_MESSAGES/,其中 LL 是语言,CC 是国家。例如,意大利语翻译应放置在 /usr/share/locale/it/ LC_MESSAGES/hello-world.mo。
textdomain 必须在程序开始时调用,以便系统可以根据当前的区域设置变量选择合适的 .mo 文件。按优先级顺序(优先级从高到低),它们是 LC_ALL、LC_MESSAGES 和 LANG。
如果程序员决定这样做,则 .mo 文件可以在许多程序之间共享。GNU fileutils 就是如此。
如果源代码发生更改,则应更新相应的 .po 文件,而不会丢失任何以前的翻译。不幸的是,简单地再次调用 xgettext 是行不通的,因为它会覆盖旧的 .po 文件。在这种情况下,程序 tupdate 派上用场。它合并两个 .po 文件,保留已完成的翻译,只要新字符串与旧字符串匹配即可。它的语法很简单
tupdate new.po old.po > latest.po
新字符串显然在 latest.po 中仍然为空,但已经翻译的字符串将在那里,而无需重新处理。
并非总是可以“直接”使用 gettext 函数。让我们看一下列表 3 中的源代码摘录作为示例。在此代码的国际化期间必须达到两个目标。首先,每个可翻译的字符串都必须出现在 .po 文件中。其次,在运行时打印每个字符串之前,我们必须将其传递给 gettext。
字符串 "You have %d %s" 提出了一个问题。我们不能简单地将 item_names 中声明的每个字符串转换为 gettext 调用,因为数组必须使用常量值初始化。
列表 4 中显示了一种解决方案。gettext_noop 是一个标记,用于使字符串可被 xgettext 识别(这就是默认情况下会查找它的原因)。翻译在运行时通过正常的 gettext 调用发生。
.po 文件具有非常简单的文本结构,可以使用任何文本编辑器进行修改。在处理它们时,Emacs 等编辑器可以设置为特殊的 po 模式。
每个消息文件都由一系列记录组成。每个记录都具有以下结构
(blank lines) # optional human comments #. optional automatic comments #: optional source code reference msgid original-string msgstr translated-string
翻译人员引入的注释应在 # 字符后立即留一个空格。自动注释由 xgettext 和 tupdate 生成,以增强文件的可读性,并允许翻译人员快速浏览源代码并找到使用字符串的行。这有时对于产生正确的翻译是必要的。
字符串的格式与 C 语言中的格式相同。例如,可以合法地写成
msgid "" "Hello " "world!\n" msgstr "" "Ciao " "mondo!\n"
如您所见,字符串可以跨越多行,反斜杠用于引入制表符和换行符等特殊字符。
不存在消息目录的 POSIX 标准——委员会无法就任何内容达成一致。
GNU gettext 不是国际化程序可以使用的唯一消息目录系统。还存在另一个基于 catgets 函数调用的库。catgets 接口受 X/Open 联盟支持,而 gettext 接口最初由 Sun 使用。
catgets 的主要缺点是必须为每条消息选择一个 唯一 标识符,并在每次调用 catgets 时传递它。这使得管理大量消息非常困难,在这些消息中,条目会定期插入和删除。但是,GNU gettext 可以在支持它的系统上使用 catgets 作为底层接口。
Linux 同时支持 gettext 和 catgets 接口。我个人的看法是,对于程序员和翻译人员来说,gettext 系统都更容易使用。
本文中引用的所有列表都可以通过匿名下载在文件 ftp.linuxjournal.com/pub/lj/listings/issue59/3023.tgz 中找到
