Skip to content
This repository
Browse code

integrating I18n into Rails

  • Loading branch information...
commit 45d41f0dadd9fa171f306ff356770c4492726f30 1 parent 40557e1
Sven Fuchs authored June 19, 2008
2  actionpack/lib/action_view.rb
@@ -32,6 +32,8 @@
32 32
 require 'action_view/partials'
33 33
 require 'action_view/template_error'
34 34
 
  35
+require 'action_view/lang/en-US.rb'
  36
+
35 37
 ActionView::Base.class_eval do
36 38
   include ActionView::Partials
37 39
 
33  actionpack/lib/action_view/helpers/active_record_helper.rb
@@ -151,12 +151,17 @@ def error_message_on(object, method, prepend_text = "", append_text = "", css_cl
151 151
       # instance yourself and set it up. View the source of this method to see how easy it is.
152 152
       def error_messages_for(*params)
153 153
         options = params.extract_options!.symbolize_keys
  154
+
154 155
         if object = options.delete(:object)
155 156
           objects = [object].flatten
156 157
         else
157 158
           objects = params.collect {|object_name| instance_variable_get("@#{object_name}") }.compact
158 159
         end
159  
-        count   = objects.inject(0) {|sum, object| sum + object.errors.count }
  160
+        
  161
+        count  = objects.inject(0) {|sum, object| sum + object.errors.count }
  162
+        locale = options[:locale]
  163
+        locale ||= request.locale if respond_to?(:request)
  164
+
160 165
         unless count.zero?
161 166
           html = {}
162 167
           [:id, :class].each do |key|
@@ -168,21 +173,29 @@ def error_messages_for(*params)
168 173
             end
169 174
           end
170 175
           options[:object_name] ||= params.first
171  
-          options[:header_message] = "#{pluralize(count, 'error')} prohibited this #{options[:object_name].to_s.gsub('_', ' ')} from being saved" unless options.include?(:header_message)
172  
-          options[:message] ||= 'There were problems with the following fields:' unless options.include?(:message)
173  
-          error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join
174 176
 
175  
-          contents = ''
176  
-          contents << content_tag(options[:header_tag] || :h2, options[:header_message]) unless options[:header_message].blank?
177  
-          contents << content_tag(:p, options[:message]) unless options[:message].blank?
178  
-          contents << content_tag(:ul, error_messages)
  177
+          I18n.with_options :locale => locale, :scope => [:active_record, :error] do |locale|
  178
+            header_message = if options.include?(:header_message)
  179
+              options[:header_message]
  180
+            else 
  181
+              object_name = options[:object_name].to_s.gsub('_', ' ')
  182
+              locale.t :header_message, :count => count, :object_name => object_name
  183
+            end
  184
+            message = options.include?(:message) ? options[:message] : locale.t(:message)
  185
+            error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join
  186
+
  187
+            contents = ''
  188
+            contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank?
  189
+            contents << content_tag(:p, message) unless message.blank?
  190
+            contents << content_tag(:ul, error_messages)
179 191
 
180  
-          content_tag(:div, contents, html)
  192
+            content_tag(:div, contents, html)
  193
+          end
181 194
         else
182 195
           ''
183 196
         end
184 197
       end
185  
-
  198
+      
186 199
       private
187 200
         def all_input_tags(record, record_name, options)
188 201
           input_block = options[:input_block] || default_input_block
81  actionpack/lib/action_view/helpers/date_helper.rb
@@ -58,35 +58,43 @@ module DateHelper
58 58
       #   distance_of_time_in_words(to_time, from_time, true)     # => over 6 years
59 59
       #   distance_of_time_in_words(Time.now, Time.now)           # => less than a minute
60 60
       #
61  
-      def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
  61
+      def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {})
  62
+        locale = options[:locale] 
  63
+        locale ||= request.locale if respond_to?(:request)
  64
+
62 65
         from_time = from_time.to_time if from_time.respond_to?(:to_time)
63 66
         to_time = to_time.to_time if to_time.respond_to?(:to_time)
64 67
         distance_in_minutes = (((to_time - from_time).abs)/60).round
65 68
         distance_in_seconds = ((to_time - from_time).abs).round
66 69
 
67  
-        case distance_in_minutes
68  
-          when 0..1
69  
-            return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds
70  
-            case distance_in_seconds
71  
-              when 0..4   then 'less than 5 seconds'
72  
-              when 5..9   then 'less than 10 seconds'
73  
-              when 10..19 then 'less than 20 seconds'
74  
-              when 20..39 then 'half a minute'
75  
-              when 40..59 then 'less than a minute'
76  
-              else             '1 minute'
77  
-            end
  70
+        I18n.with_options :locale => locale, :scope => :'datetime.distance_in_words' do |locale|
  71
+          case distance_in_minutes
  72
+            when 0..1
  73
+              return distance_in_minutes == 0 ? 
  74
+                     locale.t(:less_than_x_minutes, :count => 1) :
  75
+                     locale.t(:x_minutes, :count => distance_in_minutes) unless include_seconds
  76
+
  77
+              case distance_in_seconds
  78
+                when 0..4   then locale.t :less_than_x_seconds, :count => 5
  79
+                when 5..9   then locale.t :less_than_x_seconds, :count => 10
  80
+                when 10..19 then locale.t :less_than_x_seconds, :count => 20
  81
+                when 20..39 then locale.t :half_a_minute
  82
+                when 40..59 then locale.t :less_than_x_minutes, :count => 1
  83
+                else             locale.t :x_minutes,           :count => 1
  84
+              end
78 85
 
79  
-          when 2..44           then "#{distance_in_minutes} minutes"
80  
-          when 45..89          then 'about 1 hour'
81  
-          when 90..1439        then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
82  
-          when 1440..2879      then '1 day'
83  
-          when 2880..43199     then "#{(distance_in_minutes / 1440).round} days"
84  
-          when 43200..86399    then 'about 1 month'
85  
-          when 86400..525599   then "#{(distance_in_minutes / 43200).round} months"
86  
-          when 525600..1051199 then 'about 1 year'
87  
-          else                      "over #{(distance_in_minutes / 525600).round} years"
  86
