23.1. gettext --- 多言語対応に関する国際化サービス

ソースコード: Lib/gettext.py


gettext モジュールは、 Python のモジュールやアプリケーションの国際化 (I18N, I-nternationalizatio-N) および地域化 (L10N, L-ocalizatio-N) サービスを提供します。 このモジュールは GNU gettext メッセージカタログの API と、より高水準で Python ファイルに適しているクラス形式の API の両方をサポートしてます。 以下で述べるインタフェースを使うことで、モジュールやアプリケーションのメッセージをある自然言語で記述しておき、後から提供する翻訳されたメッセージのカタログによって様々な自然言語環境で実行できます。

ここでは Python のモジュールやアプリケーションを地域化するためのいくつかのヒントも提供しています。

23.1.1. GNU gettext API

gettext モジュールでは、以下の GNU gettext API に非常に良く似た API を提供しています。 この API を使う場合、アプリケーション全体の翻訳に影響します。 アプリケーションが単一の言語しか扱わず、ユーザのロケールに従って言語が選ばれるのなら、たいていはこの API が求めているものです。 Python モジュールを地域化していたり、アプリケーションの実行中に言語を切り替える必要がある場合は、この API ではなくおそらくクラス形式の API を使いたくなるでしょう。

gettext.bindtextdomain(domain, localedir=None)

domain をロケールディレクトリ localedir に対応付けます。 具体的には、 gettext は与えられたドメインに対するバイナリ形式の .mo ファイルを探しに、(Unixでは) localedir/language/LC_MESSAGES/domain.mo というパスを見に行きます。 ここで language はそれぞれ環境変数 LANGUAGELC_ALLLC_MESSAGESLANG の中から検索されます。

localedir が省略されるか None の場合、現在 domain に対応付けられているロケールディレクトリが返されます。 [1]

gettext.bind_textdomain_codeset(domain, codeset=None)

domaincodeset に対応付け、 lgettext(), ldgettext(), lngettext(), ldngettext() 関数が返すバイト文字列のエンコード方式を変更します。 codeset が省略された場合は、現在 domain に対応付けられているコードセットを返します。

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 を返します。

複数形の選択公式はカタログのヘッダから取得されます。 選択公式は自由変数 n を持つ C または Python の式です; その式の評価結果はカタログにある複数形のインデックスになります。 .po ファイルで用いられる詳細な文法と、様々な言語における選択公式については GNU gettext ドキュメント を参照してください。

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

ngettext() と同様ですが、指定された domain からメッセージを探します。

gettext.lgettext(message)
gettext.ldgettext(domain, message)
gettext.lngettext(singular, plural, n)
gettext.ldngettext(domain, singular, plural, n)

それぞれに対応する先頭の l が無い関数 (gettext(), dgettext(), ngettext(), dngettext()) と同じですが、エンコーディングが bind_textdomain_codeset() を使って明示的に設定されていない場合、翻訳結果は優先システムエンコーディングでエンコードされたバイト文字列として返されます。

警告

これらの関数はエンコードされたバイト列を返すため Python 3 で使うのは避けるべきです。 ほとんどの Python アプリケーションでは、人間が読むテキストをバイト列ではなく文字列として扱いたいので、 Unicode 文字列を返す代わりの関数を使う方が良いです。 さらに言うと、翻訳文字列にエンコーディング上の問題があった場合、 Unicode 関連の予期しない例外を受け取るかもしれません。 このように l*() 関数は問題や制約を抱えているので、将来の Python バージョンで非推奨となる可能性があります。

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.'))

23.1.2. クラス形式の API

gettext モジュールのクラス形式の API は、 GNU gettext API よりも高い柔軟性と利便性を持っています。 Python のアプリケーションやモジュールを地域化するにはこちらを使うことをお勧めします。 gettext には、GNU .mo 形式のファイルを構文解析する処理の実装と、文字列を返すメソッドを持つ "翻訳" クラスが定義されています。 この "翻訳" クラスのインスタンスも、自分自身を組み込み名前空間に関数 _() として配置できます。

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

この関数は標準的な .mo ファイル検索アルゴリズムを実装しています。 textdomain() と同じく、 domain を引数にとります。オプションの localedirbindtextdomain() と同じです。またオプションの languages は文字列を列挙したリストで、各文字列は言語コードを表します。

localedir が与えられていない場合、標準のシステムロケールディレクトリが使われます。 [2] languages が与えられなかった場合、以下の環境変数: LANGUAGELC_ALLLC_MESSAGES 、および LANG が検索されます。空でない値を返した最初の候補が languages 変数として使われます。この環境変数は言語名をコロンで分かち書きしたリストを含んでいなければなりません。 find() はこの文字列をコロンで分割し、言語コードの候補リストを生成します。

find() は次に言語コードを展開および正規化し、リストの各要素について、以下のパス構成:

