Active Recordエラー情報のローカライズ


ActiveRecordエラーメッセージは、すでにフォーマットされている英語の文字列であり、ローカライズ処理が不便です.ローカライズするには、エラーデータを保持し、表示時にローカル言語にフォーマットする必要があります.しかしActiveRecordはエラーメッセージを文字列にフォーマットするのが早すぎて、基本的にローカライズの道を断ち切った.
ActiveRecordエラーメッセージをローカライズするために、パッチを適用します.このコードの検証を確認すると、フォーマット文字列は各検証方法に分散しており、一つ一つ書き換えるのはあまりお得ではありません.幸いにもdefaultが呼び出されましたerror_Messagesメソッドでエラー情報文字列を取得するため,ここで少し文章を書くことを考える.

class ActiveRecord::ValidateError
  attr_reader :error
  def initialize(error, format, *args)
    @error = error
    @format = format
    @args = args
  end
  
  def to_s
    return @format if @args.empty?
    @format % @args
  end
end

class ActiveRecord::Errors
  def self.default_error_messages
    def self.default_error_messages
      @@_error_messages
    end
    @@_error_messages = {}
    @@default_error_messages.each do |key, value|
      @@_error_messages[key] = ActiveRecord::ValidateError.new(key, value)
    end
    @@_error_messages
  end

これでソースを絞めて、errorsの中は文字列ではなく、ValidateErrorオブジェクトになりました.検証コードでは「%」を使用して文字列をフォーマットするため、ValidateErrorクラスでも「%」:

class ActiveRecord::ValidateError
  def % (*args)
    self.class.new(@error, @format, *args)
  end
end

ValidateErrorオブジェクトを返します.
テストでfullが見つかりますMessagesでエラーが発生しました.なぜなら、「」+msgを使用しているからです.このmsgは現在ValidateErrorオブジェクトです.自然にこのように加算することはできません.ValidateErrorで実現する方法も見つかりません.pythonとDで書き換えることができるようです.rメソッドは、rubyで似たようなものがあるかどうか分かりません.実現策が見つからなかった以上、full_Messagesも書き換えました.

class ActiveRecord::Errors
  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.to_s
        else
          full_messages << @base.class.human_attribute_name(attr) + " " + msg[b].to_s[/b]
        end
      end
    end
    full_messages
  end
end

これだけコードを付けても書き換えます...
次に、ローカライズを追加し、一時的にformatメソッドを追加して、ローカル言語のフォーマット文字列を受け入れることを考慮することができます.

class ActiveRecord::ValidateError
  def format(format)
    format ||= @format
    [b]return format if @error == :string[/b]
    return format if @args.empty?
    format % @args
  end
end

太字行は予約された拡張です.
ローカリゼーションが可能になりました.

<%
  zh_cn_error_messages = {
    :inclusion => "        ", 
    :exclusion => "    ", 
    :invalid => "  ", 
    :confirmation => "     ", 
    :accepted  => "    ", 
    :empty => "    ", 
    :blank => "    ", 
    :too_long => "   (   %d    )", 
    :too_short => "   (   %d    )", 
    :wrong_length => "     (   %d    )", 
    :taken => "    ", 
    :not_a_number => "    " ,
    :must_number => "     "
  }
  
  en_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"
  }
%>