+            when 2..44           then locale.t :x_minutes,      :count => distance_in_minutes
  87
+            when 45..89          then locale.t :about_x_hours,  :count => 1
  88
+            when 90..1439        then locale.t :about_x_hours,  :count => (distance_in_minutes.to_f / 60.0).round
  89
+            when 1440..2879      then locale.t :x_days,         :count => 1
  90
+            when 2880..43199     then locale.t :x_days,         :count => (distance_in_minutes / 1440).round
  91
+            when 43200..86399    then locale.t :about_x_months, :count => 1
  92
+            when 86400..525599   then locale.t :x_months,       :count => (distance_in_minutes / 43200).round
  93
+            when 525600..1051199 then locale.t :about_x_years,  :count => 1
  94
+            else                      locale.t :over_x_years,   :count => (distance_in_minutes / 525600).round
  95
+          end
88 96
         end
89  
-      end
  97
+      end      
90 98
 
91 99
       # Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
92 100
       #
@@ -498,13 +506,19 @@ def select_day(date, options = {}, html_options = {})
498 506
       #   select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...))
499 507
       #
500 508
       def select_month(date, options = {}, html_options = {})
  509
+        locale = options[:locale] 
  510
+        locale ||= request.locale if respond_to?(:request)
  511
+
501 512
         val = date ? (date.kind_of?(Fixnum) ? date : date.month) : ''
502 513
         if options[:use_hidden]
503 514
           hidden_html(options[:field_name] || 'month', val, options)
504 515
         else
505 516
           month_options = []
506  
-          month_names = options[:use_month_names] || (options[:use_short_month] ? Date::ABBR_MONTHNAMES : Date::MONTHNAMES)
  517
+          month_names = options[:use_month_names] || begin
  518
