言語別テンプレートの選択 の履歴(No.1)
更新- 履歴一覧
- 差分 を表示
- 現在との差分 を表示
- ソース を表示
- ソフトウェア/rails/言語ネゴシエーション/言語別テンプレートの選択 へ行く。
- 1
言語の優先順位を元に利用するテンプレートを選択する†
テンプレートの選択は 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- なら "png" が、入っている。
指定されていない場合には "html" になる。
以下は,テンプレートファイルを探す手順:
- 与えられたファイル名から .erb などの拡張子を取り除いたものを template_file_name_without_extention とする
- #{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.accepts_languages の順で行う。
_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 if memoized? :_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.accepts_languages.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 について†
_pick_template は効率化のため memoize されている。=> 参照
ところが多言語対応した後は controller.request.accepts_languages の値によって _pick_template 関数の返す値が変化するため、_pick_template を memoize するわけに行かない。
効率を落とさないため、代わりに _pick_template_sub を memoize する。
ということで unmemoize/memoize を入れたのが上のコード。
ActiveSupport::Memoizable.unmemoize が無いので実装した。
LANG:ruby(linenumber) module ActiveSupport module Memoizable def memoized?(symbol) original_method = :"_unmemoized_#{symbol}" class_eval <<-EOS, __FILE__, __LINE__ method_defined?(:#{original_method}) EOS end 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 を入れた。
テスト†
LANG:console $ jed app/controllers/test_controller.rb class TestController < ApplicationController end $ echo "Test" > app/views/test/index.html.en.erb $ echo "テスト" > app/views/test/index.html.ja.erb $ (restart script/server) $ 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
レイアウトに関するテスト†
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 http://localhost:3000/test.ja 日本語: テスト $ wgetc http://localhost:3000/test.en English: Test
部分テンプレートに関するテスト†
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 http://localhost:3000/test.ja 日本語:テスト ばっちり $ wgetc http://localhost:3000/test.en English: Test OK
テンプレートファイルの拡張子を正しく認識させる†
以上、とってもうまく行っているように見えるけど、実は問題がある。
LANG:console $ wgetc --save-header http://localhost:3000/test.en HTTP/1.1 200 OK Etag: "122cdccf079db5eebad7e19ccc3b311b" Connection: Keep-Alive Content-Type: html.en; charset=utf-8 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 にキャッシュされるので、 その必要は無い。
テスト†
LANG:console $ (restart script/server) $ 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 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
今度はばっちり。