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 を使う場合、メッセージ翻訳の影響はアプリケーション全体に及ぼすことになります。アプリケーションが単一の言語しか扱わず、各言語に依存する部分をユーザのロケール情報によって選ぶのなら、ほとんどの場合この方法でやりたいことを実現できます。Python モジュールを地域化していたり、アプリケーションの実行中に言語を切り替えたい場合、おそらくクラスに基づいた API を使いたくなるでしょう。

gettext.bindtextdomain(domain, localedir=None)

domain をロケール辞書 localedir に結び付け (bind) ます。具体的には、 gettext は与えられたドメインに対するバイナリ形式の .mo ファイルを、(Unixでは) localedir/language/LC_MESSAGES/domain.mo から探します。ここで languages はそれぞれ環境変数 LANGUAGELC_ALLLC_MESSAGES 、および LANG の中から検索されます。

localedir が省略されるか None の場合、現在 domain に結び付けられている内容が返されます。 [1]

gettext.bind_textdomain_codeset(domain, codeset=None)

domaincodeset に結び付けて、 gettext() ファミリの関数が返す文字列のエンコード方式を変更します。 codeset を省略すると、現在結び付けられているコードセットを返します。

gettext.textdomain(domain=None)

現在のグローバルドメインを調べたり変更したりします。 domainNone の場合、現在のグローバルドメインが返されます。それ以外の場合にはグローバルドメインは domain に設定され、設定されたグローバルドメインを返します。

gettext.gettext(message)

現在のグローバルドメイン、言語、およびロケール辞書に基づいて、 message の特定地域向けの翻訳を返します。通常、ローカルな名前空間ではこの関数に _() という別名をつけます (下の例を参照してください)。

gettext.lgettext(message)

gettext() と同じですが、 bind_textdomain_codeset() で特にエンコードを指定しない限り、翻訳結果を優先システムエンコーディング (preferred system encoding) で返します。

gettext.dgettext(domain, message)

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

gettext.ldgettext(domain, message)

dgettext() と同じですが、 bind_textdomain_codeset() で特にエンコードを指定しない限り、翻訳結果を優先システムエンコーディング (preferred system encoding) で返します。

gettext.ngettext(singular, plural, n)

gettext() と同様ですが、複数形の場合を考慮しています。翻訳文字列が見つかった場合、 n の様式を適用し、その結果得られたメッセージを返します (言語によっては二つ以上の複数形があります)。翻訳文字列が見つからなかった場合、 n が 1 なら singular を返します; そうでない場合 plural を返します。

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

gettext.lngettext(singular, plural, n)

ngettext() と同じですが、 bind_textdomain_codeset() で特にエンコードを指定しない限り、翻訳結果を優先システムエンコーディング (preferred system encoding) で返します。

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

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

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

dngettext() と同じですが、 bind_textdomain_codeset() で特にエンコードを指定しない限り、翻訳結果を優先システムエンコーディング (preferred system encoding) で返します。

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 形式のファイルを解釈し、文字列形式でメッセージを返す 「翻訳」 クラスを定義しています。この 「翻訳」 クラスのインスタンスも、組み込み名前空間に関数 _() として組みこみ (install) できます。

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)

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

複数のファイルが発見された場合、後で見つかったファイルは前に見つかったファイルの代替でと見なされ、後で見つかった方が利用されます。代替の設定を可能にするには、 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() を代替オブジェクトに転送します。そうでない場合、翻訳されたメッセージを返します。派生クラスで上書きするメソッドです。

lgettext(message)

代替オブジェクトが設定されている場合、 lgettext() を代替オブジェクトに転送します。そうでない場合、翻訳されたメッセージを返します。派生クラスで上書きするメソッドです。

ngettext(singular, plural, n)

代替オブジェクトが設定されている場合、 ngettext() を代替オブジェクトに転送します。そうでない場合、翻訳されたメッセージを返します。派生クラスで上書きするメソッドです。

lngettext(singular, plural, n)

代替オブジェクトが設定されている場合、 lngettext() を代替オブジェクトに転送します。そうでない場合、翻訳されたメッセージを返します。派生クラスで上書きするメソッドです。

info()

「protected」 の _info 変数を返します。

charset()

翻訳メッセージカタログファイルのエンコーディングである 「protected」 の _charset 変数を返します。

output_charset()

lgettext()lngettext() の中で翻訳メッセージとして返す文字列のエンコーディングを定義している 「protected」 の _output_charset 変数を返します。

set_output_charset(charset)

翻訳メッセージとして返す文字列のエンコードを決める、 「protected」 の変数 _output_charset を変更します。

install(names=None)

このメソッドは self.gettext() を組み込み名前空間に組み入れ、 _ と結び付けます。

names パラメータには、 _() 以外に組み込みの名前空間にインストールしたい関数名のシーケンスを指定します。サポートしている名前は 'gettext' (self.gettext() に対応します)、 'ngettext' (self.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 が送出されることがあります。

以下のメソッドは基底クラスの実装からオーバライドされています:

GNUTranslations.gettext(message)

カタログから message id を検索して、対応するメッセージ文字列を、 Unicode でエンコードして返します。 message id に対するエントリがカタログに存在せず、フォールバックが設定されている場合、フォールバック検索はオブジェクトの gettext() メソッドに転送されます。そうでない場合、 message id 自体が返されます。

GNUTranslations.lgettext(message)

gettext() と同じですが、翻訳結果は選択された出力 charset でエンコードされたバイト文字列として返されます。あるいは set_output_charset() で特にエンコーディングが指定されていなければ、優先システムエンコーディングで返します。

GNUTranslations.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}
GNUTranslations.lngettext(singular, plural, n)

gettext() と同じですが、翻訳結果は選択された出力 charset でエンコードされたバイト文字列として返されます。あるいは set_output_charset() で特にエンコーディングが指定されていなければ、優先システムエンコーディングで返します。

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.lgettext

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() に関する脚注を参照してください。