+            (options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names').t locale
  519
+          end
507 520
           month_names.unshift(nil) if month_names.size < 13
  521
+
508 522
           1.upto(12) do |month_number|
509 523
             month_name = if options[:use_month_numbers]
510 524
               month_number
@@ -522,7 +536,7 @@ def select_month(date, options = {}, html_options = {})
522 536
           end
523 537
           select_html(options[:field_name] || 'month', month_options.join, options, html_options)
524 538
         end
525  
-      end
  539
+      end      
526 540
 
527 541
       # Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius
528 542
       # can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the +options+. Both ascending and descending year
@@ -612,15 +626,17 @@ def to_datetime_select_tag(options = {}, html_options = {})
612 626
 
613 627
       private
614 628
         def date_or_time_select(options, html_options = {})
  629
+          locale = options[:locale]
  630
+
615 631
           defaults = { :discard_type => true }
616 632
           options  = defaults.merge(options)
617 633
           datetime = value(object)
618 634
           datetime ||= default_time_from_options(options[:default]) unless options[:include_blank]
619  
-
  635
+    
620 636
           position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
621 637
 
622  
-          order = (options[:order] ||= [:year, :month, :day])
623  
-
  638
+          order = options[:order] ||= :'date.order'.t(locale)
  639
+    
624 640
           # Discard explicit and implicit by not being included in the :order
625 641
           discard = {}
626 642
           discard[:year]   = true if options[:discard_year] or !order.include?(:year)
@@ -629,19 +645,19 @@ def date_or_time_select(options, html_options = {})
629 645
           discard[:hour]   = true if options[:discard_hour]
630 646
           discard[:minute] = true if options[:discard_minute] or discard[:hour]
631 647
           discard[:second] = true unless options[:include_seconds] && !discard[:minute]
632  
-
  648
+    
633 649
           # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are valid
634 650
           # (otherwise it could be 31 and february wouldn't be a valid date)
635 651
           if datetime && discard[:day] && !discard[:month]
636 652
             datetime = datetime.change(:day => 1)
637 653
           end
638  
-
  654
+    
639 655
           # Maintain valid dates by including hidden fields for discarded elements
640 656
           [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
641  
-
  657
+    
642 658
           # Ensure proper ordering of :hour, :minute and :second
643 659
           [:hour, :minute, :second].each { |o| order.delete(o); order.push(o) }
644  
-
  660
+    
645 661
           date_or_time_select = ''
646 662
           order.reverse.each do |param|
647 663
             # Send hidden fields for discarded elements once output has started
@@ -656,9 +672,8 @@ def date_or_time_select(options, html_options = {})
656 672
                 when :second then options[:include_seconds] ? " : " : ""
657 673
                 else ""
658 674
               end)
659  
-
660 675
           end
661  
-
  676
+    
662 677
           date_or_time_select
663 678
         end
664 679
 
52  actionpack/lib/action_view/helpers/form_options_helper.rb
@@ -276,16 +276,25 @@ def option_groups_from_collection_for_select(collection, group_method, group_lab
276 276
       # that they will be listed above the rest of the (long) list.
277 277
       #
278 278
       # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
279  
-      def country_options_for_select(selected = nil, priority_countries = nil)
  279
+      def country_options_for_select(*args)
  280
+        options = args.extract_options!
  281
+        
  282
+        locale = options[:locale]
  283
+        locale ||= request.locale if respond_to?(:request)
  284
+        
  285
+        selected, priority_countries = *args        
  286
+        countries = :'countries.names'.t options[:locale]
280 287
         country_options = ""
281 288
 
282 289
         if priority_countries
  290
+          # TODO priority_countries need to be translated?
283 291
           country_options += options_for_select(priority_countries, selected)
284 292
           country_options += "<option value=\"\" disabled=\"disabled\">-------------</option>\n"
285 293
         end
286 294
 
287  
-        return country_options + options_for_select(COUNTRIES, selected)
  295
+        return country_options + options_for_select(countries, selected)
288 296
       end
  297
+      
289 298
 
290 299
       # Returns a string of option tags for pretty much any time zone in the
291 300
       # world. Supply a TimeZone name as +selected+ to have it marked as the
@@ -340,43 +349,8 @@ def option_value_selected?(value, selected)
340 349
         end
341 350
 
342 351
         # All the countries included in the country_options output.
343  
-        COUNTRIES = ["Afghanistan", "Aland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola",
344  
-          "Anguilla", "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria",
345  
-          "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin",
346  
-          "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", "Botswana", "Bouvet Island", "Brazil",
347  
-          "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia",
348  
-          "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China",
349  
-          "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo",
350  
-          "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba",
351  
-          "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt",
352  
-          "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)",
353  
-          "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia",
354  
-          "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea",
355  
-          "Guinea-Bissau", "Guyana", "Haiti", "Heard and McDonald Islands", "Holy See (Vatican City State)",
356  
-          "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq",
357  
-          "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya",
358  
-          "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan",
359  
-          "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya",
360  
-          "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic Of",
361  
-          "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique",
362  
-          "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of",
363  
-          "Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru",
364  
-          "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger",
365  
-          "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau",
366  
-          "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines",
367  
-          "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation",
368  
-          "Rwanda", "Saint Barthelemy", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia",
369  
-          "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino",
370  
-          "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore",
371  
-          "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa",
372  
-          "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname",
373  
-          "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic",
374  
-          "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste",
375  
-          "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan",
376  
-          "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom",
377  
-          "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela",
378  
-          "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara",
379  
-          "Yemen", "Zambia", "Zimbabwe"] unless const_defined?("COUNTRIES")
  352
+        # only included for backwards compatibility, please use the I18n interface
  353
+        COUNTRIES = :'countries.names'.t 'en-US' unless const_defined?("COUNTRIES")
380 354
     end
381 355
 
382 356
     class InstanceTag #:nodoc:
18  actionpack/lib/action_view/helpers/number_helper.rb
@@ -69,13 +69,19 @@ def number_to_phone(number, options = {})
69 69
       #  number_to_currency(1234567890.50, :unit => "&pound;", :separator => ",", :delimiter => "", :format => "%n %u")
70 70
       #  # => 1234567890,50 &pound;
71 71
       def number_to_currency(number, options = {})
72  
-        options   = options.stringify_keys
73  
-        precision = options["precision"] || 2
74  
-        unit      = options["unit"] || "$"
75  
-        separator = precision > 0 ? options["separator"] || "." : ""
76  
-        delimiter = options["delimiter"] || ","
77  
-        format    = options["format"] || "%u%n"
  72
+        options = options.symbolize_keys
  73
+        
  74
+        locale = options[:locale]
  75
+        locale ||= request.locale if respond_to?(:request)
78 76
 
  77
+        defaults  = :'currency.format'.t(locale) || {}
  78
+        precision = options[:precision] || defaults[:precision]
  79
+        unit      = options[:unit]      || defaults[:unit]
  80
+        separator = options[:separator] || defaults[:separator]
  81
+        delimiter = options[:delimiter] || defaults[:delimiter]
  82
+        format    = options[:format]    || defaults[:format]
  83
+        separator = '' if precision == 0
  84
+        
79 85
         begin
80 86
           parts = number_with_precision(number, precision).split('.')
81 87
           format.gsub(/%n/, number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s).gsub(/%u/, unit)
93  actionpack/lib/action_view/lang/en-US.rb
... ...
@@ -0,0 +1,93 @@
  1
+I18n.backend.add_translations :'en-US', {
  2
+  :date => {
  3
+    :formats => {
  4
+      :default => "%Y-%m-%d",
  5
+      :short => "%b %d",
  6
+      :long => "%B %d, %Y",
  7
+    },
  8
+    :day_names => Date::DAYNAMES,
  9
+    :abbr_day_names => Date::ABBR_DAYNAMES,
  10
+    :month_names => Date::MONTHNAMES,
  11
+    :abbr_month_names => Date::ABBR_MONTHNAMES,
  12
+    :order => [:year, :month, :day]
  13
+  },
  14
+  :time => {
  15
+    :formats => {
  16
+      :default => "%a, %d %b %Y %H:%M:%S %z",
  17
+      :short => "%d %b %H:%M",
  18
+      :long => "%B %d, %Y %H:%M",
  19
+    },
  20
+    :am => 'am',
  21
+    :pm => 'pm'
  22
+  },
  23
+  :datetime => {
  24
+    :distance_in_words => {
  25
+      :half_a_minute       => 'half a minute',
  26
+      :less_than_x_seconds => ['less than 1 second', 'less than {{count}} seconds'],
  27
+      :x_seconds           => ['1 second', '{{count}} seconds'],
  28
+      :less_than_x_minutes => ['less than a minute', 'less than {{count}} minutes'],
  29
+      :x_minutes           => ['1 minute', '{{count}} minutes'],
  30
+      :about_x_hours       => ['about 1 hour', 'about {{count}} hours'],
  31
+      :x_days              => ['1 day', '{{count}} days'],
  32
+      :about_x_months      => ['about 1 month', 'about {{count}} months'],
  33
+      :x_months            => ['1 month', '{{count}} months'],
  34
+      :about_x_years       => ['about 1 year', 'about {{count}} year'],
  35
+      :over_x_years        => ['over 1 year', 'over {{count}} years']
  36
+    }
  37
+  },
  38
+  :currency => {
  39
+    :format => {
  40
+      :unit => '$',
  41
+      :precision => 2,
  42
+      :separator => '.',
  43
+      :delimiter => ',',
  44
+      :format => '%u%n',
  45
+    }
  46
+  },
  47
+  :countries => {
  48
+    :names => ["Afghanistan", "Aland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola",
  49
+      "Anguilla", "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria",
  50
+      "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin",
  51
+      "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", "Botswana", "Bouvet Island", "Brazil",
  52
+      "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia",
  53
+      "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China",
  54
+      "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo",
  55
+      "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba",
  56
+      "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt",
  57
+      "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)",
  58
+      "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia",
  59
+      "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", 
  60
+      "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea",
  61
+      "Guinea-Bissau", "Guyana", "Haiti", "Heard and McDonald Islands", "Holy See (Vatican City State)",
  62
+      "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq",
  63
+      "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya",
  64
+      "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan",
  65
+      "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya",
  66
+      "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic Of",
  67
+      "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique",
  68
+      "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of",
  69
+      "Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru",
  70
+      "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger",
  71
+      "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau",
  72
+      "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines",
  73
+      "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation",
  74
+      "Rwanda", "Saint Barthelemy", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia",
  75
+      "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino",
  76
+      "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore",
  77
+      "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa",
  78
+      "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname",
  79
+      "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic",
  80
+      "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste",
  81
+      "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan",
  82
+      "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom",
  83
+      "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela",
  84
+      "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara",
  85
+      "Yemen", "Zambia", "Zimbabwe"]
  86
+  },
  87
+  :active_record => {
  88
+    :error => {
  89
+      :header_message => ["1 error prohibited this {{object_name}} from being saved", "{{count}} errors prohibited this {{object_name}} from being saved"],
  90
+      :message => "There were problems with the following fields:"
  91
+    }            
  92
+  }  
  93
+}
47  actionpack/test/template/active_record_helper_i18n_test.rb
... ...
@@ -0,0 +1,47 @@
  1
+require 'abstract_unit'
  2
+
  3
+class ActiveRecordHelperI18nTest < Test::Unit::TestCase
  4
+  include ActionView::Helpers::ActiveRecordHelper
  5
+  
  6
+  attr_reader :request
  7
+  def setup
  8
+    @request = mock
  9
+    @object = stub :errors => stub(:count => 1, :full_messages => ['full_messages'])
  10
+    stubs(:content_tag).returns 'content_tag'
  11
+    
  12
+    I18n.stubs(:t).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').returns "1 error prohibited this  from being saved"
  13
+    I18n.stubs(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).returns 'There were problems with the following fields:'
  14
+  end
  15
+
  16
+  def test_error_messages_for_given_a_locale_it_does_not_check_request_for_locale
  17
+    request.expects(:locale).never
  18
+    @object.errors.stubs(:count).returns 0
  19
+    error_messages_for(:object => @object, :locale => 'en-US')
  20
+  end
  21
+  
  22
+  def test_error_messages_for_given_no_locale_it_checks_request_for_locale
  23
+    request.expects(:locale).returns 'en-US'
  24
+    @object.errors.stubs(:count).returns 0
  25
+    error_messages_for(:object => @object)
  26
+  end
  27
+  
  28
+  def test_error_messages_for_given_a_header_message_option_it_does_not_translate_header_message
  29
+    I18n.expects(:translate).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').never
  30
+    error_messages_for(:object => @object, :header_message => 'header message', :locale => 'en-US')
  31
+  end
  32
+
  33
+  def test_error_messages_for_given_no_header_message_option_it_translates_header_message
  34
+    I18n.expects(:t).with(:'header_message', :locale => 'en-US', :scope => [:active_record, :error], :count => 1, :object_name => '').returns 'header message'
  35
+    error_messages_for(:object => @object, :locale => 'en-US')
  36
+  end
  37
+  
  38
+  def test_error_messages_for_given_a_message_option_it_does_not_translate_message
  39
+    I18n.expects(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).never
  40
+    error_messages_for(:object => @object, :message => 'message', :locale => 'en-US')
  41
+  end
  42
+
  43
+  def test_error_messages_for_given_no_message_option_it_translates_message
  44
+    I18n.expects(:t).with(:'message', :locale => 'en-US', :scope => [:active_record, :error]).returns 'There were problems with the following fields:'
  45
+    error_messages_for(:object => @object, :locale => 'en-US')
  46
+  end
  47
+end
99  actionpack/test/template/date_helper_i18n_test.rb
... ...
@@ -0,0 +1,99 @@
  1
+require 'abstract_unit'
  2
+
  3
+class DateHelperDistanceOfTimeInWordsI18nTests < Test::Unit::TestCase
  4
+  include ActionView::Helpers::DateHelper
  5
+  attr_reader :request
  6
+  
  7
+  def setup
  8
+    @request = mock
  9
+    @from = Time.mktime(2004, 6, 6, 21, 45, 0)
  10
+  end
  11
+  
  12
+  # distance_of_time_in_words
  13
+
  14
+  def test_distance_of_time_in_words_given_a_locale_it_does_not_check_request_for_locale
  15
+    request.expects(:locale).never
  16
+    distance_of_time_in_words @from, @from + 1.second, false, :locale => 'en-US'
  17
+  end
  18
+  
  19
+  def test_distance_of_time_in_words_given_no_locale_it_checks_request_for_locale
  20
+    request.expects(:locale).returns 'en-US'
  21
+    distance_of_time_in_words @from, @from + 1.second
  22
+  end
  23
+  
  24
+  def test_distance_of_time_in_words_calls_i18n
  25
+    { # with include_seconds
  26
+      [2.seconds,  true]  => [:'less_than_x_seconds', 5],   
  27
+      [9.seconds,  true]  => [:'less_than_x_seconds', 10],  
  28
+      [19.seconds, true]  => [:'less_than_x_seconds', 20],  
  29
+      [30.seconds, true]  => [:'half_a_minute',       nil], 
  30
+      [59.seconds, true]  => [:'less_than_x_minutes', 1], 
  31
+      [60.seconds, true]  => [:'x_minutes',           1], 
  32
+      
  33
+      # without include_seconds
  34
+      [29.seconds, false] => [:'less_than_x_minutes', 1],
  35
+      [60.seconds, false] => [:'x_minutes',           1],
  36
+      [44.minutes, false] => [:'x_minutes',           44],
  37
+      [61.minutes, false] => [:'about_x_hours',       1],
  38
+      [24.hours,   false] => [:'x_days',              1],
  39
+      [30.days,    false] => [:'about_x_months',      1],
  40
+      [60.days,    false] => [:'x_months',            2],
  41
+      [1.year,     false] => [:'about_x_years',       1],
  42
+      [3.years,    false] => [:'over_x_years',        3]
  43
+      
  44
+      }.each do |passed, expected|
  45
+      assert_distance_of_time_in_words_translates_key passed, expected
  46
+    end
  47
+  end
  48
+  
  49
+  def assert_distance_of_time_in_words_translates_key(passed, expected)
  50
+    diff, include_seconds = *passed
  51
+    key, count = *expected    
  52
+    to = @from + diff
  53
+
  54
+    options = {:locale => 'en-US', :scope => :'datetime.distance_in_words'}
  55
+    options[:count] = count if count
  56
+    
  57
+    I18n.expects(:t).with(key, options)
  58
+    distance_of_time_in_words(@from, to, include_seconds, :locale => 'en-US')
  59
+  end
  60
+end
  61
+  
  62
+class DateHelperSelectTagsI18nTests < Test::Unit::TestCase
  63
+  include ActionView::Helpers::DateHelper
  64
+  attr_reader :request
  65
+  
  66
+  def setup
  67
+    @request = mock
  68
+    I18n.stubs(:translate).with(:'date.month_names', 'en-US').returns Date::MONTHNAMES
  69
+  end
  70
+  
  71
+  # select_month
  72
+  
  73
+  def test_select_month_given_use_month_names_option_does_not_translate_monthnames
  74
+    I18n.expects(:translate).never
  75
+    select_month(8, :locale => 'en-US', :use_month_names => Date::MONTHNAMES)
  76
+  end
  77
+  
  78
+  def test_select_month_translates_monthnames
  79
+    I18n.expects(:translate).with(:'date.month_names', 'en-US').returns Date::MONTHNAMES
  80
+    select_month(8, :locale => 'en-US')
  81
+  end
  82
+  
  83
+  def test_select_month_given_use_short_month_option_translates_abbr_monthnames
  84
+    I18n.expects(:translate).with(:'date.abbr_month_names', 'en-US').returns Date::ABBR_MONTHNAMES
  85
+    select_month(8, :locale => 'en-US', :use_short_month => true)
  86
+  end
  87
+  
  88
+  # date_or_time_select
  89
+  
  90
+  def test_date_or_time_select_given_an_order_options_does_not_translate_order
  91
+    I18n.expects(:translate).never
  92
+    datetime_select('post', 'updated_at', :order => [:year, :month, :day], :locale => 'en-US')
  93
+  end
  94
+  
  95
+  def test_date_or_time_select_given_no_order_options_translates_order
  96
+    I18n.expects(:translate).with(:'date.order', 'en-US').returns [:year, :month, :day]
  97
+    datetime_select('post', 'updated_at', :locale => 'en-US')
  98
+  end
  99
+end
26  actionpack/test/template/form_options_helper_i18n_test.rb
... ...
@@ -0,0 +1,26 @@
  1
+require 'abstract_unit'
  2
+
  3
+class FormOptionsHelperI18nTests < Test::Unit::TestCase
  4
+  include ActionView::Helpers::FormOptionsHelper
  5
+  attr_reader :request
  6
+  
  7
+  def setup
  8
+    @request = mock
  9
+  end
  10
+
  11
+  def test_country_options_for_select_given_a_locale_it_does_not_check_request_for_locale
  12
+    request.expects(:locale).never
  13
+    country_options_for_select :locale => 'en-US'
  14
+  end
  15
+  
  16
+  def test_country_options_for_select_given_no_locale_it_checks_request_for_locale
  17
+    request.expects(:locale).returns 'en-US'
  18
+    country_options_for_select
  19
+  end
  20
+
  21
+  def test_country_options_for_select_translates_country_names
  22
+    countries = ActionView::Helpers::FormOptionsHelper::COUNTRIES
  23
+    I18n.expects(:translate).with(:'countries.names', 'en-US').returns countries
  24
+    country_options_for_select :locale => 'en-US'
  25
+  end  
  26
+end
27  actionpack/test/template/number_helper_i18n_test.rb
... ...
@@ -0,0 +1,27 @@
  1
+require 'abstract_unit'
  2
+
  3
+class NumberHelperI18nTests < Test::Unit::TestCase
  4
+  include ActionView::Helpers::NumberHelper
  5
+  
  6
+  attr_reader :request
  7
+  def setup
  8
+    @request = mock
  9
+    @defaults = {:separator => ".", :unit => "$", :format => "%u%n", :delimiter => ",", :precision => 2}
  10
+    I18n.backend.add_translations 'en-US', :currency => {:format => @defaults}
  11
+  end
  12
+
  13
+  def test_number_to_currency_given_a_locale_it_does_not_check_request_for_locale
  14
+    request.expects(:locale).never
  15
+    number_to_currency(1, :locale => 'en-US')
  16
+  end
  17
+
  18
+  def test_number_to_currency_given_no_locale_it_checks_request_for_locale
  19
+    request.expects(:locale).returns 'en-US'
  20
+    number_to_currency(1)
  21
+  end
  22
+
  23
+  def test_number_to_currency_translates_currency_formats
  24
+    I18n.expects(:translate).with(:'currency.format', 'en-US').returns @defaults
  25
+    number_to_currency(1, :locale => 'en-US')
  26
+  end
  27
+end
2  activerecord/lib/active_record.rb
@@ -80,3 +80,5 @@
80 80
 require 'active_record/connection_adapters/abstract_adapter'
81 81
 
82 82
 require 'active_record/schema_dumper'
  83
+
  84
+require 'active_record/lang/en-US.rb'
25  activerecord/lib/active_record/lang/en-US.rb
... ...
@@ -0,0 +1,25 @@
  1
+I18n.backend.add_translations :'en-US', {
  2
+  :active_record => {
  3
+    :error_messages => {
  4
+      :inclusion => "is not included in the list",
  5
+      :exclusion => "is reserved",
  6
+      :invalid => "is invalid",
  7
+      :confirmation => "doesn't match confirmation",
  8
+      :accepted  => "must be accepted",
  9
+      :empty => "can't be empty",
  10
+      :blank => "can't be blank",
  11
+      :too_long => "is too long (maximum is {{count}} characters)",
  12
+      :too_short => "is too short (minimum is {{count}} characters)",
  13
+      :wrong_length => "is the wrong length (should be {{count}} characters)",
  14
+      :taken => "has already been taken",
  15
+      :not_a_number => "is not a number",
  16
+      :greater_than => "must be greater than {{count}}",
  17
+      :greater_than_or_equal_to => "must be greater than or equal to {{count}}",
  18
+      :equal_to => "must be equal to {{count}}",
  19
+      :less_than => "must be less than {{count}}",
  20
+      :less_than_or_equal_to => "must be less than or equal to {{count}}",
  21
+      :odd => "must be odd",
  22
+      :even => "must be even"
  23
+    }            
  24
+  }
  25
+}
184  activerecord/lib/active_record/validations.rb
@@ -23,30 +23,30 @@ def initialize(base) # :nodoc:
23 23
       @base, @errors = base, {}
24 24
     end
25 25
 
26  
-    @@default_error_messages = {
27  
-      :inclusion => "is not included in the list",
28  
-      :exclusion => "is reserved",
29  
-      :invalid => "is invalid",
30  
-      :confirmation => "doesn't match confirmation",
31  
-      :accepted  => "must be accepted",
32  
-      :empty => "can't be empty",
33  
-      :blank => "can't be blank",
34  
-      :too_long => "is too long (maximum is %d characters)",
35  
-      :too_short => "is too short (minimum is %d characters)",
36  
-      :wrong_length => "is the wrong length (should be %d characters)",
37  
-      :taken => "has already been taken",
38  
-      :not_a_number => "is not a number",
39  
-      :greater_than => "must be greater than %d",
40  
-      :greater_than_or_equal_to => "must be greater than or equal to %d",
41  
-      :equal_to => "must be equal to %d",
42  
-      :less_than => "must be less than %d",
43  
-      :less_than_or_equal_to => "must be less than or equal to %d",
44  
-      :odd => "must be odd",
45  
-      :even => "must be even"
46  
-    }
47  
-
48  
-    # Holds a hash with all the default error messages that can be replaced by your own copy or localizations.
49  
-    cattr_accessor :default_error_messages
  26
+    # @@default_error_messages = {
  27
+    #   :inclusion => "is not included in the list",
  28
+    #   :exclusion => "is reserved",
  29
+    #   :invalid => "is invalid",
  30
+    #   :confirmation => "doesn't match confirmation",
  31
+    #   :accepted  => "must be accepted",
  32
+    #   :empty => "can't be empty",
  33
+    #   :blank => "can't be blank",
  34
+    #   :too_long => "is too long (maximum is %d characters)",
  35
+    #   :too_short => "is too short (minimum is %d characters)",
  36
+    #   :wrong_length => "is the wrong length (should be %d characters)",
  37
+    #   :taken => "has already been taken",
  38
+    #   :not_a_number => "is not a number",
  39
+    #   :greater_than => "must be greater than %d",
  40
+    #   :greater_than_or_equal_to => "must be greater than or equal to %d",
  41
+    #   :equal_to => "must be equal to %d",
  42
+    #   :less_than => "must be less than %d",
  43
+    #   :less_than_or_equal_to => "must be less than or equal to %d",
  44
+    #   :odd => "must be odd",
  45
+    #   :even => "must be even"
  46
+    # }
  47
+    # 
  48
+    # # Holds a hash with all the default error messages that can be replaced by your own copy or localizations.
  49
+    # cattr_accessor :default_error_messages
50 50
 
51 51
 
52 52
     # Adds an error to the base object instead of any particular attribute. This is used
@@ -61,27 +61,34 @@ def add_to_base(msg)
61 61
     # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
62 62
     # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
63 63
     # If no +msg+ is supplied, "invalid" is assumed.
64  
-    def add(attribute, msg = @@default_error_messages[:invalid])
65  
-      @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
66  
-      @errors[attribute.to_s] << msg
67  
-    end
  64
+    def add(attribute, message = nil)
  65
+      message ||= :"active_record.error_messages.invalid".t
  66
+      @errors[attribute.to_s] ||= []
  67
+      @errors[attribute.to_s] << message
  68
+    end    
68 69
 
69 70
     # Will add an error message to each of the attributes in +attributes+ that is empty.
70  
-    def add_on_empty(attributes, msg = @@default_error_messages[:empty])
  71
+    def add_on_empty(attributes, custom_message = nil)
71 72
       for attr in [attributes].flatten
72 73
         value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
73  
-        is_empty = value.respond_to?("empty?") ? value.empty? : false
74  
-        add(attr, msg) unless !value.nil? && !is_empty
  74
+        is_empty = value.respond_to?("empty?") ? value.empty? : false        
  75
+        add(attr, generate_message(attr, :empty, :default => custom_message)) unless !value.nil? && !is_empty
75 76
       end
76 77
     end
77 78
 
78 79
     # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
79  
-    def add_on_blank(attributes, msg = @@default_error_messages[:blank])
  80
+    def add_on_blank(attributes, custom_message = nil)
80 81
       for attr in [attributes].flatten
81 82
         value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
82  
-        add(attr, msg) if value.blank?
  83
+        add(attr, generate_message(attr, :blank, :default => custom_message)) if value.blank?
83 84
       end
84 85
     end
  86
+    
  87
+    def generate_message(attr, key, options = {})
  88
+      scope = [:active_record, :error_messages]
  89
+      key.t(options.merge(:scope => scope + [:custom, @base.class.name.downcase, attr])) || 
  90
+      key.t(options.merge(:scope => scope))
  91
+    end
85 92
 
86 93
     # Returns true if the specified +attribute+ has errors associated with it.
87 94
     #
@@ -166,22 +173,25 @@ def each_full
166 173
     #   company = Company.create(:address => '123 First St.')
167 174
     #   company.errors.full_messages # =>
168 175
     #     ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
169  
-    def full_messages
  176
+    def full_messages(options = {})
170 177
       full_messages = []
  178
+      locale = options[:locale]
171 179
 
172 180
       @errors.each_key do |attr|
173  
-        @errors[attr].each do |msg|
174  
-          next if msg.nil?
175  
-
  181
+        @errors[attr].each do |message|
  182
+          next unless message
  183
+    
176 184
           if attr == "base"
177  
-            full_messages << msg
  185
+            full_messages << message
178 186
           else
179  
-            full_messages << @base.class.human_attribute_name(attr) + " " + msg
  187
+            key = :"active_record.human_attribute_names.#{@base.class.name.underscore.to_sym}.#{attr}" 
  188
+            attr_name = key.t(locale) || @base.class.human_attribute_name(attr)
  189
+            full_messages << attr_name + " " + message
180 190
           end
181 191
         end
182 192
       end
183 193
       full_messages
184  
-    end
  194
+    end 
185 195
 
186 196
     # Returns true if no errors have been added.
187 197
     def empty?
@@ -388,15 +398,18 @@ def validates_each(*attrs)
388 398
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
389 399
       #   method, proc or string should return or evaluate to a true or false value.
390 400
       def validates_confirmation_of(*attr_names)
391  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save }
  401
+        configuration = { :on => :save }
392 402
         configuration.update(attr_names.extract_options!)
393 403
 
394 404
         attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" }))
395 405
 
396 406
         validates_each(attr_names, configuration) do |record, attr_name, value|
397  
-          record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
  407
+          unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
  408
+            message = record.errors.generate_message(attr_name, :confirmation, :default => configuration[:message])
  409
+            record.errors.add(attr_name, message) 
  410
+          end
398 411
         end
399  
-      end
  412
+      end 
400 413
 
401 414
       # Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example:
402 415
       #
@@ -422,7 +435,7 @@ def validates_confirmation_of(*attr_names)
422 435
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
423 436
       #   method, proc or string should return or evaluate to a true or false value.
424 437
       def validates_acceptance_of(*attr_names)
425  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" }
  438
+        configuration = { :on => :save, :allow_nil => true, :accept => "1" }
426 439
         configuration.update(attr_names.extract_options!)
427 440
 
428 441
         db_cols = begin
@@ -434,7 +447,10 @@ def validates_acceptance_of(*attr_names)
434 447
         attr_accessor(*names)
435 448
 
436 449
         validates_each(attr_names,configuration) do |record, attr_name, value|
437  
-          record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept]
  450
