Catalyst+Template-Toolkit(Catalyst::View::TT)で国際化
はてぶコメントより、typesterさん曰くCatalyst::Plugin::I18N - I18N for Catalyst - metacpan.orgでできるよとのこと。お〜。名前は昔見たことがあったのですが中身まで見てませんでした。今回の話(と次に書こうと思っていた話)のほとんどがCatalyst::Plugin::I18Nに含まれていますね。Template周りをちょこっといじるだけで良さそうです。コメント本当にありがとうございました。助かります。
このエントリは「Locale::Maketextの使い方」ということでお願いします。
はじめに
同じ内容のページを、ユーザの環境に応じて英語版と日本語版を切り替えたい。という話です。
よくある「使用するテンプレートを言語毎に用意する」という方法では、英語版テンプレートと日本語版テンプレートの同期を取るのが非常に面倒になります。そこで「テンプレートは英語版を一つだけ用意して、実行時に内容を書き換える」という方法でやってみます。
GNUな環境ではgettextを使うのがデファクトスタンダードです。gettextを一言で説明すると「言語毎のメッセージファイルを用いて、例えば"Hello"という文字列リテラルを"こんにちわ"に実行時に変換するライブラリ」ということになります。(gettextについてはhttp://home.catv.ne.jp/pp/ginoue/memo/gettext.html辺りを参考に)
Perlのコアモジュールには、gettextと同じようなことを行うLocale::Maketextが含まれているのでそれを使います。
Locale::Maketext
まずLocale::Maketextを準備します。
lib/MyApp/L10N.pm
package MyApp::L10N; use base qw(Locale::Maketext); 1;
これはパッケージを定義してMyApp専用の名前空間を用意しているだけです。Locale::Maketextは「my $handle = MyApp::L10N->get_handle('ja');」のようにハンドルを取得して使うのですが、この場合「MyApp::L10N::ja」が使用されることになります。
"L10N"の部分は何でもいいのですが、変更した場合は以下のメッセージファイルのパッケージ名も併せて変更してください。
lib/MyApp/L10N/ja.pm
これがメッセージファイル(gettextでのpoファイル)になります。
package MyApp::L10N::ja; use base qw(MyApp::L10N); our %Lexicon = ( '_AUTO' => 1, 'Hello' => 'こんにちわ', );
この%Lexiconに翻訳文字列を定義していきます。
「'_AUTO' => 1」は、翻訳文字列が見つからなかった場合にエラーとならないようにするためのものです。
もし上の例で_AUTOが偽の場合、
フレーズ | 結果 |
Hello | こんにちわ |
World | × |
Worldが定義されていないので「maketext doesn't know how to say: World」というメッセージでcroakしてしまいます。
_AUTOが真の場合、
フレーズ | 結果 |
Hello | こんにちわ |
World | World |
と、翻訳こそされないもののエラーにはなりません。
「全て翻訳済みでなければならない」という要件がある場合は、_AUTOを偽にしておいて未翻訳のものがあったらcroakするようにしておくと良いでしょう。普通は_AUTO=>1で良いと思います。
lib/MyApp/L10N/en.pm
package MyApp::L10N::en; use base qw(MyApp::L10N); our %Lexicon = ( '_AUTO' => 1, );
Locale::Maketextにはfallbackの仕組みが備わっていて、ハンドルを取得する際に指定した言語が見つからない場合、i-default(i_default.pm), en(en.pm), en-US(en_US.pm)がこの順番で試行されます。
今回は英語版テンプレートを基本とするため、フレーズは全て英語となります。なので_AUTOを真にしておくだけで英語版の翻訳(?)は完了です。
Locale::Maketextの準備は完了
以上でLocale::Maketextの準備は完了です。この段階で
use MyApp::L10N; my $language = 'ja'; my $lh = MyApp::L10N->get_handle($language) or die; print $lh->maketext('Hello');
とjaを指定すると「こんにちわ」が出力され、
use MyApp::L10N; my $language = 'en'; my $lh = MyApp::L10N->get_handle($language) or die; print $lh->maketext('Hello');
とenを指定すると「Hello」が出力されるようになっています。
Catalyst+Template-Toolkitに適用する
テンプレートをこのように書いてみます。
Hello <br> _(Hello)
gettextでは、慣例で翻訳すべき文字列リテラルを_(...)と囲むようになっていますのでそれに合わせています。この目印の付け方はLocale::Maketextの管轄ではないので好きな形式にできます。<span lang="x-auto">...</span>みたいにしても構いません。(ちなみにちなむと"x-"はlang属性の私的利用です。RFC3066、RFC3282を参照)
lib/MyApp/View/TT.pm
あとはCatalyst::View::TT#processを上書き(MyApp::View::TTで再定義)すればOKです。
package MyApp::View::TT; use base qw(Catalyst::View::TT); use MyApp::L10N; sub process { my ( $self, $c ) = @_; $self->SUPER::process($c); my @languages = split /,/msx, $c->req->header('Accept-Language'); foreach my $language (@languages) { $language =~ s/;.*//msx; } my $lh = MyApp::L10N->get_handle(@languages) or die; my $output = $c->res->body; $output =~ s{_\((.+?)\)}{$lh->maketext($1)}egmsx; $c->res->body($output); return; } 1;
ここでは、通常のCatalyst::View::TT#processを行なった後に、Locale::Maketextを使ってbodyを書き換えています。言語の指定にはAccept-Languageヘッダを使用しています。
実行
この状態で実行すると
英語環境 | 日本語環境 |
---|---|
Hello Hello |
Hello こんにちわ |
と表示されます。
Firefoxでは「ツール」→「オプション」→「詳細」→「言語設定」で「日本語 [ja]」を削除したり追加したりすると確認できると思います。
おわりに
ところで、Accept-Languageは優先度の高い順番に記述されているとは限りません。優先順位がenよりjaの方が高い場合でも、Accept-Languageに現れる順番がen,jaとなっていることもあります。これを解決するにはHTTP::Negotiateを使う方法があります。それはまた別エントリで書きます。
また、今回の例では「翻訳すべき文字列を抽出してlib/MyApp/L10N/ja.pmに追加する」という作業が面倒です。まあそれ用のスクリプトを書いてもいいのですが、翻訳を変更する度にpmファイルを書き換えるというのもナンセンスです。gettext用のメッセージファイルをそのままLocale::Maketextで使用するLocale::Maketext::Gettext - Joins the gettext and Maketext frameworks - metacpan.orgというモジュールを使えば、gettextのmoファイルを読み込めるようになりpmファイルの更新が不要になります。またxgettext、msgmergeなどのgettext周りのツールが使えるようになります。msgmergeは単純な更新ではなく差分更新ができ、使用されなくなった文字列をマーキングしたりもしてくれます。
「xgettextでpotファイルを更新」→「msgmergeでpoファイルを差分更新」→「誰かに翻訳させる」→「msgfmtでmoファイルを生成」という流れが最強です。翻訳以外はMakefileで自動的に実行されるようにしておけば完璧です。Locale::Maketext::Gettextに関してもまた別エントリで書きます。