PHP/WordPress で国際化対応のコードを書く

PHP/WordPress で国際化対応プログラムを書くことがあったので、得た知識をまとめておきます。

gettext について

自分で gettext を使ったことがなくても .mo という拡張子のつくファイルをどこかでみたことはあると思います。 プログラムの中で


_("Enter your name:")

と書いたものを以下のようなエントリを含むソースファイル (.po) から生成した翻訳ファイル (.mo) を使って実行時に翻訳します。


msgid  "Enter your name:"
msgstr "名前を入力してください"

すると最初の _() は以下に翻訳されます。 PHP では _ は gettext 関数のエイリアスとなっており、この関数により文字列が変換されるのです。


"名前を入力してください"

一般的にプログラムで文字列出力時にならないと確定できない部分には printf のようにプレースホルダーを使用可能な関数を用います。 PHP でも printf や sprintf が用意されているのでプレースホルダーつき文字列を引数としてこれらの関数を呼び出すことができますし、以下のように番号つきプレースホルダーを使って順番を変えるテクニックも国際化対応ではよく使われます。


msgid  "Address: %1$s, %2$s"       (US は番地から)
msgstr "住所: %2$s %1$s"       (日本は市区町村から)

Gnu gettext

翻訳ファイルの作成にあたって、私が主に使っているのは Gnu gettext です。 .mo ファイルを作る場合、とりあえず使用する Gnu gettext パッケージのコマンドは以下の通りです。 作業の流れにそって紹介します。

xgettext
ソースファイルから .pot (テンプレートとして使う .po ファイル)
msginit
.pot ファイルから .po ファイルを生成
msgfmt
.po ファイルから .mo ファイルを生成
msgmerge
ソースファイルを修正したときに、修正後に再生成した .pot ファイルと旧 .po ファイルから新 .po ファイルを生成。

実行時に気をつけるのは xgettext のオプション (これは後述) と msgmerge のファイルの指定間違いぐらいで、あとはすんなり使用できると思います。 各コマンドとも出力ファイルは -o で指定できます。 それぞれのコマンドの詳細はここでは取り上げないので、公式マニュアルを参照してください。 Web 検索すれば、.pot ファイルから日本語翻訳ファイルを作る方法は、その他のツールを使う場合含め多くの事例を見つけることができると思います。

