论坛首页 编程语言技术论坛

Active Record 错误信息本地化

浏览 14867 次
该帖已经被评为良好帖
作者 正文
   发表时间:2006-10-12  
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对象了。由于验证代码中使用"%"来格式化字符串(可查询validates_length_of实现代码),所以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不太建议使用,它只有一个参数,要保留原有功能的情况下,扩展的余地较小,还不如增加一个功能多点的方法,暂不去管它了。修改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>


效果图:
  • 大小: 58.9 KB
  • 大小: 11.7 KB
   发表时间:2006-10-12  
格式挺难看呢。。。。
0 请登录后投票
   发表时间:2006-10-12  
好贴,干吗不发布到论坛去?
0 请登录后投票
   发表时间:2006-10-12  
主要是还没写完,能不能写完了再发过去?

----------------------------------
怎么评论也过来了呢。。
0 请登录后投票
   发表时间:2006-10-13  
好想有一个类似的插件
activeheart
0 请登录后投票
   发表时间:2006-10-13  
melin 写道
好想有一个类似的插件
activeheart

按我的理解,activeheart只实现了“汉化”或“日语化”的功能,也就是启动后错误信息只能配置成一种语言,并不算是本地化,这个应该在Agile里配置部分提到过了。

我这里是为本地化提供一个支持,既然同一页面上可以显示不同语言,自然可以根据客户语言显示不同的提示信息。
0 请登录后投票
   发表时间:2006-10-13  
好帖子,应该精华.我目前是直接重写error_messages_for来显示message.茄子的方法是最彻底的
0 请登录后投票
   发表时间:2006-10-13  
好帖子,应该精华
0 请登录后投票
   发表时间:2006-10-14  
我找了一个方法,但此方法有点小Bug,如果qeizi可以看出来改一下,这个方法是不错的方法,我感觉。
module ApplicationHelper
  def error_messages_for(object_name, options = {})
    options = options.symbolize_keys
    object = instance_variable_get("@#{object_name}")
    unless object.errors.empty?
      error_lis = []
      object.errors.each{ |key,msg| error_lis << content_tag("li", msg) }  
      content_tag("div",
      content_tag(options[:header_tag] || "h3","发生#{object.errors.count}个错误"
      ) +
      content_tag("ul", error_lis), "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation" )
    end
  end
end
这个方法,可以实现中文错误提示,同时不显示字段名了。但确实我试了试有Bug,就是当一个表单还没有填写时,只是被下发时,在标记<div> <%= error_messages_for 'user' %></div>处报错。
提示:
#{RAILS_ROOT}/app/helpers/application_helper.rb:6:in `error_messages_for'
可惜呀,可惜。不然这是个非常好的方法了。
对了,这个是放在application.rb里的。
0 请登录后投票
   发表时间:2006-10-14  
jerry 写道
我找了一个方法,但此方法有点小Bug,如果qeizi可以看出来改一下,这个方法是不错的方法,我感觉。
module ApplicationHelper
  def error_messages_for(object_name, options = {}) 
    options = options.symbolize_keys 
    object = instance_variable_get("@#{object_name}") 
    unless object.errors.empty? 
      error_lis = [] 
      object.errors.each{ |key,msg| error_lis << content_tag("li", msg) }   
      content_tag("div", 
      content_tag(options[:header_tag] || "h3","发生#{object.errors.count}个错误" 
      ) + 
      content_tag("ul", error_lis), "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation" ) 
    end 
  end 
end

这个方法,可以实现中文错误提示,同时不显示字段名了。但确实我试了试有Bug,就是当一个表单还没有填写时,只是被下发时,在标记<div> <%= error_messages_for 'user' %></div>处报错。
提示:
#{RAILS_ROOT}/app/helpers/application_helper.rb:6:in `error_messages_for'
可惜呀,可惜。不然这是个非常好的方法了。
对了,这个是放在application.rb里的。

上面这句话的意思是?

你这个方法是“汉化”吧?你给的出错信息太模糊了,你的第6行是哪一行?抛出的异常是什么?这些都是应该给出的。不过我感觉这类排错应该可以自己解决的,发在这里好像有违这个论坛的宗旨。。

原来以为你的第6行是error_list = [],但这行是不可能出错的。所以猜测可能出错在unless那一行,出错原因应该是nil.errors吧,这种错误应该是error_messages_for使用错误了。如果你的变量名是@post,那么它的使用方法是:error_messages_for('post'),这样还出错的话,直接检查你的@post是不是没给值,去掉补丁测试看看。
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics