globalize 3.0.0

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