gettext --- 多语种国际化服务

源代码: Lib/gettext.py


gettext 模块为 Python 模块和应用提供了国际化 (I18N) 和本地化 (L10N) 服务。它同时支持 GNU gettext 消息编目 API 和一个更高级别的、基于类的 API,后者可能更适合 Python 文件。下面描述的接口允许你以一种自然语言编写模块和应用程序消息,并提供一个翻译消息的目录,以便在不同的自然语言下运行。

本文还提供了一些关于本地化 Python 模块和应用程序的提示。

GNU gettext API

gettext 模块定义了以下 API,它与 GNU gettext API 非常相似。如果使用此 API,将会全局性地影响整个应用的翻译。如果你的应用是单语种的,且语言选择取决于用户的区域设置,这通常是你所期望的。如果你正在本地化一个 Python 模块,或者你的应用需要动态切换语言,你可能希望改用基于类的 API。

gettext.bindtextdomain(domain, localedir=None)

domain 绑定到区域设置目录 localedir。具体来说,gettext 会使用路径(在 Unix 上)localedir/language/LC_MESSAGES/domain.mo 来查找给定域的二进制 .mo 文件,其中 language 会依次在环境变量 LANGUAGELC_ALLLC_MESSAGESLANG 中搜索。

如果 localedir 被省略或为 None,则返回 domain 的当前绑定。[1]

gettext.textdomain(domain=None)

更改或查询当前的全局域。如果 domainNone,则返回当前的全局域,否则将全局域设置为 domain,并返回该值。

gettext.gettext(message)

基于当前的全局域、语言和区域设置目录,返回 message 的本地化翻译。这个函数通常在本地命名空间中被别名为 _() (见下文示例)。

gettext.dgettext(domain, message)

类似于 gettext(),但在指定的 domain 中查找消息。

gettext.ngettext(singular, plural, n)

类似于 gettext(),但考虑了复数形式。如果找到翻译,则将复数形式规则应用于 n,并返回最终的消息(某些语言有两种以上的复数形式)。如果未找到翻译,则在 n 为 1 时返回 singular;否则返回 plural

复数形式规则取自目录的头部。它是一个 C 或 Python 表达式,带有一个自由变量 n;该表达式求值后得到目录中复数形式的索引。关于在 .po 文件中使用的精确语法以及各种语言的复数规则,请参阅 GNU gettext 文档

gettext.dngettext(domain, singular, plural, n)

类似于 ngettext(),但在指定的 domain 中查找消息。

gettext.pgettext(context, message)
gettext.dpgettext(domain, context, message)
gettext.npgettext(context, singular, plural, n)
gettext.dnpgettext(domain, context, singular, plural, n)

与前缀中不带 p 的相应函数(即 gettext()dgettext()ngettext()dngettext())类似,但翻译仅限于给定的消息 context

在 3.8 版本加入。

注意,GNU gettext 也定义了一个 dcgettext() 方法,但它被认为不实用,因此目前未实现。

下面是一个此 API 的典型用法示例

import gettext
gettext.bindtextdomain('myapplication', '/path/to/my/language/directory')
gettext.textdomain('myapplication')
_ = gettext.gettext
# ...
print(_('This is a translatable string.'))

基于类的 API

gettext 模块的基于类的 API 提供了比 GNU gettext API 更大的灵活性和便利性。这是本地化 Python 应用程序和模块的推荐方式。gettext 定义了一个 GNUTranslations 类,它实现了对 GNU .mo 格式文件的解析,并提供了返回字符串的方法。该类的实例也可以将自身作为函数 _() 安装到内置命名空间中。

gettext.find(domain, localedir=None, languages=None, all=False)

此函数实现了标准的 .mo 文件搜索算法。它接受一个 domain,与 textdomain() 接受的相同。可选的 localedirbindtextdomain() 中的相同。可选的 languages 是一个字符串列表,其中每个字符串都是一个语言代码。

如果未提供 localedir,则使用默认的系统区域设置目录。[2] 如果未提供 languages,则会搜索以下环境变量:LANGUAGELC_ALLLC_MESSAGESLANG。第一个返回非空值的环境变量将被用于 languages 变量。环境变量应包含一个以冒号分隔的语言列表,该列表将被冒号分割以产生预期的语言代码字符串列表。

find() 然后会扩展和规范化这些语言,然后遍历它们,搜索由以下部分组成的现有文件:

localedir/language/LC_MESSAGES/domain.mo

第一个存在的此类文件名由 find() 返回。如果未找到此类文件,则返回 None。如果给定了 all,它会返回一个包含所有文件名的列表,顺序与它们在语言列表或环境变量中出现的顺序相同。

