Active Record 错误信息本地化

ActiveRecord出错信息是已经格式化过的英文字符串,这很不方便做本地化处理。要想做本地化,必须保留错误数据,在显示时再格式化为本地语言。不过ActiveRecord过早地把错误信息格式化为字符串,基本上已经断绝了本地化这条路。

为了让ActiveRecord错误信息可以本地化,我采用打补丁的方式。查看验证这部分代码,发现格式化字符串分散在各个验证方法中,一一重写不大合算。好在它是调用default_error_messages方法来取得错误信息字符串,于是考虑在这里做点文章。

[code]
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
[/code]

这下掐了源头,errors里面不再是字符串,而是ValidateError对象了。由于验证代码中使用"%"来格式化字符串(可查询validates_length_of实现代码),所以ValidateError类也需要实现"%":

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

还是返回一个ValidateError对象。

现在测试会发现full_messages里面出错,原因是它使用的是" " + msg,这个msg现在是ValidateError对象了,自然不能这样相加,也没找到好的办法在ValidateError中实现。python和D中似乎可以重写_r方法,在ruby中不知道有没有类似的玩意。既然没找到实现办法,只好把full_messages也重写了:

[code]
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
[/code]
加了这么一点代码也要重写。。。

接着可以考虑增加本地化了,暂时为它加上一个format方法,接受一个本地语言的格式化字符串:
[code]
class ActiveRecord::ValidateError
def format(format)
format ||= @format
[b]return format if @error == :string[/b]
return format if @args.empty?
format % @args
end
end
[/code]
加粗行是预留的扩展。

现在已经可以实现本地化了:
[code]
<%
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>
[/code]

同样的错误信息在同一页面上显示了2种语言。

error_messages_for方法还没有实现本地化,实际上它只是个helper而已,完全可以不用它。考虑到它使用也很广泛,把它也打上补丁吧。这个方法里面有2处字符串被硬编码在里面了,可以考虑提取出来。另外full_messages方法也过早地把错误信息字符串化了,为了不影响原有功能,在重写的error_messages_for里面调用另一个功能相近的方法。
[code]
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
[/code]

现在可以使用了:
[code]
<%
<%= error_messages_for 'post' %>

<%= error_messages_for 'post', :error_messages => zh_cn_error_messages,
:prompt => "下列字段发生错误:",
:caption => "保存帖子时发生了 #{@post.errors.count} 个错误" %>
[/code]
同样是在同一个页面上显示了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,所以只要重写这个就行了。

[code]
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
[/code]

测试代码:
[code]
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
[/code]
views/posts/_form.rhtml
[code]
<%
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>
[/code]

效果图:
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值