globalize 3.0.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.
@@ -0,0 +1,27 @@
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
+ #raise 'z' if value.nil? # TODO
23
+ self[locale][name.to_s] = value
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,173 @@
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
+ scoped.merge(translation_class.with_locales(*locales))
8
+ end
9
+
10
+ def with_translations(*locales)
11
+ locales = translated_locales if locales.empty?
12
+ includes(:translations).with_locales(locales).with_required_attributes
13
+ end
14
+
15
+ def with_required_attributes
16
+ required_translated_attributes.inject(scoped) do |scope, name|
17
+ scope.where("#{translated_column_name(name)} IS NOT NULL")
18
+ end
19
+ end
20
+
21
+ def with_translated_attribute(name, value, locales = nil)
22
+ locales ||= Globalize.fallbacks
23
+ with_translations.where(
24
+ translated_column_name(name) => value,
25
+ translated_column_name(:locale) => Array(locales).map(&:to_s)
26
+ )
27
+ end
28
+
29
+ def translated?(name)
30
+ translated_attribute_names.include?(name.to_sym)
31
+ end
32
+
33
+ def required_attributes
34
+ validators.map { |v| v.attributes if v.is_a?(ActiveModel::Validations::PresenceValidator) }.flatten
35
+ end
36
+
37
+ def required_translated_attributes
38
+ translated_attribute_names & required_attributes
39
+ end
40
+
41
+ def translation_class
42
+ @translation_class ||= begin
43
+ klass = self.const_get(:Translation) rescue nil
44
+ if klass.nil? || klass.class_name != (self.class_name + "Translation")
45
+ klass = self.const_set(:Translation, Class.new(Globalize::ActiveRecord::Translation))
46
+ end
47
+
48
+ klass.belongs_to :globalized_model, :class_name => self.name, :foreign_key => translation_options[:foreign_key]
49
+ klass
50
+ end
51
+ end
52
+
53
+ def translations_table_name
54
+ translation_class.table_name
55
+ end
56
+
57
+ def translated_column_name(name)
58
+ "#{translation_class.table_name}.#{name}"
59
+ end
60
+
61
+ if RUBY_VERSION < '1.9'
62
+ def respond_to?(method_id, *args, &block)
63
+ supported_on_missing?(method_id) || super
64
+ end
65
+ else
66
+ def respond_to_missing?(method_id, include_private = false)
67
+ supported_on_missing?(method_id) || super
68
+ end
69
+ end
70
+
71
+ def supported_on_missing?(method_id)
72
+ return super unless RUBY_VERSION < '1.9' || respond_to?(:translated_attribute_names)
73
+ match = defined?(::ActiveRecord::DynamicFinderMatch) && (::ActiveRecord::DynamicFinderMatch.match(method_id) || ::ActiveRecord::DynamicScopeMatch.match(method_id))
74
+ return false if match.nil?
75
+
76
+ attribute_names = match.attribute_names.map(&:to_sym)
77
+ translated_attributes = attribute_names & translated_attribute_names
78
+ return false if translated_attributes.empty?
79
+
80
+ untranslated_attributes = attribute_names - translated_attributes
81
+ return false if untranslated_attributes.any?{|unt| ! respond_to?(:"scoped_by_#{unt}")}
82
+ return [match, attribute_names, translated_attributes, untranslated_attributes]
83
+ end
84
+
85
+ def method_missing(method_id, *arguments, &block)
86
+ match, attribute_names, translated_attributes, untranslated_attributes = supported_on_missing?(method_id)
87
+ return super unless match
88
+
89
+ scope = scoped
90
+
91
+ translated_attributes.each do |attr|
92
+ scope = scope.with_translated_attribute(attr, arguments[attribute_names.index(attr)])
93
+ end
94
+
95
+ untranslated_attributes.each do |unt|
96
+ index = attribute_names.index(unt)
97
+ raise StandarError unless index
98
+ scope = scope.send(:"scoped_by_#{unt}", arguments[index])
99
+ end
100
+
101
+ if defined?(::ActiveRecord::DynamicFinderMatch) && match.is_a?(::ActiveRecord::DynamicFinderMatch)
102
+ if match.instantiator? and scope.blank?
103
+ return scope.find_or_instantiator_by_attributes match, attribute_names, *arguments, &block
104
+ end
105
+ match_finder_method = match.finder.to_s
106
+ match_finder_method << "!" if match.bang? && ::ActiveRecord::VERSION::STRING >= "3.1.0"
107
+ return scope.send(match_finder_method).tap do |found|
108
+ found.is_a?(Array) ? found.map { |f| f.translations.reload } : found.translations.reload unless found.nil?
109
+ end
110
+ end
111
+ return scope
112
+ end
113
+
114
+ def find_or_instantiator_by_attributes(match, attributes, *args)
115
+ options = args.size > 1 && args.last(2).all?{ |a| a.is_a?(Hash) } ? args.extract_options! : {}
116
+ protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
117
+ args.each_with_index do |arg, i|
118
+ if arg.is_a?(Hash)
119
+ protected_attributes_for_create = args[i].with_indifferent_access
120
+ else
121
+ unprotected_attributes_for_create[attributes[i]] = args[i]
122
+ end
123
+ end
124
+
125
+ record = if ::ActiveRecord::VERSION::STRING < "3.1.0"
126
+ new do |r|
127
+ r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty?
128
+ r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty?
129
+ end
130
+ else
131
+ new(protected_attributes_for_create, options) do |r|
132
+ r.assign_attributes(unprotected_attributes_for_create, :without_protection => true)
133
+ end
134
+ end
135
+ yield(record) if block_given?
136
+ record.send(match.bang? ? :save! : :save) if match.instantiator.eql?(:create)
137
+
138
+ record
139
+ end
140
+
141
+ protected
142
+
143
+ def translated_attr_accessor(name)
144
+ define_method(:"#{name}=") do |value|
145
+ write_attribute(name, value)
146
+ end
147
+ define_method(name) do |*args|
148
+ Globalize::Interpolation.interpolate(name, self, args)
149
+ end
150
+ alias_method :"#{name}_before_type_cast", name
151
+ end
152
+
153
+ def translations_accessor(name)
154
+ define_method(:"#{name}_translations") do
155
+ result = translations.each_with_object(HashWithIndifferentAccess.new) do |translation, result|
156
+ result[translation.locale] = translation.send(name)
157
+ end
158
+ globalize.stash.keys.each_with_object(result) do |locale, result|
159
+ result[locale] = globalize.fetch_stash(locale, name) if globalize.stash_contains?(locale, name)
160
+ end
161
+ end
162
+ define_method(:"#{name}_translations=") do |value|
163
+ value.each do |(locale, value)|
164
+ write_attribute name, value, :locale => locale
165
+ end
166
+ end
167
+ end
168
+
169
+ end
170
+
171
+ end
172
+
173
+ 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,224 @@
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 self.included(base)
15
+ # Maintain Rails 3.0.x compatibility while adding Rails 3.1.x compatibility
16
+ if base.method_defined?(:assign_attributes)
17
+ base.class_eval %{
18
+ def assign_attributes(attributes, options = {})
19
+ with_given_locale(attributes) { super }
20
+ end
21
+ }
22
+ else
23
+ base.class_eval %{
24
+ def attributes=(attributes, *args)
25
+ with_given_locale(attributes) { super }
26
+ end
27
+
28
+ def update_attributes!(attributes, *args)
29
+ with_given_locale(attributes) { super }
30
+ end
31
+
32
+ def update_attributes(attributes, *args)
33
+ with_given_locale(attributes) { super }
34
+ end
35
+ }
36
+ end
37
+ end
38
+
39
+ def write_attribute(name, value, options = {})
40
+ if translated?(name)
41
+ # Deprecate old use of locale
42
+ unless options.is_a?(Hash)
43
+ warn "[DEPRECATION] passing 'locale' as #{options.inspect} is deprecated. Please use {:locale => #{options.inspect}} instead."
44
+ options = {:locale => options}
45
+ end
46
+ options = {:locale => Globalize.locale}.merge(options)
47
+
48
+ # Dirty tracking, paraphrased from
49
+ # ActiveRecord::AttributeMethods::Dirty#write_attribute.
50
+ name_str = name.to_s
51
+ if attribute_changed?(name_str)
52
+ # If there's already a change, delete it if this undoes the change.
53
+ old = changed_attributes[name_str]
54
+ changed_attributes.delete(name_str) if value == old
55
+ else
56
+ # If there's not a change yet, record it.
57
+ old = globalize.fetch(options[:locale], name)
58
+ old = old.clone if old.duplicable?
59
+ changed_attributes[name_str] = old if value != old
60
+ end
61
+
62
+ globalize.write(options[:locale], name, value)
63
+ else
64
+ super(name, value)
65
+ end
66
+ end
67
+
68
+ def read_attribute(name, options = {})
69
+ # Deprecate old use of locale
70
+ unless options.is_a?(Hash)
71
+ warn "[DEPRECATION] passing 'locale' as #{options.inspect} is deprecated. Please use {:locale => #{options.inspect}} instead."
72
+ options = {:locale => options}
73
+ end
74
+
75
+ options = {:translated => true, :locale => nil}.merge(options)
76
+ if self.class.translated?(name) and options[:translated]
77
+ if (value = globalize.fetch(options[:locale] || Globalize.locale, name))
78
+ value
79
+ else
80
+ super(name)
81
+ end
82
+ else
83
+ super(name)
84
+ end
85
+ end
86
+
87
+ def attribute_names
88
+ translated_attribute_names.map(&:to_s) + super
89
+ end
90
+
91
+ def translated?(name)
92
+ self.class.translated?(name)
93
+ end
94
+
95
+ def translated_attributes
96
+ translated_attribute_names.inject({}) do |attributes, name|
97
+ attributes.merge(name.to_s => translation.send(name))
98
+ end
99
+ end
100
+
101
+ # This method is basically the method built into Rails
102
+ # but we have to pass {:translated => false}
103
+ def untranslated_attributes
104
+ attrs = {}
105
+ attribute_names.each do |name|
106
+ attrs[name] = read_attribute(name, {:translated => false})
107
+ end
108
+ attrs
109
+ end
110
+
111
+ def set_translations(options)
112
+ options.keys.each do |locale|
113
+ translation = translation_for(locale) ||
114
+ translations.build(:locale => locale.to_s)
115
+
116
+ options[locale].each do |key, value|
117
+ translation.send :"#{key}=", value
118
+ end
119
+ translation.save
120
+ end
121
+ globalize.reset
122
+ end
123
+
124
+ def reload(options = nil)
125
+ translation_caches.clear
126
+ translated_attribute_names.each { |name| @attributes.delete(name.to_s) }
127
+ globalize.reset
128
+ super(options)
129
+ end
130
+
131
+ def clone
132
+ obj = super
133
+ return obj unless respond_to?(:translated_attribute_names)
134
+
135
+ obj.instance_variable_set(:@translations, nil) if new_record? # Reset the collection because of rails bug: http://pastie.org/1521874
136
+ obj.instance_variable_set(:@globalize, nil )
137
+ each_locale_and_translated_attribute do |locale, name|
138
+ obj.globalize.write(locale, name, globalize.fetch(locale, name) )
139
+ end
140
+
141
+ return obj
142
+ end
143
+
144
+ def translation
145
+ translation_for(::Globalize.locale)
146
+ end
147
+
148
+ def translation_for(locale, build_if_missing = true)
149
+ unless translation_caches[locale]
150
+ # Fetch translations from database as those in the translation collection may be incomplete
151
+ _translation = translations.detect{|t| t.locale.to_s == locale.to_s}
152
+ _translation ||= translations.with_locale(locale).first unless translations.loaded?
153
+ _translation ||= translations.build(:locale => locale) if build_if_missing
154
+ translation_caches[locale] = _translation if _translation
155
+ end
156
+ translation_caches[locale]
157
+ end
158
+
159
+ def translation_caches
160
+ @translation_caches ||= {}
161
+ end
162
+
163
+ def globalize_fallbacks(locale)
164
+ Globalize.fallbacks(locale)
165
+ end
166
+
167
+ def rollback
168
+ translation_caches[::Globalize.locale] = translation.previous_version
169
+ end
170
+
171
+ def column_for_attribute name
172
+ translated_attribute_names.include?(name) ? globalize.send(:column_for_attribute, name) : super
173
+ end
174
+
175
+ private
176
+
177
+ def update(*)
178
+ I18n.with_locale(read_attribute(:locale) || I18n.default_locale) do
179
+ super
180
+ end
181
+ end
182
+
183
+ def create(*)
184
+ I18n.with_locale(read_attribute(:locale) || I18n.default_locale) do
185
+ super
186
+ end
187
+ end
188
+
189
+ protected
190
+
191
+ def each_locale_and_translated_attribute
192
+ used_locales.each do |locale|
193
+ translated_attribute_names.each do |name|
194
+ yield locale, name
195
+ end
196
+ end
197
+ end
198
+
199
+ def used_locales
200
+ locales = globalize.stash.keys.concat(globalize.stash.keys).concat(translations.translated_locales)
201
+ locales.uniq!
202
+ locales
203
+ end
204
+
205
+ def save_translations!
206
+ globalize.save_translations!
207
+ translation_caches.clear
208
+ end
209
+
210
+ def with_given_locale(attributes, &block)
211
+ attributes.symbolize_keys! if attributes.respond_to?(:symbolize_keys!)
212
+
213
+ locale = respond_to?(:locale=) ? attributes.try(:[], :locale) :
214
+ attributes.try(:delete, :locale)
215
+
216
+ if locale
217
+ Globalize.with_locale(locale, &block)
218
+ else
219
+ yield
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,191 @@
1
+ require 'digest/sha1'
2
+
3
+ module Globalize
4
+ module ActiveRecord
5
+ module Migration
6
+ attr_reader :globalize_migrator
7
+
8
+ def globalize_migrator
9
+ @globalize_migrator ||= Migrator.new(self)
10
+ end
11
+
12
+ delegate :create_translation_table!, :add_translation_fields!, :drop_translation_table!,
13
+ :translation_index_name, :translation_locale_index_name,
14
+ :to => :globalize_migrator
15
+
16
+ class Migrator
17
+ include Globalize::ActiveRecord::Exceptions
18
+
19
+ attr_reader :model, :fields
20
+ delegate :translated_attribute_names, :connection, :table_name,
21
+ :table_name_prefix, :translations_table_name, :columns, :to => :model
22
+
23
+ def initialize(model)
24
+ @model = model
25
+ end
26
+
27
+ def create_translation_table!(fields = {}, options = {})
28
+ @fields = fields
29
+ # If we have fields we only want to create the translation table with those fields
30
+ complete_translated_fields if fields.blank?
31
+ validate_translated_fields
32
+
33
+ create_translation_table
34
+ add_translation_fields!(fields, options)
35
+ create_translations_index
36
+ clear_schema_cache!
37
+ end
38
+
39
+ def add_translation_fields!(fields, options = {})
40
+ @fields = fields
41
+ validate_translated_fields
42
+
43
+ add_translation_fields
44
+ clear_schema_cache!
45
+ move_data_to_translation_table if options[:migrate_data]
46
+ remove_source_columns if options[:remove_source_columns]
47
+ clear_schema_cache!
48
+ end
49
+
50
+ def remove_source_columns
51
+ connection.remove_columns(table_name, *fields.keys)
52
+ end
53
+
54
+ def drop_translation_table!(options = {})
55
+ move_data_to_model_table if options[:migrate_data]
56
+ drop_translations_index
57
+ drop_translation_table
58
+ clear_schema_cache!
59
+ end
60
+
61
+ # This adds all the current translated attributes of the model
62
+ # It's a problem because in early migrations would add all the translated attributes
63
+ def complete_translated_fields
64
+ translated_attribute_names.each do |name|
65
+ fields[name] = column_type(name) unless fields[name]
66
+ end
67
+ end
68
+
69
+ def create_translation_table
70
+ connection.create_table(translations_table_name) do |t|
71
+ t.references table_name.sub(/^#{table_name_prefix}/, '').singularize, :null => false
72
+ t.string :locale, :null => false
73
+ t.timestamps
74
+ end
75
+ end
76
+
77
+ def add_translation_fields
78
+ connection.change_table(translations_table_name) do |t|
79
+ fields.each do |name, options|
80
+ if options.is_a? Hash
81
+ t.column name, options.delete(:type), options
82
+ else
83
+ t.column name, options
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ def create_translations_index
90
+ connection.add_index(
91
+ translations_table_name,
92
+ "#{table_name.sub(/^#{table_name_prefix}/, "").singularize}_id",
93
+ :name => translation_index_name
94
+ )
95
+ # index for select('DISTINCT locale') call in translation.rb
96
+ connection.add_index(
97
+ translations_table_name,
98
+ :locale,
99
+ :name => translation_locale_index_name
100
+ )
101
+ end
102
+
103
+ def drop_translation_table
104
+ connection.drop_table(translations_table_name)
105
+ end
106
+
107
+ def drop_translations_index
108
+ connection.remove_index(translations_table_name, :name => translation_index_name)
109
+ end
110
+
111
+ def move_data_to_translation_table
112
+ model.find_each do |record|
113
+ translation = record.translation_for(I18n.default_locale) || record.translations.build(:locale => I18n.default_locale)
114
+ fields.each do |attribute_name, attribute_type|
115
+ translation[attribute_name] = record.read_attribute(attribute_name, {:translated => false})
116
+ end
117
+ translation.save!
118
+ end
119
+ end
120
+
121
+ def move_data_to_model_table
122
+ add_missing_columns
123
+
124
+ # Find all of the translated attributes for all records in the model.
125
+ all_translated_attributes = @model.all.collect{|m| m.attributes}
126
+ all_translated_attributes.each do |translated_record|
127
+ # Create a hash containing the translated column names and their values.
128
+ translated_attribute_names.inject(fields_to_update={}) do |f, name|
129
+ f.update({name.to_sym => translated_record[name.to_s]})
130
+ end
131
+
132
+ # Now, update the actual model's record with the hash.
133
+ @model.update_all(fields_to_update, {:id => translated_record['id']})
134
+ end
135
+ end
136
+
137
+ def validate_translated_fields
138
+ fields.each do |name, options|
139
+ raise BadFieldName.new(name) unless valid_field_name?(name)
140
+ if options.is_a? Hash
141
+ raise BadFieldType.new(name, options[:type]) unless valid_field_type?(name, options[:type])
142
+ else
143
+ raise BadFieldType.new(name, options) unless valid_field_type?(name, options)
144
+ end
145
+ end
146
+ end
147
+
148
+ def column_type(name)
149
+ columns.detect { |c| c.name == name.to_s }.try(:type)
150
+ end
151
+
152
+ def valid_field_name?(name)
153
+ translated_attribute_names.include?(name)
154
+ end
155
+
156
+ def valid_field_type?(name, type)
157
+ !translated_attribute_names.include?(name) || [:string, :text].include?(type)
158
+ end
159
+
160
+ def translation_index_name
161
+ index_name = "index_#{translations_table_name}_on_#{table_name.singularize}_id"
162
+ index_name.size < connection.index_name_length ? index_name :
163
+ "index_#{Digest::SHA1.hexdigest(index_name)}"[0, connection.index_name_length]
164
+ end
165
+
166
+ def translation_locale_index_name
167
+ index_name = "index_#{translations_table_name}_on_locale"
168
+ index_name.size < connection.index_name_length ? index_name :
169
+ "index_#{Digest::SHA1.hexdigest(index_name)}"[0, connection.index_name_length]
170
+ end
171
+
172
+ def clear_schema_cache!
173
+ connection.schema_cache.clear! if connection.respond_to? :schema_cache
174
+ model::Translation.reset_column_information
175
+ model.reset_column_information
176
+ end
177
+
178
+ private
179
+
180
+ def add_missing_columns
181
+ translated_attribute_names.map(&:to_s).each do |attribute|
182
+ unless model.column_names.include?(attribute)
183
+ connection.add_column(table_name, attribute, model::Translation.columns_hash[attribute].type)
184
+ end
185
+ end
186
+ end
187
+
188
+ end
189
+ end
190
+ end
191
+ end