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