localedir/language/LC_MESSAGES/domain.mo

からなる実在するファイルの探索を反復的に行います。 find() は上記のような実在するファイルで最初に見つかったものを返します。該当するファイルが見つからなかった場合、 None が返されます。 all が与えられていれば、全ファイル名のリストが言語リストまたは環境変数で指定されている順番に並べられたものを返します。

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

domainlocaledirlanguages に基づいて Translations インスタンスを返します。 domainlocaledirlanguages はまず find() に渡され、関連付けられている .mo ファイルパスのリストを取得します。 同一の .mo ファイル名を持つインスタンスはキャッシュされます。 実際にインスタンス化されるクラスは、 class_ が与えられていた場合はそのクラスで、そうでない場合には GNUTranslations です。 クラスのコンストラクタは単一の引数として file object を取らなければなりません。 codeset が与えられた場合、 lgettext() メソッドおよび lngettext() メソッドで翻訳文字列のエンコードに使う文字集合を変更します。

複数の .mo ファイルがあった場合、後ろのファイルは前のファイルのフォールバックとして利用されます。 フォールバックの設定のために、 copy.copy() を使いキャッシュから翻訳オブジェクトを複製します; こうすることで、実際のインスタンスデータはキャッシュのものと共有されたままになります。

.mo ファイルが見つからなかった場合、 fallback が偽 (デフォルト値) ならこの関数は OSError を送出し、 fallback が真なら NullTranslations インスタンスが返されます。

バージョン 3.3 で変更: 以前は OSError ではなく IOError が送出されていました。

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

translation()domainlocaledir 、および codeset を渡してできる関数 _() を Python の組み込み名前空間に組み込みます。

names パラメータについては、翻訳オブジェクトの install() メソッドの説明を参照ください。

以下に示すように、通常はアプリケーション中の文字列を関数 _() の呼び出しで包み込んで翻訳対象候補であることを示します:

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

利便性を高めるためには、 _() 関数を Python の組み込み名前空間に組み入れる必要があります。こうすることで、アプリケーション内の全てのモジュールからアクセスできるようになります。

23.1.2.1. NullTranslations クラス

翻訳クラスは、元のソースファイル中のメッセージ文字列から翻訳されたメッセージ文字列への変換処理が実際に実装されているクラスです。 全ての翻訳クラスで基底クラスとして使われているクラスが NullTranslations です; このクラスは、独自の翻訳クラスを実装するのに使える基本的なインタフェースを提供しています。 以下に NullTranslations のメソッドを示します:

class gettext.NullTranslations(fp=None)

オプションの ファイルオブジェクト fp を取ります。この引数は基底クラスでは無視されます。このメソッドは "保護された (protected)" インスタンス変数 _info および _charset を初期化します。これらの変数の値は派生クラスで設定することができます。同様に _fallback も初期化しますが、この値は add_fallback() で設定されます。その後、 fpNone でない場合 self._parse(fp) を呼び出します。

_parse(fp)

基底クラスでは何もしない (no-op) ようになっています。このメソッドの役割はファイルオブジェクト fp を引数に取り、ファイルからデータを読み出し、メッセージカタログを初期化することです。サポートされていないメッセージカタログ形式を使っている場合、その形式を解釈するためにはこのメソッドを上書きしなくてはなりません。

add_fallback(fallback)

fallback を現在の翻訳オブジェクトの代替オブジェクトとして追加します。翻訳オブジェクトが与えられたメッセージに対して翻訳メッセージを提供できない場合、この代替オブジェクトに問い合わせることになります。

gettext(message)

フォールバックが設定されている場合、フォールバックの gettext() に処理を移譲します。 そうでない場合、引数として受け取った message を返します。 派生クラスで上書きするメソッドです。

ngettext(singular, plural, n)

フォールバックが設定されている場合、フォールバックの ngettext() に処理を移譲します。 そうでない場合、 n が 1 なら singular を返します; それ以外なら plural を返します。 派生クラスで上書きするメソッドです。

lgettext(message)
lngettext(singular, plural, n)

gettext() および ngettext() と同じですが、エンコーディングが set_output_charset() で明示的に設定されていない場合、翻訳結果は優先システムエンコーディングでエンコードされたバイト文字列として返されます。 派生クラスで上書きするメソッドです。

警告

これらのメソッドは Python 3 で使うのは避けるべきです。 lgettext() 関数に対する警告を参照してください。

info()

"protected" の _info 変数を返します。

charset()

メッセージカタログファイルのエンコーディングを返します。

output_charset()

lgettext()lngettext() の返り値となる翻訳メッセージで使われているエンコーディングを返します。

set_output_charset(charset)

返り値の翻訳メッセージで使われるエンコーディングを変更します。

install(names=None)