+          unless value == configuration[:accept]
  451
+            message = record.errors.generate_message(attr_name, :accepted, :default => configuration[:message])
  452
+            record.errors.add(attr_name, message) 
  453
+          end
438 454
         end
439 455
       end
440 456
 
@@ -461,7 +477,7 @@ def validates_acceptance_of(*attr_names)
461 477
       #   method, proc or string should return or evaluate to a true or false value.
462 478
       #
463 479
       def validates_presence_of(*attr_names)
464  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save }
  480
+        configuration = { :on => :save }
465 481
         configuration.update(attr_names.extract_options!)
466 482
 
467 483
         # can't use validates_each here, because it cannot cope with nonexistent attributes,
@@ -505,11 +521,7 @@ def validates_presence_of(*attr_names)
505 521
       #   method, proc or string should return or evaluate to a true or false value.
506 522
       def validates_length_of(*attrs)
507 523
         # Merge given options with defaults.
508  
-        options = {
509  
-          :too_long     => ActiveRecord::Errors.default_error_messages[:too_long],
510  
-          :too_short    => ActiveRecord::Errors.default_error_messages[:too_short],
511  
-          :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length]
512  
-        }.merge(DEFAULT_VALIDATION_OPTIONS)
  524
+        options = {}.merge(DEFAULT_VALIDATION_OPTIONS)
513 525
         options.update(attrs.extract_options!.symbolize_keys)