msgfmt のデフォルト動作についてちょっと追記。

  • fuzzy とマークされたエントリは無視されるので、内容を確認し fuzzy を消しておく必要がある
  • msgstr が空 ("") のエントリは無視されるのでわざわざ消す必要なし
  • どの翻訳ファイルが使用されるか

    PHP の gettext の場合、翻訳ファイルの存在するディレクトリは bindtextdomain の呼び出しによって「ドメイン」 (通常はアプリケーション名) に結びつけられ、ドメインとロケールの設定から読み出す翻訳ファイルが決まります。 指定されたドメインでベースとなるディレクトリが確定し、指定されたロケールでその配下のどの翻訳ファイルが読み出されるか決まる、ということです。 bindtextdomain で "./lang" ディレクトリを指定したとき、ロケール ja 用の翻訳ファイルをは以下の場所に置きます。 詳しくは gettext のマニュアルの実行例をご覧ください。

    
    ./lang/ja/LC_MESSAGES/modulename.mo
    

    ドメインの指定は textdomain を呼んでセットしておくか、dgettext のようにドメインつきの gettext 系関数を使用します。

    ロケールの変更

    続いてロケールの設定ですが、PHP ではデフォルトのロケール設定は php.ini の ‘intl.default_locale’ の値が使用されます。 これを変えたいときは、Locale::setDefault を呼び出します。 以下はブラウザの設定によりロケールを変更する実行例です。 ブラウザに設定されている言語を HTTP リクエストヘッダーから読み出すのに Locale::acceptFromHttp という関数が用意されています。

    
    if ( !empty( Locale::acceptFromHttp( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) ) {
      Locale::setDefault( Locale::acceptFromHttp( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) );
    }
    

    ソースファイルを作成する

    国際化を念頭に置くのであれば最初から文字列は英語で gettext 系関数を用いて書くべきです。 何故最初からかというと、やはり後から国際化する方が手間がかかるからです。 量としてもそうですが、質としても後付け国際化はさほど面白くない作業になると思うので、最初からやっておきましょう。

    そして、何故英語かというと、残念ながら日本語から各言語に翻訳してくれる人は英語から翻訳してくれる人に比べてずっと少ないですし、そもそも ASCII 文字でないとツールが対応していないからです。 嘆いても仕方ないので、意味が伝わることを目標に英語を使って書いてみましょう。 意味さえ通じれば、きっと誰かがまともな英語翻訳ファイルを作ってくれるはずです!

    PHP の gettext とコンテキスト

    PHP で gettext を使うには必要なモジュールを組み込んでおく必要があります。 Fedora であれば php-intl というパッケージが必要でした。 また、Windows 環境では以下の拡張モジュールが必要でした。

    • php_gettext.dll
    • php_intl.dll

    例えば、英語の「YES/NO」と日本語の「はい/いいえ」が全く同じではないことはみなさんご存知だと思います。 それは _("YES") を "はい" と訳す場合と (まれにですが) "いいえ" と訳す場合があるということですが、このような場合 _("YES", "context") のように 2つめの引数を使ってどの訳語とするかの判断に用います。 この 2つめの引数をコンテキスト情報と呼び、多くのプログラミング言語ではコンテキスト対応した i18n 関数ライブラリが用意されています。

    しかし残念なことに PHP 標準の gettext 系関数はではコンテキストつきで翻訳を行う関数が用意されていません。 従って、これをやろうとするとMozilla Developer Network に紹介されているように以下のような自作関数を用いなければなりません。 (この Mozilla Developer Network のページは一読をお勧めします)

    
    function pgettext($context, $msgid) {
        $contextString = "{$context}\004{$msgid}";
        $translation = _($contextString);
        if ($translation == $contextString)  return $msgid;
        else  return $translation;
    }
    
    function ___($message, $context ="") {
        if($context != "") {
            return pgettext($context, $message);
        } else {
            return _($message);
        }
    }
    

    これで ___("YES", "context") という呼び出しができるようになります。 ここで問題となるのは Gnu の xgettext でソースファイルから .pot を作成するときに、デフォルトではこのような独自関数を扱ってくれないということです。 従って、先の関数を使うためには以下のようなオプションをつける必要があります。

    
    --keyword=___:1 --keyword=___:1,2c
    

    これは ___() 関数を抽出対象とし、2つ以上引数を持つ時は、1番目の引数が変換対象文字列、「c」の付いている 2番目めの引数がコンテキストであることを示しています。 –keyword オプションの詳細はxgettext のマニュアルの「Language specific options」の項にあります。 各プログラミング言語ごとの対応関数もそこに一覧として出ています。

    ところで、1点ハマったのですが、理由はよくわかりませんが以下のように書いた部分が xgettext でエラーとなってしまいました。

    
    <noscript><?php echo _("Please turn on JavaScript"); ?></noscript>
    

    以下のようにコードを直すとエラーを回避できました。 どのような条件でエラーになるのか細かく見ていませんが、このようなことで回避できる場合もあるということです。

    
    (php の別の場所で)
    $msg_js_on = _("Please turn on JavaScript");
    (html の部分で)
    <noscript><?php echo $msg_js_on; ?></noscript>
    

    WordPress の gettext

    PHP 標準関数では足りないので、WordPress では以下のように多くの独自に用意された gettext 系関数が用意されています。 また、ロケールの扱いも独自に実装されており、翻訳ファイルを読み込むディレクトリも PHP の gettext と異なります。

    __(), _e(), _n(), _x(), _ex(), _nx(), esc_attr__(), esc_attr_e(), esc_attr_x(), esc_html__(), esc_html_e(), esc_html_x(), _n_noop(), _nx_noop()

    こうして見ると関数の数は多いのですが、以下のように分類できます。

    • e 付きは echo する
    • n 付きは単数形/複数形の違いを扱う
    • x 付きはコンテキストあり
    • esc_attr 付きは attribute として使うためにエスケープする
    • esc_html 付きは HTML テキスト用としてエスケープする
    • noop 付きは翻訳用配列を用意するための関数

    _n_noop()/_nx_noop() についてはちょっとわかりづらいので補足します。 といっても _n_noop() の公式マニュアルの例が全てなのでこれを説明します。

    
    $messages = array(
    	'post' => _n_noop('%s post', '%s posts'),
    	'page' => _n_noop('%s pages', '%s pages')
    );
    ...
    $message = $messages[$type];
    $usable_text = sprintf( translate_nooped_plural( $message, $count, $domain ), $count );
    

    このコードはポストタイプが投稿か固定ページによって表示するメッセージを変化させる例で、表示用の配列を作るのに _n_noop を使用しています。 同じことは別の書き方でもできるでしょうが、このように書くと実行時に翻訳されるのは実際に使用するポストタイプのメッセージのみになります。

    これだけ関数があると全てを xgettext のオプションとして指定するのは大変なのですが、実行例は見当たりませんでした。 これには理由があって、WordPress に関してはPOT ファイルを作成するためのツールが用意されているので xgettext は不要なのです。 ツールは SVN リポジトリよりチェックアウトして使用します。 以下はプラグイン用の .po (.pot) ファイル生成実行例ですが、my-work-dirmy-plugin-dir は自分の環境に合わせて変えて実行します。 テーマの場合は wp-plugin を wp-theme に変えて実行します。

    $ svn co http://i18n.svn.wordpress.org/tools/trunk/ my-work-dir
    $ cd my-work-dir
    $ php makepot.php wp-plugin my-plugin-dir
    

    WordPress の国際化を行うのであれば、このページを一通り読んでおくべきです。 ディレクトリとドメインの扱いについてもそこにまとめられています。

    まとめ

    PHP/WordPress の gettext についてまとめました。 興味のある方はこれをとっかかりにして i18n 対応のコードを書いてみてください。