このメソッドは gettext() を組み込み名前空間にインストールし、変数 _ に束縛します。

names パラメータを与える場合には、 _() 以外では組み込み名前空間に配置したい関数名を列挙したシーケンスでなければなりません。 サポートされている名前は 'gettext''ngettext''lgettext''lngettext' です。

この方法はアプリケーションで _() 関数を利用できるようにするための最も便利な方法ですが、唯一の手段でもあるので注意してください。この関数はアプリケーション全体、とりわけ組み込み名前空間に影響するので、地域化されたモジュールで _() を組み入れることができないのです。その代わりに、以下のコードを使って _() を使えるようにしなければなりません。:

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

この操作は _() をモジュール内だけのグローバル名前空間に組み入れるので、モジュール内の _() の呼び出しだけに影響します。

23.1.2.2. GNUTranslations クラス

gettext モジュールでは NullTranslations から派生したもう一つのクラス: GNUTranslations を提供しています。このクラスはビッグエンディアン、およびリトルエンディアン両方のバイナリ形式の GNU gettext .mo ファイルを読み出せるように _parse() を上書きしています。

GNUTranslations はまた、翻訳カタログ以外に、オプションのメタデータを読み込んで解釈します。GNU gettext では、空の文字列に対する変換先としてメタデータを取り込むことが慣習になっています。このメタデータは RFC 822 形式の key: value のペアになっており、 Project-Id-Version キーを含んでいなければなりません。キー Content-Type があった場合、 charset の特性値 (property) は "保護された" _charset インスタンス変数を初期化するために用いられます。値がない場合には、デフォルトとして None が使われます。エンコードに用いられる文字セットが指定されている場合、カタログから読み出された全てのメッセージ id とメッセージ文字列は、指定されたエンコードを用いて Unicode に変換され、そうでなければ ASCII エンコーディングとみなされます。

メッセージ id もユニコード文字列として解釈されるので、すべての *gettext() メソッドはメッセージ id をバイト文字列ではなくユニコード文字列と仮定するでしょう。

key/value ペアの集合全体は辞書型データ中に配置され、"保護された" _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}
lgettext(message)
lngettext(singular, plural, n)

gettext() および ngettext() と同じですが、エンコーディングが set_output_charset() で明示的に設定されていない場合、翻訳結果は優先システムエンコーディングでエンコードされたバイト文字列として返されます。

警告

これらのメソッドは Python 3 で使うのは避けるべきです。 lgettext() 関数に対する警告を参照してください。

23.1.2.3. Solaris メッセージカタログ機構のサポート

Solaris オペレーティングシステムでは、独自の .mo バイナリファイル形式を定義していますが、この形式に関するドキュメントが手に入らないため、現時点ではサポートされていません。

23.1.2.4. Catalog コンストラクタ

GNOME では、James Henstridge によるあるバージョンの gettext モジュールを使っていますが、このバージョンは少し異なった API を持っています。ドキュメントに書かれている利用法は:

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

となっています。過去のモジュールとの互換性のために、 Catalog() は前述の translation() 関数の別名になっています。

このモジュールと Henstridge のバージョンとの間には一つ相違点があります: 彼のカタログオブジェクトはマップ型の API を介したアクセスがサポートされていましたが、この API は使われていないらしく、現在はサポートされていません。

23.1.3. プログラムやモジュールを国際化する

国際化 (I18N, I-nternationalizatio-N) とは、プログラムを複数の言語に対応させる操作を指します。地域化 (L10N, L-ocalizatio-N) とは、すでに国際化されているプログラムを特定地域の言語や文化的な事情に対応させることを指します。Python プログラムに多言語メッセージ機能を追加するには、以下の手順を踏む必要があります:

  1. プログラムやモジュールで翻訳対象とする文字列に特殊なマークをつけて準備します
  2. マークづけをしたファイルに一連のツールを走らせ、生のメッセージカタログを生成します
  3. 特定の言語へのメッセージカタログの翻訳を作成します
  4. メッセージ文字列を適切に変換するために gettext モジュールを使います

ソースコードを I18N 化する準備として、ファイル内の全ての文字列を探す必要があります。翻訳を行う必要のある文字列はどれも _('...') --- すなわち関数 _() の呼び出しで包むことでマーク付けしなくてはなりません。例えば以下のようにします:

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

この例では、文字列 'writing a log message' が翻訳対象候補としてマーク付けされており、文字列 'mylog.txt' および 'w' はされていません。

飜訳対象の文字列を抽出するツールもあります。 オリジナルの GNU gettext は C と C++ のソースコードしかサポートしませんが、拡張版の xgettext は Python を含めた多くの言語で書かれたコードを読み取り、飜訳できる文字列を発見します。 Babel は Python の国際化ライブラリで、飜訳文字列の抽出とメッセージカタログのコンパイルを行う file:pybabel スクリプトがあります。 François Pinard が開発した xpot と呼ばれるプログラムは同じような処理を行え、彼の po-utils package の一部として利用可能です。