514 526
 
515 527
         # Ensure that one and only one range option is specified.
@@ -531,15 +543,14 @@ def validates_length_of(*attrs)
531 543
           when :within, :in
532 544
             raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
533 545
 
534  
-            too_short = options[:too_short] % option_value.begin
535  
-            too_long  = options[:too_long]  % option_value.end
536  
-
537 546
             validates_each(attrs, options) do |record, attr, value|
538 547
               value = value.split(//) if value.kind_of?(String)
539 548
               if value.nil? or value.size < option_value.begin
540  
-                record.errors.add(attr, too_short)
  549
+                message = record.errors.generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin)                
  550
+                record.errors.add(attr, message)
541 551
               elsif value.size > option_value.end
542  
-                record.errors.add(attr, too_long)
  552
+                message = record.errors.generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end)
  553
+                record.errors.add(attr, message)
543 554
               end
544 555
             end
545 556
           when :is, :minimum, :maximum
@@ -549,11 +560,14 @@ def validates_length_of(*attrs)
549 560
             validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
550 561
             message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
551 562
 
552  
-            message = (options[:message] || options[message_options[option]]) % option_value
553  
-
554 563
             validates_each(attrs, options) do |record, attr, value|
555 564
               value = value.split(//) if value.kind_of?(String)
556  
-              record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
  565
+              unless !value.nil? and value.size.method(validity_checks[option])[option_value]
  566
+                key = message_options[option]
  567
+                custom_message = options[:message] || options[key]
  568
+                message = record.errors.generate_message(attr, key, :default => custom_message, :count => option_value)
  569
+                record.errors.add(attr, message) 
  570
+              end
557 571
             end
558 572
         end
559 573
       end
@@ -595,7 +609,7 @@ def validates_length_of(*attrs)
595 609
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
596 610
       #   method, proc or string should return or evaluate to a true or false value.
597 611
       def validates_uniqueness_of(*attr_names)
598  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true }
  612
