globalize-danibachar 5.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/Appraisals +33 -0
  3. data/CHANGELOG.md +111 -0
  4. data/CONTRIBUTING.md +52 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE +22 -0
  7. data/README.md +437 -0
  8. data/Rakefile +55 -0
  9. data/lib/globalize.rb +98 -0
  10. data/lib/globalize/active_record.rb +14 -0
  11. data/lib/globalize/active_record/act_macro.rb +96 -0
  12. data/lib/globalize/active_record/adapter.rb +108 -0
  13. data/lib/globalize/active_record/adapter_dirty.rb +56 -0
  14. data/lib/globalize/active_record/attributes.rb +26 -0
  15. data/lib/globalize/active_record/class_methods.rb +129 -0
  16. data/lib/globalize/active_record/exceptions.rb +13 -0
  17. data/lib/globalize/active_record/instance_methods.rb +246 -0
  18. data/lib/globalize/active_record/migration.rb +215 -0
  19. data/lib/globalize/active_record/translated_attributes_query.rb +181 -0
  20. data/lib/globalize/active_record/translation.rb +45 -0
  21. data/lib/globalize/interpolation.rb +28 -0
  22. data/lib/globalize/version.rb +3 -0
  23. data/lib/i18n/missing_translations_log_handler.rb +41 -0
  24. data/lib/i18n/missing_translations_raise_handler.rb +25 -0
  25. data/lib/patches/active_record/persistence.rb +17 -0
  26. data/lib/patches/active_record/query_method.rb +3 -0
  27. data/lib/patches/active_record/rails4/query_method.rb +35 -0
  28. data/lib/patches/active_record/rails4/serialization.rb +22 -0
  29. data/lib/patches/active_record/rails4/uniqueness_validator.rb +42 -0
  30. data/lib/patches/active_record/rails5/uniqueness_validator.rb +47 -0
  31. data/lib/patches/active_record/rails5_1/serialization.rb +22 -0
  32. data/lib/patches/active_record/rails5_1/uniqueness_validator.rb +45 -0
  33. data/lib/patches/active_record/relation.rb +12 -0
  34. data/lib/patches/active_record/serialization.rb +5 -0
  35. data/lib/patches/active_record/uniqueness_validator.rb +7 -0
  36. data/lib/patches/active_record/xml_attribute_serializer.rb +23 -0
  37. metadata +264 -0
