言語ネゴシエーション のバックアップの現在との差分(No.6)
更新- バックアップ一覧
- 差分 を表示
- ソース を表示
- バックアップ を表示
- ソフトウェア/rails/言語ネゴシエーション へ行く。
- 追加された行はこの色です。
- 削除された行はこの色です。
SIZE(22){COLOR(RED){このページの内容は非常に古いです(Rails 1.x.x)。最新の Rails では洗練された国際化の機構が標準で入っているため、下記は読むだけ無駄な内容になっています。}} ~ >>>> [[Rails4 向けの内容はこちら>ソフトウェア/rails/国際化]] ~ ---- [[公開メモ]] #contents * Rails で言語ネゴシエーション(Language Negotiation) [#h05f99dd] http://blog.omdb-beta.org/ を見ながら、ブラウザの Language Negotiation の機能を使って rails アプリケーションの多言語化する方法を調べてみた。 rails のバージョンが上がっているせいと、それ以外の問題とで、結構苦労したのでメモ。 元々の情報が古いもので、結局かなりの部分を1から書いたので、 ここにメモしておく。 やりたい内容としては + views/*/*.html.ja.erb と views/*/*.html.en.erb とを両方作っておき、 閲覧者の環境設定、手動選択に合わせて自動的に切り替えて送信する ** やりたいこと [#w57d45b9] + views/*/*.html.ja.erb とか views/*/*.html.en.erb とか、言語ごとに テンプレートファイルを作っておき、閲覧者の環境設定、手動選択に合わせて 自動的に言語別テンプレートを切り替えて使う + キャッシュされたページも apache のネゴシエーション機能を使って 自動で切り替わるようにする 自動で言語を切り替えて送信できるようにする + エラーメッセージなど、テンプレートファイルの切換で対応しきれない部分は I18n.t を使って多言語化できるようにする ** ブラウザ設定における表示言語の優先順位 [#w3484f9d] の3点。 1. は、ブラウザの言語設定で日本語と英語のどちらを優先しているかを判別して、 優先順位の高い方で表示するという話。 rails アプリケーションには、通常通り ブラウザはこの言語設定を元に、Accept-Language: というヘッダ情報をサーバーに 送るので、rails 側でこのヘッダ情報から表示言語を選べばよい。 http://rails.server/controller~ http://rails.server/controller/action~ http://rails.server/controller/action/id~ http://rails.server/controller/action/id.html~ ** キャッシュに対する apache2 の言語ネゴシエーション [#w6b05fcf] などの他、 2. について。 http://rails.server/controller.ja~ http://rails.server/controller/action.ja~ http://rails.server/controller/action/id.ja~ http://rails.server/controller/action/id.html.ja~ rails のページキャッシュ機能は、表示内容を .html ファイルとして保存する事で、 次回からは rails を通さず、apache で直接処理させるというものなので、 適切な名前でキャッシュファイルを作成する事と、apache2 にキャッシュファイルを 正しく認識させる事が必要になる。 のように言語を指定した形でもアクセス可能とする。 ** その他の多言語化情報 [#f2bd1cf6] 言語指定の無いアドレスでアクセスされたときには ブラウザとのネゴシエーションによって使うテンプレートを決める。 rails アプリの多言語化については他にも、 ** 言語の優先順位 [#w3484f9d] -- ネゴシエーションっぽいもの - [[ロケールによるテンプレート切り替え>http://www.yotabanana.com/lab/?date=20060224#p01]] - ネゴシエーションではなく Ruby-GetText を使った方法やネゴシエーションとの比較 -- [[Railsで日本語を使う時に必須のパッケージ Ruby-GetText>http://blog.masuidrive.jp/articles/2006/07/03/gettext]] -- [[Ruby on RailsでRuby-GetText-Packageを使う>http://www.yotabanana.com/hiki/ja/ruby-gettext-howto-ror.html]] -- [[ruby-gettext>http://tech.feedforce.jp/ruby-gettext.html]] -- [[Rails で行こう! - Ruby on Rails を学ぶ>http://d.hatena.ne.jp/elm200/20070719/1184856941]] -- [[Rails のためのものぐさな Web アプリケーションの国際化手法>http://d.hatena.ne.jp/secondlife/20070207/1170835130]] - rails エラーメッセージの日本語化 -- [[RubyOnRails を使ってみる 【第 5 回】 ActiveHeart>http://jp.rubyist.net/magazine/?0012-RubyOnRails]] -- /usr/share/rails/actionpack/lib/action_view/locale/xx.yml とか 使用する言語の優先順位を決めるための情報源としては、 などがあるようだけれど、よく調べていない。 + ユーザーが手動で選んだ言語を使う prams[:rails_language] + ユーザーが以前に手動で選んだ言語を使う cookie[:rails_language] + ブラウザの言語設定にある優先順位を使う header['Accept-Language'] + アプリケーションのデフォルトの優先順位を使う ENV['RAILS_ACCEPTABLE_LANGUAGES'] 以下は試行錯誤の経過とその成果物 ** キャッシュに対する apache2 の言語ネゴシエーション [#w6b05fcf] * negotiation という名前のアプリケーションの作成 [#vb4e8dd6] rails のページキャッシュ機能は、表示内容を .html ファイルとして保存する事で、 次回からは rails を通さず、apache で直接処理させるというもの。 テスト用に negotiation という名前のアプリケーションを作成。 rails を通らないので、行うこととしては、~ 適切な名前でキャッシュファイルを作成する事と、~ apache2 にキャッシュファイルを正しく認識させる事と、~ の2つになる。 LANG:console $ rails negotiation $ cd negotiation ** その他の多言語化方法 [#f2bd1cf6] * 開発用サーバーの起動と環境の確認 [#s35cbbb4] rails アプリの多言語化については、これまで Ruby-GetText, I18n, ActiveHeart など、 開発者ごとに異なる方法で行われてきた。 LANG:console $ script/server & $ wget -qO- http://localhost:3000/ | html2text ... ****** Welcome aboard ****** ***** You’re riding Ruby on Rails! ***** **** About_your_application’s_environment **** ... $ $ ls db/* ls: cannot access db/*: そのようなファイルやディレクトリはありません $ wget -qO- http://localhost:3000/rails/info/properties | html2text Ruby version 1.8.7 (i486-linux) RubyGems version 1.3.2 Rails version 2.2.2 Active Record version 2.2.2 Action Pack version 2.2.2 Active Resource version 2.2.2 Action Mailer version 2.2.2 Active Support version 2.2.2 Edge Rails revision unknown Application root /home/samba/www/rails/negotiation Environment development Database adapter sqlite3 Database schema version 0 $ ls db/* db/development.sqlite3 そんな中、rails 2.2 から、rails 本体の推奨する多言語化手法が I18n に決まったようなので、 今後は I18n を用いるアプリケーションが増えると考えられる。 このとき自動的に sqlite3 のデータベースが作成される。 多言語化の方法には大きく分けて次の2つがある。 + 言語ごとに別のテンプレートファイルを用いる方法 + テンプレートファイルは共通にして、中で現れる文言を1つずつその都度翻訳する * test コントローラとビューの作成 [#k0a5e25a] Ruby-GetText はこの両方の機能を持っているが、~ I18n は 2. のみを行うもの。 LANG:console $ script/generate controller Test exists app/controllers/ exists app/helpers/ create app/views/test exists test/functional/ create app/controllers/test_controller.rb create test/functional/test_controller_test.rb create app/helpers/test_helper.rb $ echo "Test" > app/views/test/index.html.erb $ (restart_server) $ wget -qO- http://localhost:3000/test Test 1. は 2. に比べて「国際化対応というのは異なる文化慣習への対応でもあるから、 単なる文言の修正では済まないこともある。テンプレートごと差し替えてしまえば そういうケースでも対応が楽。」という利点があると言われる。 * 多言語化したビューの作成 [#b2db2fc3] 逆に 1. は 2. に比べて、アプリケーションの手直しでテンプレートに変更が 必要になったとき、アプリケーション開発者が翻訳済みのテンプレートに手を 入れられずに、テンプレートがメンテナンス不可能になる事態が生じる。 今作ったビューを削除して、日本語版と英語版を作成。 というような欠点もあると言われている。[[参照>http://www.yotabanana.com/lab/?date=20060224#p01]] まだ rails に手を入れていないので、そのままではエラーになる。 開発者が翻訳者を兼ねている、~ ページキャッシュを行う、~ あたりを考慮すると 2. も良い物だと思うので、実装したのがこの記事。 LANG:console $ rm app/views/test/index.html.erb $ echo "Test" > app/views/test/index.html.en.erb $ echo "テスト" > app/views/test/index.html.ja.erb $ wget -O- http://localhost:3000/test エラー 404: Not Found * 試行錯誤の経過 [#e232eb64] これを正しく表示できるようにするのが目標なのだが、 その前に rails に言語ファイル選択の優先順位を教える必要がある。 + [[開発&テスト用環境の構築>ソフトウェア/rails/言語ネゴシエーション/開発用&テスト用環境の構築]] -- negotiation という名前のアプリケーションの作成 -- 開発用サーバーの起動と環境の確認 -- test コントローラとビューの作成 + [[routesの設定と言語選択の優先順位>ソフトウェア/rails/言語ネゴシエーション/routesの設定と言語選択の優先順位]] -- 多言語化したビューの作成 -- config/routes.rb の設定 -- 言語が指定されていない時を含めた言語選択の優先順位]] + [[言語別テンプレートの選択>ソフトウェア/rails/言語ネゴシエーション/言語別テンプレートの選択]] -- 言語の優先順位を元に利用するテンプレートを選択する]] --- memoize について -- テンプレートファイルの拡張子を正しく認識させる + [[キャッシュの多言語対応>ソフトウェア/rails/言語ネゴシエーション/キャッシュの多言語対応]] -- ページキャッシュの多言語対応 -- ページキャッシュの多言語化に対応させるための apache の設定 -- アクションキャッシュの多言語対応 -- フラグメントキャッシュの多言語対応 + [[プラグイン化と公開準備>ソフトウェア/rails/言語ネゴシエーション/プラグイン化と公開準備]] -- プラグイン化 -- テストケース -- fcgi 化 + [[github を使った公開>ソフトウェア/rails/言語ネゴシエーション/github を使った公開]] -- git リポジトリ(ローカル)の作成 -- プロジェクトを github に登録 -- git を使った編集作業例 * config/routes.rb の設定 [#w47da434] * プラグインのダウンロード [#l2dd967e] 多言語化後は、各ページに対して github からどうぞ。 http://localhost:3000/test http://localhost:3000/test.ja http://localhost:3000/test/index http://localhost:3000/test/index.ja http://localhost:3000/test/index/0 http://localhost:3000/test/index/0.ja http://localhost:3000/test/index/0.html http://localhost:3000/test/index/0.html.ja http://github.com/osamutake/RailsLanguageNegotiationPlugin/tree/master などの形でアクセスできるようにする。 * プラグインの使い方 [#sb57c7b2] - .ja などの言語指定がない場合には、ブラウザが指定する言語で表示する - .ja などの言語指定があれば、その言語で表示する - 一旦言語指定を行ったら、以降は言語指定がない場合にも直前に行った言語指定と 同じ言語で表示する - 表示したい言語のテンプレートファイルが無ければ、ある物を使って表示する 受け入れ可能な言語のリストを ActionController::AbstractRequest に持たせることにする。 ActionController::AbstractRequest.acceptable_languages= :ja, :en * TODO [#ye7b917c] これ以外の言語が指定されても無視される。 ** ページキャッシュ上での言語切換(Cookie 書き換え) [#o28ad6fc] route の設定は、 http://rails.server/controller.ja~ などの、言語指定の付いたアドレスで参照してページキャッシュが返される場合、 Cookie の書き換えが起きないので、表示言語の切換ができない。 map.connect ':controller/:action/:id.:rails_language', :requirements => { :rails_language => /ja|en/ } ページに JavaScript を埋め込んで、location.href に言語指定が含まれている時に Cookie を書き換える事で対応できるはず。 のようにする。/^(ja|en)$/ ではなく /ja|en/ とするのが正しいらしい。 <%= link_to "link text", { :rails_language=>:ja }, { :on_click="document.cookie='rails_language=ja'" } %> そのためのコードを以下のように書いた。 のような感じ。 config/routes.rb LANG:ruby(linenumber) class ActionController::AbstractRequest def self.acceptable_languages=(v) @acceptable_languages = v.is_a?(Array) ? v : [v] class << @acceptable_languages def to_regexp Regexp.new join("|") end end end def self.acceptable_languages @acceptable_languages end def self.acceptable_language?(l) @acceptable_languages.include?(l.to_sym) end end ActionController::AbstractRequest.acceptable_languages= :ja, :en ActionController::Routing::Routes.draw do |map| lang_regexp= ActionController::AbstractRequest.acceptable_languages.to_regexp map.connect ':controller.:rails_language', :format => 'html', :action => 'index', :requirements => { :rails_language => lang_regexp } map.connect ':controller/:action.:rails_language', :format => 'html', :requirements => { :rails_language => lang_regexp } map.connect ':controller/:action/:id.:rails_language', :format => 'html', :requirements => { :rails_language => lang_regexp } map.connect ':controller/:action/:id', :format => 'html' map.connect ':controller/:action/:id.:format.:rails_language', :requirements => { :rails_language => lang_regexp } map.connect ':controller/:action/:id.:format', :defaults => { :action => 'index', :format => 'html' } end もちろん、ブラウザ側で javascript や cookie を off にされていれば 動作しないが、それは仕方のないところ。 ** テスト [#s18f0fb5] <noscript> タグで javascript を on にするよう書いておくべき。 LANG:console $ jed app/controllers/test_controller.rb class TestController < ApplicationController def index render :text => params[:rails_language].inspect end end $ wget -qO- http://localhost:3000/test nil $ wget -qO- http://localhost:3000/test.ja "ja" $ wget -qO- http://localhost:3000/test.html.ja 404: Not Found $ wget -qO- http://localhost:3000/test/index nil $ wget -qO- http://localhost:3000/test/index.ja "ja" $ wget -qO- http://localhost:3000/test/index.html.ja 404: Not Found $ wget -qO- http://localhost:3000/test/index/0.html nil $ wget -qO- http://localhost:3000/test/index/0.html.ja "ja" ** ActionMailer のネゴシエーション [#c283a97a] * 言語が指定されていない時を含めた言語選択の優先順位 [#k5f2df0a] こちらもしないと片手落ちか。 優先順位は以下のようにする。 参照 > http://d.hatena.ne.jp/kusakari/20090226/1235616295 + 指定された言語 (指定されていれば) + 前回指定された言語 (もしあれば session に保存しておく) + ブラウザから送られる Accept-Language (もしあれば) + ActionController::AbstractRequest.acceptable_languages に記述された言語(記述された順) と思ったら、何もしなくても上記で対応しているみたい。 この順で提供可能な言語(作成されている言語テンプレート)を検索して、 見つかった物を使って表示する。 テストケースを追加すべき。 優先順位を決定するコードは以下の通り: * コメント [#kfcbe2ab] config/environment.rb の末尾に LANG:ruby class ActionController::AbstractRequest # will be set by ActionView::Base._pick_template attr :language, true def language_order! @language_order= ! @env['HTTP_ACCEPT_LANGUAGE'] ? [] : @env['HTTP_ACCEPT_LANGUAGE'].split(",").collect {|l| l.gsub(/;.*$/, '').gsub(/-.*$/, '').downcase.to_sym }.delete_if { |l| !ActionController::AbstractRequest.acceptable_language?(l) } @language_order+= ActionController::AbstractRequest.acceptable_languages @language_order.unshift(session[:rails_language].to_sym) if session[:rails_language] @language_order.uniq! end def language_order @language_order ||= language_order! end end class ApplicationController before_filter :set_language def set_language session[:rails_language]= params[:rails_language] if params[:rails_language] && ActionController::AbstractRequest.acceptable_language?(params[:rails_language]) request.language_order! end end 気づいた点など、ぜひ突っ込みを入れて下さい。 ** テスト [#h4a02e5d] #article_kcaptcha LANG:console $ jed app/controllers/test_controller.rb class TestController < ApplicationController def index render :text => request.language_order.inspect + "\n" end end $ wget -qO- http://localhost:3000/test [:ja, :en] $ wget -qO- --header="Accept-Language:en" http://localhost:3000/test [:en, :ja] $ wget -qO- --header="Accept-Language:ja" http://localhost:3000/test [:ja, :en] $ wget -qO- http://localhost:3000/test [:ja, :en] $ wget -qO- http://localhost:3000/test.en [:ja, :en] $ alias wgetc="wget -qO- --load-cookies=tmp/wget.cookie --save-cookies=tmp/wget.cookie --keep-session-cookies" $ wgetc http://localhost:3000/test [:ja, :en] $ cat tmp/wget.cookie # HTTP cookie file. # Generated by Wget on 2009-06-06 13:18:08. # Edit at your own risk. localhost:3000 FALSE / FALSE 0 _test_session BAh7BiIKZmxhc2h... $ wgetc http://localhost:3000/test.en [:en, :ja] $ cat tmp/wget.cookie # HTTP cookie file. # Generated by Wget on 2009-06-06 13:20:42. # Edit at your own risk. localhost:3000 FALSE / FALSE 0 _test_session BAh7BzoTcmFpbHN... $ wgetc http://localhost:3000/test [:en, :ja] $ wgetc http://localhost:3000/test.ja [:ja, :en] $ wgetc http://localhost:3000/test [:ja, :en] 正しく切り換えができている事が分かる。 ACCEPT_LANGUAGE や rails_language に不正な言語指定が送られても はじくようにしてある。 * 言語の優先順位を元に利用するテンプレートを選択する [#tdc62b56] テンプレートの選択は ActionView::Base._pick_template で行われる。 デフォルトではコントローラ名とアクション名を controller/action という形に 繋げたテンプレートファイル名に対してこの _pick_template_sub が呼ばれ、 以下の順で実際に適用するテンプレートファイルを見つけ出す。 テンプレートファイル名を明示的に指定して render が呼ばれた場合や、 レイアウトファイル、部分テンプレート(partial) に対する render でも、、 実際のテンプレートファイルを探すにはこの関数が呼び出される。 高速化のため、この関数が呼び出される時点で、 self.view_paths[file_name_without_extention] にテンプレートファイルの 一覧が作成されていて、.erb などの拡張子を除いたファイル名を与えると、 (もしテンプレートファイルが存在すれば)それを ActionView::Template オブジェクトとして取り出す事ができるようになっている。 template_format は routes で出てきた params[:format] のことで、~ wget -qO- http://localhost:3000/test/index/0.html~ なら "html" が、~ wget -qO- http://localhost:3000/test/index/0.xml~ なら "xml" が、~ wget -qO- http://localhost:3000/test/index/0.png~ なら "png" が、入っている。~ 指定されていない場合には "html" になる。 以下は,テンプレートファイルを探す手順: + 与えられたファイル名から .erb などの拡張子を取り除く + #{template_file_name_without_extention}.#{template_format} があればそれを使う + template_file_name_without_extention があればそれを使う + partial からの呼び出しでは、ルートテンプレートの format_and_extention を使って~ #{template_file_name}.#{first_render.format_and_extension} があればそれを使う + フォーマットが js つまり JavaScript だったら、~ #{template_file_name}.html があればそれを使う + 以上で見つからなければ、app/views フォルダ以外にあるかもしれないので、 とりあえずそのまま与えられた文字列を ActionView::Template.new に渡してみる 言語ごとのテンプレートファイルを使うには、これらの他に .ja などを加えた テンプレートも検索する事になる。もちろん request.language_order の順で。 _pick_template_sub は template_file_name に lang=".ja" を加えた テンプレートを検索するヘルパー関数として作成した。~ lang="" とすれば、もとの _pick_template と同様の動作になる。 #{template_file_name}.#{first_render.format_and_extension} の検索について:~ first_render.format_and_extension は "html.erb" のようなものになる可能性があるので、 .ja をこの間に挟んで "html.ja.erb" を作らなければならない。~ first_render.format_and_extension が "html.ja.erb" の形をしている場合には、 以下のコードでは "html.ja.ja.erb" を探す事になってしまうけれど、実害はないはず。 memoize/unmemoize については次項を参照 LANG:ruby(linenumber) class ActionView::Base private def _pick_template_sub(template_file_name, lang="") if template = self.view_paths["#{template_file_name}.#{template_format}#{lang}"] return template elsif template = self.view_paths["#{template_file_name}#{lang}"] return template elsif (first_render = @_render_stack.first) && first_render.respond_to?(:format_and_extension) m= first_render.format_and_extension.match(/(.*)(\.\w+)?$/) template = self.view_paths["#{template_file_name}.#{m[1]}#{lang}#{m[2]}"] return template end if template_format == :js && template = self.view_paths["#{template_file_name}.html#{lang}"] @template_format = :html return template end nil end memoize :_pick_template_sub unmemoize :_pick_template def _pick_template(template_path) return template_path if template_path.respond_to?(:render) path = template_path.sub(/^\//, '') if ( m = path.match(/(.*)\.(\w+)$/) ) && ActionView::Template.template_handler_extensions.include?(m[2]) template_file_name, template_file_extension = m[1], m[2] else template_file_name = path end # search for localized version if controller && controller.respond_to?(:request) controller.request.language_order.each do |lang| if template = _pick_template_sub(template_file_name, ".#{lang}") controller.request.language= lang return template end end end # search for not localized version if template = _pick_template_sub(template_file_name) template else # not found in view_paths template = ActionView::Template.new(template_path, view_paths) if self.class.warn_cache_misses && logger logger.debug "[PERFORMANCE] Rendering a template that was " + "not found in view path. Templates outside the view path are " + "not cached and result in expensive disk operations. Move this " + "file into #{view_paths.join(':')} or add the folder to your " + "view path list" end template end end end ** memoize について [#o48332bd] _pick_template は効率化のため memoize されている。=> [[参照>http://wota.jp/ac/?date=20081025#p11]] ところが多言語化した後は controller.request.language_order の値によって _pick_template 関数の返す値が変化するため、_pick_template を memoize するわけに行かない。 効率を落とさないためには、代わりに _pick_template_sub を memoize すればいい。 ということで unmemoize/memoize を入れたのが上のコード。 ActiveSupport::Memoizable.unmemoize が無いので実装した。 LANG:ruby(linenumber) module ActiveSupport module Memoizable def unmemoize(*symbols) symbols.each do |symbol| original_method = :"_unmemoized_#{symbol}" memoized_ivar = MEMOIZED_IVAR.call(symbol) class_eval <<-EOS, __FILE__, __LINE__ raise "Not memoized #{symbol}" if !method_defined?(:#{original_method}) undef #{symbol} alias #{symbol} #{original_method} #{memoized_ivar}= nil EOS end end end end 実際には上のコードでは _pick_template を上書きしてしまっているので unmemoize の必要はない。 上書きしたコードをもう一度 memoize し直すような用途には unmemoize をしておかないと いけないのと、memoize されてないことを明示するため、unmemoize を入れた。 ** テスト [#nb0c36b7] LANG:console $ jed app/controllers/test_controller.rb class TestController < ApplicationController def index end end $ echo "Test" > app/views/test/index.html.en.erb $ echo "テスト" > app/views/test/index.html.ja.erb $ rm tmp/wget.cookie $ wgetc -qO- http://localhost:3000/test テスト $ wgetc --header="Accept-Language:en" http://localhost:3000/test Test $ wgetc -qO- http://localhost:3000/test テスト $ wgetc -qO- http://localhost:3000/test.en Test $ wgetc http://localhost:3000/test Test $ wgetc --header="Accept-Language:ja" http://localhost:3000/test Test ** レイアウトに関するテスト [#j36ad09b] LANG:console $ echo "日本語: <%= @content_for_layout %>" > app/views/layouts/application.html.ja.erb $ echo "English: <%= @content_for_layout %>" > app/views/layouts/application.html.en.erb $ wgetc -qO- http://localhost:3000/test.ja 日本語: テスト $ wgetc -qO- http://localhost:3000/test.en English: Test ** 部分テンプレートに関するテスト [#vc77e9b2] LANG:console $ echo "Test <%= render :partial=>'ok' %>" > app/views/test/index.html.en.erb $ echo "テスト <%= render :partial=>'ok' %>" > app/views/test/index.html.ja.erb $ echo "OK" > app/views/test/_ok.html.en.erb $ echo "ばっちり" > app/views/test/_ok.html.ja.erb $ wgetc -qO- http://localhost:3000/test.ja 日本語:テスト ばっちり $ wgetc -qO- http://localhost:3000/test.en English: Test OK * テンプレートファイルの拡張子を正しく認識させる [#tdc62b56] 以上、とってもうまく行っているように見えるけど、実は問題がある。 LANG:console $ wgetc --save-header -qO- http://localhost:3000/test.en HTTP/1.1 200 OK Etag: "122cdccf079db5eebad7e19ccc3b311b" Connection: Keep-Alive Content-Type: html.en; charset=utf-8 Date: Sat, 06 Jun 2009 06:31:12 GMT Server: WEBrick/1.3.1 (Ruby/1.8.7/2008-08-11) X-Runtime: 11ms Content-Length: 21 Cache-Control: private, max-age=0, must-revalidate Set-Cookie: _test_session=BAh7BzoTcm... English: Test OK $ どこが問題かというと、 Content-Type: html.en; charset=utf-8 の部分。 使っているテンプレートファイル名が test/index.html.en.erb なので、これが {test/}{index}.{html.en}.{erb} のように分割されて、html.en の部分を mime-type として返してしまっている。 この部分を受け持っているのは ActionView::Template クラスで、変更すべきは {test/}{index}.{html}.{en}.{erb} のように正しく切る、というところのみなのだが、実際に直すのは多少大がかりになる。 LANG:ruby(linenumber) module ActionView class Template attr_accessor :language def initialize(template_path, load_paths = []) template_path = template_path.dup @base_path, @name, @format, @language, @extension = split(template_path) @base_path.to_s.gsub!(/\/$/, '') # Push to split method @load_path, @filename = find_full_path(template_path, load_paths) # Extend with partial super powers extend RenderablePartial if @name =~ /^_/ end unmemoize :format_and_extension def format_and_extension (extensions = [format, language, extension].compact.join(".")).blank? ? nil : extensions end memoize :format_and_extension unmemoize :path def path [base_path, [name, format, language, extension].compact.join('.')].compact.join('/') end memoize :path unmemoize :path_without_extension def path_without_extension [base_path, [name, format, language].compact.join('.')].compact.join('/') end memoize :path_without_extension unmemoize :path_without_format_and_extension def path_without_format_and_extension [base_path, name].compact.join('/') end memoize :path_without_format_and_extension private # Returns file split into an array # [base_path, name, format, language, extension] def split(file) if m = file.match(/^(.*\/)?([^\.]+)(?:\.(\w+)(?:\.(\w+)(?:\.(\w+)(?:\.(\w+))?)?)?)?$/) if m[6] # multi part format [m[1], m[2], "#{m[3]}.#{m[4]}", m[5], m[6]] elsif m[5] if ActionController::AbstractRequest.acceptable_language?(m[4]) [m[1], m[2], m[3], m[4], m[5]] else # multi part format [m[1], m[2], "#{m[3]}.#{m[4]}", m[5]] end elsif m[4] # no format if valid_extension?(m[4]) if ActionController::AbstractRequest.acceptable_language?(m[3]) [m[1], m[2], nil, m[3], m[4]] else # Single format [m[1], m[2], m[3], nil, m[4]] end else [m[1], m[2], m[3], m[4], nil] end else if valid_extension?(m[3]) [m[1], m[2], nil, nil, m[3]] elsif ActionController::AbstractRequest.acceptable_language?(m[3]) [m[1], m[2], nil, m[3], nil] else [m[1], m[2], m[3], nil, nil] end end end end end end split はかなり重たい関数なので memoize したくなるが、 実際には ActionView::Template のインスタンス自体が ActionView::Base.view_paths にキャッシュされるので、 その必要は無い。 ** テスト [#he34416a] LANG:console $ wgetc --save-header -qO- http://localhost:3000/test.en HTTP/1.1 200 OK Etag: "122cdccf079db5eebad7e19ccc3b311b" Connection: Keep-Alive Content-Type: text/html; charset=utf-8 Date: Sat, 06 Jun 2009 06:43:20 GMT Server: WEBrick/1.3.1 (Ruby/1.8.7/2008-08-11) X-Runtime: 13ms Content-Length: 21 Cache-Control: private, max-age=0, must-revalidate English: Test OK 今度はばっちり。 * ページキャッシュの多言語対応 [#z6cc2a4b] まずは rails そのままで、ページキャッシュ機能をONにしてみる。 LANG:console $ jed config/environments/development.rb config.action_controller.perform_caching = true $ jed app/controllers/test_controller.rb class TestController < ApplicationController caches_page :index def index end end $ wgetc -qO- http://localhost:3000/test English: Test OK $ ls public/test* public/test.html $ cat public/test.html English: Test OK $ wgetc -qO- http://localhost:3000/test.en English: Test OK $ ls public/test* public/test.html public/test.en test.html というファイルができてしまうと、 次回からは言語設定にかかわらず常にこれが表示されてしまうため、うまくいかない。 ページキャッシュは ActionController::Caching::Pages が作成する。~ このときファイル名に言語指定を正しく付ければよい。 ファイル名に付ける言語指定は、path として指定された物では無く、 実際に使ったテンプレートの言語にすべき。 ちなみに cache_page が暗黙に呼び出される時、引数は与えられないので、 キャッシュファイル名は request.path から作る事になる。 LANG:ruby(linenumber) module ActionController::Caching::Pages def cache_page(content = nil, options = nil) return unless perform_caching && caching_allowed path = case options when Hash url_for(options.merge( :only_path => true, :skip_relative_url_root => true, :format => params[:format], :rails_language => request.language)) when String options else p= request.path.split('.') p.pop if ActionController::AbstractRequest.acceptable_language?(p.last) p[0]+= self.class.page_cache_extension if p.count==1 p << request.language p.join('.') end self.class.cache_page(content || response.body, path) end end ** テスト [#t5c0d140] LANG:console $ rm -r public/test* $ wgetc -qO- http://localhost:3000/test English: Test OK $ ls public/test* public/test*/* public/test.html.en $ wgetc -qO- http://localhost:3000/test.en English: Test OK $ ls public/test* public/test*/* public/test.html.en $ wgetc -qO- http://localhost:3000/test.ja 日本語:テスト ばっちり $ ls public/test* public/test*/* public/test.html.en public/test.html.ja $ wgetc -qO- http://localhost:3000/test 日本語:テスト ばっちり $ ls public/test* public/test*/* public/test.html.en public/test.html.ja $ wgetc -qO- http://localhost:3000/test/index 日本語:テスト ばっちり $ ls public/test* public/test*/* public/test.html.en public/test.html.ja public/test: index.html.ja $ wgetc -qO- http://localhost:3000/test/index/0 日本語:テスト ばっちり $ ls public/test* public/test*/* public/test.html.en public/test.html.ja public/test/index.html.ja public/test: index index.html.ja public/test/index: 0.html.ja $ wgetc -qO- http://localhost:3000/test/index/0.html 日本語:テスト ばっちり $ ls public/test* public/test*/* public/test.html.en public/test.html.ja public/test/index.html.ja public/test: index index.html.ja public/test/index: 0.html.ja $ wgetc -qO- http://localhost:3000/test/index/0.html.ja 日本語:テスト ばっちり $ ls public/test* public/test*/* public/test.html.en public/test.html.ja public/test/index.html.ja public/test: index index.html.ja public/test/index: 0.html.ja * ページキャッシュの多言語化に対応させるための apache の設定 [#t2a0cd95] まずは rails を apache から cgi 経由で production 環境で動かす。 LANG:console $ cp db/development.sqlite3 db/production.sqlite3 $ ln -s public/ /var/www/negotiation $ wgetc http://localhost/negotiation | html2text ... ****** Welcome aboard ****** ***** You’re riding Ruby on Rails! ***** **** About_your_application’s_environment **** ... $ cat > public/.htaccess SetEnv RAILS_RELATIVE_URL_ROOT /negotiation SetEnv RAILS_ENV production RewriteEngine On RewriteRule ^$ index.html [QSA] RewriteRule ^([^.]+)$ $1.html [QSA] RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ dispatch.cgi [QSA,L] ^D $ echo "Test <%= render :partial=>'ok' %>" > app/views/test/index.html.en.erb $ echo "テスト <%= render :partial=>'ok' %>" > app/views/test/index.html.ja.erb $ wgetc http://localhost/negotiation/test 日本語: テスト ばっちり $ wgetc http://localhost/negotiation/test.en English: Test OK $ wgetc http://localhost/negotiation/test English: Test OK では、ページキャッシュを ON にしてテスト。 LANG:console $ jed app/controllers/test_controller.rb class TestController < ApplicationController caches_page :index # caches_action :index def index end end $ ls public/test* ls: cannot access public/test*: そのようなファイルやディレクトリはありません $ wgetc http://localhost/negotiation/test.ja 日本語: テスト ばっちり $ ls public/test* public/test.html.ja $ wgetc http://localhost/negotiation/test 日本語: テスト ばっちり $ tail /var/log/apache2/rewrite.log [perdir /home/samba/www/negotiation/] pass through /home/samba/www/negotiation/test.html.ja $ wgetc http://localhost/negotiation/test.ja 日本語: テスト ばっちり $ tail /var/log/apache2/rewrite.log [perdir /home/samba/www/negotiation/] pass through /home/samba/www/negotiation/dispatch.cgi $ rm temp/wget.cookie $ wgetc --header="Accept-Language: en" http://localhost/negotiation/test エラー 406: Not Acceptable $ wgetc http://localhost/negotiation/test.en English: Test OK $ ls public/test* public/test.html.en public/test.html.ja $ wgetc http://localhost/negotiation/test 日本語: テスト ばっちり $ tail /var/log/apache2/rewrite.log [perdir /home/samba/www/negotiation/] pass through /home/samba/www/negotiation/test.html.ja $ rm temp/wget.cookie $ wgetc --header="Accept-Language: en" http://localhost/negotiation/test English: Test OK $ wgetc --header="Accept-Language: ja" http://localhost/negotiation/test 日本語: テスト ばっちり まず、public/test.html.ja が作成された後も http://localhost/negotiation/test.ja の読み込みがキャッシュファイルではなく cgi 呼び出しに rewrite されている点。 これは、 LANG:Shell(linenumber) RewriteCond %{HTTP_ACCEPT} text/html RewriteCond %{SCRIPT_FILENAME} !\.html\. RewriteCond %{SCRIPT_FILENAME} (.*)\.(..)$ RewriteCond %1.html.%2 -f RewriteRule (.*)\.(..)$ $1.html.$2 [L] というルールを追加してやる事でキャッシュを読ませる事ができる。 ルールの意味としては、 + ACCEPT ヘッダに text/html が含まれていて + ファイル名が .html. を含まず + ファイル名が .ja や .en のように、2文字の拡張子で終わっていて + .html を追加すると、同名のファイルが見つかる + という条件の下、.html を追加して、test.en を test.html.en に書き換える 次に、public/test.html.ja があって public/test.html.en が無い 状況で wgetc --header="Accept-Language: en" http://localhost/negotiation/test すると 406 エラーになる。 これは、要求された en のファイルが無いためだ。 本来であればこれを dispatch.cgi に rewrite したいところだが、 その方法が分からなかったので、 LANG:Shell LanguagePriority ja en ForceLanguagePriority Prefer Fallback としてやることで、望みの言語が見つからない場合には ja, en の順に探して、 見つかった物を返すようにした。 これだと英語を要求したのに日本語の答えが返って来る、 という意味で完全な動作とは言えないが、 apache のネゴシエーションのルールからするとこれは仕方のないところか。 最後の部分で確かめたように、 public/test.html.en と public/test.html.ja とが両方あれば Accept-Language が効くので、そのページを表示した最初の一人だけが 被害を被るという事で、あきらめる。 もう一点、wgetc http://localhost/negotiation/test.en の後の wgetc http://localhost/negotiation/test 呼び出しで 日本語が表示されてしまっている部分に不具合がある。 これは、直前に選択した言語を apache が覚えていないせいだ(あたりまえ)。 SetEnvIf Cookie "rails_language=(..)" prefer-language=$1 とすることで、cookie の値に応じて言語を選ぶことができる。 さらに、 RewriteCond %{ENV:prefer-language} !=$2 RewriteRule (.*\.html\.)(..) $1%{ENV:prefer-language} とする。 これは、test.html.ja が存在して test.html.en が存在しない時、 cookies["rails_language"]=="en" であっても test の呼び出しが apache のネゴシエーションで test.html.ja になってしまうので、 これを検出して test.html.en に書き直すためのもの。 当然 test.html.en は存在しないので、結果的に dispatch.cgi に rewrite されて test.html.en が作られる事になる。 最終的に、public/.htaccess の内容は SetEnv RAILS_RELATIVE_URL_ROOT /negotiation SetEnv RAILS_ENV production LanguagePriority ja en ForceLanguagePriority Prefer Fallback SetEnvIf Cookie "rails_language=(..)" prefer-language=$1 RewriteEngine On RewriteRule ^$ index.html [QSA] RewriteRule ^([^.]+)$ $1.html [QSA] RewriteCond %{HTTP_ACCEPT} text/html RewriteCond %{SCRIPT_FILENAME} !\.html\. RewriteCond %{SCRIPT_FILENAME} (.*)\.(..) RewriteCond %1.html.%2 -f RewriteRule (.*)\.(..) $1.html.$2 [L] RewriteCond %{ENV:prefer-language} !=$2 RewriteRule (.*\.html\.)(..) $1%{ENV:prefer-language} RewriteCond %{SCRIPT_FILENAME} !-f RewriteRule ^(.*)$ dispatch.cgi [QSA,L] となった。 ** テスト [#f34fa374] * アクションキャッシュ [#gd9d14bc] まずは rails そのままで、アクションキャッシュ機能をONにしてみる。 LANG:console $ jed app/controllers/test_controller.rb class TestController < ApplicationController # caches_page :index caches_action :index def index end end $ wgetc -qO- http://localhost:3000/test.en English: Test OK $ tail log/development.log Cached fragment miss: views/localhost:3000/test (0.1ms) $ wgetc -qO- http://localhost:3000/test English: Test OK $ wgetc -qO- http://localhost:3000/test.en Cached fragment miss: views/localhost:3000/test?format=en (0.1ms) キャッシュ内容を表すキーが views/localhost:3000/test や views/localhost:3000/test?format=en などと なってしまっている。本来 views/localhost:3000/test.en であるべきだ。 このあたりの処理は、ActionController::Caching::Actions::ActionCachePath で行われている。 LANG:ruby(linenumber) module ActionController::Caching::Actions class ActionCachePath attr_reader :language def initialize(controller, options = {}, infer_extension=true) if infer_extension and options.is_a? Hash request_extension = extract_extension(controller.request) options = controller.params.merge( options.reverse_merge(:format => request_extension, :rails_language => controller.request.language_order.first)) end path = controller.url_for(options).split('://').last if infer_extension @extension = request_extension add_extension!(path, @extension) end @path = URI.unescape(path) end private def add_extension!(path, extension) if extension p= path.split('.') p.insert(-2, extension) unless path =~ /\b#{Regexp.escape(extension)}\b/ path.join('.') end end def extract_extension(request) p= request.path.split('.') # drop file name p.shift # drop language p.pop if !p.empty? && ActionController::AbstractRequest.acceptable_language?(p.last) extension = p.join('.') # If there's no extension in the path, check request.format extension = request.cache_format if extension=="" extension end end end 8〜10行目の部分、元のコードでは controller.params とマージせずに うまく行っているのだけど、このコードでは controller.params とマージしないと action や id の指定が無視されるようだったので、対症療法として入れてある。 後で要チェック request.language_order.first の言語テンプレートファイルが存在しない時、 キャッシュキーには request.language_order.first で示される言語指定が 付くにもかかわらず、中身は提供可能な言語で表示された内容になるなあ・・・ まあ、問題は無いかな? ** テスト [#n1f7d9d6] LANG:console $ wgetc -qO- http://localhost:3000/test 日本語:テスト ばっちり $ less log/development.log Cached fragment miss: views/localhost:3000/test.ja (0.1ms) $ wgetc -qO- http://localhost:3000/test 日本語:テスト ばっちり $ less log/development.log Cached fragment hit: views/localhost:3000/test.ja (0.1ms) $ wgetc -qO- http://localhost:3000/test.ja 日本語:テスト ばっちり $ less log/development.log Cached fragment hit: views/localhost:3000/test.ja (0.1ms) $ wgetc -qO- http://localhost:3000/test.en English: Test OK $ less log/development.log Cached fragment miss: views/localhost:3000/test.en (0.1ms) $ wgetc -qO- http://localhost:3000/test.en English: Test OK $ less log/development.log Cached fragment hit: views/localhost:3000/test.en (0.1ms) $ wgetc -qO- http://localhost:3000/test English: Test OK $ less log/development.log Cached fragment hit: views/localhost:3000/test.en (0.1ms) $ wgetc -qO- http://localhost:3000/test/index English: Test OK $ less log/development.log Cached fragment hit: views/localhost:3000/test.en (0.1ms) $ wgetc -qO- http://localhost:3000/test/index/0 English: Test OK $ less log/development.log Cached fragment miss: views/localhost:3000/test/index/0.en (0.1ms) $ wgetc -qO- http://localhost:3000/test/index/0.html English: Test OK $ less log/development.log Cached fragment hit: views/localhost:3000/test/index/0.en (0.1ms) http://localhost:3000/test/index.en へのアクセスに対して~ views/localhost:3000/test.en が、~ http://localhost:3000/test/index/0.html.en へのアクセスに対して~ views/localhost:3000/test/index/0.en が、~ それぞれ引かれていることに注目。 url_for で変換し直している効果が現れている。 routes の記述順によっては逆に冗長な記述への変換(/test を /test/index へ、など) が起こってしまう場合があるので、routes では省略形をより上の方に書くのが良いようだ。 * フラグメントキャッシュ [#p06455c8] まずは例によって rails そのままで、フラグメントキャッシュ機能を使ってみる。 LANG:console $ echo "<% cache do %>Test <%= render :partial=>'ok' %><% end %>" > app/views/test/index.html.en.erb $ echo "<% cache do %>テスト <%= render :partial=>'ok' %><% end %>" > app/views/test/index.html.ja.erb $ jed app/controllers/test_controller.rb class TestController < ApplicationController # caches_page :index # caches_action :index def index end end $ wgetc -qO- http://localhost:3000/test English: Test OK $ less log/development.log Cached fragment miss: views/localhost:3000/test (0.1ms) やっぱりうまく行っていない。 ここは、views/localhost:3000/test.en となってほしいところ。 これは ActiveSupport::Cache のテリトリーらしい。 LANG:ruby(linenumber) module ActionController module Caching module Fragments def fragment_cache_key(key) ActiveSupport::Cache.expand_cache_key( key.is_a?(Hash) ? url_for(key.reverse_merge( :rails_language=>request.language_order.first) ).split("://").last : key, :views) end end end end ** テスト [#le18b1e9] LANG:console $ wgetc http://localhost:3000/test English: Test OK $ less log/development.log Cached fragment miss: views/localhost:3000/test.en (0.1ms) $ wgetc http://localhost:3000/test English: Test OK $ less log/development.log Cached fragment hit: views/localhost:3000/test.en (0.1ms) $ echo "<% cache :param1=>:test do %>Test <%= render :partial=>'ok' %><% end %>" > app/views/test/index.html.en.erb $ wgetc http://localhost:3000/test English: Test OK $ less log/development.log Cached fragment miss: views/localhost:3000/test.en?param1=test (0.1ms) うまく行っているみたい。
Counter: 9533 (from 2010/06/03),
today: 2,
yesterday: 0