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,124 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module ClassMethods
4
+ delegate :translated_locales, :set_translations_table_name, :to => :translation_class
5
+
6
+ if ::ActiveRecord::VERSION::STRING < "5.0.0"
7
+ def columns_hash
8
+ super.except(*translated_attribute_names.map(&:to_s))
9
+ end
10
+ end
11
+
12
+ def with_locales(*locales)
13
+ all.merge translation_class.with_locales(*locales)
14
+ end
15
+
16
+ def with_translations(*locales)
17
+ locales = translated_locales if locales.empty?
18
+ preload(:translations).joins(:translations).readonly(false).with_locales(locales).tap do |query|
19
+ query.distinct! unless locales.flatten.one?
20
+ end
21
+ end
22
+
23
+ def with_required_attributes
24
+ warn 'with_required_attributes is deprecated and will be removed in the next release of Globalize.'
25
+ required_translated_attributes.inject(all) do |scope, name|
26
+ scope.where("#{translated_column_name(name)} IS NOT NULL")
27
+ end
28
+ end
29
+
30
+ def with_translated_attribute(name, value, locales = Globalize.fallbacks)
31
+ with_translations.where(
32
+ translated_column_name(name) => value,
33
+ translated_column_name(:locale) => Array(locales).map(&:to_s)
34
+ )
35
+ end
36
+
37
+ def translated?(name)
38
+ translated_attribute_names.include?(name.to_sym)
39
+ end
40
+
41
+ def required_attributes
42
+ warn 'required_attributes is deprecated and will be removed in the next release of Globalize.'
43
+ validators.map { |v| v.attributes if v.is_a?(ActiveModel::Validations::PresenceValidator) }.flatten
44
+ end
45
+
46
+ def required_translated_attributes
47
+ warn 'required_translated_attributes is deprecated and will be removed in the next release of Globalize.'
48
+ translated_attribute_names & required_attributes
49
+ end
50
+
51
+ def translation_class
52
+ @translation_class ||= begin
53
+ if self.const_defined?(:Translation, false)
54
+ klass = self.const_get(:Translation, false)
55
+ else
56
+ klass = self.const_set(:Translation, Class.new(Globalize::ActiveRecord::Translation))
57
+ end
58
+
59
+ klass.belongs_to :globalized_model, :class_name => self.name, :foreign_key => translation_options[:foreign_key], inverse_of: :translations
60
+ klass
61
+ end
62
+ end
63
+
64
+ def translations_table_name
65
+ translation_class.table_name
66
+ end
67
+
68
+ def translated_column_name(name)
69
+ "#{translation_class.table_name}.#{name}"
70
+ end
71
+
72
+ private
73
+
74
+ # Override the default relation method in order to return a subclass
75
+ # of ActiveRecord::Relation with custom finder methods for translated
76
+ # attributes.
77
+ def relation
78
+ super.extending!(QueryMethods)
79
+ end
80
+
81
+ protected
82
+
83
+ def define_translated_attr_reader(name)
84
+ define_method(name) do |*args|
85
+ Globalize::Interpolation.interpolate(name, self, args)
86
+ end
87
+ alias_method :"#{name}_before_type_cast", name
88
+ end
89
+
90
+ def define_translated_attr_writer(name)
91
+ define_method(:"#{name}=") do |value|
92
+ write_attribute(name, value)
93
+ end
94
+ end
95
+
96
+ def define_translated_attr_accessor(name)
97
+ define_translated_attr_reader(name)
98
+ define_translated_attr_writer(name)
99
+ end
100
+
101
+ def define_translations_reader(name)
102
+ define_method(:"#{name}_translations") do
103
+ hash = translated_attribute_by_locale(name)
104
+ globalize.stash.keys.each_with_object(hash) do |locale, result|
105
+ result[locale] = globalize.fetch_stash(locale, name) if globalize.stash_contains?(locale, name)
106
+ end
107
+ end
108
+ end
109
+
110
+ def define_translations_writer(name)
111
+ define_method(:"#{name}_translations=") do |value|
112
+ value.each do |(locale, _value)|
113
+ write_attribute name, _value, :locale => locale
114
+ end
115
+ end
116
+ end
117
+
118
+ def define_translations_accessor(name)
119
+ define_translations_reader(name)
120
+ define_translations_writer(name)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,13 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module Exceptions
4
+ class MigrationError < StandardError; end
5
+
6
+ class BadFieldName < MigrationError
7
+ def initialize(field)
8
+ super("Missing translated field #{field.inspect}")
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,218 @@
1
+ module Globalize
2
+ module ActiveRecord
3
+ module InstanceMethods
4
+ delegate :translated_locales, :to => :translations
5
+
6
+ def globalize
7
+ @globalize ||= Adapter.new(self)
8
+ end
9
+
10
+ def attributes
11
+ super.merge(translated_attributes)
12
+ end
13
+
14
+ def attributes=(new_attributes, *options)
15
+ super unless new_attributes.respond_to?(:stringify_keys) && new_attributes.present?
16
+ attributes = new_attributes.stringify_keys
17
+ with_given_locale(attributes) { super(attributes.except("locale"), *options) }
18
+ end
19
+
20
+ def assign_attributes(new_attributes, *options)
21
+ super unless new_attributes.respond_to?(:stringify_keys) && new_attributes.present?
22
+ attributes = new_attributes.stringify_keys
23
+ with_given_locale(attributes) { super(attributes.except("locale"), *options) }
24
+ end
25
+
26
+ def write_attribute(name, value, *args, &block)
27
+ return super(name, value, *args, &block) unless translated?(name)
28
+
29
+ options = {:locale => Globalize.locale}.merge(args.first || {})
30
+
31
+ globalize.write(options[:locale], name, value)
32
+ end
33
+
34
+ def [](attr_name)
35
+ if translated?(attr_name)
36
+ read_attribute(attr_name)
37
+ else
38
+ read_attribute(attr_name) { |n| missing_attribute(n, caller) }
39
+ end
40
+ end
41
+
42
+ def read_attribute(name, options = {}, &block)
43
+ options = {:translated => true, :locale => nil}.merge(options)
44
+ return super(name, &block) unless options[:translated]
45
+
46
+ if translated?(name)
47
+ if !(value = globalize.fetch(options[:locale] || Globalize.locale, name)).nil?
48
+ value
49
+ else
50
+ super(name, &block)
51
+ end
52
+ else
53
+ super(name, &block)
54
+ end
55
+ end
56
+
57
+ def attribute_names
58
+ translated_attribute_names.map(&:to_s) + super
59
+ end
60
+
61
+ delegate :translated?, :to => :class
62
+
63
+ def translated_attributes
64
+ translated_attribute_names.inject({}) do |attributes, name|
65
+ attributes.merge(name.to_s => send(name))
66
+ end
67
+ end
68
+
69
+ # This method is basically the method built into Rails
70
+ # but we have to pass {:translated => false}
71
+ def untranslated_attributes
72
+ attribute_names.inject({}) do |attrs, name|
73
+ attrs[name] = read_attribute(name, {:translated => false}); attrs
74
+ end
75
+ end
76
+
77
+ def set_translations(options)
78
+ options.keys.each do |locale|
79
+ translation = translation_for(locale) ||
80
+ translations.build(:locale => locale.to_s)
81
+
82
+ options[locale].each do |key, value|
83
+ translation.send :"#{key}=", value
84
+ translation.globalized_model.send :"#{key}=", value
85
+ end
86
+ translation.save if persisted?
87
+ end
88
+ globalize.reset
89
+ end
90
+
91
+ def reload(options = nil)
92
+ translation_caches.clear
93
+ translated_attribute_names.each { |name| @attributes.reset(name.to_s) }
94
+ globalize.reset
95
+ super(options)
96
+ end
97
+
98
+ def initialize_dup(other)
99
+ @globalize = nil
100
+ @translation_caches = nil
101
+ super
102
+ other.each_locale_and_translated_attribute do |locale, name|
103
+ globalize.write(locale, name, other.globalize.fetch(locale, name) )
104
+ end
105
+ end
106
+
107
+ def translation
108
+ translation_for(::Globalize.locale)
109
+ end
110
+
111
+ def translation_for(locale, build_if_missing = true)
112
+ unless translation_caches[locale]
113
+ # Fetch translations from database as those in the translation collection may be incomplete
114
+ _translation = translations.detect{|t| t.locale.to_s == locale.to_s}
115
+ _translation ||= translations.with_locale(locale).first unless translations.loaded?
116
+ _translation ||= translations.build(:locale => locale) if build_if_missing
117
+ translation_caches[locale] = _translation if _translation
118
+ end
119
+ translation_caches[locale]
120
+ end
121
+
122
+ def translation_caches
123
+ @translation_caches ||= {}
124
+ end
125
+
126
+ def translations_by_locale
127
+ translations.each_with_object(HashWithIndifferentAccess.new) do |t, hash|
128
+ hash[t.locale] = block_given? ? yield(t) : t
129
+ end
130
+ end
131
+
132
+ def translated_attribute_by_locale(name)
133
+ translations_by_locale(&:"#{name}")
134
+ end
135
+
136
+ # Get available locales from translations association, without a separate distinct query
137
+ def available_locales
138
+ translations.map(&:locale).uniq
139
+ end
140
+
141
+ def globalize_fallbacks(locale)
142
+ Globalize.fallbacks(locale)
143
+ end
144
+
145
+ def save(*)
146
+ result = Globalize.with_locale(translation.locale || I18n.default_locale) do
147
+ without_fallbacks do
148
+ super
149
+ end
150
+ end
151
+ if result
152
+ globalize.clear_dirty
153
+ end
154
+
155
+ result
156
+ end
157
+
158
+ def column_for_attribute name
159
+ return super if translated_attribute_names.exclude?(name)
160
+
161
+ globalize.send(:column_for_attribute, name)
162
+ end
163
+
164
+ def cache_key
165
+ [super, translation.cache_key].join("/")
166
+ end
167
+
168
+ def changed?
169
+ changed_attributes.present? || translations.any?(&:changed?)
170
+ end
171
+
172
+ # need to access instance variable directly since changed_attributes
173
+ # is frozen as of Rails 4.2
174
+ def original_changed_attributes
175
+ @changed_attributes
176
+ end
177
+
178
+ protected
179
+
180
+ def each_locale_and_translated_attribute
181
+ used_locales.each do |locale|
182
+ translated_attribute_names.each do |name|
183
+ yield locale, name
184
+ end
185
+ end
186
+ end
187
+
188
+ def used_locales
189
+ locales = globalize.stash.keys.concat(globalize.stash.keys).concat(translations.translated_locales)
190
+ locales.uniq!
191
+ locales
192
+ end
193
+
194
+ def save_translations!
195
+ globalize.save_translations!
196
+ translation_caches.clear
197
+ end
198
+
199
+ def with_given_locale(_attributes, &block)
200
+ attributes = _attributes.stringify_keys
201
+
202
+ if locale = attributes.try(:delete, "locale")
203
+ Globalize.with_locale(locale, &block)
204
+ else
205
+ yield
206
+ end
207
+ end
208
+
209
+ def without_fallbacks
210
+ before = self.fallbacks_for_empty_translations
211
+ self.fallbacks_for_empty_translations = false
212
+ yield
213
+ ensure
214
+ self.fallbacks_for_empty_translations = before
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,210 @@
1
+ require 'digest/sha1'
2
+
3
+ module Globalize
4
+ module ActiveRecord
5
+ module Migration
6
+ def globalize_migrator
7
+ @globalize_migrator ||= Migrator.new(self)
8
+ end
9
+
10
+ delegate :create_translation_table!, :add_translation_fields!,
11
+ :drop_translation_table!, :translation_index_name,
12
+ :translation_locale_index_name, :to => :globalize_migrator
13
+
14
+ class Migrator
15
+ include Globalize::ActiveRecord::Exceptions
16
+
17
+ attr_reader :model
18
+ delegate :translated_attribute_names, :connection, :table_name,
19
+ :table_name_prefix, :translations_table_name, :columns, :to => :model
20
+
21
+ def initialize(model)
22
+ @model = model
23
+ end
24
+
25
+ def fields
26
+ @fields ||= complete_translated_fields
27
+ end
28
+
29
+ def create_translation_table!(fields = {}, options = {})
30
+ extra = options.keys - [:migrate_data, :remove_source_columns, :unique_index]
31
+ if extra.any?
32
+ raise ArgumentError, "Unknown migration #{'option'.pluralize(extra.size)}: #{extra}"
33
+ end
34
+ @fields = fields
35
+ # If we have fields we only want to create the translation table with those fields
36
+ complete_translated_fields if fields.blank?
37
+ validate_translated_fields
38
+
39
+ create_translation_table
40
+ add_translation_fields!(fields, options)
41
+ create_translations_index(options)
42
+ clear_schema_cache!
43
+ end
44
+
45
+ def add_translation_fields!(fields, options = {})
46
+ @fields = fields
47
+ validate_translated_fields
48
+ add_translation_fields
49
+ clear_schema_cache!
50
+ move_data_to_translation_table if options[:migrate_data]
51
+ remove_source_columns if options[:remove_source_columns]
52
+ clear_schema_cache!
53
+ end
54
+
55
+ def remove_source_columns
56
+ connection.remove_columns(table_name, *fields.keys)
57
+ end
58
+
59
+ def drop_translation_table!(options = {})
60
+ move_data_to_model_table if options[:migrate_data]
61
+ drop_translations_index
62
+ drop_translation_table
63
+ clear_schema_cache!
64
+ end
65
+
66
+ # This adds all the current translated attributes of the model
67
+ # It's a problem because in early migrations would add all the translated attributes
68
+ def complete_translated_fields
69
+ translated_attribute_names.each do |name|
70
+ @fields[name] ||= column_type(name)
71
+ end
72
+ end
73
+
74
+ def create_translation_table
75
+ connection.create_table(translations_table_name) do |t|
76
+ t.references table_name.sub(/^#{table_name_prefix}/, '').singularize, :null => false, :index => false, :type => column_type(model.primary_key).to_sym
77
+ t.string :locale, :null => false
78
+ t.timestamps :null => false
79
+ end
80
+ end
81
+
82
+ def add_translation_fields
83
+ connection.change_table(translations_table_name) do |t|
84
+ fields.each do |name, options|
85
+ if options.is_a? Hash
86
+ t.column name, options.delete(:type), options
87
+ else
88
+ t.column name, options
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ def create_translations_index(options)
95
+ foreign_key = "#{table_name.sub(/^#{table_name_prefix}/, "").singularize}_id".to_sym
96
+ connection.add_index(
97
+ translations_table_name,
98
+ foreign_key,
99
+ :name => translation_index_name
100
+ )
101
+ # index for select('DISTINCT locale') call in translation.rb
102
+ connection.add_index(
103
+ translations_table_name,
104
+ :locale,
105
+ :name => translation_locale_index_name
106
+ )
107
+
108
+ if options[:unique_index]
109
+ connection.add_index(
110
+ translations_table_name,
111
+ [foreign_key, :locale],
112
+ :name => translation_unique_index_name,
113
+ unique: true
114
+ )
115
+ end
116
+ end
117
+
118
+ def drop_translation_table
119
+ connection.drop_table(translations_table_name)
120
+ end
121
+
122
+ def drop_translations_index
123
+ if connection.indexes(translations_table_name).map(&:name).include?(translation_index_name)
124
+ connection.remove_index(translations_table_name, :name => translation_index_name)
125
+ end
126
+ if connection.indexes(translations_table_name).map(&:name).include?(translation_locale_index_name)
127
+ connection.remove_index(translations_table_name, :name => translation_locale_index_name)
128
+ end
129
+ end
130
+
131
+ def move_data_to_translation_table
132
+ model.find_each do |record|
133
+ translation = record.translation_for(I18n.locale) || record.translations.build(:locale => I18n.locale)
134
+ fields.each do |attribute_name, attribute_type|
135
+ translation[attribute_name] = record.read_attribute(attribute_name, {:translated => false})
136
+ end
137
+ translation.save!
138
+ end
139
+ end
140
+
141
+ def move_data_to_model_table
142
+ add_missing_columns
143
+
144
+ # Find all of the translated attributes for all records in the model.
145
+ all_translated_attributes = model.all.collect{|m| m.attributes}
146
+ all_translated_attributes.each do |translated_record|
147
+ # Create a hash containing the translated column names and their values.
148
+ translated_attribute_names.inject(fields_to_update={}) do |f, name|
149
+ f.update({name.to_sym => translated_record[name.to_s]})
150
+ end
151
+
152
+ # Now, update the actual model's record with the hash.
153
+ model.where(model.primary_key.to_sym => translated_record[model.primary_key]).update_all(fields_to_update)
154
+ end
155
+ end
156
+
157
+ def validate_translated_fields
158
+ fields.each do |name, options|
159
+ raise BadFieldName.new(name) unless valid_field_name?(name)
160
+ end
161
+ end
162
+
163
+ def column_type(name)
164
+ columns.detect { |c| c.name == name.to_s }.try(:type)
165
+ end
166
+
167
+ def valid_field_name?(name)
168
+ translated_attribute_names.include?(name)
169
+ end
170
+
171
+ def translation_index_name
172
+ truncate_index_name "index_#{translations_table_name}_on_#{table_name.singularize}_id"
173
+ end
174
+
175
+ def translation_locale_index_name
176
+ truncate_index_name "index_#{translations_table_name}_on_locale"
177
+ end
178
+
179
+ def translation_unique_index_name
180
+ truncate_index_name "index_#{translations_table_name}_on_#{table_name.singularize}_id_and_locale"
181
+ end
182
+
183
+ def clear_schema_cache!
184
+ connection.schema_cache.clear! if connection.respond_to? :schema_cache
185
+ model::Translation.reset_column_information
186
+ model.reset_column_information
187
+ end
188
+
189
+ private
190
+
191
+ def truncate_index_name(index_name)
192
+ if index_name.size < connection.index_name_length
193
+ index_name
194
+ else
195
+ "index_#{Digest::SHA1.hexdigest(index_name)}"[0, connection.index_name_length]
196
+ end
197
+ end
198
+
199
+ def add_missing_columns
200
+ clear_schema_cache!
201
+ translated_attribute_names.map(&:to_s).each do |attribute|
202
+ unless model.column_names.include?(attribute)
203
+ connection.add_column(table_name, attribute, model::Translation.columns_hash[attribute].type)
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end