言語別テンプレートの選択 の変更点
更新- 追加された行はこの色です。
- 削除された行はこの色です。
- ソフトウェア/rails/言語ネゴシエーション/言語別テンプレートの選択 へ行く。
- ソフトウェア/rails/言語ネゴシエーション/言語別テンプレートの選択 の差分を削除
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: 5426 (from 2010/06/03),
today: 1,
yesterday: 2