globalize-r5 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
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)