+        configuration = { :case_sensitive => true }
599 613
         configuration.update(attr_names.extract_options!)
600 614
 
601 615
         validates_each(attr_names,configuration) do |record, attr_name, value|
@@ -654,8 +668,11 @@ def validates_uniqueness_of(*attr_names)
654 668
             if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text?
655 669
               found = results.any? { |a| a[attr_name.to_s] == value }
656 670
             end
657  
-
658  
-            record.errors.add(attr_name, configuration[:message]) if found
  671
+            
  672
+            if found
  673
+              message = record.errors.generate_message(attr_name, :taken, :default => configuration[:message])
  674
+              record.errors.add(attr_name, message) 
  675
+            end
659 676
           end
660 677
         end
661 678
       end
@@ -685,13 +702,16 @@ def validates_uniqueness_of(*attr_names)
685 702
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
686 703
       #   method, proc or string should return or evaluate to a true or false value.
687 704
       def validates_format_of(*attr_names)
688  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
  705
+        configuration = { :on => :save, :with => nil }
689 706
         configuration.update(attr_names.extract_options!)
690 707
 
691 708
         raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
692 709
 
693 710
         validates_each(attr_names, configuration) do |record, attr_name, value|
694  
-          record.errors.add(attr_name, configuration[:message] % value) unless value.to_s =~ configuration[:with]
  711
