globalize-r5 5.1.0

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