globalize-r5 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +111 -0
  3. data/CONTRIBUTING.md +37 -0
  4. data/Gemfile +26 -0
  5. data/LICENSE +22 -0
  6. data/README.md +423 -0
  7. data/Rakefile +55 -0
  8. data/globalize.gemspec +27 -0
  9. data/issue_template.rb +38 -0
  10. data/lib/globalize.rb +90 -0
  11. data/lib/globalize/active_record.rb +14 -0
  12. data/lib/globalize/active_record/act_macro.rb +95 -0
  13. data/lib/globalize/active_record/adapter.rb +99 -0
  14. data/lib/globalize/active_record/adapter_dirty.rb +53 -0
  15. data/lib/globalize/active_record/attributes.rb +26 -0
  16. data/lib/globalize/active_record/class_methods.rb +122 -0
  17. data/lib/globalize/active_record/exceptions.rb +19 -0
  18. data/lib/globalize/active_record/instance_methods.rb +219 -0
  19. data/lib/globalize/active_record/migration.rb +192 -0
  20. data/lib/globalize/active_record/query_methods.rb +113 -0
  21. data/lib/globalize/active_record/translation.rb +45 -0
  22. data/lib/globalize/interpolation.rb +28 -0
  23. data/lib/globalize/version.rb +3 -0
  24. data/lib/i18n/missing_translations_log_handler.rb +41 -0
  25. data/lib/i18n/missing_translations_raise_handler.rb +25 -0
  26. data/lib/patches/active_record/persistence.rb +17 -0
  27. data/lib/patches/active_record/query_method.rb +3 -0
  28. data/lib/patches/active_record/rails4/query_method.rb +35 -0
  29. data/lib/patches/active_record/rails4/uniqueness_validator.rb +39 -0
  30. data/lib/patches/active_record/rails5/uniqueness_validator.rb +47 -0
  31. data/lib/patches/active_record/relation.rb +12 -0
  32. data/lib/patches/active_record/serialization.rb +21 -0
  33. data/lib/patches/active_record/uniqueness_validator.rb +5 -0
  34. data/lib/patches/active_record/xml_attribute_serializer.rb +23 -0
  35. metadata +206 -0
