routesの設定と言語選択の優先順位 の履歴(No.2)
更新多言語化したビューの作成†
今作ったビューを削除して、日本語版と英語版を作成。
まだ rails に手を入れていないので、そのままではエラーになる。
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
これを正しく表示できるようにするのが目標なのだが、 その前に rails に言語ファイル選択の優先順位を教える必要がある。
config/routes.rb の設定†
多言語化後は、各ページに対して
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
などの形でアクセスできるようにする。
- .ja などの言語指定がない場合
- ブラウザからも指定が無ければサーバー側の優先順位で表示言語を選ぶ
- ブラウザが優先順位を指定していれば、その順で表示言語を選ぶ
- .ja などの言語指定があれば、その言語で表示する
- 一旦言語指定を行ったら、以降は言語指定がない場合にサーバーやブラウザの優先順位を無視して、 直前に行った言語指定と同じ言語で表示する
- 表示したい言語のテンプレートファイルが無ければ、ある物を使って表示する
3. は、明示的に表示言語を選択できるようにするためのもの。
受け入れ可能な言語のリストは config/routes.rb の先頭で
ENV['RAILS_ACCEPTABLE_LANGUAGES'] ||= 'ja|en'
として設定する事にする。
サーバー側の優先順位はここでの指定順で決まる物とする。
これ以外の言語が指定されても無視することにする。
route の設定は、
map.connect ':controller/:action/:id.:rails_language', :requirements => { :rails_language => /ja|en/ }
のようになる。/^(ja|en)$/ ではなく /ja|en/ とするのが正しいらしい。
以下がコード。
config/routes.rb
LANG:ruby(linenumber) ENV['RAILS_ACCEPTABLE_LANGUAGES'] ||= 'ja|en' ActionController::Routing::Routes.draw do |map| lang_regexp= Regexp.new( ENV['RAILS_ACCEPTABLE_LANGUAGES'] ) 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
テスト†
LANG:console $ (restart script/server) $ jed app/controllers/test_controller.rb class TestController < ApplicationController def index render :text => params[:rails_language].inspect + "\n" end end $ wget -qO- http://localhost:3000/test nil $ wget -qO- http://localhost:3000/test.ja "ja" $ wget -O- 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 -O- 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"
言語指定を正しく行えている事が分かる。
言語が指定されていない時を含めた言語選択の優先順位†
優先順位は以下のようにする。
- 指定された言語 (指定されていれば)
- 前回指定された言語 (もしあれば cookie に保存しておく)
- ブラウザから送られる Accept-Language (もしあれば)
- RAILS_ACCEPTABLE_LANGUAGES に記述された言語(記述された順)
この順で提供可能な言語(作成されている言語テンプレート)を検索し、 見つかった物を使って表示する。
優先順位を決定するコードは以下の通り:
config/environment.rb の末尾に
LANG:ruby(linenumber) class ActionController::AbstractRequest def self.acceptable_languages @acceptable_languages ||= ENV['RAILS_ACCEPTABLE_LANGUAGES'].split('|').map {|l| l.to_sym} end def self.acceptable_language?(l) acceptable_languages.include?(l.to_sym) end end class ActionController::AbstractRequest # will be set by ActionView::Base._pick_template attr :language, true def accepts_languages!(language=nil) @accepts_languages= [ language ? language.to_sym : nil, ( cookie = cookies['rails_language'] and l = cookie.first and ActionController::AbstractRequest.acceptable_language?(l) ) ? l.to_sym : nil, ( @env['HTTP_ACCEPT_LANGUAGE'] || '' ).split(",").collect {|l| lsym= l.split(/;|\-/,2)[0].downcase.to_sym ActionController::AbstractRequest.acceptable_language?(lsym) ? lsym : nil }, ActionController::AbstractRequest.acceptable_languages ].flatten.compact.uniq end def accepts_languages @accepts_languages || accepts_languages! end end class ActionController::Base # before_filter :set_language # fails for some unknown reason. # following is a workaround. def perform_action_with_set_rails_language set_rails_language perform_action_without_set_rails_language end alias_method_chain :perform_action, :set_rails_language protected def set_rails_language rails_language= params[:rails_language] if rails_language && ActionController::AbstractRequest.acceptable_language?(rails_language) cookies['rails_language']= rails_language request.accepts_languages!(rails_language) end end end
ブラウザから送られる Accept ヘッダーを解釈して、 ブラウザの受け入れ可能な Mime::Type の配列にして返す関数が ActionController::AbstractRequest::accepts という名前であるのに習って、 Accept-Language を解釈して配列にして返す関数を accepts_languages とした。
一旦指定された言語選択は、ActionController::Base.set_rails_language にて cookies['rails_language'] に保存される。
クッキーの書き換えを行った場合には、accepts_languages を更新する必要がある。 このための関数が accepts_languages! 。
set_rails_language を before_fileter に登録したかったのだけれど、 なぜかうまく行かなかったので、perform_action の前に無理矢理はさんだ。
実際にどの言語で送信されるかは、優先順位に従ってテンプレートファイルを検索し、 初めて見つかった物が何であるかによって決まる。
request.language はこの実際のテンプレートファイルの言語を表すプロパティで、 後から ActionView::Base._pick_template によって設定される事になる。
テスト†
LANG:console $ jed app/controllers/test_controller.rb class TestController < ApplicationController def index render :text => request.accepts_languages.inspect + "\n" end end $ (restart script/server) $ wget -qO- http://localhost:3000/test [:ja, :en] $ wget -qO- --header="Accept-Language:ja" http://localhost:3000/test [:ja, :en] $ wget -qO- --header="Accept-Language:en" http://localhost:3000/test [:en, :ja] $ wget -qO- http://localhost:3000/test [:ja, :en] $ wget -qO- http://localhost:3000/test.en [:en, :ja] $ alias wgetc="wget -qO- --load-cookies=tmp/wget.cookie --save-cookies=tmp/wget.cookie --keep-session-cookies" $ cat tmp/wget.cookie cat: tmp/wget.cookie: そのようなファイルやディレクトリはありません $ wgetc http://localhost:3000/test [:ja, :en] $ cat tmp/wget.cookie # HTTP cookie file. # Edit at your own risk. $ wgetc http://localhost:3000/test.en [:en, :ja] $ cat tmp/wget.cookie # HTTP cookie file. # Edit at your own risk. localhost:3000 FALSE / FALSE 0 _negotiation_session BAh7BiIKZm... localhost:3000 FALSE / FALSE 0 rails_language en $ 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, cookie['rails_language'] に不正な言語指定が送られてもはじくようになっている。