PHP で GNU gettext
GNU gettext は、ソフトウェアの国際化のためのライブラリ・コマンド群。
大まかな流れとしては以下のようになる
- ソース中から多言語対応すべき文字列を抽出 (POT作成)
gettext コマンド。 - それをもとに各言語用に対訳ファイルを作成 (PO作成)
msginit コマンド。 - 対訳ファイルをバイナリ化 (MO作成)
msgfmt コマンド。
関連用語
msgid
メッセージID。多言語対応すべき文字列のこと。
プログラムソース中に書かれることになる。
msgstr
msgid に対する翻訳。
ファイル
POT (Portable Object Template)
ソースから抽出された、多言語対応すべき文字列(msgid)のリスト。
PO (Portable Object)
POT を元に、対訳(msgstr)が書かれたファイル。各言語分存在。
SCMの管理対象とすべきもの。
MO (Machine Object)
PO をコンパイルしてバイナリ化したもの。
実際に利用されるランタイムはこのファイルのみ。
バイナリファイルなので、SCMの管理対象とはせず、デプロイ時にPOから生成するのがよい。
コマンド
xgettext
ソースから多言語対応すべき文字列を抽出して POT を作成する
msginit
POT から PO を作成
msgfmt
PO を MO にコンパイル
msgmerge
変更された新しい POT と 既存の PO をマージする。
PHPでやってみる
gettext と、 php-gettext が導入済みであること。
ソース作成
<?php setlocale(LC_MESSAGES, ''); // use envvar $domain = 'sample'; bindtextdomain($domain, __DIR__.'/locale'); textdomain($domain); bind_textdomain_codeset('test', 'UTF-8'); echo _("開始しました"), "\n";
- LC_MESSAGES は環境変数のものを使う
- デフォルトの domain(MOのファイル名) は
sample
とする - MOのエンコーディングは UTF-8 とする
- _() は gettext() のalias
POT作成
$ xgettext --from-code=utf-8 --output=sample.pot test.php
sample.pot
が作られた。中身はテキストファイル。
メッセージID(msgid) とそれに対する対訳(msgstr) の対応になっている。
msgstrは空になっている。POTはテンプレートなのでこれでよい。
# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2015-04-15 01:14+0900\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: test.php:9 msgid "開始しました" msgstr ""
PO作成
POT をもとに PO を作る。
配置先は決められていて、 {LANG}/LC_MESSAGES/*.po
となる。
ここでは locale
という階層の下に作ることにした。
$ mkdir -p locale/en_US/LC_MESSAGES $ msginit -i sample.pot -o locale/en_US/LC_MESSAGES/sample.po --locale=en_US --no-translator Created locale/en_US/LC_MESSAGES/sample.po.
locale/en_US/LC_MESSAGES/sample.po
ができたので中身を見てみる。
単なる POT のコピーだが、 msgstr に msgid と同じものが入っている。
POT をコピーしても問題ない。
# English translations for PACKAGE package. # Copyright (C) 2015 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Automatically generated, 2015. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2015-04-15 01:14+0900\n" "PO-Revision-Date: 2015-04-15 01:14+0900\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: test.php:9 msgid "開始しました" msgstr "開始しました"
対訳を書く。
これは en_US のファイルなので、英語を書く。
--- locale/en_US/LC_MESSAGES/test.po.org 2015-04-15 01:29:55.455065174 +0900 +++ locale/en_US/LC_MESSAGES/test.po 2015-04-15 01:30:42.101061980 +0900 @@ -18,4 +18,4 @@ #: test.php:9 msgid "開始しました\n" -msgstr "開始しました\n" +msgstr "started\n"
LANG
は、環境によって使えるものが違う。
何が使えるのかは locale -a
で確認できる。
PO をコンパイル
PO をコンパイルして MO を作成する。
$ msgfmt -o locale/en_US/LC_MESSAGES/sample.mo locale/en_US/LC_MESSAGES/sample.po
動作確認
$ php test.php
開始しました
$ LC_MESSAGES=en_US php test.php
started
エディタ
POT, PO はテキストファイルのため、テキストエディタで編集可能。
しかし、規模が大きくなったり、Fuzzyが増えてきたりすると非効率になってくる。
オープンソースである poedit が編集に使いやすい。
編集には基本的にこれを使った方がよい。
gettext自体も内包しており、xgettext, msginit, msgfmt, msgmerge もこのソフト単体で可能。
(しかし多くの場合はスクリプトで一括して行った方が効率はよい)
マージ
ソースに変更が行われた場合、ソースからPOT抽出~POTからPO作成&翻訳~MO作成 を再度行う必要がある。
しかし PO には既に翻訳済みのメッセージも存在するため、単純な上書きをするわけにはいかない。
再生成した POT と、既存の PO をマージをするためのツールが用意されている。
POT はいつでも再生成、上書きして問題ない。
msgmerge -U locale/en_US/LC_MESAGES/sample.po sample.pot
ソースから削除された == POT から削除された msgid は PO からも削除されるし、
ソースに追加された == POT に追加された msgid は PO にも追加される。
msgid が変更された == POT から削除されつつ似たようなものが追加された ものについては、
候補になりうるものを PO から削除せず、候補としてマークする (Fuzzy)。
Fuzzy状態のものは無効であり、 gettext() の対象にはならない。
Fuzzyのコメントを削除すれば対象となるが、専用のエディタを使った方がわかりやすく、安全である。
gettext関数
変数の埋め込み
「こんにちは、xxさん」のように、変数を埋め込みたい場合がある。
これは sprintf() を使うことで対応できる。
ソース
echo sprintf(_('こんにちは %s さん'), '則巻アラレ'), "\n";
PO
msgid "こんにちは %s さん" msgstr "Hello, %s"
実行結果
$ php test.php
こんにちは 則巻アラレ さん
$ LC_MESSAGES=en_US php test.php
Hello, 則巻アラレ
順序の入れ替え
日付の y-m-d
と d-m-y
、名前の lastName firstName
と firstName lastName
など、
言語によってメッセージ中に変数の表示順が変わるケースがある。
これはこのように対応する。
ソース
$dayMsg = _('今日は %1$s年 %2$s月 %3$s日 です'); echo sprintf($dayMsg, '2015', '04', '15'), "\n";
PO
msgid "今日は %1$s年 %2$s月 %3$s日 です" msgstr "%3$s-%2$s-%1$s"
実行結果
$ php test.php 今日は 2015年 04月 15日 です $ LC_MESSAGES=en_US php test.php 15-04-2015
複数形
言語によって、単数形・複数形で表現方法が違う場合がある。
意識しなくてよい例が日本語、意識する必要がある例が英語。
メッセージIDがどちらの言語かによって書きかたが違う。
日本語→英語の場合は以下のようになる。
ソース
echo sprintf(ngettext('%d 件のユーザがみつかりました', '%d 件のユーザがみつかりました', 1), 1), "\n"; echo sprintf(ngettext('%d 件のユーザがみつかりました', '%d 件のユーザがみつかりました', 2), 2), "\n";
PO
"Plural-Forms: nplurals=2; plural=n != 1;\n" msgid "%d 件のユーザがみつかりました" msgid_plural "%d 件のユーザがみつかりました" msgstr[0] "One user found." msgstr[1] "%d users found."
実行結果
$ php test.php 1 件のユーザがみつかりました 2 件のユーザがみつかりました $ LC_MESSAGES=en_US php test.php One user found. 2 users found.
英語→日本語の場合は、POはこのようになる。
"Plural-Forms: nplurals=1; plural=0;\n" msgid "One user found." msgid_plural "%d users found" msgstr[0] "%s 件のユーザがみつかりました"
まとめると、
ngettext(単数時メッセージID, 複数時メッセージID, 数)
をソースに書くPlural-Forms
で、その言語の単数/複数ルールを定義。
日本語のような区別のない言語であればnplurals=1; plural=0;
英語のような区別のある言語であればnplurals=2; plural=n != 1;
3個以上の場合などより複雑な場合もここにルールを定義して対応するmsgid
が単数時メッセージID、msgid_plural
が複数時メッセージIDmsgstr[0]
が単数時の対訳、msgstr[1]
が複数時の対訳
ドメイン分け
PO を用途ごとに複数ファイルに分けることができる。
これらのPOファイルの名前を ドメイン と呼ぶ。
dgettext($domain, $msgid)
で、ドメインを指定してメッセージを取得できる。
文脈
文字列としては同じでも、文脈により意味が異なるケースがある。
たとえば Post
は記事そのものでもあるし、投稿する行為でもある。
これらを別物として扱う必要がある場合は Post
をそのまま msgid にすることができない。
追加の文字列を与えたものを msgid として定義、gettext() 後に外すのがよい。
echo preg_replace('/__Noun\z/u', '', _('Post__Noun'));
msgid は Post__Noun
、それに対応する翻訳がなければ Post
が出力される。