nulogy-delocalize 0.3.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009-2011 Clemens Kofler and contributors
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,233 @@
1
+ delocalize
2
+ ==========
3
+
4
+ [![Build Status](https://secure.travis-ci.org/clemens/delocalize.png)](http://travis-ci.org/clemens/delocalize)
5
+
6
+ delocalize provides localized date/time and number parsing functionality for Rails.
7
+
8
+ Installation
9
+ ------------
10
+
11
+ You can use delocalize as a gem (preferred). Using delocalize as a Rails plugin has been discontinued and is no supported. If you want/need to use delocalize as a gem (I really don't see a reason why you'd want to), consider using the `0-2-stable` branch.
12
+
13
+ ### Rails 3
14
+
15
+ To use delocalize, put the following gem requirement in your `Gemfile`:
16
+
17
+ gem "delocalize"
18
+
19
+ ### Rails 2
20
+
21
+ Note: Support for Rails 2 has been discontinued. This version is only considered stable for Rails 3. If you need Rails 2 support, please use the `0.2.x` versions or the `0-2-stable` branch respectively.
22
+
23
+ To use delocalize, put the following gem requirement in your `environment.rb`:
24
+
25
+ config.gem "delocalize", :source => 'http://gemcutter.org'
26
+
27
+ In Rails 2.3, alternatively, you can use it with Bundler. See http://gembundler.com/rails23.html for instructions.
28
+
29
+ What does it do? And how do I use it?
30
+ --------------------------------------
31
+
32
+ Delocalize, just as the name suggest, does pretty much the opposite of localize.
33
+
34
+ In the grey past, if you want your users to be able to input localized data, such as dates and numbers, you had to manually override attribute accessors:
35
+
36
+ def price=(price)
37
+ write_attribute(:price, price.gsub(',', '.'))
38
+ end
39
+
40
+ delocalize does this under the covers -- all you need is your regular translation data (as YAML or Ruby file) where you need Rails' standard translations:
41
+
42
+ de:
43
+ number:
44
+ format:
45
+ separator: ','
46
+ delimiter: '.'
47
+ date:
48
+ input:
49
+ formats: [:default, :long, :short] # <- this and ...
50
+
51
+ formats:
52
+ default: "%d.%m.%Y"
53
+ short: "%e. %b"
54
+ long: "%e. %B %Y"
55
+ only_day: "%e"
56
+
57
+ day_names: [Sonntag, Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag]
58
+ abbr_day_names: [So, Mo, Di, Mi, Do, Fr, Sa]
59
+ month_names: [~, Januar, Februar, März, April, Mai, Juni, Juli, August, September, Oktober, November, Dezember]
60
+ abbr_month_names: [~, Jan, Feb, Mär, Apr, Mai, Jun, Jul, Aug, Sep, Okt, Nov, Dez]
61
+ order: [ :day, :month, :year ]
62
+
63
+ time:
64
+ input:
65
+ formats: [:long, :medium, :short, :default, :time] # <- ... this are the only non-standard keys
66
+ formats:
67
+ default: "%A, %e. %B %Y, %H:%M Uhr"
68
+ short: "%e. %B, %H:%M Uhr"
69
+ long: "%A, %e. %B %Y, %H:%M Uhr"
70
+ time: "%H:%M"
71
+
72
+ am: "vormittags"
73
+ pm: "nachmittags"
74
+
75
+ For dates and times, you have to define input formats which are taken from the actual formats. The important thing here is to define input formats sorted by descending complexity; in other words: the format which contains the most (preferably non-numeric) information should be first in the list because it can produce the most reliable match. Exception: If you think there most complex format is not the one that most users will input, you can put the most-used in front so you save unnecessary iterations.
76
+
77
+ Careful with formats containing only numbers: It's very hard to produce reliable matches if you provide multiple strictly numeric formats!
78
+
79
+ delocalize then overrides `to_input_field_tag` in ActionView's `InstanceTag` so you can use localized text fields:
80
+
81
+ <% form_for @product do |f| %>
82
+ <%= f.text_field :name %>
83
+ <%= f.text_field :released_on %>
84
+ <%= f.text_field :price %>
85
+ <% end %>
86
+
87
+ In this example, a user can enter the release date and the price just like he's used to in his language, for example:
88
+
89
+ > Name: "Couch"
90
+ > Released on: "12. Oktober 2009"
91
+ > Price: "2.999,90"
92
+
93
+ When saved, ActiveRecord automatically converts these to a regular Ruby date and number.
94
+
95
+ Edit forms then also show localized dates/numbers. By default, dates and times are localized using the format named :default in your locale file. So with the above locale file, dates would use `%d.%m.%Y` and times would use `%A, %e. %B %Y, %H:%M Uhr`. Numbers are also formatted using your locale's thousands delimiter and decimal separator.
96
+
97
+ You can also customize the output using some options:
98
+
99
+ The price should always show two decimal digits and we don't need the delimiter:
100
+ <%= f.text_field :price, :precision => 2, :delimiter => '' %>
101
+
102
+ The `released_on` date should be shown in the `:full` format:
103
+ <%= f.text_field :released_on, :format => :full %>
104
+
105
+ Since `I18n.localize` supports localizing `strftime` strings, we can also do this:
106
+ <%= f.text_field :released_on, :format => "%B %Y" %>
107
+
108
+ ### Ruby 1.9 + Psych YAML Parser
109
+
110
+ You will need to adjust the localization formatting when using the new YAML parser Psych. Below is an example error message you may receive in your logs as well as an example of acceptable formatting and helpful links for reference:
111
+
112
+ __Error message from logs:__
113
+
114
+ Psych::SyntaxError (couldn't parse YAML at line x column y):
115
+
116
+ __References:__
117
+
118
+ The solution can be found here: http://stackoverflow.com/questions/4980877/rails-error-couldnt-parse-yaml#answer-5323060
119
+
120
+ http://pivotallabs.com/users/mkocher/blog/articles/1692-yaml-psych-and-ruby-1-9-2-p180-here-there-be-dragons
121
+
122
+ __Psych Preferred Formatting:__
123
+
124
+ en:
125
+ number:
126
+ format:
127
+ separator: '.'
128
+ delimiter: ','
129
+ precision: 2
130
+ date:
131
+ input:
132
+ formats:
133
+ - :default
134
+ - :long
135
+ - :short
136
+ formats:
137
+ default: "%m/%d/%Y"
138
+ short: "%b %e"
139
+ long: "%B %e, %Y"
140
+ only_day: "%e"
141
+ day_names:
142
+ - Sunday
143
+ - Monday
144
+ - Tuesday
145
+ - Wednesday
146
+ - Thursday
147
+ - Friday
148
+ - Saturday
149
+ abbr_day_names:
150
+ - Sun
151
+ - Mon
152
+ - Tue
153
+ - Wed
154
+ - Thur
155
+ - Fri
156
+ - Sat
157
+ month_names:
158
+ - ~
159
+ - January
160
+ - February
161
+ - March
162
+ - April
163
+ - May
164
+ - June
165
+ - July
166
+ - August
167
+ - September
168
+ - October
169
+ - November
170
+ - December
171
+ abbr_month_names:
172
+ - ~
173
+ - Jan
174
+ - Feb
175
+ - Mar
176
+ - Apr
177
+ - May
178
+ - Jun
179
+ - Jul
180
+ - Aug
181
+ - Sep
182
+ - Oct
183
+ - Nov
184
+ - Dec
185
+ order:
186
+ - :month
187
+ - :day
188
+ - :year
189
+ time:
190
+ input:
191
+ formats:
192
+ - :default
193
+ - :long
194
+ - :short
195
+ - :time
196
+ formats:
197
+ default: "%m/%d/%Y %I:%M%p"
198
+ short: "%B %e %I:%M %p"
199
+ long: "%A, %B %e, %Y %I:%M%p"
200
+ time: "%l:%M%p"
201
+ am: "am"
202
+ pm: "pm"
203
+
204
+ ### Compatibility
205
+
206
+ * Tested with Rails 2.3.5 in Ruby 1.8.7, Ruby 1.9.1 and Ruby 1.9.2 (head)
207
+ * Tested with Rails 3 Beta 3 in Ruby 1.9.2 (head)
208
+
209
+ ### Contributors
210
+
211
+ People who have contributed to delocalize (in no particular order):
212
+
213
+ * [Fernando Luizao](http://github.com/fernandoluizao)
214
+ * [Stephan Zalewski](http://github.com/stepahn)
215
+ * [Lailson Bandeira](http://github.com/lailsonbm)
216
+ * [Carlos Antonio da Silva](http://github.com/carlosantoniodasilva)
217
+ * [Michele Franzin](http://github.com/michelefranzin)
218
+ * [Raphaela Wrede](https://github.com/rwrede)
219
+ * [Jan De Poorter](https://github.com/DefV)
220
+ * [Blake Lucchesi](https://github.com/BlakeLucchesi)
221
+ * [Ralph von der Heyden](https://github.com/ralph)
222
+
223
+ ### TODO
224
+
225
+ * Improve test coverage
226
+ * Separate Ruby/Rails stuff to make it usable outside Rails
227
+ * Decide on other ActionView hacks (e.g. `text_field_tag`)
228
+ * Implement AM/PM support
229
+ * Cleanup, cleanup, cleanup ...
230
+
231
+ Copyright (c) 2009-2011 Clemens Kofler <clemens@railway.at>
232
+ <http://www.railway.at/>
233
+ Released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rdoc/task'
4
+
5
+ desc 'Test the delocalize gem.'
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'lib'
8
+ t.libs << 'test'
9
+ t.pattern = 'test/delocalize_test.rb'
10
+ t.verbose = true
11
+ end
12
+
13
+ desc 'Default: run unit tests.'
14
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.1
data/lib/delocalize.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'delocalize/ruby_ext'
2
+ require 'delocalize/i18n_ext'
3
+ require 'delocalize/localized_date_time_parser'
4
+
5
+ if defined?(Rails::Railtie)
6
+ require 'delocalize/railtie'
7
+ elsif defined?(Rails::Initializer)
8
+ raise "This version of delocalize is only compatible with Rails 3.0 or newer"
9
+ end
@@ -0,0 +1,30 @@
1
+ require 'active_support/core_ext/module/attribute_accessors'
2
+
3
+ module I18n
4
+ mattr_accessor :enable_delocalization
5
+ I18n.enable_delocalization = true
6
+
7
+ class << self
8
+ def delocalization_enabled?
9
+ !!I18n.enable_delocalization
10
+ end
11
+
12
+ def delocalization_disabled?
13
+ !delocalization_enabled?
14
+ end
15
+
16
+ def with_delocalization_disabled(&block)
17
+ old_value = I18n.enable_delocalization
18
+ I18n.enable_delocalization = false
19
+ yield
20
+ I18n.enable_delocalization = old_value
21
+ end
22
+
23
+ def with_delocalization_enabled(&block)
24
+ old_value = I18n.enable_delocalization
25
+ I18n.enable_delocalization = true
26
+ yield
27
+ I18n.enable_delocalization = old_value
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,75 @@
1
+ # TODO:
2
+ # * AM/PM calculation
3
+ # * proper documentation (comments)
4
+ module Delocalize
5
+ class LocalizedDateTimeParser
6
+ # extend/change this according to your needs by merging your custom regexps
7
+ REGEXPS = {
8
+ '%B' => "(#{Date::MONTHNAMES.compact.join('|')})", # long month name
9
+ '%b' => "(#{Date::ABBR_MONTHNAMES.compact.join('|')})", # short month name
10
+ '%m' => "(\\d{2})", # numeric month
11
+ '%A' => "(#{Date::DAYNAMES.join('|')})", # full day name
12
+ '%a' => "(#{Date::ABBR_DAYNAMES.join('|')})", # short day name
13
+ '%Y' => "(\\d{4})", # long year
14
+ '%y' => "(\\d{2})", # short year
15
+ '%e' => "(\\s\\d|\\d{2})", # short day
16
+ '%d' => "(\\d{2})", # full day
17
+ '%H' => "(\\d{2})", # hour (24)
18
+ '%M' => "(\\d{2})", # minute
19
+ '%S' => "(\\d{2})" # second
20
+ }
21
+
22
+ class << self
23
+ def parse(datetime, type)
24
+ return unless datetime
25
+ return datetime if datetime.respond_to?(:strftime) # already a Date/Time object -> no need to parse it
26
+
27
+ translate_month_and_day_names(datetime)
28
+ input_formats(type).each do |original_format|
29
+ next unless datetime =~ /^#{apply_regex(original_format)}$/
30
+
31
+ datetime = DateTime.strptime(datetime, original_format)
32
+ return Date == type ?
33
+ datetime.to_date :
34
+ Time.zone.local(datetime.year, datetime.mon, datetime.mday, datetime.hour, datetime.min, datetime.sec)
35
+ end
36
+ default_parse(datetime, type)
37
+ end
38
+
39
+ private
40
+ def default_parse(datetime, type)
41
+ return if datetime.blank?
42
+ begin
43
+ today = Date.current
44
+ parsed = Date._parse(datetime)
45
+ return if parsed.empty? # the datetime value is invalid
46
+ # set default year, month and day if not found
47
+ parsed.reverse_merge!(:year => today.year, :mon => today.mon, :mday => today.mday)
48
+ datetime = Time.zone.local(*parsed.values_at(:year, :mon, :mday, :hour, :min, :sec))
49
+ Date == type ? datetime.to_date : datetime
50
+ rescue
51
+ datetime
52
+ end
53
+ end
54
+
55
+ def translate_month_and_day_names(datetime)
56
+ translated = [:month_names, :abbr_month_names, :day_names, :abbr_day_names].map do |key|
57
+ I18n.t(key, :scope => :date)
58
+ end.flatten.compact
59
+
60
+ original = (Date::MONTHNAMES + Date::ABBR_MONTHNAMES + Date::DAYNAMES + Date::ABBR_DAYNAMES).compact
61
+ translated.each_with_index { |name, i| datetime.gsub!(name, original[i]) }
62
+ end
63
+
64
+ def input_formats(type)
65
+ # Date uses date formats, all others use time formats
66
+ type = type == Date ? :date : :time
67
+ I18n.t(:"#{type}.formats").slice(*I18n.t(:"#{type}.input.formats")).values
68
+ end
69
+
70
+ def apply_regex(format)
71
+ format.gsub(/(#{REGEXPS.keys.join('|')})/) { |s| REGEXPS[$1] }
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,18 @@
1
+ # TODO:
2
+ # * proper documentation (comments)
3
+ module Delocalize
4
+ class LocalizedNumericParser
5
+ class << self
6
+ # Parse numbers removing unneeded characters and replacing decimal separator
7
+ # through I18n. This will return a valid Ruby Numeric value (as String).
8
+ def parse(value)
9
+ if value.is_a?(String)
10
+ separator = I18n.t(:'number.format.separator')
11
+ delimiter = I18n.t(:'number.format.delimiter')
12
+ value = value.gsub(delimiter, '').gsub(separator, '.')
13
+ end
14
+ value
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,57 @@
1
+ require 'action_view'
2
+
3
+ # TODO: also override other methods like to_check_box_tag since they might contain numeric values?
4
+ # ActionView needs some patching too
5
+
6
+ ActionView::Helpers::InstanceTag.class_eval do
7
+ include ActionView::Helpers::NumberHelper
8
+
9
+ alias original_to_input_field_tag to_input_field_tag
10
+ def to_input_field_tag(field_type, options = {})
11
+ options.symbolize_keys!
12
+ # numbers and dates/times should be localized unless value is already defined
13
+ if object && options[:value].blank? && object.respond_to?(:column_for_attribute) && column = object.column_for_attribute(method_name)
14
+ # a little verbose
15
+ if column.number? || column.date? || column.time?
16
+ value = object.send(method_name)
17
+
18
+ if column.number?
19
+ number_options = I18n.t(:'number.format')
20
+ separator = options.delete(:separator) || number_options[:separator]
21
+ delimiter = options.delete(:delimiter) || number_options[:delimiter]
22
+ precision = options.delete(:precision) || number_options[:precision]
23
+ opts = { :separator => separator, :delimiter => delimiter, :precision => precision }
24
+ # integers don't need a precision
25
+ opts.merge!(:precision => 0) if column.type == :integer
26
+
27
+ hidden_for_integer = field_type == 'hidden' && column.type == :integer
28
+
29
+ # the number will be formatted only if it has no numericality errors
30
+ if object.respond_to?(:errors) && !Array(object.errors[method_name]).try(:include?, 'is not a number')
31
+ # we don't format integer hidden fields because this breaks nested_attributes
32
+ options[:value] = number_with_precision(value, opts) unless hidden_for_integer
33
+ end
34
+ elsif column.date? || column.time?
35
+ options[:value] = value ? I18n.l(value, :format => options.delete(:format)) : nil
36
+ end
37
+ end
38
+ end
39
+
40
+ original_to_input_field_tag(field_type, options)
41
+ end
42
+ end
43
+
44
+ # TODO: does it make sense to also override FormTagHelper methods?
45
+ # ActionView::Helpers::FormTagHelper.class_eval do
46
+ # include ActionView::Helpers::NumberHelper
47
+ #
48
+ # alias original_text_field_tag text_field_tag
49
+ # def text_field_tag(name, value = nil, options = {})
50
+ # value = options.delete(:value) if options.key?(:value)
51
+ # if value.is_a?(Numeric)
52
+ # value = number_with_delimiter(value)
53
+ # end
54
+ # original_text_field_tag(name, value, options)
55
+ # end
56
+ # end
57
+
@@ -0,0 +1,80 @@
1
+ require 'active_record'
2
+
3
+ require 'active_record/connection_adapters/abstract/schema_definitions'
4
+ begin
5
+ require 'active_record/connection_adapters/column'
6
+ rescue LoadError
7
+ # Not Rails 3.1, it seems
8
+ end
9
+
10
+ # let's hack into ActiveRecord a bit - everything at the lowest possible level, of course, so we minimalize side effects
11
+ ActiveRecord::ConnectionAdapters::Column.class_eval do
12
+ def date?
13
+ klass == Date
14
+ end
15
+
16
+ def time?
17
+ klass == Time
18
+ end
19
+ end
20
+
21
+ ActiveRecord::Base.class_eval do
22
+ def write_attribute_with_localization(attr_name, original_value)
23
+ new_value = original_value
24
+ if column = column_for_attribute(attr_name.to_s)
25
+ if column.date?
26
+ new_value = Date.parse_localized(original_value) rescue original_value
27
+ elsif column.time?
28
+ new_value = Time.parse_localized(original_value) rescue original_value
29
+ end
30
+ end
31
+ write_attribute_without_localization(attr_name, new_value)
32
+ end
33
+ alias_method_chain :write_attribute, :localization
34
+
35
+ def convert_number_column_value_with_localization(value)
36
+ value = convert_number_column_value_without_localization(value)
37
+ value = Numeric.parse_localized(value) if I18n.delocalization_enabled?
38
+ value
39
+ end
40
+ alias_method_chain :convert_number_column_value, :localization
41
+
42
+ def field_changed?(attr, old, value)
43
+ if column = column_for_attribute(attr)
44
+ if column.number? && column.null && (old.nil? || old == 0) && value.blank?
45
+ # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
46
+ # Hence we don't record it as a change if the value changes from nil to ''.
47
+ # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
48
+ # be typecast back to 0 (''.to_i => 0)
49
+ value = nil
50
+ elsif column.number?
51
+ value = column.type_cast(convert_number_column_value_with_localization(value))
52
+ else
53
+ value = column.type_cast(value)
54
+ end
55
+ end
56
+
57
+ old != value
58
+ end
59
+ end
60
+
61
+ ActiveRecord::Base.instance_eval do
62
+ def define_method_attribute=(attr_name)
63
+ if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
64
+ method_body, line = <<-EOV, __LINE__ + 1
65
+ def #{attr_name}=(original_time)
66
+ time = original_time
67
+ unless time.acts_like?(:time)
68
+ time = time.is_a?(String) ? (I18n.delocalization_enabled? ? Time.zone.parse_localized(time) : Time.zone.parse(time)) : time.to_time rescue time
69
+ end
70
+ time = time.in_time_zone rescue nil if time
71
+ write_attribute(:#{attr_name}, original_time)
72
+ @attributes_cache["#{attr_name}"] = time
73
+ end
74
+ EOV
75
+ generated_attribute_methods.module_eval(method_body, __FILE__, line)
76
+ else
77
+ super
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,7 @@
1
+ require 'delocalize/localized_date_time_parser'
2
+
3
+ ActiveSupport::TimeZone.class_eval do
4
+ def parse_localized(time_with_zone)
5
+ Delocalize::LocalizedDateTimeParser.parse(time_with_zone, self.class)
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ module Delocalize
2
+ class Railtie < Rails::Railtie
3
+ initializer "delocalize" do |app|
4
+ ActiveSupport.on_load :active_record do
5
+ require 'delocalize/rails_ext/active_record'
6
+ end
7
+
8
+ ActiveSupport.on_load :action_view do
9
+ require 'delocalize/rails_ext/action_view'
10
+ end
11
+
12
+ require 'delocalize/rails_ext/time_zone'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ require 'delocalize/ruby_ext/date'
2
+ require 'delocalize/ruby_ext/datetime'
3
+ require 'delocalize/ruby_ext/time'
4
+ require 'delocalize/ruby_ext/numeric'
@@ -0,0 +1,9 @@
1
+ require 'delocalize/localized_date_time_parser'
2
+
3
+ Date.class_eval do
4
+ class << self
5
+ def parse_localized(date)
6
+ Delocalize::LocalizedDateTimeParser.parse(date, self)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require 'delocalize/localized_date_time_parser'
2
+
3
+ DateTime.class_eval do
4
+ class << self
5
+ def parse_localized(datetime)
6
+ Delocalize::LocalizedDateTimeParser.parse(datetime, self)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require 'delocalize/localized_numeric_parser'
2
+
3
+ Numeric.class_eval do
4
+ class << self
5
+ def parse_localized(value)
6
+ Delocalize::LocalizedNumericParser.parse(value)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require 'delocalize/localized_date_time_parser'
2
+
3
+ Time.class_eval do
4
+ class << self
5
+ def parse_localized(time)
6
+ Delocalize::LocalizedDateTimeParser.parse(time, self)
7
+ end
8
+ end
9
+ end
data/test/database.yml ADDED
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: ":memory:"
@@ -0,0 +1,315 @@
1
+ # encoding: utf-8
2
+
3
+ require 'test_helper'
4
+ require 'active_record/test_case'
5
+ require 'action_view/test_case'
6
+
7
+ class DelocalizeActiveRecordTest < ActiveRecord::TestCase
8
+ def setup
9
+ Time.zone = 'Berlin' # make sure everything works as expected with TimeWithZone
10
+ Timecop.freeze(Time.zone.local(2009, 3, 1, 12, 0))
11
+ @product = Product.new
12
+ end
13
+
14
+ test "delocalizes localized number" do
15
+ @product.price = '1.299,99'
16
+ assert_equal 1299.99, @product.price
17
+
18
+ @product.price = '-1.299,99'
19
+ assert_equal -1299.99, @product.price
20
+ end
21
+
22
+ test "delocalizes localized date with year" do
23
+ date = Date.civil(2009, 10, 19)
24
+
25
+ @product.released_on = '19. Oktober 2009'
26
+ assert_equal date, @product.released_on
27
+
28
+ @product.released_on = '19.10.2009'
29
+ assert_equal date, @product.released_on
30
+ end
31
+
32
+ test "delocalizes localized date with year even if locale changes" do
33
+ date = Date.civil(2009, 10, 19)
34
+
35
+ @product.released_on = '19. Oktober 2009'
36
+ assert_equal date, @product.released_on
37
+
38
+ I18n.with_locale :tt do
39
+ @product.released_on = '10|11|2009'
40
+ date = Date.civil(2009, 11, 10)
41
+ assert_equal date, @product.released_on
42
+ end
43
+ end
44
+
45
+ test "delocalizes localized date without year" do
46
+ date = Date.civil(Date.today.year, 10, 19)
47
+
48
+ @product.released_on = '19. Okt'
49
+ assert_equal date, @product.released_on
50
+ end
51
+
52
+ test "delocalizes localized datetime with year" do
53
+ time = Time.zone.local(2009, 3, 1, 12, 0, 0)
54
+
55
+ @product.published_at = 'Sonntag, 1. März 2009, 12:00 Uhr'
56
+ assert_equal time, @product.published_at
57
+
58
+ @product.published_at = '1. März 2009, 12:00 Uhr'
59
+ assert_equal time, @product.published_at
60
+ end
61
+
62
+ test "delocalizes with fallback locale" do
63
+ I18n::Backend::Simple.include(I18n::Backend::Fallbacks)
64
+ I18n.fallbacks[:xx] = [:xx, :tt]
65
+
66
+ I18n.with_locale :xx do
67
+ @product.released_on = '10|11|2009'
68
+ date = Date.civil(2009, 11, 10)
69
+ assert_equal date, @product.released_on
70
+ end
71
+ end
72
+
73
+ test "delocalizes localized datetime without year" do
74
+ time = Time.zone.local(Date.today.year, 3, 1, 12, 0, 0)
75
+
76
+ @product.published_at = '1. März, 12:00 Uhr'
77
+ assert_equal time, @product.published_at
78
+ end
79
+
80
+ # TODO can I somehow do this smarter? or should I use another zone w/o DST?
81
+ if Time.current.dst?
82
+ test "delocalizes localized time (DST)" do
83
+ now = Date.today
84
+ time = Time.zone.local(now.year, now.month, now.day, 9, 0, 0)
85
+ @product.cant_think_of_a_sensible_time_field = '09:00 Uhr'
86
+ assert_equal time, @product.cant_think_of_a_sensible_time_field
87
+ end
88
+ else
89
+ test "delocalizes localized time (non-DST)" do
90
+ now = Date.today
91
+ time = Time.zone.local(now.year, now.month, now.day, 8, 0, 0)
92
+ @product.cant_think_of_a_sensible_time_field = '08:00 Uhr'
93
+ assert_equal time, @product.cant_think_of_a_sensible_time_field
94
+ end
95
+ end
96
+
97
+ test "invalid dates should be delocalized to nil" do
98
+ date = '32. Oktober 2009'
99
+ @product.released_on = date
100
+ assert_equal nil, @product.released_on
101
+ assert_equal date, @product.released_on_before_type_cast
102
+ end
103
+
104
+ test "uses default parse if format isn't found (non-DST)" do
105
+ date = Date.civil(2009, 10, 19)
106
+
107
+ @product.released_on = '2009/10/19'
108
+ assert_equal date, @product.released_on
109
+
110
+ time = Time.zone.local(2009, 3, 1, 12, 0, 0)
111
+ @product.published_at = '2009/03/01 12:00'
112
+ assert_equal time, @product.published_at
113
+
114
+ now = Time.current
115
+ time = Time.zone.local(now.year, now.month, now.day, 8, 0, 0)
116
+ @product.cant_think_of_a_sensible_time_field = '08:00'
117
+ assert_equal time, @product.cant_think_of_a_sensible_time_field
118
+ end
119
+
120
+ test "should return nil if the input is empty or invalid" do
121
+ @product.released_on = ""
122
+ assert_nil @product.released_on
123
+
124
+ @product.released_on = "aa"
125
+ assert_nil @product.released_on
126
+ end
127
+
128
+ test "doesn't raise when attribute is nil" do
129
+ assert_nothing_raised {
130
+ @product.price = nil
131
+ @product.released_on = nil
132
+ @product.published_at = nil
133
+ @product.cant_think_of_a_sensible_time_field = nil
134
+ }
135
+ end
136
+
137
+ test "uses default formats if enable_delocalization is false" do
138
+ I18n.enable_delocalization = false
139
+
140
+ @product.price = '1299.99'
141
+ assert_equal 1299.99, @product.price
142
+
143
+ @product.price = '-1299.99'
144
+ assert_equal -1299.99, @product.price
145
+ end
146
+
147
+ test "uses default formats if called with with_delocalization_disabled" do
148
+ I18n.with_delocalization_disabled do
149
+ @product.price = '1299.99'
150
+ assert_equal 1299.99, @product.price
151
+
152
+ @product.price = '-1299.99'
153
+ assert_equal -1299.99, @product.price
154
+ end
155
+ end
156
+
157
+ test "uses localized parsing if called with with_delocalization_enabled" do
158
+ I18n.with_delocalization_enabled do
159
+ @product.price = '1.299,99'
160
+ assert_equal 1299.99, @product.price
161
+
162
+ @product.price = '-1.299,99'
163
+ assert_equal -1299.99, @product.price
164
+ end
165
+ end
166
+
167
+ test "dirty attributes must detect changes in decimal columns" do
168
+ @product.price = 10
169
+ @product.save
170
+ @product.price = "10,34"
171
+ assert @product.price_changed?
172
+ end
173
+
174
+ test "dirty attributes must detect changes in float columns" do
175
+ @product.weight = 10
176
+ @product.save
177
+ @product.weight = "10,34"
178
+ assert @product.weight_changed?
179
+ end
180
+
181
+ test "attributes that didn't change shouldn't be marked dirty" do
182
+ @product.name = "Good cookies, Really good"
183
+ @product.save
184
+ @product.name = "Good cookies, Really good"
185
+ assert !@product.name_changed?
186
+ end
187
+
188
+ test "should remember the value before type cast" do
189
+ @product.price = "asd"
190
+ assert_equal @product.price, 0
191
+ assert_equal @product.price_before_type_cast, "asd"
192
+ end
193
+ end
194
+
195
+ class DelocalizeActionViewTest < ActionView::TestCase
196
+ include ActionView::Helpers::FormHelper
197
+
198
+ def setup
199
+ Time.zone = 'Berlin' # make sure everything works as expected with TimeWithZone
200
+ @product = Product.new
201
+ end
202
+
203
+ test "shows text field using formatted number" do
204
+ @product.price = 1299.9
205
+ assert_dom_equal '<input id="product_price" name="product[price]" size="30" type="text" value="1.299,90" />',
206
+ text_field(:product, :price)
207
+ end
208
+
209
+ test "shows text field using formatted number with options" do
210
+ @product.price = 1299.995
211
+ assert_dom_equal '<input id="product_price" name="product[price]" size="30" type="text" value="1,299.995" />',
212
+ text_field(:product, :price, :precision => 3, :delimiter => ',', :separator => '.')
213
+ end
214
+
215
+ test "shows text field using formatted number without precision if column is an integer" do
216
+ @product.times_sold = 20
217
+ assert_dom_equal '<input id="product_times_sold" name="product[times_sold]" size="30" type="text" value="20" />',
218
+ text_field(:product, :times_sold)
219
+
220
+ @product.times_sold = 2000
221
+ assert_dom_equal '<input id="product_times_sold" name="product[times_sold]" size="30" type="text" value="2.000" />',
222
+ text_field(:product, :times_sold)
223
+ end
224
+
225
+ test "shows text field using formatted date" do
226
+ @product.released_on = Date.civil(2009, 10, 19)
227
+ assert_dom_equal '<input id="product_released_on" name="product[released_on]" size="30" type="text" value="19.10.2009" />',
228
+ text_field(:product, :released_on)
229
+ end
230
+
231
+ test "shows text field using formatted date and time" do
232
+ @product.published_at = Time.zone.local(2009, 3, 1, 12, 0, 0)
233
+ # careful - leading whitespace with %e
234
+ assert_dom_equal '<input id="product_published_at" name="product[published_at]" size="30" type="text" value="Sonntag, 1. März 2009, 12:00 Uhr" />',
235
+ text_field(:product, :published_at)
236
+ end
237
+
238
+ test "shows text field using formatted date with format" do
239
+ @product.released_on = Date.civil(2009, 10, 19)
240
+ assert_dom_equal '<input id="product_released_on" name="product[released_on]" size="30" type="text" value="19. Oktober 2009" />',
241
+ text_field(:product, :released_on, :format => :long)
242
+ end
243
+
244
+ test "shows text field using formatted date and time with format" do
245
+ @product.published_at = Time.zone.local(2009, 3, 1, 12, 0, 0)
246
+ # careful - leading whitespace with %e
247
+ assert_dom_equal '<input id="product_published_at" name="product[published_at]" size="30" type="text" value=" 1. März, 12:00 Uhr" />',
248
+ text_field(:product, :published_at, :format => :short)
249
+ end
250
+
251
+ test "shows text field using formatted time with format" do
252
+ @product.cant_think_of_a_sensible_time_field = Time.zone.local(2009, 3, 1, 9, 0, 0)
253
+ assert_dom_equal '<input id="product_cant_think_of_a_sensible_time_field" name="product[cant_think_of_a_sensible_time_field]" size="30" type="text" value="09:00 Uhr" />',
254
+ text_field(:product, :cant_think_of_a_sensible_time_field, :format => :time)
255
+ end
256
+
257
+ test "integer hidden fields shouldn't be formatted" do
258
+ @product.times_sold = 1000
259
+ assert_dom_equal '<input id="product_times_sold" name="product[times_sold]" type="hidden" value="1000" />',
260
+ hidden_field(:product, :times_sold)
261
+ end
262
+
263
+ test "doesn't raise an exception when object is nil" do
264
+ assert_nothing_raised {
265
+ text_field(:not_here, :a_text_field)
266
+ }
267
+ end
268
+
269
+ test "doesn't raise for nil Date/Time" do
270
+ @product.published_at, @product.released_on, @product.cant_think_of_a_sensible_time_field = nil
271
+ assert_nothing_raised {
272
+ text_field(:product, :published_at)
273
+ text_field(:product, :released_on)
274
+ text_field(:product, :cant_think_of_a_sensible_time_field)
275
+ }
276
+ end
277
+
278
+ test "doesn't override given :value" do
279
+ @product.price = 1299.9
280
+ assert_dom_equal '<input id="product_price" name="product[price]" size="30" type="text" value="1.499,90" />',
281
+ text_field(:product, :price, :value => "1.499,90")
282
+ end
283
+
284
+ test "doesn't convert the value if field has numericality errors" do
285
+ @product = ProductWithValidation.new(:price => 'this is not a number')
286
+ @product.valid?
287
+ assert_dom_equal %(<div class="field_with_errors"><input id="product_price" name="product[price]" size="30" type="text" value="this is not a number" /></div>),
288
+ text_field(:product, :price)
289
+ end
290
+
291
+ test "should convert the value if field have non-numericality errors, but have other errors, e.g. business rules" do
292
+ @product = ProductWithBusinessValidation.new(:price => '1.337,66')
293
+ @product.valid?
294
+ assert_dom_equal %(<div class="field_with_errors"><input id="product_price" name="product[price]" size="30" type="text" value="1.337,66" /></div>),
295
+ text_field(:product, :price)
296
+ end
297
+
298
+ test "doesn't raise an exception when object isn't an ActiveReccord" do
299
+ @product = NonArProduct.new
300
+ assert_nothing_raised {
301
+ text_field(:product, :name)
302
+ text_field(:product, :times_sold)
303
+ text_field(:product, :published_at)
304
+ text_field(:product, :released_on)
305
+ text_field(:product, :cant_think_of_a_sensible_time_field)
306
+ text_field(:product, :price, :value => "1.499,90")
307
+ }
308
+ end
309
+
310
+ test "formats field with default value correctly" do
311
+ assert_dom_equal '<input id="product_some_value_with_default" name="product[some_value_with_default]" size="30" type="text" value="0,00" />',
312
+ text_field(:product, :some_value_with_default)
313
+ end
314
+ end
315
+
@@ -0,0 +1,104 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ Bundler.require(:default, :development)
7
+
8
+ require 'test/unit'
9
+
10
+ require 'delocalize/rails_ext/action_view'
11
+ require 'delocalize/rails_ext/active_record'
12
+ require 'delocalize/rails_ext/time_zone'
13
+
14
+ de = {
15
+ :date => {
16
+ :input => {
17
+ :formats => [:long, :short, :default]
18
+ },
19
+ :formats => {
20
+ :default => "%d.%m.%Y",
21
+ :short => "%e. %b",
22
+ :long => "%e. %B %Y",
23
+ :only_day => "%e"
24
+ },
25
+ :day_names => %w(Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag),
26
+ :abbr_day_names => %w(So Mo Di Mi Do Fr Sa),
27
+ :month_names => [nil] + %w(Januar Februar März April Mai Juni Juli August September Oktober November Dezember),
28
+ :abbr_month_names => [nil] + %w(Jan Feb Mär Apr Mai Jun Jul Aug Sep Okt Nov Dez)
29
+ },
30
+ :time => {
31
+ :input => {
32
+ :formats => [:long, :medium, :short, :default, :time]
33
+ },
34
+ :formats => {
35
+ :default => "%A, %e. %B %Y, %H:%M Uhr",
36
+ :short => "%e. %B, %H:%M Uhr",
37
+ :medium => "%e. %B %Y, %H:%M Uhr",
38
+ :long => "%A, %e. %B %Y, %H:%M Uhr",
39
+ :time => "%H:%M Uhr"
40
+ },
41
+ :am => 'vormittags',
42
+ :pm => 'nachmittags'
43
+ },
44
+ :number => {
45
+ :format => {
46
+ :precision => 2,
47
+ :separator => ',',
48
+ :delimiter => '.'
49
+ }
50
+ },
51
+ :activerecord => {
52
+ :errors => {
53
+ :messages => {
54
+ :not_a_number => 'is not a number'
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ # deeply clone the hash for a fantasy language called tt
61
+ tt = Marshal.load(Marshal.dump(de))
62
+ tt[:date][:formats][:default] = '%d|%m|%Y'
63
+
64
+ I18n.backend.store_translations :de, de
65
+ I18n.backend.store_translations :tt, tt
66
+
67
+ I18n.locale = :de
68
+
69
+ class NonArProduct
70
+ attr_accessor :name, :price, :times_sold,
71
+ :cant_think_of_a_sensible_time_field,
72
+ :released_on, :published_at
73
+ end
74
+
75
+ class Product < ActiveRecord::Base
76
+ end
77
+
78
+ class ProductWithValidation < Product
79
+ validates_numericality_of :price
80
+ validates_presence_of :price
81
+ end
82
+
83
+ class ProductWithBusinessValidation < Product
84
+ validate do |record|
85
+ if record.price > 10
86
+ record.errors.add(:price, :invalid)
87
+ end
88
+ end
89
+ end
90
+
91
+ config = YAML.load_file(File.dirname(__FILE__) + '/database.yml')
92
+ ActiveRecord::Base.establish_connection(config['test'])
93
+
94
+ ActiveRecord::Base.connection.create_table :products do |t|
95
+ t.string :name
96
+ t.date :released_on
97
+ t.datetime :published_at
98
+ t.time :cant_think_of_a_sensible_time_field
99
+ t.decimal :price
100
+ t.float :weight
101
+ t.integer :times_sold
102
+ t.decimal :some_value_with_default, :default => 0, :precision => 20, :scale => 2
103
+ end
104
+
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nulogy-delocalize
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Clemens Kofler
9
+ - Nulogy
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-05-24 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rails
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '3.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '3.0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: sqlite3
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: 1.3.4
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: 1.3.4
47
+ - !ruby/object:Gem::Dependency
48
+ name: timecop
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.3.5
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ version: 0.3.5
63
+ description: Delocalize is a tool for parsing localized dates/times and numbers.
64
+ email: clemens@railway.at
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files:
68
+ - README.markdown
69
+ files:
70
+ - MIT-LICENSE
71
+ - Rakefile
72
+ - VERSION
73
+ - lib/delocalize.rb
74
+ - lib/delocalize/i18n_ext.rb
75
+ - lib/delocalize/localized_date_time_parser.rb
76
+ - lib/delocalize/localized_numeric_parser.rb
77
+ - lib/delocalize/rails_ext/action_view.rb
78
+ - lib/delocalize/rails_ext/active_record.rb
79
+ - lib/delocalize/rails_ext/time_zone.rb
80
+ - lib/delocalize/railtie.rb
81
+ - lib/delocalize/ruby_ext.rb
82
+ - lib/delocalize/ruby_ext/date.rb
83
+ - lib/delocalize/ruby_ext/datetime.rb
84
+ - lib/delocalize/ruby_ext/numeric.rb
85
+ - lib/delocalize/ruby_ext/time.rb
86
+ - README.markdown
87
+ - test/database.yml
88
+ - test/delocalize_test.rb
89
+ - test/test_helper.rb
90
+ homepage: http://github.com/clemens/delocalize
91
+ licenses: []
92
+ post_install_message:
93
+ rdoc_options:
94
+ - --charset=UTF-8
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ! '>='
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 1.8.24
112
+ signing_key:
113
+ specification_version: 3
114
+ summary: Localized date/time and number parsing
115
+ test_files:
116
+ - test/database.yml
117
+ - test/delocalize_test.rb
118
+ - test/test_helper.rb