gettext.translation(domain, localedir=None, languages=None, class_=None, fallback=False)

基于 domainlocaledirlanguages 返回一个 *Translations 实例,这些参数首先被传递给 find() 以获取相关 .mo 文件路径的列表。具有相同 .mo 文件名的实例会被缓存。如果提供了 class_,则实际实例化的类是 class_,否则是 GNUTranslations。该类的构造函数必须接受一个 文件类对象 参数。

如果找到多个文件,后面的文件将作为前面文件的备用。为了允许设置备用,使用 copy.copy() 从缓存中克隆每个翻译对象;实际的实例数据仍然与缓存共享。

如果未找到 .mo 文件,当 fallback 为假(默认值)时,此函数会引发 OSError,当 fallback 为真时,则返回一个 NullTranslations 实例。

在 3.3 版本发生变更: 过去会引发 IOError,现在它是 OSError 的别名。

在 3.11 版本发生变更: codeset 参数已被移除。

gettext.install(domain, localedir=None, *, names=None)

此函数将 _() 函数安装到 Python 的内置命名空间中,基于传递给 translation() 函数的 domainlocaledir

关于 names 参数,请参阅翻译对象的 install() 方法的描述。

如下所示,你通常通过将字符串包装在对 _() 函数的调用中来标记应用程序中需要翻译的字符串,例如:

print(_('This string will be translated.'))

为方便起见,你希望将 _() 函数安装在 Python 的内置命名空间中,这样它就可以在应用程序的所有模块中轻松访问。

在 3.11 版本发生变更: names 现在是仅限关键字的参数。

NullTranslations

翻译类实际上实现了将原始源文件消息字符串翻译成翻译后消息字符串的功能。所有翻译类使用的基类是 NullTranslations;它提供了可用于编写自定义专用翻译类的基本接口。以下是 NullTranslations 的方法:

class gettext.NullTranslations(fp=None)

接受一个可选的文件类对象 fp,基类会忽略它。初始化“受保护的”实例变量 _info_charset,这些变量由派生类设置,以及通过 add_fallback() 设置的 _fallback。如果 fp 不为 None,则调用 self._parse(fp)

_parse(fp)

在基类中不执行任何操作,此方法接受文件对象 fp,并从文件中读取数据,初始化其消息目录。如果你有不受支持的消息目录文件格式,应重写此方法以解析你的格式。

add_fallback(fallback)

fallback 添加为当前翻译对象的备用对象。当翻译对象无法为给定消息提供翻译时,应查询备用对象。

gettext(message)

如果已设置备用对象,则将 gettext() 转发给备用对象。否则,返回 message。在派生类中被重写。

ngettext(singular, plural, n)

如果已设置备用对象,则将 ngettext() 转发给备用对象。否则,当 n 为 1 时返回 singular;否则返回 plural。在派生类中被重写。

pgettext(context, message)

如果已设置备用对象,则将 pgettext() 转发给备用对象。否则,返回翻译后的消息。在派生类中被重写。

在 3.8 版本加入。

npgettext(context, singular, plural, n)

如果已设置备用对象,则将 npgettext() 转发给备用对象。否则,返回翻译后的消息。在派生类中被重写。

在 3.8 版本加入。

info()

返回一个包含在消息目录文件中找到的元数据的字典。

charset()

返回消息目录文件的编码。

install(names=None)

此方法将 gettext() 安装到内置命名空间中,并将其绑定到 _

如果给定了 names 参数,它必须是一个序列,包含除 _() 之外,你希望安装到内置命名空间中的函数名。支持的名称有 'gettext''ngettext''pgettext''npgettext'

请注意,这只是让 _() 函数在你的应用程序中可用的一种方式,尽管是最方便的方式。因为它会全局性地影响整个应用程序,特别是内置命名空间,所以本地化的模块绝不应该安装 _()。相反,它们应该使用以下代码使 _() 对其模块可用:

import gettext
t = gettext.translation('mymodule', ...)
_ = t.gettext

这仅将 _() 放入模块的全局命名空间,因此只影响该模块内的调用。

在 3.8 版本发生变更: 添加了 'pgettext''npgettext'

GNUTranslations

gettext 模块提供了一个额外的类,它派生自 NullTranslationsGNUTranslations。这个类重写了 _parse(),以支持读取大端和小端格式的 GNU gettext 格式的 .mo 文件。

GNUTranslations 会从翻译目录中解析可选的元数据。GNU gettext 的惯例是将元数据作为空字符串的翻译。此元数据采用 RFC 822 风格的 key: value 对,并应包含 Project-Id-Version 键。如果找到 Content-Type 键,则 charset 属性用于初始化“受保护的” _charset 实例变量,如果未找到,则默认为 None。如果指定了字符集编码,则从目录中读取的所有消息 ID 和消息字符串都将使用此编码转换为 Unicode,否则假定为 ASCII。