@@ -0,0 +1,113 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module QueryMethods
4
+ class WhereChain < ::ActiveRecord::QueryMethods::WhereChain
5
+ def not(opts, *rest)
6
+ if parsed = @scope.parse_translated_conditions(opts)
7
+ @scope.join_translations.where.not(parsed, *rest)
8
+ else
9
+ super
10
+ end
11
+ end
12
+ end
13
+
14
+ def where(opts = :chain, *rest)
15
+ if opts == :chain
16
+ WhereChain.new(spawn)
17
+ else
18
+ translated, normal = translated_vs_normal(opts)
19
+ if not translated.empty?
20
+ join_translations(super(normal, *rest), translated)
21
+ else
22
+ super
23
+ end
24
+ end
25
+ end
26
+
27
+ def order(opts, *rest)
28
+ if respond_to?(:translated_attribute_names) && parsed = parse_translated_order(opts)
29
+ join_translations super(parsed)
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ def exists?(conditions = :none)
36
+ if parsed = parse_translated_conditions(conditions)
37
+ with_translations_in_fallbacks.exists?(parsed)
38
+ else
39
+ super
40
+ end
41
+ end
42
+
43
+ def translated_vs_normal(opts)
44
+ translated = {}
45
+ if opts.is_a?(Hash) && respond_to?(:translated_attribute_names) && (opts.symbolize_keys.keys & translated_attribute_names).present?
46
+ opts = opts.dup
47
+ opts.each do |k,v|
48
+ translated[k] = opts.delete(k) || opts.delete(key.to_s) if translated_column? k.to_sym
49
+ end
50
+ end
51
+ return translated, opts
52
+ end
53
+
54
+ def with_translations_in_fallbacks
55
+ with_translations(Globalize.fallbacks)
56
+ end
57
+
58
+ def parse_translated_conditions(opts)
59
+ if opts.is_a?(Hash) && respond_to?(:translated_attribute_names) && (keys = opts.symbolize_keys.keys & translated_attribute_names).present?
60
+ opts = opts.dup
61
+ keys.each { |key| opts[translated_column_name(key)] = opts.delete(key) || opts.delete(key.to_s) }
62
+ opts
63
+ end
64
+ end
65
+ if ::ActiveRecord::VERSION::STRING < "5.0.0"
66
+ def where_values_hash(*args)
67
+ return super unless respond_to?(:translations_table_name)
68
+ equalities = respond_to?(:with_default_scope) ? with_default_scope.where_values : where_values
69
+ equalities = equalities.grep(Arel::Nodes::Equality).find_all { |node|
70
+ node.left.relation.name == translations_table_name
71
+ }
72
+
73
+ binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
74
+
75
+ super.merge(Hash[equalities.map { |where|
76
+ name = where.left.name
77
+ [name, binds.fetch(name.to_s) { right = where.right; right.is_a?(Arel::Nodes::Casted) ? right.val : right }]
78
+ }])
79
+ end
80
+ end
81
+
82
+ def join_translations(relation = self, opts = {})
83
+ if relation.joins_values.include?(:translations)
84
+ rel = relation
85
+ else
86
+ rel = relation.with_translations_in_fallbacks
87
+ end
88
+ rel.with_where(opts)
89
+ end
90
+
91
+ private
92
+
93
+ def parse_translated_order(opts)
94
+ case opts
95
+ when Hash
96
+ ordering = opts.map do |column, direction|
97
+ klass = translated_column?(column) ? translation_class : self
98
+ klass.arel_table[column].send(direction)
99
+ end
100
+ order(ordering).order_values
101
+ when Symbol
102
+ translated_column_name(opts) if translated_attribute_names.include?(opts)
103
+ else # failsafe returns nothing
104
+ nil
105
+ end
106
+ end
107
+
108
+ def translated_column?(column)
109
+ translated_attribute_names.include?(column)
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,45 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ class Translation < ::ActiveRecord::Base
4
+
5
+ validates :locale, :presence => true
6
+
7
+ class << self
8
+ # Sometimes ActiveRecord queries .table_exists? before the table name
9
+ # has even been set which results in catastrophic failure.
10
+ def table_exists?
11
+ table_name.present? && super
12
+ end
13
+
14
+ def with_locales(*locales)
15
+ # Avoid using "IN" with SQL queries when only using one locale.
16
+ locales = locales.flatten.map(&:to_s)
17
+ locales = locales.first if locales.one?
18
+ where :locale => locales
19
+ end
20
+ alias with_locale with_locales
21
+
22
+ def translated_locales
23
+ select('DISTINCT locale').order(:locale).map(&:locale)
24
+ end
25
+ end
26
+
27
+ def locale
28
+ _locale = read_attribute :locale
29
+ _locale.present? ? _locale.to_sym : _locale
30
+ end
31
+
32
+ def locale=(locale)
33
+ write_attribute :locale, locale.to_s
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ # Setting this will force polymorphic associations to subclassed objects
40
+ # to use their table_name rather than the parent object's table name,
41
+ # which will allow you to get their models back in a more appropriate
42
+ # format.
43
+ #
44
+ # See http://www.ruby-forum.com/topic/159894 for details.
45
+ Globalize::ActiveRecord::Translation.abstract_class = true
@@ -0,0 +1,28 @@
1
+ module Globalize
2
+ module Interpolation
3
+ def interpolate(name, model, args)
4
+ translation = model.read_attribute(name, {:locale => locale_from(args)})
5
+ try_interpolation translation, interpolation_args_from(args)
6
+ end
7
+
8
+ private
9
+
10
+ def interpolation_args_from(args)
11
+ args.detect {|a| a.is_a? Hash }
12
+ end
13
+
14
+ def locale_from(args)
15
+ args.detect {|a| !a.is_a? Hash }
16
+ end
17
+
18
+ def try_interpolation(translation,args)
19
+ if args
20
+ I18n.interpolate(translation,args)
21
+ else
22
+ translation
23
+ end
24
+ end
25
+
26
+ extend self
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Globalize
2
+ Version = '5.1.0'
3
+ end
@@ -0,0 +1,41 @@
1
+ # A simple exception handler that behaves like the default exception handler
2
+ # but additionally logs missing translations to a given log.
3
+ #
4
+ # Useful for identifying missing translations during testing.
5
+ #
6
+ # E.g.
7
+ #
8
+ # require 'globalize/i18n/missing_translations_log_handler'
9
+ # I18n.missing_translations_logger = RAILS_DEFAULT_LOGGER
10
+ # I18n.exception_handler = :missing_translations_log_handler
11
+ #
12
+ # To set up a different log file:
13
+ #
14
+ # logger = Logger.new("#{RAILS_ROOT}/log/missing_translations.log")
15
+ # I18n.missing_translations_logger = logger
16
+
17
+ module I18n
18
+ @@missing_translations_logger = nil
19
+
20
+ class << self
21
+ def missing_translations_logger
22
+ @@missing_translations_logger ||= begin
23
+ require 'logger' unless defined?(Logger)
24
+ Logger.new(STDOUT)
25
+ end
26
+ end
27
+
28
+ def missing_translations_logger=(logger)
29
+ @@missing_translations_logger = logger
30
+ end
31
+
32
+ def missing_translations_log_handler(exception, locale, key, options)
33
+ if MissingTranslationData === exception
34
+ missing_translations_logger.warn(exception.message)
35
+ return exception.message
36
+ else
37
+ raise exception
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ # A simple exception handler that behaves like the default exception handler
2
+ # but also raises on missing translations.
3
+ #
4
+ # Useful for identifying missing translations during testing.
5
+ #
6
+ # E.g.
7
+ #
8
+ # require 'globalize/i18n/missing_translations_raise_handler'
9
+ # I18n.exception_handler = :missing_translations_raise_handler
10
+ module I18n
11
+ class << self
12
+ def missing_translations_raise_handler(exception, locale, key, options)
13
+ raise exception
14
+ end
15
+ end
16
+ end
17
+
18
+ I18n.exception_handler = :missing_translations_raise_handler
19
+
20
+ ActionView::Helpers::TranslationHelper.module_eval do
21
+ def translate(key, options = {})
22
+ I18n.translate(key, options)
23
+ end
24
+ alias :t :translate
25
+ end
@@ -0,0 +1,17 @@
1
+ module Globalize
2
+ module Persistence
3
+ # Updates the associated record with values matching those of the instance attributes.
4
+ # Returns the number of affected rows.
5
+ def _update_record(attribute_names = self.attribute_names)
6
+ attribute_names_without_translated = attribute_names.select{ |k| not respond_to?('translated?') or not translated?(k) }
7
+ super(attribute_names_without_translated)
8
+ end
9
+
10
+ def _create_record(attribute_names = self.attribute_names)
11
+ attribute_names_without_translated = attribute_names.select{ |k| not respond_to?('translated?') or not translated?(k) }
12
+ super(attribute_names_without_translated)
13
+ end
14
+ end
15
+ end
16
+
17
+ ActiveRecord::Persistence.send(:prepend, Globalize::Persistence)
@@ -0,0 +1,3 @@
1
+ if ::ActiveRecord::VERSION::STRING < "5.0.0"
2
+ require_relative 'rails4/query_method'
3
+ end
@@ -0,0 +1,35 @@
1
+ require 'active_record/attribute_methods/query'
2
+
3
+ module ActiveRecord
4
+ module AttributeMethods
5
+ module Query
6
+ def query_attribute(attr_name)
7
+ unless value = read_attribute(attr_name)
8
+ false
9
+ else
10
+ column = self.class.columns_hash[attr_name]
11
+ if column.nil?
12
+
13
+ # TODO submit a rails patch
14
+
15
+ # not sure what active_record tests say but i guess this should mean:
16
+ # call to_i and check zero? if the value is a Numeric or starts with
17
+ # a digit, so it can meaningfully be typecasted by to_i
18
+
19
+ # if Numeric === value || value !~ /[^0-9]/
20
+ if Numeric === value || value.to_s =~ /^[0-9]/
21
+ !value.to_i.zero?
22
+ else
23
+ return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
24
+ !value.blank?
25
+ end
26
+ elsif column.number?
27
+ !value.zero?
28
+ else
29
+ !value.blank?
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,39 @@
1
+ require 'active_record/validations/uniqueness.rb'
2
+
3
+ ActiveRecord::Validations::UniquenessValidator.class_eval do
4
+ def validate_each_with_translations(record, attribute, value)
5
+ klass = record.class
6
+ if klass.translates? && klass.translated?(attribute)
7
+ finder_class = klass.translation_class
8
+ table = finder_class.arel_table
9
+
10
+ relation = build_relation(finder_class, table, attribute, value).and(table[:locale].eq(Globalize.locale))
11
+ relation = relation.and(table[klass.reflect_on_association(:translations).foreign_key].not_eq(record.send(:id))) if record.persisted?
12
+
13
+ translated_scopes = Array(options[:scope]) & klass.translated_attribute_names
14
+ untranslated_scopes = Array(options[:scope]) - translated_scopes
15
+
16
+ untranslated_scopes.each do |scope_item|
17
+ scope_value = record.send(scope_item)
18
+ reflection = klass.reflect_on_association(scope_item)
19
+ if reflection
20
+ scope_value = record.send(reflection.foreign_key)
21
+ scope_item = reflection.foreign_key
22
+ end
23
+ relation = relation.and(find_finder_class_for(record).arel_table[scope_item].eq(scope_value))
24
+ end
25
+
26
+ translated_scopes.each do |scope_item|
27
+ scope_value = record.send(scope_item)
28
+ relation = relation.and(table[scope_item].eq(scope_value))
29
+ end
30
+
31
+ if klass.unscoped.with_translations.where(relation).exists?
32
+ record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value))
33
+ end
34
+ else
35
+ validate_each_without_translations(record, attribute, value)
36
+ end
37
+ end
38
+ alias_method_chain :validate_each, :translations
39
+ end
@@ -0,0 +1,47 @@
1
+ module Globalize
2
+ module Validations
3
+ module UniquenessValidator
4
+ def validate_each(record, attribute, value)
5
+ klass = record.class
6
+ if klass.translates? && klass.translated?(attribute)
7
+ finder_class = klass.translation_class
8
+ finder_table = finder_class.arel_table
9
+ relation = build_relation(finder_class, finder_table, attribute, value).where(locale: Globalize.locale)
10
+ relation = relation.where.not(klass.reflect_on_association(:translations).foreign_key => record.send(:id)) if record.persisted?
11
+
12
+
13
+ translated_scopes = Array(options[:scope]) & klass.translated_attribute_names
14
+ untranslated_scopes = Array(options[:scope]) - translated_scopes
15
+
16
+ relation = relation.joins(:globalized_model) if untranslated_scopes.present?
17
+ untranslated_scopes.each do |scope_item|
18
+ scope_value = record.send(scope_item)
19
+ reflection = klass.reflect_on_association(scope_item)
20
+ if reflection
21
+ scope_value = record.send(reflection.foreign_key)
22
+ scope_item = reflection.foreign_key
23
+ end
24
+ relation = relation.where(find_finder_class_for(record).table_name => { scope_item => scope_value })
25
+ end
26
+
27
+ translated_scopes.each do |scope_item|
28
+ scope_value = record.send(scope_item)
29
+ relation = relation.where(scope_item => scope_value)
30
+ end
31
+ relation = relation.merge(options[:conditions]) if options[:conditions]
32
+
33
+ # if klass.unscoped.with_translations.where(relation).exists?
34
+ if relation.exists?
35
+ error_options = options.except(:case_sensitive, :scope, :conditions)
36
+ error_options[:value] = value
37
+ record.errors.add(attribute, :taken, error_options)
38
+ end
39
+ else
40
+ super(record, attribute, value)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ ActiveRecord::Validations::UniquenessValidator.prepend Globalize::Validations::UniquenessValidator
@@ -0,0 +1,12 @@
1
+ if ::ActiveRecord::VERSION::STRING >= "5.0.0"
2
+ module Globalize
3
+ module Relation
4
+ def where_values_hash(relation_table_name = table_name)
5
+ return super unless respond_to?(:translations_table_name)
6
+ super.merge(super(translations_table_name))
7
+ end
8
+ end
9
+ end
10
+
11
+ ActiveRecord::Relation.prepend Globalize::Relation
12
+ end
@@ -0,0 +1,21 @@
1
+ module Globalize
2
+ module AttributeMethods
3
+ module Serialization
4
+ def serialize(attr_name, class_name_or_coder = Object)
5
+ super(attr_name, class_name_or_coder)
6
+
7
+ coder = if class_name_or_coder == ::JSON
8
+ ::ActiveRecord::Coders::JSON
9
+ elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
10
+ class_name_or_coder
11
+ else
12
+ ::ActiveRecord::Coders::YAMLColumn.new(class_name_or_coder)
13
+ end
14
+
15
+ self.globalize_serialized_attributes[attr_name] = coder
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ ActiveRecord::AttributeMethods::Serialization::ClassMethods.send(:prepend, Globalize::AttributeMethods::Serialization)