lolita-translation 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,295 +1,299 @@
1
- require 'stringio'
2
-
3
- class ActiveRecord::Base
4
- # Provides ability to add the translations for the model using delegate pattern.
5
- # Uses has_many association to the ModelNameTranslation.
6
- #
7
- # For example you have model Article with attributes title and text.
8
- # You want that attributes title and text to be translated.
9
- # For this reason you need to generate new model ArticleTranslation.
10
- # In migration you need to add:
11
- #
12
- # create_table :article_translations do |t|
13
- # t.references :article, :null => false
14
- # t.string :locale, :length => 2, :null => false
15
- # t.string :name, :null => false
16
- # end
17
- #
18
- # add_index :articles, [:article_id, :locale], :unique => true, :name => 'unique_locale_for_article_id'
19
- #
20
- # And in the Article model:
21
- #
22
- # translations :title, :text
23
- #
24
- # This will adds:
25
- #
26
- # * named_scope (translated) and has_many association to the Article model
27
- # * locale presence validation to the ArticleTranslation model.
28
- #
29
- # Notice: if you want to have validates_presence_of :article, you should use :inverse_of.
30
- # Support this by yourself. Better is always to use artile.translations.build() method.
31
- #
32
- # For more information please read API. Feel free to write me an email to:
33
- # dmitry.polushkin@gmail.com.
34
- #
35
- # ===
36
- #
37
- # You also can pass attributes and options to the translations class method:
38
- #
39
- # translations :title, :text, :fallback => true, :writer => true, :nil => nil
40
- #
41
- # ===
42
- #
43
- # Configuration options:
44
- #
45
- # * <tt>:fallback</tt> - if translation for the current locale not found.
46
- # By default true.
47
- # Uses algorithm of fallback:
48
- # 0) current translation (using I18n.locale);
49
- # 1) default locale (using I18n.default_locale);
50
- # 2) :nil value (see <tt>:nil</tt> configuration option)
51
- # * <tt>:reader</tt> - add reader attributes to the model and delegate them
52
- # to the translation model columns. Add's fallback if it is set to true.
53
- # * <tt>:writer</tt> - add writer attributes to the model and assign them
54
- # to the translation model attributes.
55
- # * <tt>:nil</tt> - when reader cant find string, it returns by default an
56
- # empty string. If you want to change this setting for example to nil,
57
- # add :nil => nil
58
- #
59
- # ===
60
- #
61
- # When you are using <tt>:writer</tt> option, you can create translations using
62
- # update_attributes method. For example:
63
- #
64
- # Article.create!
65
- # Article.update_attributes(:title => 'title', :text => 'text')
66
- #
67
- # ===
68
- #
69
- # <tt>translated</tt> named_scope is useful when you want to find only those
70
- # records that are translated to a specific locale.
71
- # For example if you want to find all Articles that is translated to an english
72
- # language, you can write: Article.translated(:en)
73
- #
74
- # <tt>has_translation?(locale)</tt> method, that returns true if object's model
75
- # have a translation for a specified locale
76
- #
77
- # <tt>translation(locale)</tt> method finds translation with specified locale.
78
- #
79
- # <tt>all_translations</tt> method that returns all possible translations in
80
- # ordered hash (useful when creating forms with nested attributes).
81
- def self.translations(*attrs)
82
- options = {
83
- :fallback => true,
84
- :reader => true,
85
- :writer => false,
86
- :nil => ''
87
- }.merge(attrs.extract_options!)
88
- options.assert_valid_keys([:fallback, :reader, :writer, :nil])
89
-
90
- class << self
91
- # adds :translations to :includes if current locale differs from default
92
- #FIXME is this enough with find or need to create chain for find_last, find_first and others?
93
- alias_method(:find_without_translations, :find) unless method_defined?(:find_without_translations)
94
- def find(*args)
95
- if args[0].kind_of?(Hash)
96
- args[0][:include] ||= []
97
- args[0][:include] << :translations
98
- end unless I18n.locale == I18n.default_locale
99
- find_without_translations(*args)
100
- end
101
- # Defines given class recursively
102
- # Example:
103
- # create_class('Cms::Text::Page', Object, ActiveRecord::Base)
104
- # => Cms::Text::Page
105
- def create_class(class_name, parent, superclass, &block)
106
- first,*other = class_name.split("::")
107
- if other.empty?
108
- klass = Class.new superclass, &block
109
- parent.const_set(first, klass)
110
- else
111
- klass = Class.new
112
- parent = unless parent.const_defined?(first)
113
- parent.const_set(first, klass)
114
- else
115
- first.constantize
116
- end
117
- create_class(other.join('::'), parent, superclass, &block)
118
- end
119
- end
120
- # defines "ModelNameTranslation" if it's not defined manualy
121
- def define_translation_class name, attrs
122
- klass = name.constantize rescue nil
123
- unless klass
124
- klass = create_class(name, Object, ActiveRecord::Base) do
125
- # set's real table name
126
- set_table_name name.sub('Translation','').constantize.table_name.singularize + "_translations"
127
- cattr_accessor :translate_attrs, :master_id
128
- # override validate to vaidate only translate fields from master Class
129
- def validate
130
- item = self.class.name.sub('Translation','').constantize.new(self.attributes.clone.delete_if{|k,_| !self.class.translate_attrs.include?(k.to_sym)})
131
- was_table_name = item.class.table_name
132
- item.class.set_table_name self.class.table_name
133
- item.valid? rescue
134
- self.class.translate_attrs.each do |attr|
135
- errors_on_attr = item.errors.on(attr)
136
- self.errors.add(attr,errors_on_attr) if errors_on_attr
137
- end
138
- item.class.set_table_name was_table_name
139
- end
140
- extend TranslationClassMethods
141
- end
142
- klass.translate_attrs = attrs
143
- else
144
- unless klass.respond_to?(:translate_attrs)
145
- klass.send(:cattr_accessor, :translate_attrs, :master_id)
146
- klass.send(:extend,TranslationClassMethods)
147
- klass.translate_attrs = attrs
148
- end
149
- end
150
-
151
- klass.extract_master_id(name)
152
- klass
153
- end
154
- # creates translation table and adds missing fields
155
- # So at first add the "translations :name, :desc" in your model
156
- # then put YourModel.sync_translation_table! in db/seed.rb and run "rake db:seed"
157
- # Later adding more fields in translations array, just run agin "rake db:seed"
158
- # If you want to remove fields do it manualy, it's safer
159
- def sync_translation_table!
160
- out = StringIO.new
161
- $stdout = out
162
- translations_class = reflections[:translations].class_name.constantize
163
- translations_table = translations_class.table_name
164
- unless ActiveRecord::Migration::table_exists?(translations_table)
165
- ActiveRecord::Migration.create_table translations_table do |t|
166
- t.integer translations_class.master_id, :null => false
167
- t.string :locale, :null => false, :limit => 5
168
- columns_has_translations.each do |col|
169
- t.send(col.type,col.name)
170
- end
171
- end
172
- ActiveRecord::Migration.add_index translations_table, [translations_class.master_id, :locale], :unique => true
173
- translations_class.reset_column_information
174
- else
175
- changes = false
176
- columns_has_translations.each do |col|
177
- unless translations_class.columns_hash.has_key?(col.name)
178
- ActiveRecord::Migration.add_column(translations_table, col.name, col.type)
179
- changes = true
180
- end
181
- end
182
- translations_class.reset_column_information if changes
183
- end
184
- $stdout = STDOUT
185
- end
186
- end
187
-
188
- translation_class_name = "#{self.name}Translation"
189
- translation_class = self.define_translation_class(translation_class_name, attrs)
190
- belongs_to = self.name.demodulize.underscore.to_sym
191
-
192
- write_inheritable_attribute :has_translations_options, options
193
- class_inheritable_reader :has_translations_options
194
-
195
- write_inheritable_attribute :columns_has_translations, (columns rescue []).collect{|col| col if attrs.include?(col.name.to_sym)}.compact
196
- class_inheritable_reader :columns_has_translations
197
-
198
- # forces given locale
199
- # I18n.locale = :lv
200
- # a = Article.find 18
201
- # a.title
202
- # => "LV title"
203
- # a.in(:en).title
204
- # => "EN title"
205
- def in locale
206
- locale.to_sym == I18n.default_locale ? self : find_translation(locale)
207
- end
208
-
209
- def find_or_build_translation(*args)
210
- locale = args.first.to_s
211
- build = args.second.present?
212
- find_translation(locale) || (build ? self.translations.build(:locale => locale) : self.translations.new(:locale => locale))
213
- end
214
-
215
- def translation(locale)
216
- find_translation(locale.to_s)
217
- end
218
-
219
- def all_translations
220
- t = I18n.available_locales.map do |locale|
221
- [locale, find_or_build_translation(locale)]
222
- end
223
- ActiveSupport::OrderedHash[t]
224
- end
225
-
226
- def has_translation?(locale)
227
- return true if locale == I18n.default_locale
228
- find_translation(locale).present?
229
- end
230
-
231
- # if object is new, then nested slaves ar built for all available locales
232
- def build_nested_translations
233
- if (I18n.available_locales.size - 1) > self.translations.size
234
- I18n.available_locales.clone.delete_if{|l| l == I18n.default_locale}.each do |l|
235
- options = {:locale => l.to_s}
236
- options[self.class.reflections[:translations].class_name.constantize.master_id] = self.id unless self.new_record?
237
- self.translations.build(options) unless self.translations.map(&:locale).include?(l.to_s)
238
- end
239
- end
240
- end
241
-
242
- if options[:reader]
243
- attrs.each do |name|
244
- send :define_method, name do
245
- unless I18n.default_locale == I18n.locale
246
- translation = self.translation(I18n.locale)
247
- if translation.nil?
248
- if has_translations_options[:fallback]
249
- (self[name].nil? || self[name].blank?) ? has_translations_options[:nil] : self[name].set_origins(self,name)
250
- else
251
- has_translations_options[:nil]
252
- end
253
- else
254
- if @return_raw_data
255
- (self[name].nil? || self[name].blank?) ? has_translations_options[:nil] : self[name].set_origins(self,name)
256
- else
257
- value = translation.send(name) and value.set_origins(self,name)
258
- end
259
- end
260
- else
261
- (self[name].nil? || self[name].blank?) ? has_translations_options[:nil] : self[name].set_origins(self,name)
262
- end
263
- end
264
- end
265
- end
266
-
267
- has_many :translations, :class_name => translation_class_name, :foreign_key => translation_class.master_id, :dependent => :destroy
268
- accepts_nested_attributes_for :translations, :allow_destroy => true, :reject_if => proc { |attributes| columns_has_translations.collect{|col| attributes[col.name].blank? ? nil : 1}.compact.empty? }
269
- translation_class.belongs_to belongs_to
270
- translation_class.validates_presence_of :locale
271
- translation_class.validates_uniqueness_of :locale, :scope => translation_class.master_id
272
-
273
- # Workaround to support Rails 2
274
- scope_method = if ActiveRecord::VERSION::MAJOR < 3 then :named_scope else :scope end
275
-
276
- send scope_method, :translated, lambda { |locale| {:conditions => ["#{translation_class.table_name}.locale = ?", locale.to_s], :joins => :translations} }
277
-
278
- #private is no good
279
-
280
- def find_translation(locale)
281
- locale = locale.to_s
282
- translations.detect { |t| t.locale == locale }
283
- end
284
- end
285
- end
286
-
287
- module TranslationClassMethods
288
- # sets real master_id it's aware of STI
289
- def extract_master_id name
290
- master_class = name.sub('Translation','').constantize
291
- #FIXME why need to check superclass ?
292
- class_name = master_class.name #!master_class.superclass.abstract_class? ? master_class.superclass.name : master_class.name
293
- self.master_id = :"#{class_name.demodulize.underscore}_id"
294
- end
1
+ require 'stringio'
2
+
3
+ class ActiveRecord::Base
4
+ # Provides ability to add the translations for the model using delegate pattern.
5
+ # Uses has_many association to the ModelNameTranslation.
6
+ #
7
+ # For example you have model Article with attributes title and text.
8
+ # You want that attributes title and text to be translated.
9
+ # For this reason you need to generate new model ArticleTranslation.
10
+ # In migration you need to add:
11
+ #
12
+ # create_table :article_translations do |t|
13
+ # t.references :article, :null => false
14
+ # t.string :locale, :length => 2, :null => false
15
+ # t.string :name, :null => false
16
+ # end
17
+ #
18
+ # add_index :articles, [:article_id, :locale], :unique => true, :name => 'unique_locale_for_article_id'
19
+ #
20
+ # And in the Article model:
21
+ #
22
+ # translations :title, :text
23
+ #
24
+ # This will adds:
25
+ #
26
+ # * named_scope (translated) and has_many association to the Article model
27
+ # * locale presence validation to the ArticleTranslation model.
28
+ #
29
+ # Notice: if you want to have validates_presence_of :article, you should use :inverse_of.
30
+ # Support this by yourself. Better is always to use artile.translations.build() method.
31
+ #
32
+ # For more information please read API. Feel free to write me an email to:
33
+ # dmitry.polushkin@gmail.com.
34
+ #
35
+ # ===
36
+ #
37
+ # You also can pass attributes and options to the translations class method:
38
+ #
39
+ # translations :title, :text, :fallback => true, :writer => true, :nil => nil
40
+ #
41
+ # ===
42
+ #
43
+ # Configuration options:
44
+ #
45
+ # * <tt>:fallback</tt> - if translation for the current locale not found.
46
+ # By default true.
47
+ # Uses algorithm of fallback:
48
+ # 0) current translation (using I18n.locale);
49
+ # 1) default locale (using I18n.default_locale);
50
+ # 2) :nil value (see <tt>:nil</tt> configuration option)
51
+ # * <tt>:reader</tt> - add reader attributes to the model and delegate them
52
+ # to the translation model columns. Add's fallback if it is set to true.
53
+ # * <tt>:writer</tt> - add writer attributes to the model and assign them
54
+ # to the translation model attributes.
55
+ # * <tt>:nil</tt> - when reader cant find string, it returns by default an
56
+ # empty string. If you want to change this setting for example to nil,
57
+ # add :nil => nil
58
+ #
59
+ # ===
60
+ #
61
+ # When you are using <tt>:writer</tt> option, you can create translations using
62
+ # update_attributes method. For example:
63
+ #
64
+ # Article.create!
65
+ # Article.update_attributes(:title => 'title', :text => 'text')
66
+ #
67
+ # ===
68
+ #
69
+ # <tt>translated</tt> named_scope is useful when you want to find only those
70
+ # records that are translated to a specific locale.
71
+ # For example if you want to find all Articles that is translated to an english
72
+ # language, you can write: Article.translated(:en)
73
+ #
74
+ # <tt>has_translation?(locale)</tt> method, that returns true if object's model
75
+ # have a translation for a specified locale
76
+ #
77
+ # <tt>translation(locale)</tt> method finds translation with specified locale.
78
+ #
79
+ # <tt>all_translations</tt> method that returns all possible translations in
80
+ # ordered hash (useful when creating forms with nested attributes).
81
+ def self.translations(*attrs)
82
+ options = {
83
+ :fallback => true,
84
+ :reader => true,
85
+ :writer => false,
86
+ :nil => ''
87
+ }.merge(attrs.extract_options!)
88
+ options.assert_valid_keys([:fallback, :reader, :writer, :nil])
89
+
90
+ class << self
91
+ # adds :translations to :includes if current locale differs from default
92
+ #FIXME is this enough with find or need to create chain for find_last, find_first and others?
93
+ alias_method(:find_without_translations, :find) unless method_defined?(:find_without_translations)
94
+ def find(*args)
95
+ if args[0].kind_of?(Hash)
96
+ args[0][:include] ||= []
97
+ args[0][:include] << :translations
98
+ end unless I18n.locale == I18n.default_locale
99
+ find_without_translations(*args)
100
+ end
101
+ # Defines given class recursively
102
+ # Example:
103
+ # create_class('Cms::Text::Page', Object, ActiveRecord::Base)
104
+ # => Cms::Text::Page
105
+ def create_class(class_name, parent, superclass, &block)
106
+ first,*other = class_name.split("::")
107
+ if other.empty?
108
+ klass = Class.new superclass, &block
109
+ parent.const_set(first, klass)
110
+ else
111
+ klass = Class.new
112
+ parent = unless parent.const_defined?(first)
113
+ parent.const_set(first, klass)
114
+ else
115
+ first.constantize
116
+ end
117
+ create_class(other.join('::'), parent, superclass, &block)
118
+ end
119
+ end
120
+ # defines "ModelNameTranslation" if it's not defined manualy
121
+ def define_translation_class name, attrs
122
+ klass = name.constantize rescue nil
123
+ unless klass
124
+ klass = create_class(name, Object, ActiveRecord::Base) do
125
+ # set's real table name
126
+ set_table_name name.sub('Translation','').constantize.table_name.singularize + "_translations"
127
+ cattr_accessor :translate_attrs, :master_id
128
+ # override validate to vaidate only translate fields from master Class
129
+ def validate
130
+ item = self.class.name.sub('Translation','').constantize.new(self.attributes.clone.delete_if{|k,_| !self.class.translate_attrs.include?(k.to_sym)})
131
+ was_table_name = item.class.table_name
132
+ item.class.set_table_name self.class.table_name
133
+ item.valid? rescue
134
+ self.class.translate_attrs.each do |attr|
135
+ errors_on_attr = item.errors.on(attr)
136
+ self.errors.add(attr,errors_on_attr) if errors_on_attr
137
+ end
138
+ item.class.set_table_name was_table_name
139
+ end
140
+ extend TranslationClassMethods
141
+ end
142
+ klass.translate_attrs = attrs
143
+ else
144
+ unless klass.respond_to?(:translate_attrs)
145
+ klass.send(:cattr_accessor, :translate_attrs, :master_id)
146
+ klass.send(:extend,TranslationClassMethods)
147
+ klass.translate_attrs = attrs
148
+ end
149
+ end
150
+
151
+ klass.extract_master_id(name)
152
+ klass
153
+ end
154
+ # creates translation table and adds missing fields
155
+ # So at first add the "translations :name, :desc" in your model
156
+ # then put YourModel.sync_translation_table! in db/seed.rb and run "rake db:seed"
157
+ # Later adding more fields in translations array, just run agin "rake db:seed"
158
+ # If you want to remove fields do it manualy, it's safer
159
+ def sync_translation_table!
160
+ out = StringIO.new
161
+ $stdout = out
162
+ translations_class = reflections[:translations].class_name.constantize
163
+ translations_table = translations_class.table_name
164
+ unless ActiveRecord::Migration::table_exists?(translations_table)
165
+ ActiveRecord::Migration.create_table translations_table do |t|
166
+ t.integer translations_class.master_id, :null => false
167
+ t.string :locale, :null => false, :limit => 5
168
+ columns_has_translations.each do |col|
169
+ t.send(col.type,col.name)
170
+ end
171
+ end
172
+ ActiveRecord::Migration.add_index translations_table, [translations_class.master_id, :locale], :unique => true
173
+ translations_class.reset_column_information
174
+ else
175
+ changes = false
176
+ columns_has_translations.each do |col|
177
+ unless translations_class.columns_hash.has_key?(col.name)
178
+ ActiveRecord::Migration.add_column(translations_table, col.name, col.type)
179
+ changes = true
180
+ end
181
+ end
182
+ translations_class.reset_column_information if changes
183
+ end
184
+ $stdout = STDOUT
185
+ end
186
+ end
187
+
188
+ translation_class_name = "#{self.name}Translation"
189
+ translation_class = self.define_translation_class(translation_class_name, attrs)
190
+ belongs_to = self.name.demodulize.underscore.to_sym
191
+
192
+ write_inheritable_attribute :has_translations_options, options
193
+ class_inheritable_reader :has_translations_options
194
+
195
+ write_inheritable_attribute :columns_has_translations, (columns rescue []).collect{|col| col if attrs.include?(col.name.to_sym)}.compact
196
+ class_inheritable_reader :columns_has_translations
197
+
198
+ # forces given locale
199
+ # I18n.locale = :lv
200
+ # a = Article.find 18
201
+ # a.title
202
+ # => "LV title"
203
+ # a.in(:en).title
204
+ # => "EN title"
205
+ def in locale
206
+ locale.to_sym == I18n.default_locale ? self : find_translation(locale)
207
+ end
208
+
209
+ def find_or_build_translation(*args)
210
+ locale = args.first.to_s
211
+ build = args.second.present?
212
+ find_translation(locale) || (build ? self.translations.build(:locale => locale) : self.translations.new(:locale => locale))
213
+ end
214
+
215
+ def translation(locale)
216
+ find_translation(locale.to_s)
217
+ end
218
+
219
+ def all_translations
220
+ t = I18n.available_locales.map do |locale|
221
+ [locale, find_or_build_translation(locale)]
222
+ end
223
+ ActiveSupport::OrderedHash[t]
224
+ end
225
+
226
+ def has_translation?(locale)
227
+ return true if locale == I18n.default_locale
228
+ find_translation(locale).present?
229
+ end
230
+
231
+ # if object is new, then nested slaves ar built for all available locales
232
+ def build_nested_translations
233
+ if (I18n.available_locales.size - 1) > self.translations.size
234
+ I18n.available_locales.clone.delete_if{|l| l == I18n.default_locale}.each do |l|
235
+ options = {:locale => l.to_s}
236
+ options[self.class.reflections[:translations].class_name.constantize.master_id] = self.id unless self.new_record?
237
+ self.translations.build(options) unless self.translations.map(&:locale).include?(l.to_s)
238
+ end
239
+ end
240
+ end
241
+
242
+ if options[:reader]
243
+ attrs.each do |name|
244
+ send :define_method, name do
245
+ unless I18n.default_locale == I18n.locale
246
+ translation = self.translation(I18n.locale)
247
+ if translation.nil?
248
+ if has_translations_options[:fallback]
249
+ (self[name].nil? || self[name].blank?) ? has_translations_options[:nil] : self[name].set_origins(self,name)
250
+ else
251
+ has_translations_options[:nil]
252
+ end
253
+ else
254
+ if @return_raw_data
255
+ (self[name].nil? || self[name].blank?) ? has_translations_options[:nil] : self[name].set_origins(self,name)
256
+ else
257
+ value = translation.send(name) and value.set_origins(self,name)
258
+ end
259
+ end
260
+ else
261
+ (self[name].nil? || self[name].blank?) ? has_translations_options[:nil] : self[name].set_origins(self,name)
262
+ end
263
+ end
264
+ end
265
+ end
266
+
267
+ @translation_attrs = attrs
268
+ def self.translation_attrs
269
+ @translation_attrs
270
+ end
271
+ has_many :translations, :class_name => translation_class_name, :foreign_key => translation_class.master_id, :dependent => :destroy
272
+ accepts_nested_attributes_for :translations, :allow_destroy => true, :reject_if => proc { |attributes| columns_has_translations.collect{|col| attributes[col.name].blank? ? nil : 1}.compact.empty? }
273
+ translation_class.belongs_to belongs_to
274
+ translation_class.validates_presence_of :locale
275
+ translation_class.validates_uniqueness_of :locale, :scope => translation_class.master_id
276
+
277
+ # Workaround to support Rails 2
278
+ scope_method = if ActiveRecord::VERSION::MAJOR < 3 then :named_scope else :scope end
279
+
280
+ send scope_method, :translated, lambda { |locale| {:conditions => ["#{translation_class.table_name}.locale = ?", locale.to_s], :joins => :translations} }
281
+
282
+ #private is no good
283
+
284
+ def find_translation(locale)
285
+ locale = locale.to_s
286
+ translations.detect { |t| t.locale == locale }
287
+ end
288
+ end
289
+ end
290
+
291
+ module TranslationClassMethods
292
+ # sets real master_id it's aware of STI
293
+ def extract_master_id name
294
+ master_class = name.sub('Translation','').constantize
295
+ #FIXME why need to check superclass ?
296
+ class_name = master_class.name #!master_class.superclass.abstract_class? ? master_class.superclass.name : master_class.name
297
+ self.master_id = :"#{class_name.demodulize.underscore}_id"
298
+ end
295
299
  end