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'] に不正な言語指定が送られてもはじくようになっている。