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 上)查找给定域的二进制 .mo 文件:localedir/language/LC_MESSAGES/domain.mo,其中将在环境变量 LANGUAGELC_ALLLC_MESSAGESLANG 中分别搜索 *language*。

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

gettext.textdomain(domain=None)

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

gettext.gettext(message)

根据当前全局域、语言和语言环境目录,返回 *message* 的本地化翻译。 此函数通常在本地命名空间中别名为 _()(请参阅下面的示例)。

gettext.dgettext(domain, message)

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

gettext.ngettext(singular, plural, n)

gettext() 类似,但考虑复数形式。 如果找到翻译,则将复数公式应用于 *n*,并返回结果消息(某些语言的复数形式不止两种)。 如果未找到翻译,则在 *n* 为 1 时返回 *singular*;否则返回 *plural*。

复数公式取自目录标题。 这是一个带有自由变量 *n* 的 C 或 Python 表达式;该表达式的计算结果为目录中复数的索引。 有关在 .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() 接受的相同。可选的 *localedir* 与 bindtextdomain() 中的相同。可选的 *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)

根据 *domain*、*localedir* 和 *languages* 返回一个 *Translations 实例,这些参数首先传递给 find() 以获取关联的 .mo 文件路径列表。具有相同 .mo 文件名的实例将被缓存。如果提供,则实例化的实际类为 *class_*,否则为 GNUTranslations。该类的构造函数必须接受单个 文件对象 参数。

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

如果未找到 .mo 文件,则如果 *fallback* 为 false(默认值),则此函数将引发 OSError;如果 *fallback* 为 true,则返回 NullTranslations 实例。

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

在 3.11 版更改: 移除了 *codeset* 参数。

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

这会将函数 _() 安装到 Python 的内置命名空间中,具体取决于传递给函数 translation() 的 *domain* 和 *localedir*。

有关 *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 模块提供了一个从 NullTranslations 派生的附加类:GNUTranslations。此类重写了 _parse() 方法,以支持读取大端和小端格式的 GNU gettext 格式的 .mo 文件。

GNUTranslations 会从翻译目录中解析可选的元数据。按照 GNU gettext 的惯例,元数据作为空字符串的翻译包含在内。此元数据采用 RFC 822 风格的 键: 对,并且应该包含 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)

在目录中查找 *context* 和 *message* 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 文件格式,但由于找不到有关此格式的文档,因此目前不支持。

目录构造函数

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 模块以便正确翻译消息字符串

为了使代码为 I18N 做好准备,您需要查看文件中的所有字符串。任何需要翻译的字符串都应通过将其包装在 _('...') 中来标记,即调用 _ 函数。例如

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.pyxgettext 类似,但只理解 Python 源代码,无法处理其他编程语言,如 C 或 C++。pygettext.py 支持与 xgettext 类似的命令行界面;有关其用法的详细信息,请运行 pygettext.py --helpmsgfmt.py 与 GNU msgfmt 二进制兼容。有了这两个程序,您可能不需要 GNU gettext 包来国际化您的 Python 应用程序。)

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

然后将这些 .po 文件的副本交给各个翻译人员,他们为每种支持的自然语言编写翻译。他们将完成的特定于语言的版本作为 <语言名称>.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”识别为可翻译为 gettext 程序,因为该参数不是字符串字面量。

另一种处理方法是使用以下示例

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

脚注