(Python には pygettext.py および msgfmt.py という名前の pure-Python 版プログラムもあります; これをインストールしてくれる Python ディストリビューションもあります。 pygettext.pyxgettext に似たプログラムですが Python のソースコードしか理解できず、 C や C++ のような他のプログラミング言語を扱えません。 pygettext.pyxgettext と同様のコマンドラインインターフェースをサポートしています; 詳しい使い方については pygettext.py --help と実行してください。 msgfmt.py は GNU msgfmt とバイナリ互換性があります。 この2つのプログラムがあれば、 GNU gettext パッケージを使わずに Python アプリケーションを国際化できるでしょう。)

xgettextpygettext のようなツールは、メッセージカタログである .po ファイルを生成します。 このファイルは人間が判読可能な構造をしていて、ソースコード中のマークが着けられた文字列と、その文字列の仮置きの訳文が一緒に書き込まれています。

生成された .po ファイルは翻訳者個々人へ頒布され、サポート対象の各自然言語への訳文が書き込まれます。 ある言語への飜訳が完了した <language-name>.po ファイルは翻訳者により返送され、 msgfmt を使い機械が読み込みやすい .mo バイナリカタログファイルへとコンパイルされます。 この .mogettext モジュールによる実行時の実際の飜訳処理で使われます。

gettext モジュールをソースコード中でどのように使うかは単一のモジュールを国際化するのか、それともアプリケーション全体を国際化するのかによります。次のふたつのセクションで、それぞれについて説明します。

23.1.3.1. モジュールを地域化する

モジュールを地域化する場合、グローバルな変更、例えば組み込み名前空間への変更を行わないように注意しなければなりません。GNU gettext API ではなく、クラス形式の API を使うべきです。

仮に対象のモジュール名を "spam" とし、モジュールの各言語における翻訳が収められた .mo ファイルが /usr/share/locale に GNU gettext 形式で置かれているとします。この場合、モジュールの最初で以下のようにします:

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

23.1.3.2. アプリケーションを地域化する

アプリケーションを地域化するのなら、関数 _() をグローバルな組み込み名前空間に組み入れなければならず、これは通常アプリケーションの主ドライバ (main driver) ファイルで行います。この操作によって、アプリケーション独自のファイルは明示的に各ファイルで _() の組み入れを行わなくても単に _('...') を使うだけで済むようになります。

単純な場合では、単に以下の短いコードをアプリケーションの主ドライバファイルに追加するだけです:

import gettext
gettext.install('myapplication')

ロケールの辞書を設定する必要がある場合、install() 関数に渡すことが出来ます:

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

23.1.3.3. 動作中 (on the fly) に言語を切り替える

多くの言語を同時にサポートする必要がある場合、複数の翻訳インスタンスを生成して、例えば以下のコードのように、インスタンスを明示的に切り替えてもかまいません。:

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()

23.1.3.4. 翻訳処理の遅延解決

コードを書く上では、ほとんどの状況で文字列はコードされた場所で翻訳されます。しかし場合によっては、翻訳対象として文字列をマークはするが、その後実際に翻訳が行われるように遅延させる必要が生じます。古典的な例は以下のようなコートです:

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 命令を実行するまで) 一時的に上書きすることができます。もしそれまでに _() をローカルな名前空間に持っていたら注意してください。

二つ目の例における _() の使い方では、パラメータが文字列リテラルではないので、 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_() でマークされている飜訳可能な文字列を見付けるように教える必要が出てきます。 xgettext, pygettext, pybabel extract, xpot は全て、コマンドラインスイッチ -k を使ってその機能をサポートしています。 この例の N_() という名前は好きに選べます; MarkThisStringForTranslation() という名前にしてしまっても構いません。

23.1.4. 謝辞

以下の人々が、このモジュールのコード、フィードバック、設計に関する助言、過去の実装、そして有益な経験談による貢献をしてくれました:

  • Peter Funk
  • James Henstridge
  • Juan David Ibáñez Palomar
  • Marc-André Lemburg
  • Martin von Löwis
  • François Pinard
  • Barry Warsaw
  • Gustavo Niemeyer

脚注

[1]標準でロケールが収められているディレクトリはシステム依存です; 例えば、RedHat Linux では /usr/share/locale ですが、 Solaris では /usr/lib/locale です。 gettext モジュールはこうしたシステム依存の標準設定をサポートしません; その代わりに sys.prefix/share/locale を標準の設定とします。この理由から、常にアプリケーションの開始時に絶対パスで明示的に指定して bindtextdomain() を呼び出すのが最良のやり方ということになります。
[2]上の bindtextdomain() に関する脚注を参照してください。