@@ -0,0 +1,129 @@
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,
60
+ class_name: self.name,
61
+ foreign_key: translation_options[:foreign_key],
62
+ inverse_of: :translations,
63
+ touch: translation_options.fetch(:touch, false)
64
+ klass
65
+ end
66
+ end
67
+
68
+ def translations_table_name
69
+ translation_class.table_name
70
+ end
71
+
72
+ def translated_column_name(name)
73
+ "#{translation_class.table_name}.#{name}"
74
+ end
75
+
76
+ private
77
+
78
+ # Override the default relation method in order to return a subclass
79
+ # of ActiveRecord::Relation with custom finder and calculation methods
80
+ # for translated attributes.
81
+ def relation
82
+ super.extending!(TranslatedAttributesQuery)
83
+ end
84
+
85
+ protected
86
+
87
+ def define_translated_attr_reader(name)
88
+ define_method(name) do |*args|
89
+ Globalize::Interpolation.interpolate(name, self, args)
90
+ end
91
+ alias_method :"#{name}_before_type_cast", name
92
+ end
93
+
94
+ def define_translated_attr_writer(name)
95
+ define_method(:"#{name}=") do |value|
96
+ write_attribute(name, value)
97
+ end
98
+ end
99
+
100
+ def define_translated_attr_accessor(name)
101
+ attribute(name, ::ActiveRecord::Type::Value.new)
102
+ define_translated_attr_reader(name)
103
+ define_translated_attr_writer(name)
104
+ end
105
+
106
+ def define_translations_reader(name)
107
+ define_method(:"#{name}_translations") do
108
+ hash = translated_attribute_by_locale(name)
109
+ globalize.stash.keys.each_with_object(hash) do |locale, result|
110
+ result[locale] = globalize.fetch_stash(locale, name) if globalize.stash_contains?(locale, name)
111
+ end
112
+ end
113
+ end
114
+
115
+ def define_translations_writer(name)
116
+ define_method(:"#{name}_translations=") do |value|
117
+ value.each do |(locale, _value)|
118
+ write_attribute name, _value, :locale => locale
119
+ end
120
+ end
121
+ end
122
+
123
+ def define_translations_accessor(name)
124
+ define_translations_reader(name)
125
+ define_translations_writer(name)
126
+ end
127
+ end
128
+ end
129
+ 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,246 @@
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
+ if Globalize.rails_52?
21
+
22
+ # In Rails 5.2 we need to override *_assign_attributes* as it's called earlier
23
+ # in the stack (before *assign_attributes*)
24
+ # See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_assignment.rb#L11
25
+ def _assign_attributes(new_attributes)
26
+ attributes = new_attributes.stringify_keys
27
+ with_given_locale(attributes) { super(attributes.except("locale")) }
28
+ end
29
+
30
+ else
31
+
32
+ def assign_attributes(new_attributes, *options)
33
+ super unless new_attributes.respond_to?(:stringify_keys) && new_attributes.present?
34
+ attributes = new_attributes.stringify_keys
35
+ with_given_locale(attributes) { super(attributes.except("locale"), *options) }
36
+ end
37
+
38
+ end
39
+
40
+ def write_attribute(name, value, *args, &block)
41
+ return super(name, value, *args, &block) unless translated?(name)
42
+
43
+ options = {:locale => Globalize.locale}.merge(args.first || {})
44
+
45
+ globalize.write(options[:locale], name, value)
46
+ end
47
+
48
+ def [](attr_name)
49
+ if translated?(attr_name)
50
+ read_attribute(attr_name)
51
+ else
52
+ read_attribute(attr_name) { |n| missing_attribute(n, caller) }
53
+ end
54
+ end
55
+
56
+ def read_attribute(attr_name, options = {}, &block)
57
+ name = if self.class.attribute_alias?(attr_name)
58
+ self.class.attribute_alias(attr_name).to_s
59
+ else
60
+ attr_name.to_s
61
+ end
62
+
63
+ name = self.class.primary_key if name == "id".freeze && self.class.primary_key
64
+
65
+ _read_attribute(name, options, &block)
66
+ end
67
+
68
+ def _read_attribute(attr_name, options = {}, &block)
69
+ translated_value = read_translated_attribute(attr_name, options, &block)
70
+ translated_value.nil? ? super(attr_name, &block) : translated_value
71
+ end
72
+
73
+ def attribute_names
74
+ translated_attribute_names.map(&:to_s) + super
75
+ end
76
+
77
+ delegate :translated?, :to => :class
78
+
79
+ def translated_attributes
80
+ translated_attribute_names.inject({}) do |attributes, name|
81
+ attributes.merge(name.to_s => send(name))
82
+ end
83
+ end
84
+
85
+ # This method is basically the method built into Rails
86
+ # but we have to pass {:translated => false}
87
+ def untranslated_attributes
88
+ attribute_names.inject({}) do |attrs, name|
89
+ attrs[name] = read_attribute(name, {:translated => false}); attrs
90
+ end
91
+ end
92
+
93
+ def set_translations(options)
94
+ options.keys.each do |locale|
95
+ translation = translation_for(locale) ||
96
+ translations.build(:locale => locale.to_s)
97
+
98
+ options[locale].each do |key, value|
99
+ translation.send :"#{key}=", value
100
+ translation.globalized_model.send :"#{key}=", value
101
+ end
102
+ translation.save if persisted?
103
+ end
104
+ globalize.reset
105
+ end
106
+
107
+ def reload(options = nil)
108
+ translation_caches.clear
109
+ translated_attribute_names.each { |name| @attributes.reset(name.to_s) }
110
+ globalize.reset
111
+ super(options)
112
+ end
113
+
114
+ def initialize_dup(other)
115
+ @globalize = nil
116
+ @translation_caches = nil
117
+ super
118
+ other.each_locale_and_translated_attribute do |locale, name|
119
+ globalize.write(locale, name, other.globalize.fetch(locale, name) )
120
+ end
121
+ end
122
+
123
+ def translation
124
+ translation_for(::Globalize.locale)
125
+ end
126
+
127
+ def translation_for(locale, build_if_missing = true)
128
+ unless translation_caches[locale]
129
+ # Fetch translations from database as those in the translation collection may be incomplete
130
+ _translation = translations.detect{|t| t.locale.to_s == locale.to_s}
131
+ _translation ||= translations.with_locale(locale).first unless translations.loaded?
132
+ _translation ||= translations.build(:locale => locale) if build_if_missing
133
+ translation_caches[locale] = _translation if _translation
134
+ end
135
+ translation_caches[locale]
136
+ end
137
+
138
+ def translation_caches
139
+ @translation_caches ||= {}
140
+ end
141
+
142
+ def translations_by_locale
143
+ translations.each_with_object(HashWithIndifferentAccess.new) do |t, hash|
144
+ hash[t.locale] = block_given? ? yield(t) : t
145
+ end
146
+ end
147
+
148
+ def translated_attribute_by_locale(name)
149
+ translations_by_locale(&:"#{name}")
150
+ end
151
+
152
+ # Get available locales from translations association, without a separate distinct query
153
+ def available_locales
154
+ translations.map(&:locale).uniq
155
+ end
156
+
157
+ def globalize_fallbacks(locale)
158
+ Globalize.fallbacks(locale)
159
+ end
160
+
161
+ def save(*)
162
+ result = Globalize.with_locale(translation.locale || I18n.default_locale) do
163
+ without_fallbacks do
164
+ super
165
+ end
166
+ end
167
+ if result
168
+ globalize.clear_dirty
169
+ end
170
+
171
+ result
172
+ end
173
+
174
+ def column_for_attribute name
175
+ return super if translated_attribute_names.exclude?(name)
176
+
177
+ globalize.send(:column_for_attribute, name)
178
+ end
179
+
180
+ def cache_key
181
+ [super, translation.cache_key].join("/")
182
+ end
183
+
184
+ def changed?
185
+ changed_attributes.present? || translations.any?(&:changed?)
186
+ end
187
+
188
+ # need to access instance variable directly since changed_attributes
189
+ # is frozen as of Rails 4.2
190
+ def original_changed_attributes
191
+ @changed_attributes
192
+ end
193
+
194
+ protected
195
+
196
+ def each_locale_and_translated_attribute
197
+ used_locales.each do |locale|
198
+ translated_attribute_names.each do |name|
199
+ yield locale, name
200
+ end
201
+ end
202
+ end
203
+
204
+ def used_locales
205
+ locales = globalize.stash.keys.concat(globalize.stash.keys).concat(translations.translated_locales)
206
+ locales.uniq!
207
+ locales
208
+ end
209
+
210
+ def save_translations!
211
+ globalize.save_translations!
212
+ translation_caches.clear
213
+ end
214
+
215
+ def with_given_locale(_attributes, &block)
216
+ attributes = _attributes.stringify_keys
217
+
218
+ if locale = attributes.try(:delete, "locale")
219
+ Globalize.with_locale(locale, &block)
220
+ else
221
+ yield
222
+ end
223
+ end
224
+
225
+ def without_fallbacks
226
+ before = self.fallbacks_for_empty_translations
227
+ self.fallbacks_for_empty_translations = false
228
+ yield
229
+ ensure
230
+ self.fallbacks_for_empty_translations = before
231
+ end
232
+
233
+ # nil or value
234
+ def read_translated_attribute(name, options)
235
+ options = {:translated => true, :locale => nil}.merge(options)
236
+ return nil unless options[:translated]
237
+ return nil unless translated?(name)
238
+
239
+ value = globalize.fetch(options[:locale] || Globalize.locale, name)
240
+ return nil if value.nil?
241
+
242
+ block_given? ? yield(value) : value
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,215 @@
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
+ column_names = *fields.keys
57
+ column_names.each do |column|
58
+ if connection.column_exists?(table_name, column)
59
+ connection.remove_column(table_name, column)
60
+ end
61
+ end
62
+ end
63
+
64
+ def drop_translation_table!(options = {})
65
+ move_data_to_model_table if options[:migrate_data]
66
+ drop_translations_index
67
+ drop_translation_table
68
+ clear_schema_cache!
69
+ end
70
+
71
+ # This adds all the current translated attributes of the model
72
+ # It's a problem because in early migrations would add all the translated attributes
73
+ def complete_translated_fields
74
+ translated_attribute_names.each do |name|
75
+ @fields[name] ||= column_type(name)
76
+ end
77
+ end
78
+
79
+ def create_translation_table
80
+ connection.create_table(translations_table_name) do |t|
81
+ t.references table_name.sub(/^#{table_name_prefix}/, '').singularize, :null => false, :index => false, :type => column_type(model.primary_key).to_sym
82
+ t.string :locale, :null => false
83
+ t.timestamps :null => false
84
+ end
85
+ end
86
+
87
+ def add_translation_fields
88
+ connection.change_table(translations_table_name) do |t|
89
+ fields.each do |name, options|
90
+ if options.is_a? Hash
91
+ t.column name, options.delete(:type), options
92
+ else
93
+ t.column name, options
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ def create_translations_index(options)
100
+ foreign_key = "#{table_name.sub(/^#{table_name_prefix}/, "").singularize}_id".to_sym
101
+ connection.add_index(
102
+ translations_table_name,
103
+ foreign_key,
104
+ :name => translation_index_name
105
+ )
106
+ # index for select('DISTINCT locale') call in translation.rb
107
+ connection.add_index(
108
+ translations_table_name,
109
+ :locale,
110
+ :name => translation_locale_index_name
111
+ )
112
+
113
+ if options[:unique_index]
114
+ connection.add_index(
115
+ translations_table_name,
116
+ [foreign_key, :locale],
117
+ :name => translation_unique_index_name,
118
+ unique: true
119
+ )
120
+ end
121
+ end
122
+
123
+ def drop_translation_table
124
+ connection.drop_table(translations_table_name)
125
+ end
126
+
127
+ def drop_translations_index
128
+ if connection.indexes(translations_table_name).map(&:name).include?(translation_index_name)
129
+ connection.remove_index(translations_table_name, :name => translation_index_name)
130
+ end
131
+ if connection.indexes(translations_table_name).map(&:name).include?(translation_locale_index_name)
132
+ connection.remove_index(translations_table_name, :name => translation_locale_index_name)
133
+ end
134
+ end
135
+
136
+ def move_data_to_translation_table
137
+ model.find_each do |record|
138
+ translation = record.translation_for(I18n.locale) || record.translations.build(:locale => I18n.locale)
139
+ fields.each do |attribute_name, attribute_type|
140
+ translation[attribute_name] = record.read_attribute(attribute_name, {:translated => false})
141
+ end
142
+ translation.save!
143
+ end
144
+ end
145
+
146
+ def move_data_to_model_table
147
+ add_missing_columns
148
+
149
+ # Find all of the translated attributes for all records in the model.
150
+ all_translated_attributes = model.all.collect{|m| m.attributes}
151
+ all_translated_attributes.each do |translated_record|
152
+ # Create a hash containing the translated column names and their values.
153
+ translated_attribute_names.inject(fields_to_update={}) do |f, name|
154
+ f.update({name.to_sym => translated_record[name.to_s]})
155
+ end
156
+
157
+ # Now, update the actual model's record with the hash.
158
+ model.where(model.primary_key.to_sym => translated_record[model.primary_key]).update_all(fields_to_update)
159
+ end
160
+ end
161
+
162
+ def validate_translated_fields
163
+ fields.each do |name, options|
164
+ raise BadFieldName.new(name) unless valid_field_name?(name)
165
+ end
166
+ end
167
+
168
+ def column_type(name)
169
+ columns.detect { |c| c.name == name.to_s }.try(:type) || :string
170
+ end
171
+
172
+ def valid_field_name?(name)
173
+ translated_attribute_names.include?(name)
174
+ end
175
+
176
+ def translation_index_name
177
+ truncate_index_name "index_#{translations_table_name}_on_#{table_name.singularize}_id"
178
+ end
179
+
180
+ def translation_locale_index_name
181
+ truncate_index_name "index_#{translations_table_name}_on_locale"
182
+ end
183
+
184
+ def translation_unique_index_name
185
+ truncate_index_name "index_#{translations_table_name}_on_#{table_name.singularize}_id_and_locale"
186
+ end
187
+
188
+ def clear_schema_cache!
189
+ connection.schema_cache.clear! if connection.respond_to? :schema_cache
190
+ model::Translation.reset_column_information
191
+ model.reset_column_information
192
+ end
193
+
194
+ private
195
+
196
+ def truncate_index_name(index_name)
197
+ if index_name.size < connection.index_name_length
198
+ index_name
199
+ else
200
+ "index_#{Digest::SHA1.hexdigest(index_name)}"[0, connection.index_name_length]
201
+ end
202
+ end
203
+
204
+ def add_missing_columns
205
+ clear_schema_cache!
206
+ translated_attribute_names.map(&:to_s).each do |attribute|
207
+ unless model.column_names.include?(attribute)
208
+ connection.add_column(table_name, attribute, model::Translation.columns_hash[attribute].type)
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end