tokuhirom's Blog

Perl でつくった web サイトを L10N する方法

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");
}

Perl から 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 からもつかいたいという要望もあるでしょう。その場合、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();