globalize-rails5 5.1.0

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