+          unless value.to_s =~ configuration[:with]
  712
+            message = record.errors.generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
  713
+            record.errors.add(attr_name, message) 
  714
+          end
695 715
         end
696 716
       end
697 717
 
@@ -715,7 +735,7 @@ def validates_format_of(*attr_names)
715 735
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
716 736
       #   method, proc or string should return or evaluate to a true or false value.
717 737
       def validates_inclusion_of(*attr_names)
718  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save }
  738
+        configuration = { :on => :save, :with => nil }
719 739
         configuration.update(attr_names.extract_options!)
720 740
 
721 741
         enum = configuration[:in] || configuration[:within]
@@ -723,7 +743,10 @@ def validates_inclusion_of(*attr_names)
723 743
         raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
724 744
 
725 745
         validates_each(attr_names, configuration) do |record, attr_name, value|
726  
-          record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value)
  746
+          unless enum.include?(value)
  747
+            message = record.errors.generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value)
  748
+            record.errors.add(attr_name, message) 
  749
+          end
727 750
         end
728 751
       end
729 752
 
@@ -747,7 +770,7 @@ def validates_inclusion_of(*attr_names)
747 770
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
748 771
       #   method, proc or string should return or evaluate to a true or false value.
749 772
       def validates_exclusion_of(*attr_names)
