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

腳註