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.
- 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 +430 -0
- data/Rakefile +55 -0
- data/lib/globalize.rb +90 -0
- data/lib/globalize/active_record.rb +14 -0
- data/lib/globalize/active_record/act_macro.rb +111 -0
- data/lib/globalize/active_record/adapter.rb +99 -0
- data/lib/globalize/active_record/adapter_dirty.rb +54 -0
- data/lib/globalize/active_record/attributes.rb +26 -0
- data/lib/globalize/active_record/class_methods.rb +124 -0
- data/lib/globalize/active_record/exceptions.rb +13 -0
- data/lib/globalize/active_record/instance_methods.rb +218 -0
- data/lib/globalize/active_record/migration.rb +210 -0
- data/lib/globalize/active_record/query_methods.rb +98 -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 +42 -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 +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,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,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,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
|