Perl でつくった web サイトを L10N する方法について簡単に説明します。今回は、日本語のサイトを英語でも表示できるようにするケースをあつかいますよ。今回は L10N の対象は Amon2 をつかったサイトとします。
基本的な翻訳機能は Locale::Maketext::Lexicon を利用します。これはなんだかんだで出来がいいのでいいとおもいます。他にもいろいろあるけど、これが一番実績もあるし安定しているようにおもいます。また、一時期はメンテが放棄されてましたが、最近またメンテされるようになったようです。
では、まず、Perl コード中の日本語のリソースを $c->loc(); でくくりましょう。
printf("ほげ\n");
みたいになってるところを
printf($c->loc("ほげ\n"));
みたいにするってことです。
つぎにテンプレートファイルの中の日本語リソースを
[% l('ふが') %]
のようにくくってください。もちろん TT 前提ですね。わかります。
こんなかんじでスクリプトをかいて、メッセージをとりだして po ファイルをつくります。もともとついてくるスクリプトもあるんですが、オプションの指定がやたら複雑になってしまいがちなので、結局素直にライブラリとして利用して自分でスクリプトをかいた方がメンテナンス、柔軟性の点ですぐれていると僕は判断しました。
#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use Locale::Maketext::Extract;
use File::Find::Rule;
my $Ext = Locale::Maketext::Extract->new(
# Specify which parser plugins to use
plugins => {
# Use Perl parser, process files with extension .pl .pm .cgi
perl => [qw/pl pm js/],
# Use TT2 parser, process files with extension .tt2 .tt .html
# or which match the regex
tt2 => [
'tx',
],
},
# Warn if a parser can't process a file
warnings => 1,
# List processed files
verbose => 1,
);
for my $lang (qw/en/) {
$Ext->read_po("po/$lang.po") if -f "po/$lang.po";
$Ext->extract_file($_) for File::Find::Rule->file()->name('*.pm')->in('lib');
$Ext->extract_file($_) for File::Find::Rule->file()->name('*.tx')->in('tmpl/PC/');
$Ext->extract_file($_) for File::Find::Rule->file()->name('*.js')->in('htdocs/static/js/');
# Set $entries_are_in_gettext_format if the .pl files above use
# loc('%1') instead of loc('[_1]')
$Ext->compile(1);
$Ext->write_po("po/$lang.po");
}
以下のようにして Language リソースを管理するクラスを定義します。
package MyApp::L10N;
use strict;
use warnings;
use utf8;
use parent 'Locale::Maketext';
use File::Spec;
use Locale::Maketext::Lexicon +{
en => [ Gettext => File::Spec->catdir( MyApp->base_dir(), 'po', 'en.po') ],
ja => [ 'Auto' ], # ソース言語はそのままだす
_preload => 1,
_auto => $ENV{DEBUG_L10N} ? 0 : 1,
_style => 'gettext',
_decode => 1, # decode characters to utf8 flagged.
};
1;
次に、$c->loc() のようにしてこれをよびだせるようにしましょう。
package MyApp::Web;
use MyApp::L10N;
sub loc { shift->l10n->maketext(@_) }
__PACKAGE__->add_trigger(
BEFORE_DISPATCH => sub {
my $c = shift;
if (my $lang = $c->req->param('lang')) {
return $c->show_error("Unknown language: $lang") if $lang !~ /^(?:en|ja)$/;
$c->session->set(lang => $lang);
}
return undef;
},
);
{
my %langs = (
ja => MyApp::L10N->get_handle('ja'),
en => MyApp::L10N->get_handle('en'),
);
sub lang {
my ($c) = @_;
return 'en' if ($c->session->get('lang') || 'ja') eq 'en';
return 'en' if ($c->req->param('lang') || 'ja') eq 'en';
return 'en' if ($c->req->header('Accept-Language')||'ja') !~ /ja/; # このへん適当なので本当はちゃんと判定したほうがいいですね!
return 'ja';
}
sub l10n {
my ($c) = @_;
return $langs{$c->lang};
}
}
つぎに $c->loc(); を Text::Xslate につなぎこみます。
use Text::Xslate qw/mark_raw html_escape/;
Text::Xslate->new(function => {
l => sub {
my $base = shift;
my @args = map { html_escape $_ } @_; # escape arguments
mark_raw(Amon2->context->loc($base, @args));
},
%others
}, %args)
はい、これで完成ですね。
せっかくの翻訳リソースを js からもつかいたいという要望もあるでしょう。その場合、js 用の gettext ライブラリをつかってもよいのですが、たいがい無駄に大仰です。そんなにこったことをしなくてよいのであれば、以下のようなスクリプトで js をはけば十分でしょう。メンテナンス的なことをかんがえると json を別ファイルにしてもいいかもしれません。po2json みたいなスクリプトもさがせばあるんですが、さがすのめんどくさいので手でかいたほうがはやいでしょう。
#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use 5.10.1;
use autodie;
use JSON;
use Text::MicroTemplate qw/:all/;
use IO::Handle;
use Locale::Maketext::Lexicon::Gettext;
open my $ifh, '<:utf8', 'po/en.po';
my $data = Locale::Maketext::Lexicon::Gettext->parse(<$ifh>);
my $mt = Text::MicroTemplate->new(escape_func => undef, template => <<'...');
? my $x = shift;
(function () {
var en_data = <?= $x ?>;
var gt = new Object;
gt.gettext = function () {
var base;
if (this.lang == 'en') {
base = en_data[arguments[0]];
} else {
base = arguments[0]; // original lang
}
if (!base) {
base = arguments[0];
}
return base.replace(/\[_([0-9])\]/g, function (full, number) { return arguments[parseInt(number)]; });
};
gt.lang = 'en';
window.Gettext = gt;
})();
function _() { return window.Gettext.gettext.apply(window.Gettext, arguments); }
...
my $tmpl = eval $mt->code();
open my $ofh, '>', 'htdocs/static/js/gettext.js';
$ofh->print($tmpl->(encode_json($data)));
$ofh->close();