750  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save }
  773
+        configuration = { :on => :save, :with => nil }
751 774
         configuration.update(attr_names.extract_options!)
752 775
 
753 776
         enum = configuration[:in] || configuration[:within]
@@ -755,7 +778,10 @@ def validates_exclusion_of(*attr_names)
755 778
         raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
756 779
 
757 780
         validates_each(attr_names, configuration) do |record, attr_name, value|
758  
-          record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value)
  781
+          if enum.include?(value)
  782
+            message = record.errors.generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value)
  783
+            record.errors.add(attr_name, message) 
  784
+          end
759 785
         end
760 786
       end
761 787
 
@@ -791,12 +817,14 @@ def validates_exclusion_of(*attr_names)
791 817
       #   not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).  The
792 818
       #   method, proc or string should return or evaluate to a true or false value.
793 819
       def validates_associated(*attr_names)
794  
-        configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save }
  820
+        configuration = { :on => :save }
795 821
         configuration.update(attr_names.extract_options!)
796 822
 
797 823
         validates_each(attr_names, configuration) do |record, attr_name, value|
798  
-          record.errors.add(attr_name, configuration[:message]) unless
799  
-            (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
  824
+          unless (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
  825
+            message = record.errors.generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
  826
+            record.errors.add(attr_name, message)
  827
+          end
800 828
         end
801 829
       end
802 830
 
@@ -844,7 +872,8 @@ def validates_numericality_of(*attr_names)
844 872
 
845 873
           if configuration[:only_integer]
846 874
             unless raw_value.to_s =~ /\A[+-]?\d+\Z/
847  
-              record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
  875
+              message = record.errors.generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
  876
+              record.errors.add(attr_name, message)
848 877
               next
849 878
             end
850 879
             raw_value = raw_value.to_i
@@ -852,7 +881,8 @@ def validates_numericality_of(*attr_names)
852 881
            begin
853 882
               raw_value = Kernel.Float(raw_value.to_s)
854 883
             rescue ArgumentError, TypeError
855  
-              record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
  884
+              message = record.errors.generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
  885
+              record.errors.add(attr_name, message)
856 886
               next
857 887
             end
858 888
           end
@@ -860,10 +890,12 @@ def validates_numericality_of(*attr_names)
860 890
           numericality_options.each do |option|
861 891
             case option
862 892
               when :odd, :even
863  
-                record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
  893
+                unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
  894
+                  message = record.errors.generate_message(attr_name, option, :value => raw_value, :default => configuration[:message])
  895
+                  record.errors.add(attr_name, message) 
  896
+                end
864 897
               else
865  
-                message = configuration[:message] || ActiveRecord::Errors.default_error_messages[option]
866  
-                message = message % configuration[option] if configuration[option]
  898
+                message = record.errors.generate_message(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option])
867 899
                 record.errors.add(attr_name, message) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
868 900
             end
869 901
           end
539  activerecord/test/cases/validations_i18n_test.rb
... ...
@@ -0,0 +1,539 @@
  1
+require "cases/helper"
  2
+require 'models/topic'
  3
+require 'models/reply'
  4
+
  5
+class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
  6
+  def setup
  7
+    reset_callbacks Topic
  8
+    @topic = Topic.new
  9
+    I18n.backend.add_translations('en-US', :active_record => {:error_messages => {:custom => nil}})