由于消息 ID 也作为 Unicode 字符串读取,所有 *gettext() 方法都将假定消息 ID 是 Unicode 字符串,而不是字节串。

整个键/值对集合被放入一个字典中,并设置为“受保护的” _info 实例变量。

如果 .mo 文件的魔数无效、主版本号不符合预期,或者在读取文件时出现其他问题,实例化 GNUTranslations 类可能会引发 OSError

class gettext.GNUTranslations

以下方法从基类实现中被重写:

gettext(message)

在目录中查找 message ID,并以 Unicode 字符串形式返回相应的消息字符串。如果目录中没有 message ID 的条目,并且已设置备用对象,则将查找转发给备用对象的 gettext() 方法。否则,返回 message ID。

ngettext(singular, plural, n)

对消息 ID 进行复数形式查找。singular 用作在目录中查找的消息 ID,而 n 用于确定使用哪种复数形式。返回的消息字符串是 Unicode 字符串。

如果消息 ID 在目录中未找到,并且指定了备用对象,则请求将转发给备用对象的 ngettext() 方法。否则,当 n 为 1 时返回 singular,在所有其他情况下返回 plural

下面是一个例子:

n = len(os.listdir('.'))
cat = GNUTranslations(somefile)
message = cat.ngettext(
    'There is %(num)d file in this directory',
    'There are %(num)d files in this directory',
    n) % {'num': n}
pgettext(context, message)

在目录中查找 contextmessage ID,并以 Unicode 字符串形式返回相应的消息字符串。如果目录中没有 message ID 和 context 的条目,并且已设置备用对象,则将查找转发给备用对象的 pgettext() 方法。否则,返回 message ID。

在 3.8 版本加入。

npgettext(context, singular, plural, n)

对消息 ID 进行复数形式查找。singular 用作在目录中查找的消息 ID,而 n 用于确定使用哪种复数形式。

如果 context 的消息 ID 在目录中未找到,并且指定了备用对象,则请求将转发给备用对象的 npgettext() 方法。否则,当 n 为 1 时返回 singular,在所有其他情况下返回 plural

在 3.8 版本加入。

Solaris 消息目录支持

Solaris 操作系统定义了自己的二进制 .mo 文件格式,但由于找不到关于该格式的文档,目前不支持。

Catalog 构造函数

GNOME 使用了 James Henstridge 的 gettext 模块的一个版本,但该版本的 API 略有不同。其文档中的用法是:

import gettext
cat = gettext.Catalog(domain, localedir)
_ = cat.gettext
print(_('hello world'))

为了与这个旧模块兼容,函数 Catalog() 是上述 translation() 函数的别名。

此模块与 Henstridge 的模块之间的一个区别是:他的目录对象支持通过映射 API 进行访问,但这似乎未被使用,因此目前不支持。

国际化你的程序和模块

国际化(I18N)指使程序能够适应多种语言的操作。本地化(L10N)指在国际化之后,使你的程序适应本地语言和文化习惯。为了给你的 Python 程序提供多语言消息,你需要采取以下步骤:

  1. 通过特别标记可翻译字符串来准备你的程序或模块

  2. 对标记过的文件运行一套工具,生成原始消息目录

  3. 创建消息目录的特定语言翻译

  4. 使用 gettext 模块,以便正确翻译消息字符串

为了准备你的代码进行国际化,你需要检查文件中的所有字符串。任何需要翻译的字符串都应该通过用 _('...') 包裹来标记——也就是说,调用函数 _。例如:

filename = 'mylog.txt'
message = _('writing a log message')
with open(filename, 'w') as fp:
    fp.write(message)

在这个例子中,字符串 'writing a log message' 被标记为待翻译,而字符串 'mylog.txt''w' 则没有。

有一些工具可以提取用于翻译的字符串。最初的 GNU gettext 只支持 C 或 C++ 源代码,但其扩展版本 xgettext 可以扫描多种语言编写的代码(包括 Python),以查找标记为可翻译的字符串。Babel 是一个 Python 国际化库,包含一个 pybabel 脚本,用于提取和编译消息目录。François Pinard 的程序 xpot 也能完成类似的工作,它是他的 po-utils 包 的一部分。

