globalize-r5 5.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +111 -0
- data/CONTRIBUTING.md +37 -0
- data/Gemfile +26 -0
- data/LICENSE +22 -0
- data/README.md +423 -0
- data/Rakefile +55 -0
- data/globalize.gemspec +27 -0
- data/issue_template.rb +38 -0
- data/lib/globalize.rb +90 -0
- data/lib/globalize/active_record.rb +14 -0
- data/lib/globalize/active_record/act_macro.rb +95 -0
- data/lib/globalize/active_record/adapter.rb +99 -0
- data/lib/globalize/active_record/adapter_dirty.rb +53 -0
- data/lib/globalize/active_record/attributes.rb +26 -0
- data/lib/globalize/active_record/class_methods.rb +122 -0
- data/lib/globalize/active_record/exceptions.rb +19 -0
- data/lib/globalize/active_record/instance_methods.rb +219 -0
- data/lib/globalize/active_record/migration.rb +192 -0
- data/lib/globalize/active_record/query_methods.rb +113 -0
- data/lib/globalize/active_record/translation.rb +45 -0
- data/lib/globalize/interpolation.rb +28 -0
- data/lib/globalize/version.rb +3 -0
- data/lib/i18n/missing_translations_log_handler.rb +41 -0
- data/lib/i18n/missing_translations_raise_handler.rb +25 -0
- data/lib/patches/active_record/persistence.rb +17 -0
- data/lib/patches/active_record/query_method.rb +3 -0
- data/lib/patches/active_record/rails4/query_method.rb +35 -0
- data/lib/patches/active_record/rails4/uniqueness_validator.rb +39 -0
- data/lib/patches/active_record/rails5/uniqueness_validator.rb +47 -0
- data/lib/patches/active_record/relation.rb +12 -0
- data/lib/patches/active_record/serialization.rb +21 -0
- data/lib/patches/active_record/uniqueness_validator.rb +5 -0
- data/lib/patches/active_record/xml_attribute_serializer.rb +23 -0
- 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,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,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)
|