何が問題か
RubyOnRalsには、ライブラリとして便利な機能がデフォルトで提供されている。「error_messages_for」もその1つ。これは、ユーザーがフォームからデータを送信してきた時、内容にもれがあったり、値が不正であることをメッセージとして出力するもの。
しかし、デフォルトは英語表記。しかも、カラム名を使用してしまうのであまりよくない。そこで、これを日本語化する。
解決策
主に3つあると思う。
- 1.「ActiveHeart」を使う(非推奨)
- 2.「ruby-gettext」を使う
- 3. 自分で変更
ActiveHeartは、作成者が非推奨していた。何らかの脆弱性なのかな。ググれば出ると思う。このライブラリの中に日本語化をサポートするものがある。
ruby-gettextも日本語化をサポートするが、こちらは日本語化だけでなく、様々な言語にアプリケーションを対応させることができる。世界向けサービスして、多くの言語に対応させたい時に使える。
上のようなライブラリを使っても良いが、今回は日本語化するだけなので、「3」の自分でやることにする(笑)
例として、次のようなものを用意する。(簡略化のため、HTMLのタグは省略)
- user.rb
class User < ActiveRecord::Base SEX_TYPES = [["男","man"],["女","woman"]] validates_presence_of :name validates_inclusion_of :sex, :in => SEX_TYPES.map{|humanized_data,data| data} end
- entry.rhtml
<%= error_messages_for :user %> <% form_for :user, :url => {:action => :save_user} do |form| -%> <%= form.text_field :name, :size => 40 %> <%= form.select :sex, User::SEX_TYPES, :prompt => "性別" %> <%= submit_tag("登録") %> <% end -%>
この状態で、nameのフィールドを空にして、また、なんらかの方法でsexを「man,woman」以外の値をフォームで送信すると、デフォルトでは、以下のようなメッセージが出力される。
2 errors prohibited this order from being saved
There were problems with the following fields:
- Name can't be blank
- Sex is not included in the list
- ソース
<div class="errorExplanation" id="errorExplanation"> <h2>2 errors prohibited this order from being saved</h2> <p>There were problems with the following fields:</p> <ul> <li>Name can't be blank</li> <li>Sex is not included in the list</li> </ul> </div>
これを日本語化する。
以下の手順で解決する
1.「Name」等のカラム名を日本語かする
まず、どうやてってメッセージが表示されるのかを理解する
header_message = "#{pluralize(count, 'error')} prohibited this #{(options[:object_name] ||params.first).to_s.gsub('_', ' ')} from being saved" error_messages = objects.map {|object| object.errors.full_messages.map {|msg|content_tag(:li, msg) } } content_tag( :div, content_tag(options[:header_tag] || :h2, header_message) << content_tag(:p, 'There were problems with the following fields:') << content_tag(:ul, error_messages), html )
まず、「"#{pluralize(count, 'error')} prohibited this〜」ってとこが、手順3で日本語化する文章の部分だ。後の話になるが、これがメソッド内に直接かかれているってことは、上書きして〜ってことはできない。
注目すべきは以下の部分
error_messages = objects.map {|object| object.errors.full_messages.map {|msg|content_tag(:li, msg) } }
「contend_tag(:li, msg)」は、「<li>msg</li>」を生成する。つまり、このコードが、「<li>Name can't be blank</li>」を生成している。そして、「object.errors.full_messages」の部分が、「Name can't be blank」を生成している。よって、この部分を調べてみる。
def full_messages full_messages = [] @errors.each_key do |attr| @errors[attr].each do |msg| next if msg.nil? if attr == "base" full_messages << msg else full_messages << @base.class.human_attribute_name(attr) + " " + msg end end end full_messages end
ここだ。
full_messages << @base.class.human_attribute_name(attr) + " " + msg
このmsgが「can't be blank」だ。
@errors.each_keyはエラーが発生したフィールドの対応するキー、つまり、["name","sex"]という配列。
そして、@errors[:attr]には、対応するメッセージが入っているということだ。今回の場合、@errors["name"] #=> "can't be blank"ということ。
しかし、メッセージは「Name can't〜」となっている。この「Name」はどっからでてきたのか。それが、「@base.class.human_attribute_name(attr) 」の部分だ。つまり、ここのメソッドをいじれば良さそう。(実際は直接いじるのではなく、オーバーライドする)さっそく調べる。
- AtiveRecord::Base.human_attribute_name
def human_attribute_name(attribute_key_name) attribute_key_name.humanize end
このhumanizeによって"name"を"Name"にしている。しかし、humanizeメソッドには引数は渡せないので、オーバーライドするならこの「human_attribute_name」メソッドだろう。これは、ActiceRecord::Baseのクラスメソッドなので、Userモデル(これはActiveRecord::Baseを継承している)でオーバーライドすればよい。その方法は以下のようになる。
1.「Name」等のカラム名を日本語かするの解決策
class User < ActiveRecord::Base HUMANIZED_KEY_NAMES = { "name" => "名前", "sex" => "性別" } SEX_TYPES = [["男","man"],["女","woman"]] validates_presence_of :name validates_inclusion_of :sex, :in => SEX_TYPES.map{|humanized_data,data| data} class << self def human_attribute_name(attribute_key_name) HUMANIZED_KEY_NAMES[attribute_key_name] || super end end end
ハッシュを使って変換を行う。ハッシュに登録されていない場合は、元の「human_attribute_name」に処理を渡す。
2.「can't be blank」等の個々のエラーメッセージを日本語化する
さっき、ActiceRecord::Errors#full_messagesを見た際に@errors[:attr]で、そのカラムに対するエラーメッセージを参照できることが分かった。なので、@errors[:attr]に代入しているところを探す。
- ActiveRecord::Errors.add
def add(attribute, msg = @@default_error_messages[:invalid]) @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil? @errors[attribute.to_s] << msg end
- ActiveRecord::Errors.add_on_blank
def add_on_blank(attributes, msg = @@default_error_messages[:blank]) for attr in [attributes].flatten value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] add(attr, msg) if value.blank? end end
この2つで、「Name」フィールドが空の場合、@errorsにその内容を記録している。add_on_blankのfor文の最後のadd(attr, msg)でaddを呼び、そこで@errors[:name]にmsg(エラーメッセージ)が登録される。
よく見ると、msgが@@default_error_messagesハッシュのエントリーで初期化されている。これが、「can't be blank」メッセージを出しているっぽい。探す。
- ActiveRecord::Errors
@@default_error_messages = { :inclusion => "is not included in the list", :exclusion => "is reserved", :invalid => "is invalid", :confirmation => "doesn't match confirmation", :accepted => "must be accepted", :empty => "can't be empty", :blank => "can't be blank", :too_long => "is too long (maximum is %d characters)", :too_short => "is too short (minimum is %d characters)", :wrong_length => "is the wrong length (should be %d characters)", :taken => "has already been taken", :not_a_number => "is not a number" } cattr_accessor :default_error_messages
これをアプリケーション側でオーバーライドする。しかし、ApplicationRecordのようなモデルから共通参照できるものはないので、「environment.rb」にてオーバーライドする。
2.「can't be blank」等の個々のエラーメッセージを日本語化するの解決策
- environment.rb
# Include your application configuration below ActiveRecord::Errors.default_error_messages.merge!( :blank => "が入力されていません", :inclusion => "が適切な値ではありません" )
今回の場合は、「blank」と[inclusion]が関係あるので、merge!メソッドでそこのみ上書き。後は、元のメッセージを使う。
3.「2 errors〜fields:」までの全体のエラーメッセージを日本語化する
今まではデフォルト設定をオーバーライドしてきたが、ここに関しては少し違う。ActionView::Helpers::ActiveRecordHelper#error_messages_forの一部で見たように、メソッド内にメッセージが直接書かれているので、これは、一度error_messages_forを使って生成されたメッセージに対して、置換を行う方法でいけると思う。具体的には次のようになる。このコードは、ApplicationHelperに記述する。
3.「2 errors〜fields:」までの全体のエラーメッセージを日本語化するの解決策
def ja_error_messages_for(*params) result = error_messages_for(*params) result.sub!(/<h2>(\d+).*<\/h2>/) do "<h2>入力項目に#{$1}つのエラーがあります</h2>" end result.sub!(/<p>.*<\/p>/,"") end
英語のメッセージの部分を抽出し、日本語の文で置き換える。
合わせてentry.rhtmlのerror_messages_forの部分を次のように変更
- entry.rhtml(修正版)
<%= ja_error_messages_for :user %>
追記
本当は、3については次のようにやりたい
alias :old_error_messages_for :error_messages_for def error_messages_for(*params) result = old_error_messages_for(*params) result.sub!(/<h2>(\d+).*<\/h2>/) do "<h2>入力項目に#{$1}つのエラーがあります</h2>" end result.sub!(/<p>.*<\/p>/,"") end
しかし上手く行かない。なぜかスーパークラスのerror_messages_forが呼ばれてしまう><;