(Python 也包含这些程序的纯 Python 版本,名为 pygettext.pymsgfmt.py;一些 Python 发行版会为你安装它们。pygettext.py 类似于 xgettext,但只理解 Python 源代码,不能处理 C 或 C++ 等其他编程语言。pygettext.py 支持类似于 xgettext 的命令行界面;关于其用法详情,请运行 pygettext.py --helpmsgfmt.py 与 GNU msgfmt 二进制兼容。有了这两个程序,你可能不需要 GNU gettext 包来国际化你的 Python 应用程序。)

xgettextpygettext 和类似工具会生成 .po 文件,即消息目录。它们是结构化的人类可读文件,包含源代码中每个标记的字符串,以及这些字符串翻译版本的占位符。

这些 .po 文件的副本随后会交给各个译者,他们为每种支持的自然语言编写翻译。他们将完成的特定语言版本以 <language-name>.po 文件的形式发回,该文件使用 msgfmt 程序编译成机器可读的 .mo 二进制目录文件。.mo 文件在运行时由 gettext 模块用于实际的翻译处理。

如何在代码中使用 gettext 模块,取决于你是在国际化单个模块还是整个应用程序。接下来的两节将分别讨论这两种情况。

本地化你的模块

如果你正在本地化你的模块,你必须注意不要做全局性的更改,例如对内置命名空间的更改。你不应该使用 GNU gettext API,而应该使用基于类的 API。

假设你的模块名为 "spam",并且该模块的各种自然语言翻译 .mo 文件以 GNU gettext 格式存放在 /usr/share/locale 中。下面是你需要在模块顶部添加的代码:

import gettext
t = gettext.translation('spam', '/usr/share/locale')
_ = t.gettext

本地化你的应用程序

如果你正在本地化你的应用程序,你可以将 _() 函数全局安装到内置命名空间中,通常在应用程序的主驱动文件中进行。这将让你所有特定于应用程序的文件都能直接使用 _('...'),而无需在每个文件中显式安装它。

在简单情况下,你只需在应用程序的主驱动文件中添加以下代码片段:

import gettext
gettext.install('myapplication')

如果你需要设置区域设置目录,可以将其传递给 install() 函数:

import gettext
gettext.install('myapplication', '/usr/share/locale')

动态切换语言

如果你的程序需要同时支持多种语言,你可能需要创建多个翻译实例,然后显式地在它们之间切换,如下所示:

import gettext

lang1 = gettext.translation('myapplication', languages=['en'])
lang2 = gettext.translation('myapplication', languages=['fr'])
lang3 = gettext.translation('myapplication', languages=['de'])

# start by using language1
lang1.install()

# ... time goes by, user selects language 2
lang2.install()

# ... more time goes by, user selects language 3
lang3.install()

延迟翻译

在大多数编码情况下,字符串在其编码的位置进行翻译。然而,有时你需要标记字符串以供翻译,但将实际翻译推迟到稍后。一个典型的例子是:

animals = ['mollusk',
           'albatross',
           'rat',
           'penguin',
           'python', ]
# ...
for a in animals:
    print(a)

在这里,你想将 animals 列表中的字符串标记为可翻译,但你实际上不想在它们被打印之前翻译它们。

下面是一种处理这种情况的方法:

def _(message): return message

animals = [_('mollusk'),
           _('albatross'),
           _('rat'),
           _('penguin'),
           _('python'), ]

del _

# ...
for a in animals:
    print(_(a))

这之所以有效,是因为 _() 的虚拟定义只是原封不动地返回字符串。并且这个虚拟定义会暂时覆盖内置命名空间中任何对 _() 的定义(直到 del 命令执行)。不过,如果你在本地命名空间中之前有 _() 的定义,要小心。

注意,第二次使用 _() 不会将“a”识别为可翻译的字符串,因为参数不是字符串字面量。

处理此问题的另一种方法是使用以下示例:

def N_(message): return message

animals = [N_('mollusk'),
           N_('albatross'),
           N_('rat'),
           N_('penguin'),
           N_('python'), ]

# ...
for a in animals:
    print(_(a))

在这种情况下,你使用函数 N_() 来标记可翻译的字符串,这不会与任何对 _() 的定义冲突。然而,你需要让你的消息提取程序知道去寻找用 N_() 标记的可翻译字符串。xgettextpygettextpybabel extractxpot 都通过使用 -k 命令行开关来支持这一点。这里选择 N_() 完全是任意的;它也可以是 MarkThisStringForTranslation()

致谢

以下人员为本模块的创建贡献了代码、反馈、设计建议、先前的实现和宝贵的经验:

  • Peter Funk

  • James Henstridge

  • Juan David Ibáñez Palomar

  • Marc-André Lemburg

  • Martin von Löwis

  • François Pinard

  • Barry Warsaw

  • Gustavo Niemeyer

脚注