言語別テンプレートの選択 の変更点

更新


SIZE(22){COLOR(RED){このページの内容は非常に古いです(Rails 1.x.x)。最新の Rails では洗練された国際化の機構が標準で入っているため、下記は読むだけ無駄な内容になっています。}}

[[ソフトウェア/rails/言語ネゴシエーション]]

#contents

* 言語の優先順位を元に利用するテンプレートを選択する [#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_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 について [#o48332bd]

_pick_template は効率化のため memoize されている。=> [[参照>http://wota.jp/ac/?date=20081025#p11]]

ところが多言語対応した後は 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 を入れた。

** テスト [#nb0c36b7]

 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

** レイアウトに関するテスト [#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 http://localhost:3000/test.ja
  日本語: テスト
 
 $ wgetc 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 http://localhost:3000/test.ja
  日本語:テスト ばっちり
  
  
 $ wgetc http://localhost:3000/test.en
  English: Test   OK
  
  

* テンプレートファイルの拡張子を正しく認識させる [#tdc62b56]

以上、とってもうまく行っているように見えるけど、実は問題がある。

 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 にキャッシュされるので、
その必要は無い。

** テスト [#he34416a]

 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
  
  
今度はばっちり。

Counter: 5087 (from 2010/06/03), today: 1, yesterday: 2