globalize-rails5 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 (33) 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 +430 -0
  7. data/Rakefile +55 -0
  8. data/lib/globalize.rb +90 -0
  9. data/lib/globalize/active_record.rb +14 -0
  10. data/lib/globalize/active_record/act_macro.rb +111 -0
  11. data/lib/globalize/active_record/adapter.rb +99 -0
  12. data/lib/globalize/active_record/adapter_dirty.rb +54 -0
  13. data/lib/globalize/active_record/attributes.rb +26 -0
  14. data/lib/globalize/active_record/class_methods.rb +124 -0
  15. data/lib/globalize/active_record/exceptions.rb +13 -0
  16. data/lib/globalize/active_record/instance_methods.rb +218 -0
  17. data/lib/globalize/active_record/migration.rb +210 -0
  18. data/lib/globalize/active_record/query_methods.rb +98 -0
  19. data/lib/globalize/active_record/translation.rb +45 -0
  20. data/lib/globalize/interpolation.rb +28 -0
  21. data/lib/globalize/version.rb +3 -0
  22. data/lib/i18n/missing_translations_log_handler.rb +41 -0
  23. data/lib/i18n/missing_translations_raise_handler.rb +25 -0
  24. data/lib/patches/active_record/persistence.rb +17 -0
  25. data/lib/patches/active_record/query_method.rb +3 -0
  26. data/lib/patches/active_record/rails4/query_method.rb +35 -0
  27. data/lib/patches/active_record/rails4/uniqueness_validator.rb +42 -0
  28. data/lib/patches/active_record/rails5/uniqueness_validator.rb +47 -0
  29. data/lib/patches/active_record/relation.rb +12 -0
  30. data/lib/patches/active_record/serialization.rb +21 -0
  31. data/lib/patches/active_record/uniqueness_validator.rb +5 -0
  32. data/lib/patches/active_record/xml_attribute_serializer.rb +23 -0
  33. metadata +204 -0
@@ -0,0 +1,98 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module QueryMethods
4
+ class WhereChain < ::ActiveRecord::QueryMethods::WhereChain
5
+ def not(opts, *rest)
6
+ if parsed = @scope.clone.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
+ elsif parsed = parse_translated_conditions(opts)
18
+ join_translations(super(parsed, *rest))
19
+ else
20
+ super
21
+ end
22
+ end
23
+
24
+ def order(opts, *rest)
25
+ if respond_to?(:translated_attribute_names) && parsed = parse_translated_order(opts)
26
+ join_translations super(parsed)
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ def exists?(conditions = :none)
33
+ if parsed = parse_translated_conditions(conditions)
34
+ with_translations_in_fallbacks.exists?(parsed)
35
+ else
36
+ super
37
+ end
38
+ end
39
+
40
+ def with_translations_in_fallbacks
41
+ with_translations(Globalize.fallbacks)
42
+ end
43
+
44
+ def parse_translated_conditions(opts)
45
+ if opts.is_a?(Hash) && respond_to?(:translated_attribute_names) && (keys = opts.symbolize_keys.keys & translated_attribute_names).present?
46
+ opts = opts.dup
47
+ keys.each { |key| opts[translated_column_name(key)] = opts.delete(key) || opts.delete(key.to_s) }
48
+ opts
49
+ end
50
+ end
51
+ if ::ActiveRecord::VERSION::STRING < "5.0.0"
52
+ def where_values_hash(*args)
53
+ return super unless respond_to?(:translations_table_name)
54
+ equalities = respond_to?(:with_default_scope) ? with_default_scope.where_values : where_values
55
+ equalities = equalities.grep(Arel::Nodes::Equality).find_all { |node|
56
+ node.left.relation.name == translations_table_name
57
+ }
58
+
59
+ binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
60
+
61
+ super.merge(Hash[equalities.map { |where|
62
+ name = where.left.name
63
+ [name, binds.fetch(name.to_s) { right = where.right; right.is_a?(Arel::Nodes::Casted) ? right.val : right }]
64
+ }])
65
+ end
66
+ end
67
+
68
+ def join_translations(relation = self)
69
+ if relation.joins_values.include?(:translations)
70
+ relation
71
+ else
72
+ relation.with_translations_in_fallbacks
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def parse_translated_order(opts)
79
+ case opts
80
+ when Hash
81
+ ordering = opts.map do |column, direction|
82
+ klass = translated_column?(column) ? translation_class : self
83
+ klass.arel_table[column].send(direction)
84
+ end
85
+ order(ordering).order_values
86
+ when Symbol
87
+ translated_column_name(opts) if translated_attribute_names.include?(opts)
88
+ else # failsafe returns nothing
89
+ nil
90
+ end
91
+ end
92
+
93
+ def translated_column?(column)
94
+ translated_attribute_names.include?(column)
95
+ end
96
+ end
97
+ end
98
+ 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,42 @@
1
+ require 'active_record/validations/uniqueness.rb'
2
+
3
+ module Globalize
4
+ module UniquenessValidatorOverride
5
+ def validate_each(record, attribute, value)
6
+ klass = record.class
7
+ if klass.translates? && klass.translated?(attribute)
8
+ finder_class = klass.translation_class
9
+ table = finder_class.arel_table
10
+
11
+ relation = build_relation(finder_class, table, attribute, value).and(table[:locale].eq(Globalize.locale))
12
+ relation = relation.and(table[klass.reflect_on_association(:translations).foreign_key].not_eq(record.send(:id))) if record.persisted?
13
+
14
+ translated_scopes = Array(options[:scope]) & klass.translated_attribute_names
15
+ untranslated_scopes = Array(options[:scope]) - translated_scopes
16
+
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.and(find_finder_class_for(record).arel_table[scope_item].eq(scope_value))
25
+ end
26
+
27
+ translated_scopes.each do |scope_item|
28
+ scope_value = record.send(scope_item)
29
+ relation = relation.and(table[scope_item].eq(scope_value))
30
+ end
31
+
32
+ if klass.unscoped.with_translations.where(relation).exists?
33
+ record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value))
34
+ end
35
+ else
36
+ super
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ ActiveRecord::Validations::UniquenessValidator.send :prepend, Globalize::UniquenessValidatorOverride
@@ -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)
@@ -0,0 +1,5 @@
1
+ if ::ActiveRecord::VERSION::STRING < "5.0.0"
2
+ require_relative 'rails4/uniqueness_validator'
3
+ else
4
+ require_relative 'rails5/uniqueness_validator'
5
+ end
@@ -0,0 +1,23 @@
1
+ begin
2
+ require 'active_record/serializers/xml_serializer'
3
+ rescue LoadError
4
+ end
5
+
6
+ module Globalize
7
+ module XmlSerializer
8
+ module Attribute
9
+ def compute_type
10
+ klass = @serializable.class
11
+ if klass.translates? && klass.translated_attribute_names.include?(name.to_sym)
12
+ :string
13
+ else
14
+ super
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ if defined?(ActiveRecord::XmlSerializer)
22
+ ActiveRecord::XmlSerializer::Attribute.send(:prepend, Globalize::XmlSerializer::Attribute)
23
+ end