`
qiezi
  • 浏览: 497752 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

Active Record 错误信息本地化

    博客分类:
  • Ruby
阅读更多
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
分享到:
评论
14 楼 richard_zhu 2007-01-28  
在http://www.iteye.com/topic/20123?page=3找到了一个human_name_plugin。

require 'active_record/connection_adapters/abstract/schema_definitions'  
  
module Foo  
  module AdjustHumanName  
    module Work  
      def self.append_features(base)  
        super  
        base.extend ClassMethods  
    ActiveRecord::ConnectionAdapters::Column.class_eval do  
      alias_method :orginal_human_name, :human_name  
      def human_name  
        return @human_name_value if @human_name_value  
        return orginal_human_name  
      end  
    end  
      end  
    end  
      
    module ClassMethods  
    def human_name(kw)  
      kw.each_pair do |key,value|  
        col = self.columns_hash[key.to_s]  
        col.instance_variable_set :@human_name_value,value.to_s  
      end  
    end   
    end  
          
  end  
end  
  
ActiveRecord::Base.class_eval do  
  include Foo::AdjustHumanName::Work  
end 
13 楼 richard_zhu 2007-01-28  
qiezi的这个补丁可不可以把 field 字段也给汉化了,虽然调用@base.class.human_attribute_name(attr)返回的是字段名的友好形式,但终归是英文的。
可不可以做到返回字段的备注信息? 或者可不可以通过扩展 Migration 给英文字段名添加其他备注信息(如对应的汉字名)?Django在设计 model 的时候就是这么做的。
希望 ruby高手们考虑一下如何实现。
12 楼 qiezi 2006-10-14  
虽然你写了这么多,我还是没到你描述了更多的信息。

1、你的第6行是代码中的哪一行?按你给的代码,应该是error_list = []这一行,如果真是这样,那么我看你的ruby解释器问题大了。。。
2、出错时的异常信息是什么?就是页面最上面那一点,你把它翻译成中文再告诉我没有错(你说的第6行),但不要把更重要的东西给漏掉了。
3、你生成scaffold测试看看,如果还有错,把测试信息最重要的部分发出来。
11 楼 cookoo 2006-10-14  
application.rb? helper为什么定义到controller里去?
10 楼 jerry 2006-10-14  
不是我不调试
这个问题就出在Application.rb的第6行上.这个代码我在Rails英文网上查过,也在Railscn上找到过.但我试过了,放在Application.rb中时如果我们用Messager_for......时都不好用.如果我们用Messager_for显示出错信息,那么下发空表单时就出错.

流程是这样的.
1.把那些代码Copy到Application.rb中,目的覆盖Messager_for方法
2.有验证的表单中一般都会有Messager_for....吧.这时下发这个表单
3.报错了.

我用另一个方法绕过了Messager_for出错,可以显示出来表单验证出错信息.
流程是这样的.
1.把那些代码Copy到Application.rb中,目的覆盖Messager_for方法
2.有验证的表单中一般都会有Messager_for....吧(先把网页这个Messager_for去掉).这时下发这个表单
3.填写表单内容.同时把表单网页中的Messager_for.....再加回来.这样网页中又有Messager_for...了
4.提交表单
5.报错的信息正确的显示出来了.
9 楼 qiezi 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是不是没给值,去掉补丁测试看看。
8 楼 jerry 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里的。
7 楼 为你而来 2006-10-13  
好帖子,应该精华
6 楼 dennis_zane 2006-10-13  
好帖子,应该精华.我目前是直接重写error_messages_for来显示message.茄子的方法是最彻底的
5 楼 qiezi 2006-10-13  
melin 写道
好想有一个类似的插件
activeheart

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

我这里是为本地化提供一个支持,既然同一页面上可以显示不同语言,自然可以根据客户语言显示不同的提示信息。
4 楼 melin 2006-10-13  
好想有一个类似的插件
activeheart
3 楼 qiezi 2006-10-12  
主要是还没写完,能不能写完了再发过去?

----------------------------------
怎么评论也过来了呢。。
2 楼 robbin 2006-10-12  
好贴,干吗不发布到论坛去?
1 楼 qiezi 2006-10-12  
格式挺难看呢。。。。

相关推荐

    基于PHP的Yii Framework php框架.zip

    10. **国际化和本地化**:Yii 提供了完善的国际化(i18n)和本地化(l10n)支持,方便开发者构建多语言应用。 11. **社区和文档**:Yii 拥有一个活跃的开发者社区,提供了详细的官方文档、教程和众多的第三方扩展,...

    yii框架 最新

    Yii提供了丰富的特性,包括MVC(模型-视图-控制器)架构模式、ActiveRecord ORM(对象关系映射)、I18N和L10N(国际化和本地化)、缓存机制、安全防护以及测试支持等,使得开发者能够快速构建大型、复杂的Web应用...

    yii框架中文手册教程和YII模板

    首先,Yii框架的核心特性包括MVC(模型-视图-控制器)设计模式、Active Record ORM(对象关系映射)、I18N和L10N(国际化和本地化)支持、缓存策略、安全性和权限管理等。MVC模式使得代码结构清晰,易于维护。Active...

    yii php框架最新版本

    8. **国际化与本地化**:Yii内置了完整的i18n(国际化)和l10n(本地化)支持,可以方便地创建多语言的应用程序。 9. **测试框架**:Yii提供了单元测试、功能测试和验收测试的工具,帮助开发者进行代码质量控制。 ...

    PHP-Yii2中文手册.rar

    它强调代码的重用性和可扩展性,提供了丰富的特性,包括MVC(Model-View-Controller)、I18N(国际化)和L10N(本地化)、缓存策略、数据库抽象层、主动记录(Active Record)、身份验证和访问控制、脚手架(Gii)...

    一个基于 Yii2 高级框架的快速开发应用引擎.zip

    8. **国际化与本地化**:Yii2 支持多语言,RageFrame2 可能预设了良好的国际化支持,便于全球化部署。 9. **缓存策略**:通过缓存机制,如文件缓存、数据库缓存、Redis 或 Memcached 等,优化了性能。 10. **文档...

    rails国际化

    9. **第三方库扩展**:除了Rails自带的i18n功能,还可以使用如`globalize`、`active_record-i18n`等库来进一步增强国际化功能,例如实现内容的多语言版本管理。 总之,Rails的国际化功能强大且易于使用,它使得构建...

    Yii框架文档(2010.7.18)

    ### 国际化和本地化 为了支持多语言应用,Yii提供了国际化支持,包括消息翻译和语言选择等功能。 ### 安全措施 安全是Web应用开发中至关重要的一环。Yii提供了多种安全措施,如数据验证、CSRF保护和安全模型属性...

    yiiframework官方最新版,包括源程序、api、文档

     9、国际化(I18N)和本地化(L10N):Yii支持消息转换,日期和时间格式,数字格式,和界面本地化。  10、分层缓存方案:Yii支持数据缓存,页面缓存,片段缓存和动态内容。缓存的存储介质,可以轻松地更改而不触及...

    Yii2的基本应用程序模板 yii-basic-app-2.0.12

    10. **国际化(i18n)和本地化(l10n)**: - 提供了翻译工具和资源管理,方便应用的多语言支持。 11. **Gii代码生成工具**: - 内置的Gii工具可以帮助快速生成模型、控制器、视图和其他代码,减少重复工作。 12...

    yii-frameword-2.0.0-beta

    10. **国际化和本地化**:Yii 2.0 支持多语言,方便开发多语言应用。 11. **模板引擎**:使用Twig或PHP作为视图模板引擎,让视图代码更加简洁。 12. **单元测试和集成测试**:Yii 2.0 提供了对PHPUnit的支持,便于...

    iCakePHP-master.zip

    CakePHP 支持多语言应用开发,通过 I18n(国际化)和 L10n(本地化)功能,开发者可以轻松地为不同地区的用户提供本地化的用户体验。 ### 扩展性和社区支持 作为成熟的 PHP 框架,CakePHP 有着丰富的插件和扩展库,...

    CodeIgniter-3.0.6框架下载

    14. **国际化与本地化**:CI 3.0.6支持多语言,通过语言文件轻松实现应用程序的国际化和本地化。 15. **开发者工具**:包括基准测试、内存消耗报告、错误调试等工具,帮助开发者分析和优化代码性能。 在下载并解压...

    Yii 1.1 Application Development Cookbook.pdf

    国际化与本地化 - **语言切换**:支持多语言界面显示。 - **日期时间格式化**:根据用户所在地区调整日期时间显示格式。 - **货币符号处理**:正确显示不同国家的货币单位。 ##### 8. 错误处理与日志记录 - **异常...

    基于yii开发的开源CMS

    2. Active Record:Yii的ActiveRecord实现使得数据库操作变得简单,通过对象方法直接与数据库交互,降低了数据层的复杂性。 3. 开发效率:Yii的代码生成工具Gii可以自动生成模型、控制器、表单等基础代码,大大提高...

    activeandroid-3.1-beta_android_merely6tt_

    ActiveAndroid是一个基于活动记录(Active Record)设计模式的轻量级对象关系映射(ORM)框架,专为Android平台打造。它使得开发者无需编写繁琐的SQLite数据库操作代码,即可实现数据的存储和查询,极大地提升了开发...

    YII开源项目

    它提供了丰富的特性,包括MVC(模型-视图-控制器)架构模式、活跃记录(Active Record)、I18N和L10N(国际化和本地化)、缓存机制、安全性控制以及测试工具等,使得开发者能够高效地构建复杂、高性能的Web应用。...

    yii2-master

    12. **国际化与本地化**:Yii2支持多语言和区域设置,便于开发全球化应用。 综上所述,Yii2框架凭借其全面的功能、高效的性能以及良好的社区支持,成为PHP开发者构建Web 2.0应用的理想选择。无论你是初学者还是经验...

    PHP YII框架教程+笔记.zip

    "YiiPPT.zip"可能包含了一系列关于Yii框架的演示文稿,详细介绍了框架的各个方面,例如路由配置、URL管理、表单处理、AJAX支持、I18N(国际化)和L10N(本地化)功能。这些PPT可能会通过实例讲解如何在Yii中实现这些...

    CI中文参考手册word版

    10. **国际化与本地化**:CI支持多语言,方便开发全球化的应用。 本手册“CI中文用户手册.doc”将详细解释以上所有特性,并深入探讨如何配置环境、安装框架、创建控制器、模型、视图,以及如何进行数据处理、表单...

Global site tag (gtag.js) - Google Analytics