i18n_alchemy 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,4 +1,12 @@
1
- ## master
1
+ ## v0.1.0 - 2013-2-27
2
+
3
+ * Fix json serialization to delegate to the target object instead of the proxy
4
+ * Improve localization for non-AR objects
5
+ * Allow custom parsers to accept a parser module
6
+ * Add support for localizing methods
7
+ * Add support for custom parsers ([@caironoleto][https://github.com/caironoleto])
8
+
9
+ ## v0.0.1 - 2012-5-8
2
10
 
3
11
  * Basic localization for numeric, date and time attributes
4
12
  * Accept hash of attributes on localized call
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  ## I18nAlchemy
2
2
 
3
+ [![Build Status](https://secure.travis-ci.org/carlosantoniodasilva/i18n_alchemy.png)](http://travis-ci.org/carlosantoniodasilva/i18n_alchemy)
4
+
3
5
  I18n date/number parsing/localization
4
6
 
5
7
  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.
@@ -85,6 +87,83 @@ I18n.with_locale :pt do
85
87
  end
86
88
  ```
87
89
 
90
+ ### Localizing methods
91
+
92
+ Given a product model with a `total` method, that is a simple calculation of `quantity * price`, you can tell **I18n::Alchemy** to localize that method for you together with the attributes:
93
+
94
+ ```ruby
95
+ class Product < ActiveRecord::Base
96
+ include I18n::Alchemy
97
+ localize :total, :using => :number
98
+
99
+ def total
100
+ quantity * price
101
+ end
102
+ end
103
+
104
+ @product = Product.first
105
+ @product.price = 1.99
106
+ @product.quantity = 10
107
+
108
+ @product.total # => 19.90
109
+ @localized.total # => "19,90"
110
+ ```
111
+
112
+ If the method has a writer method, in this case `total=`, that'd get a parsed version for input values as well.
113
+
114
+ With `localize` is possible to localize objects that aren't inheriting from `ActiveRecord::Base`, as long that
115
+ your class have both reader and writer methods available:
116
+
117
+ ```ruby
118
+ class Product
119
+ include I18n::Alchemy
120
+ localize :released_at, :using => :date
121
+
122
+ attr_accessor :released_at
123
+ end
124
+ ```
125
+
126
+ ### Custom Parsers
127
+
128
+ If you want to customize the way an attribute is parsed/localized, you can create a custom parser that looks like this:
129
+
130
+ ```ruby
131
+ module MyCustomDateParser
132
+ include I18n::Alchemy::DateParser
133
+ extend self
134
+
135
+ def localize(value)
136
+ I18n.localize value, :format => :custom
137
+ end
138
+
139
+ protected
140
+
141
+ def i18n_format
142
+ I18n.t(:custom, :scope => [:date, :formats])
143
+ end
144
+ end
145
+ ```
146
+
147
+ And then just configure the attribute you want to use with this new parser:
148
+
149
+ ```ruby
150
+ class Product < ActiveRecord::Base
151
+ include I18n::Alchemy
152
+ custom_parsers :released_month => MyCustomDateParser
153
+ end
154
+ ```
155
+
156
+ By doing this, **I18n::Alchemy** will be set up to use your custom parser for that particular attribute, which in this case will make use of the `:custom` date format in your i18n locale.
157
+
158
+ If you are using `localize`, you can mix the custom parsers with your existing configuration:
159
+
160
+ ```ruby
161
+ class Product < ActiveRecord::Base
162
+ include I18n::Alchemy
163
+ localize :total, :using => MyCustomNumberParser
164
+ end
165
+ ```
166
+
88
167
  ## I18n configuration
89
168
 
90
169
  Right now the lib uses the same configuration for numeric, date and time values from Rails:
data/lib/i18n_alchemy.rb CHANGED
@@ -1,17 +1,38 @@
1
1
  require "date"
2
- require "active_record"
2
+ require "active_support"
3
+ require "active_support/core_ext/class/attribute"
3
4
  require "i18n"
4
5
  require "i18n_alchemy/date_parser"
5
6
  require "i18n_alchemy/time_parser"
6
7
  require "i18n_alchemy/numeric_parser"
7
8
  require "i18n_alchemy/attribute"
8
9
  require "i18n_alchemy/association_parser"
10
+ require "i18n_alchemy/attributes_parsing"
9
11
  require "i18n_alchemy/proxy"
10
12
 
11
13
  module I18n
12
14
  module Alchemy
15
+ extend ActiveSupport::Concern
16
+
13
17
  def localized(attributes=nil, *args)
14
18
  I18n::Alchemy::Proxy.new(self, attributes, *args)
15
19
  end
20
+
21
+ included do
22
+ class_attribute :localized_methods,
23
+ :instance_reader => false, :instance_writer => false
24
+ self.localized_methods = {}
25
+ end
26
+
27
+ module ClassMethods
28
+ def localize(*methods)
29
+ options = methods.extract_options!
30
+ parser = options[:using]
31
+ methods = methods.each_with_object({}) do |method_name, hash|
32
+ hash[method_name] = parser
33
+ end
34
+ self.localized_methods = self.localized_methods.merge(methods)
35
+ end
36
+ end
16
37
  end
17
38
  end
@@ -4,10 +4,8 @@ module I18n
4
4
  attr_reader :target_class, :association_name
5
5
 
6
6
  def initialize(target_class, association_name)
7
- @target_class = target_class
7
+ @target_class = target_class
8
8
  @association_name = association_name
9
- @association = @target_class.reflect_on_association(@association_name)
10
- @proxy = @association.klass.new.localized
11
9
  end
12
10
 
13
11
  # Parse nested attributes for one-to-one and collection association
@@ -20,15 +18,11 @@ module I18n
20
18
  # parse(:posts_attributes => { "81u21udjsndja" => {:title => 'Foo!'}, "akmsams" => {:title => 'Baz!'}})
21
19
  #
22
20
  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) }
21
+ if association.macro == :has_many
22
+ attributes = attributes.is_a?(Hash) ? attributes.values : attributes
23
+ attributes.map { |value_attributes| proxy.send(:parse_attributes, value_attributes) }
30
24
  else
31
- @proxy.send(:parse_attributes, attributes)
25
+ proxy.send(:parse_attributes, attributes)
32
26
  end
33
27
  end
34
28
 
@@ -44,6 +38,16 @@ module I18n
44
38
  def association_name_attributes
45
39
  "#{association_name}_attributes"
46
40
  end
41
+
42
+ private
43
+
44
+ def association
45
+ @association ||= @target_class.reflect_on_association(@association_name)
46
+ end
47
+
48
+ def proxy
49
+ @proxy ||= @association.klass.new.localized
50
+ end
47
51
  end
48
52
  end
49
53
  end
@@ -0,0 +1,50 @@
1
+ module I18n
2
+ module Alchemy
3
+ module AttributesParsing
4
+ def attributes=(attributes)
5
+ @target.attributes = parse_attributes(attributes)
6
+ end
7
+
8
+ # This method is added to the proxy even thought it does not exist in
9
+ # Rails 3.0 (only >= 3.1).
10
+ def assign_attributes(attributes, *args)
11
+ if @target.respond_to?(:assign_attributes)
12
+ @target.assign_attributes(parse_attributes(attributes), *args)
13
+ else
14
+ self.attributes = attributes
15
+ end
16
+ end
17
+
18
+ def update_attributes(attributes, *args)
19
+ @target.update_attributes(parse_attributes(attributes), *args)
20
+ end
21
+
22
+ def update_attributes!(attributes, *args)
23
+ @target.update_attributes!(parse_attributes(attributes), *args)
24
+ end
25
+
26
+ def update_attribute(attribute, value)
27
+ attributes = parse_attributes(attribute => value)
28
+ @target.update_attribute(attribute, attributes.values.first)
29
+ end
30
+
31
+ private
32
+
33
+ def parse_attributes(attributes)
34
+ attributes = attributes.stringify_keys
35
+
36
+ @localized_attributes.each do |column_name, attribute|
37
+ next unless attributes.key?(column_name)
38
+ attributes[column_name] = attribute.parse(attributes[column_name])
39
+ end
40
+ @localized_associations.each do |association_parser|
41
+ association_name = association_parser.association_name_attributes
42
+ next unless attributes.key?(association_name)
43
+ attributes[association_name] = association_parser.parse(attributes[association_name])
44
+ end
45
+
46
+ attributes
47
+ end
48
+ end
49
+ end
50
+ end
@@ -20,7 +20,11 @@ module I18n
20
20
  protected
21
21
 
22
22
  def build_object(parsed_date)
23
- Date.new(*parsed_date.values_at(:year, :mon, :mday))
23
+ Date.new(*extract_date(parsed_date))
24
+ end
25
+
26
+ def extract_date(parsed_date)
27
+ parsed_date.values_at(:year, :mon, :mday).compact
24
28
  end
25
29
 
26
30
  def i18n_format
@@ -2,62 +2,37 @@ module I18n
2
2
  module Alchemy
3
3
  # Depend on AS::BasicObject which has a "blank slate" - no methods.
4
4
  class Proxy < ActiveSupport::BasicObject
5
+ include AttributesParsing
6
+
5
7
  # TODO: cannot assume _id is always a foreign key.
6
8
  # Find a better way to find that and skip these columns.
7
9
  def initialize(target, attributes=nil, *args)
8
10
  @target = target
9
- @localized_attributes = {}
10
-
11
- @target.class.columns.each do |column|
12
- next if column.primary || column.name.ends_with?("_id")
13
11
 
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
12
+ @localized_attributes = {}
13
+ @localized_associations = []
20
14
 
21
- @localized_associations = @target.class.nested_attributes_options.map do |association_name, options|
22
- ::I18n::Alchemy::AssociationParser.new(@target.class, association_name)
15
+ build_methods
16
+ if active_record_compatible?
17
+ build_attributes
18
+ build_associations
23
19
  end
24
20
 
25
21
  assign_attributes(attributes, *args) if attributes
26
22
  end
27
23
 
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
24
  # Override to_param to always return the +proxy.to_param+. This allow us
56
25
  # to integrate with action view.
57
26
  def to_param
58
27
  @target.to_param
59
28
  end
60
29
 
30
+ # Override to_json to always call +to_json+ on the target object, instead of
31
+ # serializing the proxy object, that may issue circular references on Ruby 1.8.
32
+ def to_json(options = nil)
33
+ @target.to_json(options)
34
+ end
35
+
61
36
  # Override to_model to always return the proxy, otherwise it returns the
62
37
  # target object. This allows us to integrate with action view.
63
38
  def to_model
@@ -83,54 +58,87 @@ module I18n
83
58
 
84
59
  private
85
60
 
61
+ def active_record_compatible?
62
+ target_class = @target.class
63
+ target_class.respond_to?(:columns) && target_class.respond_to?(:nested_attributes_options)
64
+ end
65
+
66
+ def build_attributes
67
+ @target.class.columns.each do |column|
68
+ column_name = column.name
69
+ next if column.primary || column_name.ends_with?("_id") || @localized_attributes.key?(column_name)
70
+
71
+ parser = detect_parser_from_column(column)
72
+ build_attribute(column_name, parser)
73
+ end
74
+ end
75
+
76
+ def build_methods
77
+ @target.class.localized_methods.each_pair do |method, parser_type|
78
+ method = method.to_s
79
+ parser = detect_parser(parser_type)
80
+ build_attribute(method, parser)
81
+ end
82
+ end
83
+
84
+ def build_associations
85
+ @target.class.nested_attributes_options.each_key do |association_name|
86
+ create_localized_association(association_name)
87
+ end
88
+ end
89
+
90
+ def build_attribute(name, parser)
91
+ return unless parser
92
+ create_localized_attribute(name, parser)
93
+ define_localized_methods(name)
94
+ end
95
+
96
+ def create_localized_association(association_name)
97
+ @localized_associations <<
98
+ AssociationParser.new(@target.class, association_name)
99
+ end
100
+
86
101
  def create_localized_attribute(column_name, parser)
87
102
  @localized_attributes[column_name] =
88
- ::I18n::Alchemy::Attribute.new(@target, column_name, parser)
103
+ Attribute.new(@target, column_name, parser)
89
104
  end
90
105
 
91
106
  def define_localized_methods(column_name)
107
+ target = @target
92
108
  class << self; self; end.instance_eval do
93
109
  define_method(column_name) do
94
110
  @localized_attributes[column_name].read
95
111
  end
96
112
 
97
113
  # Before type cast must be localized to integrate with action view.
98
- define_method("#{column_name}_before_type_cast") do
114
+ method_name = "#{column_name}_before_type_cast"
115
+ define_method(method_name) do
99
116
  @localized_attributes[column_name].read
100
- end
117
+ end if target.respond_to?(method_name)
101
118
 
102
- define_method("#{column_name}=") do |value|
119
+ method_name = "#{column_name}="
120
+ define_method(method_name) do |value|
103
121
  @localized_attributes[column_name].write(value)
104
- end
122
+ end if target.respond_to?(method_name)
105
123
  end
106
124
  end
107
125
 
108
- def detect_parser(column)
109
- case
110
- when column.number?
126
+ def detect_parser_from_column(column)
127
+ detect_parser(column.number? ? :number : column.type)
128
+ end
129
+
130
+ def detect_parser(type_or_parser)
131
+ case type_or_parser
132
+ when :number
111
133
  NumericParser
112
- when column.type == :date
134
+ when :date
113
135
  DateParser
114
- when column.type == :datetime || column.type == :timestamp
136
+ when :datetime, :timestamp
115
137
  TimeParser
138
+ when ::Module
139
+ type_or_parser
116
140
  end
117
141
  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
142
  end
135
143
  end
136
144
  end
@@ -1,5 +1,5 @@
1
1
  module I18n
2
2
  module Alchemy
3
- VERSION = "0.0.1"
3
+ VERSION = "0.1.0"
4
4
  end
5
5
  end
@@ -1,6 +1,6 @@
1
1
  require "test_helper"
2
2
 
3
- class ActionViewTest < MiniTest::Unit::TestCase
3
+ class ActionViewTest < I18n::Alchemy::TestCase
4
4
  def setup
5
5
  @template = ActionView::Base.new
6
6
  @product = Product.new(
@@ -0,0 +1,14 @@
1
+ module MyCustomDateParser
2
+ include I18n::Alchemy::DateParser
3
+ extend self
4
+
5
+ def localize(value)
6
+ I18n.localize value, :format => :custom
7
+ end
8
+
9
+ protected
10
+
11
+ def i18n_format
12
+ I18n.t(:custom, :scope => [:date, :formats])
13
+ end
14
+ end
@@ -11,6 +11,7 @@ ActiveRecord::Schema.define do
11
11
  t.decimal :price
12
12
  t.integer :quantity
13
13
  t.date :released_at
14
+ t.date :released_month
14
15
  t.datetime :updated_at
15
16
  t.timestamp :last_sale_at
16
17
  t.references :supplier
@@ -19,7 +20,7 @@ ActiveRecord::Schema.define do
19
20
 
20
21
  create_table :suppliers do |t|
21
22
  t.string :name
22
- t.decimal :a_decimal
23
+ t.timestamp :created_at
23
24
  end
24
25
 
25
26
  create_table :accounts do |t|
@@ -1,6 +1,6 @@
1
1
  require "test_helper"
2
2
 
3
- class DateParserTest < MiniTest::Unit::TestCase
3
+ class DateParserTest < I18n::Alchemy::TestCase
4
4
  def setup
5
5
  @parser = I18n::Alchemy::DateParser
6
6
  @date = Date.new(2011, 12, 31)
@@ -20,6 +20,12 @@ class DateParserTest < MiniTest::Unit::TestCase
20
20
  end
21
21
  end
22
22
 
23
+ def test_parsers_string_dates_with_implicit_day
24
+ I18n.with_locale :jp do
25
+ assert_equal "2011-12-01", @parser.parse("12/2011")
26
+ end
27
+ end
28
+
23
29
  def test_parsers_returns_the_given_string_when_invalid_date
24
30
  assert_equal "31/12/2011", @parser.parse("31/12/2011")
25
31
  end
@@ -37,4 +43,10 @@ class DateParserTest < MiniTest::Unit::TestCase
37
43
  assert_equal "31/12/2011", @parser.localize(@date)
38
44
  end
39
45
  end
46
+
47
+ def test_localizes_date_values_with_implicit_day
48
+ I18n.with_locale :jp do
49
+ assert_equal "12/2011", @parser.localize(@date)
50
+ end
51
+ end
40
52
  end
@@ -1,6 +1,6 @@
1
1
  require "test_helper"
2
2
 
3
- class NumericParserTest < MiniTest::Unit::TestCase
3
+ class NumericParserTest < I18n::Alchemy::TestCase
4
4
  def setup
5
5
  @parser = I18n::Alchemy::NumericParser
6
6
  end
@@ -0,0 +1,142 @@
1
+ require "test_helper"
2
+
3
+ class ProxyAttributesParsingTest < I18n::Alchemy::ProxyTestCase
4
+ # Attributes
5
+ def test_initializes_proxy_with_attributes
6
+ @localized = @product.localized(
7
+ :name => "Banana", :price => "0,99", :released_at => "28/02/2011")
8
+
9
+ assert_equal 0.99, @product.price
10
+ assert_equal "0,99", @localized.price
11
+
12
+ assert_equal Date.new(2011, 2, 28), @product.released_at
13
+ assert_equal "28/02/2011", @localized.released_at
14
+ end
15
+
16
+ def test_initializes_proxy_with_attributes_and_skips_mass_assignment_security_protection_when_without_protection_is_used
17
+ @localized = @product.localized(attributes_hash, :without_protection => true)
18
+ if support_assign_attributes_without_protection?
19
+ assert_equal 'My Precious!', @localized.my_precious
20
+ else
21
+ assert_nil @localized.my_precious
22
+ end
23
+ assert_equal 1, @localized.quantity
24
+ end
25
+
26
+ def test_assign_attributes
27
+ @localized.assign_attributes(:price => '1,99')
28
+ assert_equal "1,99", @localized.price
29
+ end
30
+
31
+ def test_mass_assigning_invalid_attribute
32
+ assert_raises(ActiveRecord::UnknownAttributeError) do
33
+ @localized.assign_attributes('i_dont_even_exist' => 40)
34
+ end
35
+ end
36
+
37
+ def test_new_with_attr_protected_attributes
38
+ @localized.assign_attributes(attributes_hash)
39
+ assert_nil @localized.my_precious
40
+ assert_equal 1, @localized.quantity
41
+ end
42
+
43
+ def test_assign_attributes_skips_mass_assignment_security_protection_when_without_protection_is_used
44
+ @localized.assign_attributes(attributes_hash, :without_protection => true)
45
+ if support_assign_attributes_without_protection?
46
+ assert_equal 'My Precious!', @localized.my_precious
47
+ else
48
+ assert_nil @localized.my_precious
49
+ end
50
+ assert_equal 1, @localized.quantity
51
+ end
52
+
53
+ def test_assign_attributes_does_not_change_given_attributes_hash
54
+ assert_attributes_hash_is_not_changed(attributes = attributes_hash) do
55
+ @localized.assign_attributes(attributes)
56
+ end
57
+ end
58
+
59
+ def test_attributes_assignment
60
+ @localized.attributes = { :price => '1,99' }
61
+ assert_equal "1,99", @localized.price
62
+ end
63
+
64
+ def test_attributes_assignment_does_not_change_given_attributes_hash
65
+ assert_attributes_hash_is_not_changed(attributes = attributes_hash) do
66
+ @localized.attributes = attributes
67
+ end
68
+ end
69
+
70
+ def test_update_attributes
71
+ @localized.update_attributes(:price => '2,88')
72
+ assert_equal '2,88', @localized.price
73
+ assert_equal 2.88, @product.reload.price
74
+ end
75
+
76
+ def test_update_attributes_does_not_change_given_attributes_hash
77
+ assert_attributes_hash_is_not_changed(attributes = attributes_hash) do
78
+ @localized.update_attributes(attributes)
79
+ end
80
+ end
81
+
82
+ def test_update_attributes!
83
+ @localized.update_attributes!(:price => '2,88')
84
+ assert_equal '2,88', @localized.price
85
+ assert_equal 2.88, @product.reload.price
86
+ end
87
+
88
+ def test_update_attributes_bang_does_not_change_given_attributes_hash
89
+ assert_attributes_hash_is_not_changed(attributes = attributes_hash) do
90
+ @localized.update_attributes!(attributes)
91
+ end
92
+ end
93
+
94
+ def test_update_attribute
95
+ @localized.update_attribute(:price, '2,88')
96
+ assert_equal '2,88', @localized.price
97
+ assert_equal 2.88, @product.reload.price
98
+ end
99
+
100
+ # Nested Attributes
101
+ def test_should_assign_for_nested_attributes_for_collection_association
102
+ @supplier_localized.assign_attributes(:products_attributes => [{:price => '1,99'}, {:price => '2,93'}])
103
+ assert_equal 2, @supplier_localized.products.size
104
+ assert_equal '1,99', @supplier_localized.products.first.localized.price
105
+ assert_equal '2,93', @supplier_localized.products.last.localized.price
106
+ end
107
+
108
+ def test_should_assign_for_nested_attributes_passing_a_hash_for_collection_with_unique_keys
109
+ @supplier_localized.assign_attributes(:products_attributes => {"0" => {:price => '2,93', "_destroy"=>"false"}, "1" => {:price => '2,85', "_destroy" => "false"}})
110
+ prices = @supplier.products.map { |p| p.localized.price }.sort
111
+ assert_equal ['2,85', '2,93'], prices
112
+ end
113
+
114
+ def test_should_assign_for_nested_attributes_for_one_to_one_association
115
+ @supplier_localized.assign_attributes(:account_attributes => {:account_number => 10, :total_money => '100,87'})
116
+ account = @supplier_localized.account
117
+ assert_equal 10, account.account_number
118
+ assert_equal '100,87', account.localized.total_money
119
+ end
120
+
121
+ def test_update_attributes_for_nested_attributes
122
+ @supplier_localized.update_attributes(:account_attributes => {:total_money => '99,87'})
123
+ assert_equal '99,87', @supplier_localized.account.localized.total_money
124
+ end
125
+
126
+ def test_attributes_assignment_for_nested
127
+ @supplier_localized.attributes = {:account_attributes => {:total_money => '88,12'}}
128
+ assert_equal '88,12', @supplier_localized.account.localized.total_money
129
+ end
130
+
131
+ private
132
+
133
+ def attributes_hash
134
+ { :my_precious => 'My Precious!', :quantity => 1 }
135
+ end
136
+
137
+ def assert_attributes_hash_is_not_changed(attributes)
138
+ yield
139
+ assert_equal 1, attributes[:quantity]
140
+ assert_equal 'My Precious!', attributes[:my_precious]
141
+ end
142
+ end
@@ -0,0 +1,52 @@
1
+ require "test_helper"
2
+
3
+ class ProxyMethodsTest < I18n::Alchemy::ProxyTestCase
4
+ def test_localizes_numeric_methods
5
+ @product.price = 1.99
6
+ @product.quantity = 10
7
+
8
+ assert_equal 19.90, @product.total
9
+ assert_equal "19,90", @localized.total
10
+ end
11
+
12
+ def test_localizes_date_methods
13
+ @product.released_at = Date.new(2011, 2, 28)
14
+ assert_equal "05/03/2011", @localized.estimated_delivery_at
15
+ end
16
+
17
+ def test_localizes_time_methods
18
+ @product.last_sale_at = Time.mktime(2011, 2, 28, 13, 25, 30)
19
+ assert_equal "05/03/2011 23:59:59", @localized.estimated_last_comission_payment_at
20
+ end
21
+
22
+ def test_create_existent_writer_methods_for_parsing_input
23
+ @localized.total = "9,99"
24
+ assert_equal "9.99", @product.total
25
+ end
26
+
27
+ def test_does_not_create_non_existent_writers
28
+ assert_raises NoMethodError do
29
+ @localized.estimated_delivery_at = nil
30
+ end
31
+ assert_raises NoMethodError do
32
+ @localized.estimated_last_comission_payment_at = nil
33
+ end
34
+ end
35
+
36
+ def test_does_not_create_before_type_cast_for_methods
37
+ assert_raises NoMethodError do
38
+ @localized.total_before_type_cast
39
+ end
40
+ assert_raises NoMethodError do
41
+ @localized.estimated_delivery_at_before_type_cast
42
+ end
43
+ assert_raises NoMethodError do
44
+ @localized.estimated_last_comission_payment_at_before_type_cast
45
+ end
46
+ end
47
+
48
+ def test_localize_non_activerecord_models
49
+ @user.created_at = Date.new(2012, 2, 28)
50
+ assert_equal "28/02/2012", @user_localized.created_at
51
+ end
52
+ end
@@ -1,19 +1,6 @@
1
1
  require "test_helper"
2
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
-
3
+ class ProxyTest < I18n::Alchemy::ProxyTestCase
17
4
  def test_delegates_orm_methods_to_target_object
18
5
  assert @product.new_record?
19
6
  assert @localized.save!(:name => "foo", :price => 1.99)
@@ -39,6 +26,11 @@ class ProxyTest < MiniTest::Unit::TestCase
39
26
  assert_equal 1, @localized.id
40
27
  end
41
28
 
29
+ def test_to_json
30
+ @supplier.products << @product
31
+ assert_equal @supplier.to_json, @supplier_localized.to_json
32
+ end
33
+
42
34
  def test_to_param
43
35
  assert_equal @product.to_param, @localized.to_param
44
36
  end
@@ -161,142 +153,19 @@ class ProxyTest < MiniTest::Unit::TestCase
161
153
  assert_equal "28/02/2011 13:25:30", @localized.last_sale_at_before_type_cast
162
154
  end
163
155
 
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
156
+ # Custom parser
157
+ def test_parses_date_attribute_input_with_custom_parser
158
+ @localized.released_month = "02/2011"
159
+ assert_equal Date.new(2011, 2, 1), @product.released_month
217
160
  end
218
161
 
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 }
162
+ def test_localizes_date_attribute_output_with_custom_parser
163
+ @product.released_month = Date.new(2011, 2, 1)
164
+ assert_equal "02/2011", @localized.released_month
295
165
  end
296
166
 
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]
167
+ def test_localizes_date_attribute_before_type_cast_output_with_custom_parser
168
+ @product.released_month = Date.new(2011, 2, 1)
169
+ assert_equal "02/2011", @localized.released_month_before_type_cast
301
170
  end
302
171
  end
@@ -47,7 +47,7 @@ module BaseTimeParserTest
47
47
  end
48
48
  end
49
49
 
50
- class TimeParserTest < MiniTest::Unit::TestCase
50
+ class TimeParserTest < I18n::Alchemy::TestCase
51
51
  def setup
52
52
  @parser = I18n::Alchemy::TimeParser
53
53
  @time = Time.mktime(2011, 12, 31, 12, 15, 45)
@@ -57,7 +57,7 @@ class TimeParserTest < MiniTest::Unit::TestCase
57
57
  end
58
58
 
59
59
 
60
- class DateTimeParserTest < MiniTest::Unit::TestCase
60
+ class DateTimeParserTest < I18n::Alchemy::TestCase
61
61
  def setup
62
62
  @parser = I18n::Alchemy::TimeParser
63
63
  @time = DateTime.new(2011, 12, 31, 12, 15, 45)
data/test/locale/en.yml CHANGED
@@ -3,6 +3,7 @@ en:
3
3
  date:
4
4
  formats:
5
5
  default: "%m/%d/%Y"
6
+ custom: "%m/%Y"
6
7
 
7
8
  time:
8
9
  formats:
@@ -0,0 +1,15 @@
1
+ jp:
2
+
3
+ date:
4
+ formats:
5
+ default: "%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
data/test/locale/pt.yml CHANGED
@@ -3,6 +3,7 @@ pt:
3
3
  date:
4
4
  formats:
5
5
  default: "%d/%m/%Y"
6
+ custom: "%m/%Y"
6
7
 
7
8
  time:
8
9
  formats:
@@ -1,11 +1,36 @@
1
1
  class Product < ActiveRecord::Base
2
2
  include I18n::Alchemy
3
+ localize :total, :using => :number
4
+ localize :estimated_delivery_at, :using => :date
5
+ localize :estimated_last_comission_payment_at, :using => :timestamp
6
+ localize :released_month, :using => MyCustomDateParser
3
7
 
4
8
  attr_protected :my_precious
5
9
 
6
10
  belongs_to :supplier
7
11
 
12
+ def initialize(*)
13
+ super
14
+ @total = nil
15
+ end
16
+
8
17
  def method_with_block
9
18
  yield "called!"
10
19
  end
20
+
21
+ def estimated_last_comission_payment_at
22
+ (last_sale_at + 5.days).end_of_day if last_sale_at?
23
+ end
24
+
25
+ def estimated_delivery_at
26
+ released_at + 5.days if released_at?
27
+ end
28
+
29
+ def total
30
+ @total || (quantity * price if quantity? && price?)
31
+ end
32
+
33
+ def total=(new_total)
34
+ @total = new_total
35
+ end
11
36
  end
@@ -7,3 +7,7 @@ class Supplier < ActiveRecord::Base
7
7
  accepts_nested_attributes_for :products
8
8
  accepts_nested_attributes_for :account
9
9
  end
10
+
11
+ class AnotherSupplier < Supplier
12
+ localize :created_at, :using => :timestamp
13
+ end
@@ -0,0 +1,6 @@
1
+ class User
2
+ include I18n::Alchemy
3
+ attr_accessor :created_at
4
+
5
+ localize :created_at, :using => :date
6
+ end
@@ -0,0 +1,7 @@
1
+ require "test_helper"
2
+
3
+ class ModelsTest < I18n::Alchemy::TestCase
4
+ def test_inheritance_configuration
5
+ assert Supplier.localized_methods != AnotherSupplier.localized_methods
6
+ end
7
+ end
data/test/test_helper.rb CHANGED
@@ -7,6 +7,7 @@ MiniTest::Unit.autorun
7
7
 
8
8
  require "i18n_alchemy"
9
9
  require "action_view"
10
+ require "active_record"
10
11
 
11
12
  # Setup I18n after other requires to make sure our locales will override any
12
13
  # ActiveSupport / ActionView defaults.
@@ -15,12 +16,32 @@ I18n.locale = :en
15
16
  I18n.load_path << Dir[File.expand_path("../locale/*.yml", __FILE__)]
16
17
 
17
18
  require "db/test_schema"
19
+ Dir["test/custom_parsers/*.rb"].each { |file| require File.expand_path(file) }
18
20
  Dir["test/models/*.rb"].each { |file| require File.expand_path(file) }
19
21
 
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"
22
+ module I18n::Alchemy
23
+ class TestCase < MiniTest::Unit::TestCase
24
+ # AR 3.0 does not have assign_attributes and without_protection option, so we
25
+ # are going to skip such tests in this version.
26
+ def support_assign_attributes_without_protection?
27
+ @support_assign_attributes ||= ActiveRecord::VERSION::STRING >= "3.1.0"
28
+ end
29
+ end
30
+
31
+ class ProxyTestCase < TestCase
32
+ def setup
33
+ @product = Product.new
34
+ @localized = @product.localized
35
+ @supplier = Supplier.new
36
+ @supplier_localized = @supplier.localized
37
+ @user = User.new
38
+ @user_localized = @user.localized
39
+
40
+ I18n.locale = :pt
41
+ end
42
+
43
+ def teardown
44
+ I18n.locale = :en
45
+ end
25
46
  end
26
47
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: i18n_alchemy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-08 00:00:00.000000000 Z
12
+ date: 2013-02-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: activerecord
16
- requirement: &70234292607060 !ruby/object:Gem::Requirement
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,21 +21,15 @@ dependencies:
21
21
  version: '3.0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70234292607060
25
- - !ruby/object:Gem::Dependency
26
- name: activesupport
27
- requirement: &70234292606460 !ruby/object:Gem::Requirement
24
+ version_requirements: !ruby/object:Gem::Requirement
28
25
  none: false
29
26
  requirements:
30
27
  - - ~>
31
28
  - !ruby/object:Gem::Version
32
29
  version: '3.0'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: *70234292606460
36
30
  - !ruby/object:Gem::Dependency
37
31
  name: i18n
38
- requirement: &70234292605940 !ruby/object:Gem::Requirement
32
+ requirement: !ruby/object:Gem::Requirement
39
33
  none: false
40
34
  requirements:
41
35
  - - ~>
@@ -43,21 +37,31 @@ dependencies:
43
37
  version: '0.5'
44
38
  type: :runtime
45
39
  prerelease: false
46
- version_requirements: *70234292605940
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '0.5'
47
46
  - !ruby/object:Gem::Dependency
48
- name: rake
49
- requirement: &70234292605400 !ruby/object:Gem::Requirement
47
+ name: actionpack
48
+ requirement: !ruby/object:Gem::Requirement
50
49
  none: false
51
50
  requirements:
52
51
  - - ~>
53
52
  - !ruby/object:Gem::Version
54
- version: 0.9.2
53
+ version: '3.0'
55
54
  type: :development
56
55
  prerelease: false
57
- version_requirements: *70234292605400
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
58
62
  - !ruby/object:Gem::Dependency
59
- name: actionpack
60
- requirement: &70234292604880 !ruby/object:Gem::Requirement
63
+ name: activerecord
64
+ requirement: !ruby/object:Gem::Requirement
61
65
  none: false
62
66
  requirements:
63
67
  - - ~>
@@ -65,18 +69,44 @@ dependencies:
65
69
  version: '3.0'
66
70
  type: :development
67
71
  prerelease: false
68
- version_requirements: *70234292604880
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '3.0'
69
78
  - !ruby/object:Gem::Dependency
70
79
  name: minitest
71
- requirement: &70234292604220 !ruby/object:Gem::Requirement
80
+ requirement: !ruby/object:Gem::Requirement
72
81
  none: false
73
82
  requirements:
74
83
  - - ~>
75
84
  - !ruby/object:Gem::Version
76
- version: 2.10.0
85
+ version: 4.3.2
77
86
  type: :development
78
87
  prerelease: false
79
- version_requirements: *70234292604220
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 4.3.2
94
+ - !ruby/object:Gem::Dependency
95
+ name: rake
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: 10.0.2
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: 10.0.2
80
110
  description: I18n date/number parsing/localization
81
111
  email:
82
112
  - carlosantoniodasilva@gmail.com
@@ -89,6 +119,7 @@ files:
89
119
  - README.md
90
120
  - lib/i18n_alchemy/association_parser.rb
91
121
  - lib/i18n_alchemy/attribute.rb
122
+ - lib/i18n_alchemy/attributes_parsing.rb
92
123
  - lib/i18n_alchemy/date_parser.rb
93
124
  - lib/i18n_alchemy/numeric_parser.rb
94
125
  - lib/i18n_alchemy/proxy.rb
@@ -96,16 +127,22 @@ files:
96
127
  - lib/i18n_alchemy/version.rb
97
128
  - lib/i18n_alchemy.rb
98
129
  - test/action_view_test.rb
130
+ - test/custom_parsers/my_custom_date_parser.rb
99
131
  - test/db/test_schema.rb
100
132
  - test/i18n_alchemy/date_parser_test.rb
101
133
  - test/i18n_alchemy/numeric_parser_test.rb
134
+ - test/i18n_alchemy/proxy/attributes_parsing_test.rb
135
+ - test/i18n_alchemy/proxy/methods_test.rb
102
136
  - test/i18n_alchemy/proxy_test.rb
103
137
  - test/i18n_alchemy/time_parser_test.rb
104
138
  - test/locale/en.yml
139
+ - test/locale/jp.yml
105
140
  - test/locale/pt.yml
106
141
  - test/models/account.rb
107
142
  - test/models/product.rb
108
143
  - test/models/supplier.rb
144
+ - test/models/user.rb
145
+ - test/models_test.rb
109
146
  - test/test_helper.rb
110
147
  homepage: ''
111
148
  licenses: []
@@ -121,7 +158,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
121
158
  version: '0'
122
159
  segments:
123
160
  - 0
124
- hash: -3439421370757510840
161
+ hash: 2006072931743591474
125
162
  required_rubygems_version: !ruby/object:Gem::Requirement
126
163
  none: false
127
164
  requirements:
@@ -130,23 +167,29 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
167
  version: '0'
131
168
  segments:
132
169
  - 0
133
- hash: -3439421370757510840
170
+ hash: 2006072931743591474
134
171
  requirements: []
135
172
  rubyforge_project: i18n_alchemy
136
- rubygems_version: 1.8.17
173
+ rubygems_version: 1.8.23
137
174
  signing_key:
138
175
  specification_version: 3
139
176
  summary: I18n date/number parsing/localization
140
177
  test_files:
141
178
  - test/action_view_test.rb
179
+ - test/custom_parsers/my_custom_date_parser.rb
142
180
  - test/db/test_schema.rb
143
181
  - test/i18n_alchemy/date_parser_test.rb
144
182
  - test/i18n_alchemy/numeric_parser_test.rb
183
+ - test/i18n_alchemy/proxy/attributes_parsing_test.rb
184
+ - test/i18n_alchemy/proxy/methods_test.rb
145
185
  - test/i18n_alchemy/proxy_test.rb
146
186
  - test/i18n_alchemy/time_parser_test.rb
147
187
  - test/locale/en.yml
188
+ - test/locale/jp.yml
148
189
  - test/locale/pt.yml
149
190
  - test/models/account.rb
150
191
  - test/models/product.rb
151
192
  - test/models/supplier.rb
193
+ - test/models/user.rb
194
+ - test/models_test.rb
152
195
  - test/test_helper.rb