i18n_alchemy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ ## master
2
+
3
+ * Basic localization for numeric, date and time attributes
4
+ * Accept hash of attributes on localized call
5
+ * Localize attributes=, assign_attributes, update_attribute, and update_attributes ([@tomas-stefano](https://github.com/tomas-stefano))
6
+ * Localize nested attributes ([@tomas-stefano](https://github.com/tomas-stefano))
7
+ * JRuby compatibility ([@sobrinho](https://github.com/sobrinho))
8
+ * Format numeric values based on the current I18n number precision, delimiter and thousand separator
9
+ * Support for Rails 3.0, 3.1 and 3.2
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Carlos Antonio da Silva (carlosantoniodasilva@gmail.com)
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.md ADDED
@@ -0,0 +1,118 @@
1
+ ## I18nAlchemy
2
+
3
+ I18n date/number parsing/localization
4
+
5
+ I18nAlchemy aims to handle date, time and number parsing, based on current I18n locale format. The main idea is to have ORMs, such as ActiveRecord for now, to automatically accept dates/numbers given in the current locale format, and return these values localized as well.
6
+
7
+ ### Why
8
+
9
+ Almost all projects I've been working so far required some sort of input using dates and numbers, and it has always been a pain due to lack of this support in ActiveRecord itself. As I do most of my projects using different date/number formats than the English defaults (I live in Brazil), we've been adopting different ways to handle that in each application.
10
+
11
+ I've already used the delocalize gem in some of these projects, and validates_timeliness parser in others, and both work pretty well actually. But I think it might work a bit different than delocalize, and mainly, I wanted to learn more about I18n and ActiveRecord internals.
12
+
13
+ ## Usage
14
+
15
+ I18nAlchemy is pretty straigthforward to use, you just need to include it in your ActiveRecord model. Lets say we are working with a Product model:
16
+
17
+ ```ruby
18
+ class Product < ActiveRecord::Base
19
+ include I18n::Alchemy
20
+ end
21
+ ```
22
+
23
+ By mixing the module into your model, you get the *localized* method:
24
+
25
+ ```ruby
26
+ @product = Product.first
27
+ @localized = @product.localized
28
+ ```
29
+
30
+ Here are some examples on how to use it with numeric values:
31
+
32
+ ```ruby
33
+ @localized.price = "1.99"
34
+
35
+ @product.price # => 1.99
36
+ @localized.price # => "1.99"
37
+
38
+ I18n.with_locale :pt do
39
+ @localized.price = "1,88"
40
+
41
+ @product.price # => 1.88
42
+ @localized.price # => "1,88"
43
+ end
44
+ ```
45
+
46
+ Please notice that the localized proxy also formats your numeric values based on the current precision in your I18n locale file:
47
+
48
+ ```ruby
49
+ @product.price = 1.3
50
+ @localized.price # => "1.30", considering a precision of 2
51
+ ```
52
+
53
+ And with thousand separators as well:
54
+
55
+ ```ruby
56
+ @product.price = 1840.32
57
+ @localized.price # => "1,840.32", considering separator = "," and delimiter = "."
58
+ ```
59
+
60
+ Some examples with date / time objects:
61
+
62
+ ```ruby
63
+ @localized.released_at = "12/31/2011"
64
+
65
+ @product.released_at # => Date.new(2011, 12, 31)
66
+ @localized.released_at # => "12/31/2011"
67
+
68
+ I18n.with_locale :pt do
69
+ @localized.released_at = "31/12/2011"
70
+
71
+ @product.released_at # => Date.new(2011, 12, 31)
72
+ @localized.released_at # => "31/12/2011"
73
+ end
74
+ ```
75
+
76
+ The localized method quacks like ActiveRecord: you can give a hash of attributes and extra options if you want, and it will delegate everything to the object, parsing the attributes before:
77
+
78
+ ```ruby
79
+ # This will parse the attributes in the given hash.
80
+ I18n.with_locale :pt do
81
+ @localized = @product.localized(:price => "1,88")
82
+
83
+ @product.price # => 1.88
84
+ @localized.price # => "1,88"
85
+ end
86
+ ```
87
+
88
+ ## I18n configuration
89
+
90
+ Right now the lib uses the same configuration for numeric, date and time values from Rails:
91
+
92
+ ```yaml
93
+ en:
94
+
95
+ date:
96
+ formats:
97
+ default: "%m/%d/%Y"
98
+
99
+ time:
100
+ formats:
101
+ default: "%m/%d/%Y %H:%M:%S"
102
+
103
+ number:
104
+ format:
105
+ separator: '.'
106
+ delimiter: ','
107
+ precision: 2
108
+ ```
109
+
110
+ Please notice the default date and time format is considered for input values for now, and it will only accept valid values matching these formats. We plan to add specific formats and to parse a list of possible input formats for I18nAlchemy, to make it more flexible, please refer to TODO file for more info.
111
+
112
+ ## Contact
113
+
114
+ Carlos Antonio da Silva (http://github.com/carlosantoniodasilva)
115
+
116
+ ## License
117
+
118
+ MIT License.
@@ -0,0 +1,49 @@
1
+ module I18n
2
+ module Alchemy
3
+ class AssociationParser
4
+ attr_reader :target_class, :association_name
5
+
6
+ def initialize(target_class, association_name)
7
+ @target_class = target_class
8
+ @association_name = association_name
9
+ @association = @target_class.reflect_on_association(@association_name)
10
+ @proxy = @association.klass.new.localized
11
+ end
12
+
13
+ # Parse nested attributes for one-to-one and collection association
14
+ #
15
+ # ==== Examples
16
+ #
17
+ # parse(:avatar_attributes => {:icon => 'sad_panda'})
18
+ # parse(:posts_attributes => [{:title => 'Foo!'}, {:title => 'Bar!'}])
19
+ # parse(:posts_attributes => { 0 => {:title => 'Foo!'}, 1 => {:title => 'Bar!'})
20
+ # parse(:posts_attributes => { "81u21udjsndja" => {:title => 'Foo!'}, "akmsams" => {:title => 'Baz!'}})
21
+ #
22
+ def parse(attributes)
23
+ if @association.macro == :has_many
24
+ attributes = if attributes.is_a?(Hash)
25
+ attributes.values
26
+ else
27
+ attributes
28
+ end
29
+ attributes.collect { |value_attributes| @proxy.send(:parse_attributes, value_attributes) }
30
+ else
31
+ @proxy.send(:parse_attributes, attributes)
32
+ end
33
+ end
34
+
35
+ # ==== Examples
36
+ #
37
+ # class Member < ActiveRecord::Base
38
+ # has_one :avatar
39
+ # accepts_nested_attributes_for :avatar
40
+ # end
41
+ #
42
+ # AssociationParser.new(Member, :avatar).association_name_attributes # => "avatar_attributes"
43
+ #
44
+ def association_name_attributes
45
+ "#{association_name}_attributes"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,23 @@
1
+ module I18n
2
+ module Alchemy
3
+ class Attribute
4
+ def initialize(target, attribute, parser)
5
+ @target = target
6
+ @attribute = attribute
7
+ @parser = parser
8
+ end
9
+
10
+ def read
11
+ @parser.localize(@target.send(@attribute))
12
+ end
13
+
14
+ def write(value)
15
+ @target.send(:"#{@attribute}=", parse(value))
16
+ end
17
+
18
+ def parse(value)
19
+ @parser.parse(value)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ module I18n
2
+ module Alchemy
3
+ module DateParser
4
+ extend self
5
+
6
+ def parse(value)
7
+ return value unless valid_for_parsing?(value)
8
+
9
+ if parsed_date = Date._strptime(value, i18n_format)
10
+ build_object(parsed_date).to_s
11
+ else
12
+ value
13
+ end
14
+ end
15
+
16
+ def localize(value)
17
+ valid_for_localization?(value) ? I18n.localize(value) : value
18
+ end
19
+
20
+ protected
21
+
22
+ def build_object(parsed_date)
23
+ Date.new(*parsed_date.values_at(:year, :mon, :mday))
24
+ end
25
+
26
+ def i18n_format
27
+ I18n.t(:default, :scope => [i18n_scope, :formats])
28
+ end
29
+
30
+ def i18n_scope
31
+ :date
32
+ end
33
+
34
+ def valid_for_localization?(value)
35
+ value.is_a?(Date)
36
+ end
37
+
38
+ def valid_for_parsing?(value)
39
+ !valid_for_localization?(value)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,57 @@
1
+ module I18n
2
+ module Alchemy
3
+ module NumericParser
4
+ extend self
5
+
6
+ def parse(value)
7
+ if valid_for_parsing?(value)
8
+ value.gsub(delimiter, '_').gsub(separator, '.')
9
+ else
10
+ value
11
+ end
12
+ end
13
+
14
+ def localize(value)
15
+ if valid_for_localization?(value)
16
+ number_with_delimiter(format("%.#{precision}f", value))
17
+ else
18
+ value
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def delimiter
25
+ translate :delimiter
26
+ end
27
+
28
+ def precision
29
+ translate :precision
30
+ end
31
+
32
+ def separator
33
+ translate :separator
34
+ end
35
+
36
+ def translate(key)
37
+ I18n.t(key, :scope => :"number.format")
38
+ end
39
+
40
+ def valid_for_localization?(value)
41
+ value.is_a?(Numeric) && !value.is_a?(Integer)
42
+ end
43
+
44
+ def valid_for_parsing?(value)
45
+ value.respond_to?(:gsub)
46
+ end
47
+
48
+ # Logic extracted from Rails' number_with_delimiter helper.
49
+ NUMBER_WITH_DELIMITER = /(\d)(?=(\d\d\d)+(?!\d))/
50
+ def number_with_delimiter(number)
51
+ parts = number.split('.')
52
+ parts[0].gsub!(NUMBER_WITH_DELIMITER, "\\1#{delimiter}")
53
+ parts.join(separator)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,136 @@
1
+ module I18n
2
+ module Alchemy
3
+ # Depend on AS::BasicObject which has a "blank slate" - no methods.
4
+ class Proxy < ActiveSupport::BasicObject
5
+ # TODO: cannot assume _id is always a foreign key.
6
+ # Find a better way to find that and skip these columns.
7
+ def initialize(target, attributes=nil, *args)
8
+ @target = target
9
+ @localized_attributes = {}
10
+
11
+ @target.class.columns.each do |column|
12
+ next if column.primary || column.name.ends_with?("_id")
13
+
14
+ parser = detect_parser(column)
15
+ if parser
16
+ create_localized_attribute(column.name, parser)
17
+ define_localized_methods(column.name)
18
+ end
19
+ end
20
+
21
+ @localized_associations = @target.class.nested_attributes_options.map do |association_name, options|
22
+ ::I18n::Alchemy::AssociationParser.new(@target.class, association_name)
23
+ end
24
+
25
+ assign_attributes(attributes, *args) if attributes
26
+ end
27
+
28
+ def attributes=(attributes)
29
+ @target.attributes = parse_attributes(attributes)
30
+ end
31
+
32
+ # This method is added to the proxy even thought it does not exist in
33
+ # Rails 3.0 (only >= 3.1).
34
+ def assign_attributes(attributes, *args)
35
+ if @target.respond_to?(:assign_attributes)
36
+ @target.assign_attributes(parse_attributes(attributes), *args)
37
+ else
38
+ self.attributes = attributes
39
+ end
40
+ end
41
+
42
+ def update_attributes(attributes, *args)
43
+ @target.update_attributes(parse_attributes(attributes), *args)
44
+ end
45
+
46
+ def update_attributes!(attributes, *args)
47
+ @target.update_attributes!(parse_attributes(attributes), *args)
48
+ end
49
+
50
+ def update_attribute(attribute, value)
51
+ attributes = parse_attributes(attribute => value)
52
+ @target.update_attribute(attribute, attributes.values.first)
53
+ end
54
+
55
+ # Override to_param to always return the +proxy.to_param+. This allow us
56
+ # to integrate with action view.
57
+ def to_param
58
+ @target.to_param
59
+ end
60
+
61
+ # Override to_model to always return the proxy, otherwise it returns the
62
+ # target object. This allows us to integrate with action view.
63
+ def to_model
64
+ self
65
+ end
66
+
67
+ # Allow calling localized methods with :send. This allows us to integrate
68
+ # with action view methods.
69
+ alias :send :__send__
70
+
71
+ # Allow calling localized methods with :try. If the method is not declared
72
+ # here, it'll be delegated to the target, losing localization capabilities.
73
+ def try(*a, &b)
74
+ __send__(*a, &b)
75
+ end
76
+
77
+ # Delegate all method calls that are not translated to the target object.
78
+ # As the proxy does not have any other method, there is no need to
79
+ # override :respond_to, just delegate it to the target as well.
80
+ def method_missing(*args, &block)
81
+ @target.send(*args, &block)
82
+ end
83
+
84
+ private
85
+
86
+ def create_localized_attribute(column_name, parser)
87
+ @localized_attributes[column_name] =
88
+ ::I18n::Alchemy::Attribute.new(@target, column_name, parser)
89
+ end
90
+
91
+ def define_localized_methods(column_name)
92
+ class << self; self; end.instance_eval do
93
+ define_method(column_name) do
94
+ @localized_attributes[column_name].read
95
+ end
96
+
97
+ # Before type cast must be localized to integrate with action view.
98
+ define_method("#{column_name}_before_type_cast") do
99
+ @localized_attributes[column_name].read
100
+ end
101
+
102
+ define_method("#{column_name}=") do |value|
103
+ @localized_attributes[column_name].write(value)
104
+ end
105
+ end
106
+ end
107
+
108
+ def detect_parser(column)
109
+ case
110
+ when column.number?
111
+ NumericParser
112
+ when column.type == :date
113
+ DateParser
114
+ when column.type == :datetime || column.type == :timestamp
115
+ TimeParser
116
+ end
117
+ end
118
+
119
+ def parse_attributes(attributes)
120
+ attributes = attributes.stringify_keys
121
+
122
+ @localized_attributes.each do |column_name, attribute|
123
+ next unless attributes.key?(column_name)
124
+ attributes[column_name] = attribute.parse(attributes[column_name])
125
+ end
126
+ @localized_associations.each do |association_parser|
127
+ association_attributes = association_parser.association_name_attributes
128
+ next unless attributes.key?(association_attributes)
129
+ attributes[association_attributes] = association_parser.parse(attributes[association_attributes])
130
+ end
131
+
132
+ attributes
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,22 @@
1
+ module I18n
2
+ module Alchemy
3
+ module TimeParser
4
+ include DateParser
5
+ extend self
6
+
7
+ protected
8
+
9
+ def build_object(parsed_date)
10
+ Time.utc(*parsed_date.values_at(:year, :mon, :mday, :hour, :min, :sec))
11
+ end
12
+
13
+ def i18n_scope
14
+ :time
15
+ end
16
+
17
+ def valid_for_localization?(value)
18
+ value.is_a?(Time) || value.is_a?(DateTime)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ module I18n
2
+ module Alchemy
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,17 @@
1
+ require "date"
2
+ require "active_record"
3
+ require "i18n"
4
+ require "i18n_alchemy/date_parser"
5
+ require "i18n_alchemy/time_parser"
6
+ require "i18n_alchemy/numeric_parser"
7
+ require "i18n_alchemy/attribute"
8
+ require "i18n_alchemy/association_parser"
9
+ require "i18n_alchemy/proxy"
10
+
11
+ module I18n
12
+ module Alchemy
13
+ def localized(attributes=nil, *args)
14
+ I18n::Alchemy::Proxy.new(self, attributes, *args)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ require "test_helper"
2
+
3
+ class ActionViewTest < MiniTest::Unit::TestCase
4
+ def setup
5
+ @template = ActionView::Base.new
6
+ @product = Product.new(
7
+ :name => "Potato",
8
+ :quantity => 10,
9
+ :price => 1.99,
10
+ :released_at => Date.new(2011, 2, 28),
11
+ :last_sale_at => Time.mktime(2011, 2, 28, 13, 25, 30)
12
+ )
13
+ @localized = @product.localized
14
+
15
+ I18n.locale = :pt
16
+ end
17
+
18
+ def teardown
19
+ I18n.locale = :en
20
+ end
21
+
22
+ def test_text_field_with_string_attribute
23
+ assert_equal '<input id="product_name" name="product[name]" size="30" type="text" value="Potato" />',
24
+ @template.text_field(:product, :name, :object => @localized)
25
+ end
26
+
27
+ def test_text_field_with_integer_attribute
28
+ assert_equal '<input id="product_quantity" name="product[quantity]" size="30" type="text" value="10" />',
29
+ @template.text_field(:product, :quantity, :object => @localized)
30
+ end
31
+
32
+ def test_text_field_with_decimal_attribute
33
+ assert_equal '<input id="product_price" name="product[price]" size="30" type="text" value="1,99" />',
34
+ @template.text_field(:product, :price, :object => @localized)
35
+ end
36
+
37
+ def test_text_field_with_date_attribute
38
+ assert_equal '<input id="product_released_at" name="product[released_at]" size="30" type="text" value="28/02/2011" />',
39
+ @template.text_field(:product, :released_at, :object => @localized)
40
+ end
41
+
42
+ def test_text_field_with_time_attribute
43
+ assert_equal '<input id="product_last_sale_at" name="product[last_sale_at]" size="30" type="text" value="28/02/2011 13:25:30" />',
44
+ @template.text_field(:product, :last_sale_at, :object => @localized)
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ ActiveRecord::Base.establish_connection(
2
+ :adapter => "sqlite3",
3
+ :database => ":memory:"
4
+ )
5
+
6
+ ActiveRecord::Schema.define do
7
+ self.verbose = false
8
+
9
+ create_table :products do |t|
10
+ t.string :name
11
+ t.decimal :price
12
+ t.integer :quantity
13
+ t.date :released_at
14
+ t.datetime :updated_at
15
+ t.timestamp :last_sale_at
16
+ t.references :supplier
17
+ t.string :my_precious
18
+ end
19
+
20
+ create_table :suppliers do |t|
21
+ t.string :name
22
+ t.decimal :a_decimal
23
+ end
24
+
25
+ create_table :accounts do |t|
26
+ t.references :supplier
27
+ t.string :account_number
28
+ t.decimal :total_money
29
+ end
30
+ end
@@ -0,0 +1,40 @@
1
+ require "test_helper"
2
+
3
+ class DateParserTest < MiniTest::Unit::TestCase
4
+ def setup
5
+ @parser = I18n::Alchemy::DateParser
6
+ @date = Date.new(2011, 12, 31)
7
+ end
8
+
9
+ def test_does_not_convert_non_string_objects
10
+ assert_equal @date, @parser.parse(@date)
11
+ end
12
+
13
+ def test_parses_valid_string_dates_with_default_i18n_locale
14
+ assert_equal "2011-12-31", @parser.parse("12/31/2011")
15
+ end
16
+
17
+ def test_parsers_string_dates_on_current_i18n_locale
18
+ I18n.with_locale :pt do
19
+ assert_equal "2011-12-31", @parser.parse("31/12/2011")
20
+ end
21
+ end
22
+
23
+ def test_parsers_returns_the_given_string_when_invalid_date
24
+ assert_equal "31/12/2011", @parser.parse("31/12/2011")
25
+ end
26
+
27
+ def test_does_not_localize_string_values
28
+ assert_equal "12/31/2011", @parser.localize("12/31/2011")
29
+ end
30
+
31
+ def test_localizes_date_values
32
+ assert_equal "12/31/2011", @parser.localize(@date)
33
+ end
34
+
35
+ def test_localizes_date_values_based_on_current_i18n_locale
36
+ I18n.with_locale :pt do
37
+ assert_equal "31/12/2011", @parser.localize(@date)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,73 @@
1
+ require "test_helper"
2
+
3
+ class NumericParserTest < MiniTest::Unit::TestCase
4
+ def setup
5
+ @parser = I18n::Alchemy::NumericParser
6
+ end
7
+
8
+ def test_does_not_convert_non_string_objects
9
+ assert_equal 1.2, @parser.parse(1.2)
10
+ end
11
+
12
+ def test_parses_valid_string_numbers
13
+ assert_equal "1.2", @parser.parse("1.2")
14
+ end
15
+
16
+ def test_parses_string_numbers_with_delimiters
17
+ assert_equal "1_001.2", @parser.parse("1,001.2")
18
+ assert_equal "1_000_001.2", @parser.parse("1,000_001.2")
19
+ end
20
+
21
+ def test_integers
22
+ assert_equal 999, @parser.parse(999)
23
+ assert_equal "999", @parser.parse("999")
24
+ end
25
+
26
+ def test_parses_string_numbers_based_on_current_i18n_locale
27
+ I18n.with_locale :pt do
28
+ assert_equal "1.2", @parser.parse("1,2")
29
+ end
30
+ end
31
+
32
+ def test_parses_string_numbers_with_delimiters_based_on_current_i18n_locale
33
+ I18n.with_locale :pt do
34
+ assert_equal "1_001.2", @parser.parse("1.001,2")
35
+ end
36
+ end
37
+
38
+ def test_does_not_localize_string_values
39
+ assert_equal "1.2", @parser.localize("1.2")
40
+ end
41
+
42
+ def test_localize_numeric_values
43
+ assert_equal "1.20", @parser.localize(1.2)
44
+ end
45
+
46
+ def test_localize_numeric_values_with_delimiters
47
+ assert_equal "123.20", @parser.localize(123.2)
48
+ assert_equal "999.27", @parser.localize(999.27)
49
+ end
50
+
51
+ def test_localize_numeric_values_with_thousand_separators
52
+ assert_equal "1,001.20", @parser.localize(1001.2)
53
+ assert_equal "1,000,001.20", @parser.localize(1_000_001.2)
54
+ end
55
+
56
+ def test_localize_numbers_based_on_current_i18n_locale
57
+ I18n.with_locale :pt do
58
+ assert_equal "1,20", @parser.localize(1.2)
59
+ end
60
+ end
61
+
62
+ def test_localize_numbers_with_delimiters_based_on_current_i18n_locale
63
+ I18n.with_locale :pt do
64
+ assert_equal "99,20", @parser.localize(99.2)
65
+ end
66
+ end
67
+
68
+ def test_localize_numbers_with_separators_based_on_current_i18n_locale
69
+ I18n.with_locale :pt do
70
+ assert_equal "1.001,20", @parser.localize(1_001.2)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,302 @@
1
+ require "test_helper"
2
+
3
+ class ProxyTest < MiniTest::Unit::TestCase
4
+ def setup
5
+ @product = Product.new
6
+ @localized = @product.localized
7
+ @supplier = Supplier.new
8
+ @supplier_localized = @supplier.localized
9
+
10
+ I18n.locale = :pt
11
+ end
12
+
13
+ def teardown
14
+ I18n.locale = :en
15
+ end
16
+
17
+ def test_delegates_orm_methods_to_target_object
18
+ assert @product.new_record?
19
+ assert @localized.save!(:name => "foo", :price => 1.99)
20
+ assert !@product.new_record?
21
+ end
22
+
23
+ def test_delegates_method_with_block_to_target_object
24
+ @localized.method_with_block do |attr|
25
+ assert_equal "called!", attr
26
+ end
27
+ end
28
+
29
+ def test_respond_to
30
+ assert_respond_to @localized, :price
31
+ assert_respond_to @localized, :price=
32
+ assert_respond_to @localized, :name
33
+ assert_respond_to @localized, :save
34
+ assert_respond_to @localized, :method_with_block
35
+ end
36
+
37
+ def test_does_not_localize_primary_key
38
+ @product.id = 1
39
+ assert_equal 1, @localized.id
40
+ end
41
+
42
+ def test_to_param
43
+ assert_equal @product.to_param, @localized.to_param
44
+ end
45
+
46
+ def test_does_not_localize_foreign_keys
47
+ @product.supplier_id = 1
48
+ assert_equal 1, @localized.supplier_id
49
+ end
50
+
51
+ def test_send
52
+ @product.name = "Potato"
53
+ assert_equal "Potato", @localized.send(:name)
54
+
55
+ @product.price = 1.88
56
+ assert_equal "1,88", @localized.send(:price)
57
+ assert_equal "1,88", @localized.send(:price_before_type_cast)
58
+ end
59
+
60
+ def test_try
61
+ @product.price = 1.99
62
+ assert_equal "1,99", @localized.try(:price)
63
+ assert_equal "1,99", @localized.try(:price_before_type_cast)
64
+ end
65
+
66
+ def test_try_with_block
67
+ @localized.try :method_with_block do |attr|
68
+ assert_equal "called!", attr
69
+ end
70
+ end
71
+
72
+ # Numeric
73
+ def test_parses_numeric_attribute_input
74
+ @localized.price = "1,99"
75
+ assert_equal 1.99, @product.price
76
+ end
77
+
78
+ def test_localizes_numeric_attribute_output
79
+ @product.price = 1.88
80
+ assert_equal "1,88", @localized.price
81
+ end
82
+
83
+ def test_localizes_numeric_attribute_before_type_cast_output
84
+ @product.price = 1.88
85
+ assert_equal "1,88", @localized.price_before_type_cast
86
+ end
87
+
88
+ def test_formats_numeric_attribute_output_when_localizing
89
+ @product.price = 1.3
90
+ assert_equal "1,30", @localized.price
91
+
92
+ @product.price = 2
93
+ assert_equal "2,00", @localized.price
94
+ end
95
+
96
+ def test_parsers_integer_attribute_input
97
+ @localized.quantity = "1,0"
98
+ assert_equal 1, @product.quantity
99
+ end
100
+
101
+ def test_localized_integer_attribute_output
102
+ @product.quantity = 1.0
103
+ assert_equal 1, @localized.quantity
104
+ end
105
+
106
+ def test_does_not_localize_other_attribute_input
107
+ @localized.name = "foo"
108
+ assert_equal "foo", @product.name
109
+ end
110
+
111
+ def test_does_not_localize_other_attribute_output
112
+ @product.name = "foo"
113
+ assert_equal "foo", @localized.name
114
+ end
115
+
116
+ # Date
117
+ def test_parses_date_attribute_input
118
+ @localized.released_at = "28/02/2011"
119
+ assert_equal Date.new(2011, 2, 28), @product.released_at
120
+ end
121
+
122
+ def test_localizes_date_attribute_output
123
+ @product.released_at = Date.new(2011, 2, 28)
124
+ assert_equal "28/02/2011", @localized.released_at
125
+ end
126
+
127
+ def test_localizes_date_attribute_before_type_cast_output
128
+ @product.released_at = Date.new(2011, 2, 28)
129
+ assert_equal "28/02/2011", @localized.released_at_before_type_cast
130
+ end
131
+
132
+ # DateTime
133
+ def test_parses_datetime_attribute_input
134
+ @localized.updated_at = "28/02/2011 13:25:30"
135
+ assert_equal Time.mktime(2011, 2, 28, 13, 25, 30), @product.updated_at
136
+ end
137
+
138
+ def test_localizes_datetime_attribute_output
139
+ @product.updated_at = Time.mktime(2011, 2, 28, 13, 25, 30)
140
+ assert_equal "28/02/2011 13:25:30", @localized.updated_at
141
+ end
142
+
143
+ def test_localizes_datetime_attribute_before_type_cast_output
144
+ @product.updated_at = Time.mktime(2011, 2, 28, 13, 25, 30)
145
+ assert_equal "28/02/2011 13:25:30", @localized.updated_at_before_type_cast
146
+ end
147
+
148
+ # Timestamp
149
+ def test_parses_timestamp_attribute_input
150
+ @localized.last_sale_at = "28/02/2011 13:25:30"
151
+ assert_equal Time.mktime(2011, 2, 28, 13, 25, 30), @product.last_sale_at
152
+ end
153
+
154
+ def test_localizes_timestamp_attribute_output
155
+ @product.last_sale_at = Time.mktime(2011, 2, 28, 13, 25, 30)
156
+ assert_equal "28/02/2011 13:25:30", @localized.last_sale_at
157
+ end
158
+
159
+ def test_localizes_timestamp_attribute_before_type_cast_output
160
+ @product.last_sale_at = Time.mktime(2011, 2, 28, 13, 25, 30)
161
+ assert_equal "28/02/2011 13:25:30", @localized.last_sale_at_before_type_cast
162
+ end
163
+
164
+ # Attributes
165
+ def test_initializes_proxy_with_attributes
166
+ @localized = @product.localized(
167
+ :name => "Banana", :price => "0,99", :released_at => "28/02/2011")
168
+
169
+ assert_equal 0.99, @product.price
170
+ assert_equal "0,99", @localized.price
171
+
172
+ assert_equal Date.new(2011, 2, 28), @product.released_at
173
+ assert_equal "28/02/2011", @localized.released_at
174
+ end
175
+
176
+ def test_initializes_proxy_with_attributes_and_skips_mass_assignment_security_protection_when_without_protection_is_used
177
+ @localized = @product.localized(attributes_hash, :without_protection => true)
178
+ if support_assign_attributes_without_protection?
179
+ assert_equal 'My Precious!', @localized.my_precious
180
+ else
181
+ assert_nil @localized.my_precious
182
+ end
183
+ assert_equal 1, @localized.quantity
184
+ end
185
+
186
+ def test_assign_attributes
187
+ @localized.assign_attributes(:price => '1,99')
188
+ assert_equal "1,99", @localized.price
189
+ end
190
+
191
+ def test_mass_assigning_invalid_attribute
192
+ assert_raises(ActiveRecord::UnknownAttributeError) do
193
+ @localized.assign_attributes('i_dont_even_exist' => 40)
194
+ end
195
+ end
196
+
197
+ def test_new_with_attr_protected_attributes
198
+ @localized.assign_attributes(attributes_hash)
199
+ assert_nil @localized.my_precious
200
+ assert_equal 1, @localized.quantity
201
+ end
202
+
203
+ def test_assign_attributes_skips_mass_assignment_security_protection_when_without_protection_is_used
204
+ @localized.assign_attributes(attributes_hash, :without_protection => true)
205
+ if support_assign_attributes_without_protection?
206
+ assert_equal 'My Precious!', @localized.my_precious
207
+ else
208
+ assert_nil @localized.my_precious
209
+ end
210
+ assert_equal 1, @localized.quantity
211
+ end
212
+
213
+ def test_assign_attributes_does_not_change_given_attributes_hash
214
+ assert_attributes_hash_is_not_changed(attributes = attributes_hash) do
215
+ @localized.assign_attributes(attributes)
216
+ end
217
+ end
218
+
219
+ def test_attributes_assignment
220
+ @localized.attributes = { :price => '1,99' }
221
+ assert_equal "1,99", @localized.price
222
+ end
223
+
224
+ def test_attributes_assignment_does_not_change_given_attributes_hash
225
+ assert_attributes_hash_is_not_changed(attributes = attributes_hash) do
226
+ @localized.attributes = attributes
227
+ end
228
+ end
229
+
230
+ def test_update_attributes
231
+ @localized.update_attributes(:price => '2,88')
232
+ assert_equal '2,88', @localized.price
233
+ assert_equal 2.88, @product.reload.price
234
+ end
235
+
236
+ def test_update_attributes_does_not_change_given_attributes_hash
237
+ assert_attributes_hash_is_not_changed(attributes = attributes_hash) do
238
+ @localized.update_attributes(attributes)
239
+ end
240
+ end
241
+
242
+ def test_update_attributes!
243
+ @localized.update_attributes!(:price => '2,88')
244
+ assert_equal '2,88', @localized.price
245
+ assert_equal 2.88, @product.reload.price
246
+ end
247
+
248
+ def test_update_attributes_bang_does_not_change_given_attributes_hash
249
+ assert_attributes_hash_is_not_changed(attributes = attributes_hash) do
250
+ @localized.update_attributes!(attributes)
251
+ end
252
+ end
253
+
254
+ def test_update_attribute
255
+ @localized.update_attribute(:price, '2,88')
256
+ assert_equal '2,88', @localized.price
257
+ assert_equal 2.88, @product.reload.price
258
+ end
259
+
260
+ # Nested Attributes
261
+ def test_should_assign_for_nested_attributes_for_collection_association
262
+ @supplier_localized.assign_attributes(:products_attributes => [{:price => '1,99'}, {:price => '2,93'}])
263
+ assert_equal 2, @supplier_localized.products.size
264
+ assert_equal '1,99', @supplier_localized.products.first.localized.price
265
+ assert_equal '2,93', @supplier_localized.products.last.localized.price
266
+ end
267
+
268
+ def test_should_assign_for_nested_attributes_passing_a_hash_for_collection_with_unique_keys
269
+ @supplier_localized.assign_attributes(:products_attributes => {"0" => {:price => '2,93', "_destroy"=>"false"}, "1" => {:price => '2,85', "_destroy" => "false"}})
270
+ prices = @supplier.products.map { |p| p.localized.price }.sort
271
+ assert_equal ['2,85', '2,93'], prices
272
+ end
273
+
274
+ def test_should_assign_for_nested_attributes_for_one_to_one_association
275
+ @supplier_localized.assign_attributes(:account_attributes => {:account_number => 10, :total_money => '100,87'})
276
+ account = @supplier_localized.account
277
+ assert_equal 10, account.account_number
278
+ assert_equal '100,87', account.localized.total_money
279
+ end
280
+
281
+ def test_update_attributes_for_nested_attributes
282
+ @supplier_localized.update_attributes(:account_attributes => {:total_money => '99,87'})
283
+ assert_equal '99,87', @supplier_localized.account.localized.total_money
284
+ end
285
+
286
+ def test_attributes_assignment_for_nested
287
+ @supplier_localized.attributes = {:account_attributes => {:total_money => '88,12'}}
288
+ assert_equal '88,12', @supplier_localized.account.localized.total_money
289
+ end
290
+
291
+ private
292
+
293
+ def attributes_hash
294
+ { :my_precious => 'My Precious!', :quantity => 1 }
295
+ end
296
+
297
+ def assert_attributes_hash_is_not_changed(attributes)
298
+ yield
299
+ assert_equal 1, attributes[:quantity]
300
+ assert_equal 'My Precious!', attributes[:my_precious]
301
+ end
302
+ end
@@ -0,0 +1,67 @@
1
+ require "test_helper"
2
+
3
+ module BaseTimeParserTest
4
+ def test_does_not_convert_non_string_objects
5
+ assert_equal @time, @parser.parse(@time)
6
+ end
7
+
8
+ # TODO: why so many time differences on the output?
9
+ def test_parses_valid_string_times_with_default_i18n_locale
10
+ output_timestamp = if RUBY_VERSION < "1.9"
11
+ "Sat Dec 31 12:15:45 UTC 2011"
12
+ else
13
+ "2011-12-31 12:15:45 UTC"
14
+ end
15
+
16
+ assert_equal output_timestamp, @parser.parse("12/31/2011 12:15:45")
17
+ end
18
+
19
+ def test_parsers_string_times_on_current_i18n_locale
20
+ I18n.with_locale :pt do
21
+ output_timestamp = if RUBY_VERSION < "1.9"
22
+ "Sat Dec 31 12:15:45 UTC 2011"
23
+ else
24
+ "2011-12-31 12:15:45 UTC"
25
+ end
26
+
27
+ assert_equal output_timestamp, @parser.parse("31/12/2011 12:15:45")
28
+ end
29
+ end
30
+
31
+ def test_parsers_returns_the_given_string_when_invalid_time
32
+ assert_equal "31/12/2011 12:15:45", @parser.parse("31/12/2011 12:15:45")
33
+ end
34
+
35
+ def test_does_not_localize_string_values
36
+ assert_equal "12/31/2011 12:15:45", @parser.localize("12/31/2011 12:15:45")
37
+ end
38
+
39
+ def test_localizes_time_values
40
+ assert_equal "12/31/2011 12:15:45", @parser.localize(@time)
41
+ end
42
+
43
+ def test_localizes_time_values_based_on_current_i18n_locale
44
+ I18n.with_locale :pt do
45
+ assert_equal "31/12/2011 12:15:45", @parser.localize(@time)
46
+ end
47
+ end
48
+ end
49
+
50
+ class TimeParserTest < MiniTest::Unit::TestCase
51
+ def setup
52
+ @parser = I18n::Alchemy::TimeParser
53
+ @time = Time.mktime(2011, 12, 31, 12, 15, 45)
54
+ end
55
+
56
+ include BaseTimeParserTest
57
+ end
58
+
59
+
60
+ class DateTimeParserTest < MiniTest::Unit::TestCase
61
+ def setup
62
+ @parser = I18n::Alchemy::TimeParser
63
+ @time = DateTime.new(2011, 12, 31, 12, 15, 45)
64
+ end
65
+
66
+ include BaseTimeParserTest
67
+ end
@@ -0,0 +1,15 @@
1
+ en:
2
+
3
+ date:
4
+ formats:
5
+ default: "%m/%d/%Y"
6
+
7
+ time:
8
+ formats:
9
+ default: "%m/%d/%Y %H:%M:%S"
10
+
11
+ number:
12
+ format:
13
+ separator: '.'
14
+ delimiter: ','
15
+ precision: 2
@@ -0,0 +1,15 @@
1
+ pt:
2
+
3
+ date:
4
+ formats:
5
+ default: "%d/%m/%Y"
6
+
7
+ time:
8
+ formats:
9
+ default: "%d/%m/%Y %H:%M:%S"
10
+
11
+ number:
12
+ format:
13
+ separator: ','
14
+ delimiter: '.'
15
+ precision: 2
@@ -0,0 +1,5 @@
1
+ class Account < ActiveRecord::Base
2
+ include I18n::Alchemy
3
+
4
+ belongs_to :supplier
5
+ end
@@ -0,0 +1,11 @@
1
+ class Product < ActiveRecord::Base
2
+ include I18n::Alchemy
3
+
4
+ attr_protected :my_precious
5
+
6
+ belongs_to :supplier
7
+
8
+ def method_with_block
9
+ yield "called!"
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ class Supplier < ActiveRecord::Base
2
+ include I18n::Alchemy
3
+
4
+ has_many :products
5
+ has_one :account
6
+
7
+ accepts_nested_attributes_for :products
8
+ accepts_nested_attributes_for :account
9
+ end
@@ -0,0 +1,26 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
3
+ Bundler.require :test
4
+
5
+ require "minitest/unit"
6
+ MiniTest::Unit.autorun
7
+
8
+ require "i18n_alchemy"
9
+ require "action_view"
10
+
11
+ # Setup I18n after other requires to make sure our locales will override any
12
+ # ActiveSupport / ActionView defaults.
13
+ I18n.default_locale = :en
14
+ I18n.locale = :en
15
+ I18n.load_path << Dir[File.expand_path("../locale/*.yml", __FILE__)]
16
+
17
+ require "db/test_schema"
18
+ Dir["test/models/*.rb"].each { |file| require File.expand_path(file) }
19
+
20
+ class MiniTest::Unit::TestCase
21
+ # AR 3.0 does not have assign_attributes and without_protection option, so we
22
+ # are going to skip such tests in this version.
23
+ def support_assign_attributes_without_protection?
24
+ @support_assign_attributes ||= ActiveRecord::VERSION::STRING >= "3.1.0"
25
+ end
26
+ end
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: i18n_alchemy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Carlos Antonio da Silva
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: &70234292607060 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70234292607060
25
+ - !ruby/object:Gem::Dependency
26
+ name: activesupport
27
+ requirement: &70234292606460 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70234292606460
36
+ - !ruby/object:Gem::Dependency
37
+ name: i18n
38
+ requirement: &70234292605940 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '0.5'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70234292605940
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: &70234292605400 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.9.2
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70234292605400
58
+ - !ruby/object:Gem::Dependency
59
+ name: actionpack
60
+ requirement: &70234292604880 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: '3.0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70234292604880
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: &70234292604220 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: 2.10.0
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70234292604220
80
+ description: I18n date/number parsing/localization
81
+ email:
82
+ - carlosantoniodasilva@gmail.com
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - CHANGELOG.md
88
+ - MIT-LICENSE
89
+ - README.md
90
+ - lib/i18n_alchemy/association_parser.rb
91
+ - lib/i18n_alchemy/attribute.rb
92
+ - lib/i18n_alchemy/date_parser.rb
93
+ - lib/i18n_alchemy/numeric_parser.rb
94
+ - lib/i18n_alchemy/proxy.rb
95
+ - lib/i18n_alchemy/time_parser.rb
96
+ - lib/i18n_alchemy/version.rb
97
+ - lib/i18n_alchemy.rb
98
+ - test/action_view_test.rb
99
+ - test/db/test_schema.rb
100
+ - test/i18n_alchemy/date_parser_test.rb
101
+ - test/i18n_alchemy/numeric_parser_test.rb
102
+ - test/i18n_alchemy/proxy_test.rb
103
+ - test/i18n_alchemy/time_parser_test.rb
104
+ - test/locale/en.yml
105
+ - test/locale/pt.yml
106
+ - test/models/account.rb
107
+ - test/models/product.rb
108
+ - test/models/supplier.rb
109
+ - test/test_helper.rb
110
+ homepage: ''
111
+ licenses: []
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ segments:
123
+ - 0
124
+ hash: -3439421370757510840
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ! '>='
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ segments:
132
+ - 0
133
+ hash: -3439421370757510840
134
+ requirements: []
135
+ rubyforge_project: i18n_alchemy
136
+ rubygems_version: 1.8.17
137
+ signing_key:
138
+ specification_version: 3
139
+ summary: I18n date/number parsing/localization
140
+ test_files:
141
+ - test/action_view_test.rb
142
+ - test/db/test_schema.rb
143
+ - test/i18n_alchemy/date_parser_test.rb
144
+ - test/i18n_alchemy/numeric_parser_test.rb
145
+ - test/i18n_alchemy/proxy_test.rb
146
+ - test/i18n_alchemy/time_parser_test.rb
147
+ - test/locale/en.yml
148
+ - test/locale/pt.yml
149
+ - test/models/account.rb
150
+ - test/models/product.rb
151
+ - test/models/supplier.rb
152
+ - test/test_helper.rb