<p><label for="post_title">Title</label><br/>
<%= text_field 'post', 'title'  %>
<%= @post.errors.on(:title).map{|e| e.join("<br />") %></p>

<p><label for="post_title">Title</label><br/>
<%= text_field 'post', 'title'  %>
<%= @post.errors.on(:title).map{|e| e.format(zh_cn_error_messages[e.error])}.join("<br />") %></p>

同じエラーメッセージが同じページに2言語表示されています.
error_messages_forメソッドはまだローカライズされていませんが、実際にはhelperにすぎません.まったく使わなくてもいいです.広く使われていることを考慮して、パッチもかけましょう.この方法では文字列が2箇所ハードコーディングされており,抽出が考えられる.またfull_Messagesメソッドも早すぎてエラーメッセージを文字列化してしまい、従来の機能に影響を及ぼさないように書き換えたerror_messages_forでは別の機能に近いメソッドを呼び出します.

class ActiveRecord::Errors
  def full_messages_with_key
    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
end

ActionView::Helpers::ActiveRecordHelper.module_eval do
  def error_messages_for(*params)
    options = params.last.is_a?(Hash) ? params.pop.symbolize_keys : {}
    objects = params.collect {|object_name| instance_variable_get("@#{object_name}") }.compact
    count   = objects.inject(0) {|sum, object| sum + object.errors.count }
    error_messages = options[:error_messages]
    unless count.zero?
      html = {}
      [:id, :class].each do |key|
        if options.include?(key)
          value = options[key]
          html[key] = value unless value.blank?
        else
          html[key] = 'errorExplanation'
        end
      end
      header_message = options[:caption] || "#{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_with_key.map {|field, error| content_tag(:li, field + " " + 
      (error_messages.nil? ? error.to_s : error.format(error_messages[error.error]))) } }
      content_tag(:div,
        content_tag(options[:header_tag] || :h2, header_message) <<
          content_tag(:p, options[:prompt] || 'There were problems with the following fields:') <<
          content_tag(:ul, error_messages),
        html
      )
    else
      ''
    end
  end
end 

使用できるようになりました.

<%
<%= error_messages_for 'post' %>

<%= error_messages_for 'post', :error_messages => zh_cn_error_messages,
                               :prompt => "        :",
                               :caption => "         #{@post.errors.count}    " %>

同じページに2言語のエラーメッセージが表示されています.
残りの処理はErrors#addとErrors#addです.to_base,add_to_baseはあまり推奨されていません.パラメータが1つしかありません.既存の機能を維持する場合は、拡張の余地が小さく、機能の多点を増やす方法で、しばらく管理しないほうがいいです.addメソッドを修正しerrorsを使用するつもりです.add(:title,:must_number,"must number")とerrors.add(:title,“must number”)の2つの使い方で、前者はローカライズをサポートしています.add_のためto_baseもaddを呼び出すので、これを書き直せばいいです.

class ActiveRecord::Errors
  alias_method :add_old, :add
  
  def add(attribute, msg = @@default_error_messages[:invalid], *args)
    return add_old(attribute, ActiveRecord::ValidateError.new(msg, *args)) if msg.is_a?(Symbol)
    unless msg.is_a?(ActiveRecord::ValidateError)
      msg = ActiveRecord::ValidateError.new(:string, msg, *args)
    end
    add_old(attribute, msg)
  end
end

テストコード:

class Post < ActiveRecord::Base
  has_many :comments, :dependent => :destroy
  validates_presence_of :title
  validates_length_of :title, :in => 3 .. 5
  
  def validate
    if title.nil? || title.blank?
      errors.add_to_base("You must specify a name or an email address" )
      errors.add(:title, :length_must_great_than, "length must > %d", 3)
      errors.add(:title, :must_number, "must number")
      errors.add(:title, "Must Must")
    end
  end
end

views/posts/_form.rhtml

<%
  zh_cn_error_messages = {
    :inclusion => "        ", 
    :exclusion => "    ", 
    :invalid => "  ", 
    :confirmation => "     ", 
    :accepted  => "    ", 
    :empty => "    ", 
    :blank => "    ", 
    :too_long => "   (   %d    )", 
    :too_short => "   (   %d    )", 
    :wrong_length => "     (   %d    )", 
    :taken => "    ", 
    :not_a_number => "    " ,
    :must_number => "     ",
    :length_must_great_than => "     %d "
  }
  
  en_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"
  }
%>

<%= error_messages_for 'post' %>

<%= error_messages_for 'post', :error_messages => zh_cn_error_messages,
                               :prompt => "        :",
                               :caption => "         #{@post.errors.count}    " %>

<!--[form:post]-->
<p><label for="post_title">Title</label><br/>
<%= text_field 'post', 'title'  %>
<%= @post.errors.on(:title).map{|e| e.format(zh_cn_error_messages[e.error])}.join("<br />") if @post.errors.on(:title) %></p>

<p><label for="post_title">Title</label><br/>
<%= text_field 'post', 'title'  %>
<%= @post.errors.on(:title).map(&:to_s).join("<br />") if @post.errors.on(:title) %></p>

効果図: