Rubyで多言語化を行なうにはMutohさんのRuby-GetText-Packageが便利そうです。
通常の po → mo の変換を行なえばメッセージを m17n できるところが良いので、作業の内容は一般的なものです。
単純な置換なら手でも作れますが、将来的に複数の言語に対応するなら、自分でロジックを組む手間が削減できます。
処理速度が速くなるかどうかは微妙で、ステータスをThread毎に格納して、全体のメッセージ文はRuntimeでキャッシュされるので手作業で置換ロジックを書くよりも多少はスピードアップが期待できそうです。ただメモリを消費してもよいならシンプルに正規表現のリストと置換する文字列の対を作った方が早いんじゃなかろうかと想像しています。
ERBだと<%= _("message") %>のように埋め込む手間がかかり、可読性は多少低下するデメリットもありそうです。
それでもGetTextモジュールを使うことで作業手順が標準化できるので、置換対象の文字列を管理する後々のメンテナンスを考えると総合的に利点が上回ると思います。
GetText自体はcgiクラスとの組み合せを想定しているので、FastCGIについてはドキュメントがなさそうでした。 そのためGetTextとFastCGIについてまとめておきます。
開発環境について
いつものようにUbuntu 10.04 LTS x86_64版を使用しています。
- OS: Ubuntu 10.04 LTS x86_64
- Ruby 1.9.2-p136 (/usr/local/bin/ruby) + gettext + fcgi
- Apache 2.2.14 + libapache2-mod-fcgid
FastCGIとCGIの違い
FastCGIは標準添付ライブラリにはないため、fcgiモジュールをgemなどでインストールする必要があります。
CGIとの違いは起動済みのプロセスにWebブラウザからアクセスする仕組みなので、プロセスの起動にかかる時間が短縮される分、メモリを常に使いますが処理スピードは速いです。
またプロセス自体は終了せずに(Rubyの場合)スレッドが各ブラウザからのリクエストを処理するため、インスタンス変数やクラス変数を適切に使うことで、キャッシュの効果により全体的なレスポンスを向上させる事もできます。
その反面、処理する単位はオブジェクト単位にしてシンプルに切り分けないと、キャッシュしたくない内容が残ったりしてプライバシー上の問題を引き起す可能性もあります。
一般的なWebコンテンツのホスティングサービスでは、FastCGIのようにプロセスが常駐するとメモリを消費するため、必要に応じてリソースを消費するCGIやPHPが一般的です。
FastCGIもCGIもWebの初期からある仕組みなので古典的に扱われますが、Web以前から起動済みのプロセスにオンライントランザクションを処理させる仕組みは、決済やら問い合せ処理やらを高速に処理する仕組みとしてよく知られていました。
起動時間とその起動処理を無視できることは、アクセスが増えていく中で安定的なレスポンスを得るための重要な要件の一つです。
PHPはApacheのモジュールとしてサーバのプロセスイメージの中で処理が完結するため速いはずですが、基本的にはあらかじめ処理内容をキャッシュするわけではないので、時間は短縮できますが、接続毎にいろいろな初期化処理が必要となる点が独特です。
さて「FastCGIは古くない!」という主張は十分したので、GetTextとの組み合せについてまとめます。
FastCGIとGetTextの組み合せ
GetText付属のサンプルをみると、CGIモジュールに対応したスクリプトがありますが、そのままではFastCGIに適用できません。
その理由はFastCGIのスレッドをプールして使い回すという性質にあります。
違いを説明する前に雛型になりそうなスクリプトは次のようになります。
#!/usr/local/bin/ruby
# -*- coding: utf-8 -*-
$:.unshift "/app/lib"
ENV['GEM_HOME'] = "/app/gems"
require 'rubygems'
require 'fcgi'
require 'cgi'
require 'erb'
require 'gettext/cgi'
require 'gettext/tools/parser/erb'
## Render
class SimpleRender
def initialize(query, env)
@query = query
@env = env
Locale::clear
Locale::set_request([query["lang"]], [], env["HTTP_ACCEPT_LANGUAGE"], env["HTTP_ACCEPT_CHARSET"])
GetText::set_output_charset("UTF-8")
GetText::bindtextdomain("sample", "/app/data/locale")
end
def render
ret = ""
content = "/app/sample.erb"
ret += ERB.new(open(content, "r:utf-8").read).result(binding)
return ret
end
def _(msgid)
GetText::_(msgid)
end
end
## Controller
class Main
def initialize(request)
@env = request.env
@query = check_query_string(CGI.parse(@env['QUERY_STRING']))
end
def run
ret = "Content-Type: text/html; charset=UTF-8\r\n\r\n"
case @query["mode"]
when "search"
sr = SimpleRender.new(@query, @env)
ret += sr.render()
end
return ret
end
private
def check_query_string(q)
ret = {}
label = "mode"
ret[label] = (q.has_key?(label) and not q[label][0].empty?) ? q[label][0] : "search"
label = "lang"
ret[label] = (q.has_key?(label) and not q[label][0].empty?) ? q[label][0] : "ja"
return ret
end
end
###############
## main loop ##
###############
FCGI.each {|request|
main = Main.new(request)
request.out.print main.run
request.finish
}
スクリプトが参照している外部ファイルについて
gemsを使うかどうかは問題ではないのですが、requireしているモジュール・ライブラリにアクセスできるように、先頭の6行は適切なパスに書き直す必要があります。
/app/sample.erbはコンテンツ本体で、次のような内容になっています。
<html>
<body>
<h1><%= _("Hello World") %></h1>
</body>
</html>
GetTextモジュールの処理の中心となる翻訳データを保存するためには /app/data/localeディレクトリを作成しています。 作成方法はmoファイルを生成するところで行なっています。
結果として、ライブラリを除いた/appディレクトリの構造は次のようになっています。
$ find . -type f ./po/en/sample.po ./po/ja/sample.po ./po/sample.pot ./Rakefile ./data/locale/en/LC_MESSAGES/sample.mo ./data/locale/ja/LC_MESSAGES/sample.mo ./sample.erb
解説:Thread毎のキャッシュの破棄
LocaleはThread.current[:current_request]に必要なデータ構造をキャッシュします。
スレッドが消滅しないかもしれないので、接続毎に処理が終った段階でデータを破棄する必要があります。
「処理が終った段階」を徹底するために、処理を始める前にデータを破棄しています。 このLocale::clearはnilを代入するだけの処理なので、スレッドが立ち上がった最初に実行しても問題ない内容になっています。
Locale::clear
Locale::set_request([query["lang"]], [], env["HTTP_ACCEPT_LANGUAGE"], env["HTTP_ACCEPT_CHARSET"])
GetText::set_output_charset("UTF-8")
GetText::bindtextdomain("sample", "/app/data/locale")
解説:クライアントの言語情報の登録
前項にあるコードの2行目 Locale::set_request()メソッド が、クライアントが利用する言語を登録しているところです。
CGIモジュールを使うサンプルでは、Locale::set_cgi()を使用していますが、今回はCGIオブジェクトは使えないため直接その内部で使用している Locale::set_requestメソッド を呼び出しています。
第一引数に指定した言語から優先度が高く、第二引数にはCookieに保存した言語情報を格納する想定ですが、cookieを使っていないので空にしています。
GetText関連ファイル(po,moファイル)の作成
今回の例では、moファイルにアクセスするために、/app/data/localeを指定しています。
必要なファイルを作成するために、まず次のような内容の/app/Rakefileファイルを作成しました。 これはMutohさんのページの説明ほぼそのままです。
$:.unshift "/app/lib"
desc "Update pot/po files."
task :updatepo do
require 'gettext/tools'
GetText.update_pofiles("sample", Dir.glob("*.erb"), "sample 1.0.0")
end
desc "Create mo-files"
task :makemo do
require 'gettext/tools'
GetText.create_mofiles
end
先頭の $:.unshift は、FastCGIスクリプトと同じで、GetTextモジュール(gettext.rb)へのパスです。
デフォルトのパスにインストールしていれば不要ですが、今回は特別な場所にモジュールを配置しているので追加しています。
ファイルを配置した後は、poファイルを作成します。
$ cd /app $ rake updatepo
"/app/po/sample.pot"が作成されるので、それを言語毎のディレクトリにコピーをして翻訳を行ないます。
$ cd /app $ mkdir po/ja po/en $ cp po/sample.pot po/ja/sample.po $ cp po/sample.pot po/en/sample.po
2つのsample.poの翻訳が終ったら moファイル を作成します。
$ rake makemo
これが無事に終ると /app/data/locale/ja/LC_MESSAGES/sample.moと/app/data/locale/en/LC_MESSAGES/sample.mo のファイルが作成されているはずです。
# Copyright (C) 2011 Yasuhiro ABE yasu@yasundial.org
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: sample 1.0.0\n"
"POT-Creation-Date: 2011-03-24 09:25+0900\n"
"PO-Revision-Date: 2011-03-24 09:25+0900\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"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
#: sample.erb:3
msgid "Hello World"
msgstr "こんにちは"
FastCGIスクリプトの実行
Apacheを適切に設定して、Options ExecCGIが設定された場所に配置してWebブラウザから呼び出します。
URLの最後に ?lang=jaや?lang=enをつけて呼び出して内容が変化することを確認します。
GetTextモジュールを使っていて気がついたこと
rmsgmergeコマンドが使えない
たぶん一斉置換か何かの影響か、gettext/tools/rmsgmerge.rb の中がおかしくなっています。
# refpot = parser.parse_file(config.refstrrefstr, PoData.new, false)
refpot = parser.parse_file(config.refpot, PoData.new, false)
これで終りかと思いきや、-hで表示される第一引数と第二引数は逆にしないといけない模様です。
いまのところRakefileの方が便利そうなので困ってはいませんが、コマンドを使って更新をpoファイルに反映したい場合には注意が必要です。
困った事にdebパッケージでruby 1.8.x用に入っているrmsgmergeも動きません。
rgettextコマンド内部で参照しているrgettext.rbへのパスがおかしい
require 'gettext/rgettext'
をrequire 'gettext/tools/rgettext'
に変更しました。
rgettextコマンド内部にあるrubyコマンドへのパスを変更する
rmsgmergeでも同じでしたが、コマンドの先頭にある#! /usr/bin/ruby
のパスを#!/usr/local/bin/ruby
に変更しました。
$:変数にライブラリディレクトリを加える
またRuby 1.9.2では$:はカレントディレクトリを指さないので、gettext.rbのあるディレクトリへのパスを$:に登録しています。
#! /usr/local/bin/ruby
# -*- coding: utf-8 -*-
=begin
rgettext - ruby version of xgettext
Copyright (C) 2005-2009 Masao Mutoh
You may redistribute it and/or modify it under the same
license terms as Ruby.
=end
$:.unshift File::join([File::dirname($0),"..","lib"])
begin
require 'gettext/tools/rgettext'
rescue LoadError
begin
require 'rubygems'
require 'gettext/tools/rgettext'
rescue LoadError
raise 'Ruby-GetText-Package are not installed.'
end
end
GetText.rgettext
ドキュメントが微妙に古くて通用しない記述もいろいろありましたが、ちゃんと動いています。
m17n のためには、gettextの標準的な作法が使えるというのは強力だと思います。
0 件のコメント:
コメントを投稿