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

(5385d) 更新


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

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

言語の優先順位を元に利用するテンプレートを選択する

テンプレートの選択は 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" になる。

以下は,テンプレートファイルを探す手順:

  1. 与えられたファイル名から .erb などの拡張子を取り除いたものを template_file_name_without_extention とする
  2. #{template_file_name_without_extention}.#{template_format} があればそれを使う
  3. template_file_name_without_extention があればそれを使う
  4. partial からの呼び出しでは、オリジナルのテンプレートの format_and_extention を使って

    #{template_file_name}.#{first_render.format_and_extension} があればそれを使う

  5. フォーマットが js つまり JavaScript だったら、

    #{template_file_name}.html があればそれを使う

  6. 以上で見つからなければ、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
 
 

今度はばっちり。


Counter: 5080 (from 2010/06/03), today: 1, yesterday: 0