translatable 0.2.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/translatable.rb CHANGED
@@ -1,261 +1,16 @@
1
- require 'active_record'
2
- require 'i18n'
3
-
4
- module ActiveRecord
5
- ###
6
- # In order to made the model Translatable, an additional fields should
7
- # should be added first to it. Here is an example of it might be implemented:
8
- #
9
- # Examples:
10
- #
11
- # class Author < ActiveRecord::Base
12
- # validates :name, :presence => true
13
- # end
14
- #
15
- # class TranslatableNews < ActiveRecord::Base #
16
- # attr_accessible :title, :content
17
- # end
18
- #
19
- # class News < ActiveRecord::Base
20
- #
21
- # belongs_to :author
22
- #
23
- # translatable do
24
- # translatable :title, :presence => true, :uniqueness => true
25
- # translatable :content, :presence => true
26
- # translatable_model "TranslatedNews"
27
- # translatable_origin :origin_id
28
- # end
29
- #
30
- # attr_accessible :author_id, :author
31
- # end
32
- #
33
- # An example of application:
34
- #
35
- # news = News.create :translations_attributes => [{title: "Resent News", content: "That is where the text goes", locale: "en"}]
36
- # news.translations.create title: "Заголовок", content: "Содержание",locale: "ru"
37
- #
38
- # news.content
39
- # # => "That is where the text goes"
40
- #
41
- # ::I18n.locale = "ru"
42
- # news.content
43
- # # => "Сюди идет текст"
44
- #
45
- # ::I18n.locale = "de"
46
- # news.content
47
- # # => nil
48
- #
49
- # ::I18n.locale = ::I18n.default_locale
50
- # news.content
51
- # # => "That is where the text goes"
52
- #
53
- module Translatable
54
-
55
- def translatable
56
- extend ActiveRecord::Translatable::ClassMethods
57
- include ActiveRecord::Translatable::InstanceMethods
58
-
59
- translatable_define_hash
60
- yield
61
- translatable_register
62
- end
63
-
64
- module ClassMethods
65
-
66
- protected
67
-
68
- ###
69
- # Fields that are translatable.
70
- # Those fields should be defined in the original model including all the related params.
71
- # Examples:
72
- #
73
- # translatable_property :title, String, required: true, unique: true
74
- # translatable_property :content, Text
75
- #
76
- # NB! Will raise an error if there was no fields specified
77
- #
78
- def translatable *args
79
- (@translatable[:properties] ||= []) << args
80
- end
81
-
82
- ###
83
- # Defines model that will be treated as translation handler.
84
- # Model can be defined as String, Symbol or Constant.
85
- # Examples:
86
- #
87
- # translated_model TranslatedNews
88
- # translated_model "TranslatedNews"
89
- # translated_model :TranslatedNews
90
- #
91
- # Default: Translatable<ModelName>
92
- #
93
- def translatable_model model_name
94
- @translatable[:model] = translatable_model_prepared model_name
95
- end
96
-
97
- ###
98
- # Define the key that the translation will be used for belongs_to association,
99
- # to communicate with original model
100
- # Example:
101
- #
102
- # translatable_origin :news
103
- #
104
- # Default: :origin
105
- #
106
- def translatable_origin origin_key
107
- @translatable[:origin] = translatable_origin_prepared origin_key
108
- end
109
-
110
- ###
111
- # Will not register the attributes as accessible.
112
- # IMPORTANT: Translatable block will be evaluated on the model after it
113
- # was loaded, so it will modify certain thing on final version. Hence this thing is needed.
114
- # Examples:
115
- #
116
- # translatable_attr_protected
117
- #
118
- # Default: false
119
- #
120
- def translatable_attr_protected
121
- @translatable[:attr_accessible] = false
122
- end
123
-
124
- ###
125
- # Will not register the attributes as accessible.
126
- # IMPORTANT: Translatable block will be evaluated on the model after it
127
- # was loaded, so it will modify certain thing on final version. Hence this thing is needed.
128
- # Examples:
129
- #
130
- # translatable_attr_protected
131
- #
132
- # Default: false
133
- #
134
- def translatable_attr_accessible
135
- @translatable[:attr_accessible] = true
136
- end
137
-
138
- ###
139
- # Define the key that the translation will be used for belongs_to association,
140
- # to communicate with original model
141
- # Example:
142
- #
143
- # translatable_origin :language
144
- #
145
- # Default: :locale
146
- #
147
- def translatable_locale locale_attr
148
- @translatable[:locale] = translatable_locale_prepared locale_attr
149
- end
150
-
151
- ###
152
- # Returns Model as a constant that deals with translations
153
- def translatable_model_prepared model_name = nil
154
- model_constant = model_name
155
- model_constant ||= "Translatable#{self.name}"
156
- model_constant.to_s.constantize
157
- end
158
-
159
-
160
- def translatable_origin_prepared origin_key = nil
161
- origin_key || "origin"
162
- end
163
-
164
- def translatable_locale_prepared locale = nil
165
- locale || "locale"
166
- end
167
-
168
- ###
169
- # Define hash that contains all the translations
170
- def translatable_define_hash
171
- @translatable = {}
172
- end
173
-
174
- ###
175
- # Handles all the registring routine, defining methods,
176
- # properties, and everything else
177
- def translatable_register
178
- raise ArgumentError.new("At least one property should be defined") if [nil, []].include?(@translatable[:properties])
179
- [:model,:origin,:locale].each { |hash_key| @translatable[hash_key] ||= send "translatable_#{hash_key}_prepared" }
180
-
181
- translatable_register_properties_for_origin
182
- translatable_register_properties_for_translatable
183
- end
184
-
185
- ###
186
- # Handle the routine to define all th required stuff on the original maodel
187
- def translatable_register_properties_for_origin
188
- has_many :translations, :class_name => @translatable[:model].name, :foreign_key => :"#{@translatable[:origin]}_id"
189
- accepts_nested_attributes_for :translations
190
- attr_accessible :translations_attributes
191
-
192
- @translatable[:properties].each do |p|
193
- accessible_as = (p.last.delete(:as) || p.first rescue p.first)
194
- self.module_eval <<-RUBY, __FILE__, __LINE__ + 1
195
- def #{accessible_as}
196
- current_translation.try(:#{p.first})
197
- end
198
- RUBY
199
- end
200
-
201
- self.module_eval <<-RUBY, __FILE__, __LINE__ + 1
202
- def translatable_set_current
203
- @current_translation = if translations.loaded?
204
- translations.select { |t| t.send(:"#{@translatable[:locale]}") == @translatable_locale }
205
- else
206
- translations.where(:"#{@translatable[:locale]}" => @translatable_locale)
207
- end.first
208
- end
209
- protected :translatable_set_current
210
- RUBY
211
- end
212
-
213
- def translatable_register_properties_for_translatable
214
- @translatable[:model].module_eval <<-RUBY, __FILE__, __LINE__ + 1
215
- validates :#{@translatable[:locale]}, :presence => true
216
- validates :#{@translatable[:locale]}, :format => { :with => /[a-z]{2}/}, :if => Proc.new {|record| !record.#{@translatable[:locale]}.blank? }
217
- validates :#{@translatable[:locale]}, :uniqueness => { :scope => :#{@translatable[:origin]}_id }
218
-
219
- belongs_to :#{@translatable[:origin]}, :class_name => "#{self.name}"
220
- RUBY
221
-
222
- unless @translatable[:attr_accessible].nil?
223
- @translatable[:model].module_eval <<-RUBY, __FILE__, __LINE__ + 1
224
- attr_#{!!@translatable[:attr_accessible] ? "accessible" : "protected" } :#{@translatable[:locale]}, :#{@translatable[:origin]}_id
225
- RUBY
226
- end
227
-
228
- @translatable[:properties].each do |p|
229
- if p.size > 1
230
- @translatable[:model].module_eval <<-RUBY, __FILE__, __LINE__ + 1
231
- validates :#{p.first}, #{p.last.inspect}
232
- RUBY
233
- end
234
- end
235
- end
236
- end
237
-
238
- module InstanceMethods
239
-
240
- def current_translation
241
- if translatable_locale_changed?
242
- @translatable_locale = ::I18n.locale.to_s
243
- translatable_set_current
244
- end
245
- @current_translation
246
- end
247
-
248
- def other_translations
249
- translations - [current_translation]
250
- end
251
-
252
- protected
253
-
254
- def translatable_locale_changed?
255
- @translatable_locale.to_s != ::I18n.locale.to_s
256
- end
1
+ require 'translatable/active_record'
2
+
3
+ if defined?(Rails)
4
+ require 'translatable/engine'
5
+
6
+ ActiveSupport.on_load(:active_record) do
7
+ ActiveSupport.on_load(:i18n) do
8
+ ActiveRecord::Base.extend Translatable::ActiveRecord
257
9
  end
258
10
  end
11
+ else
12
+ require 'active_record'
13
+ require 'i18n'
14
+
15
+ ActiveRecord::Base.extend Translatable::ActiveRecord
259
16
  end
260
-
261
- ActiveRecord::Base.extend ActiveRecord::Translatable
@@ -0,0 +1,276 @@
1
+ # encoding: utf-8
2
+ require 'test_helper'
3
+ require 'support/models/news'
4
+ require 'support/models/posts'
5
+ require 'support/models/messages'
6
+
7
+ class TranslatableTest < Test::Unit::TestCase
8
+ context "Translatable hash" do
9
+ should "Define default" do
10
+ th = News.instance_variable_get :@translatable
11
+
12
+ assert_kind_of Hash, th
13
+ assert th.has_key?(:properties)
14
+ end
15
+
16
+ should "Has dafault model" do
17
+ assert_equal ::TranslatableNews, News.send(:translatable_model_prepared, 'TranslatableNews')
18
+ end
19
+ end
20
+
21
+ context "Translatable model preparation" do
22
+ should "Accept constant" do
23
+ assert_equal ::TranslatableNews, News.send(:translatable_model_prepared, ::TranslatableNews)
24
+ end
25
+
26
+ should "Accept string" do
27
+ assert_equal ::TranslatableNews, News.send(:translatable_model_prepared, "TranslatableNews")
28
+ end
29
+
30
+ should "Accept symbol" do
31
+ assert_equal ::TranslatableNews, News.send(:translatable_model_prepared, :TranslatableNews)
32
+ end
33
+ end
34
+
35
+ context "Instance" do
36
+ should "Respond to translatable methods" do
37
+ news = News.new
38
+
39
+ assert news.respond_to?(:title), "title methods is missing for News instance"
40
+ assert news.respond_to?(:content), "content methods is missing for News instance"
41
+ end
42
+
43
+ should "Creates without translation" do
44
+ news = News.create
45
+
46
+ assert news.persisted?
47
+ assert_nil TranslatableNews.last
48
+ end
49
+
50
+ should "Have no other translation" do
51
+ news = News.create :translations_attributes => [{ :title => "Заголовок", :content => "Содержание", :locale => "ru"}]
52
+
53
+ assert news.persisted?
54
+
55
+ t_news = TranslatableNews.last
56
+ assert_equal [t_news], news.other_translations
57
+ ::I18n.locale = :ru
58
+ assert_equal [], news.other_translations
59
+ ::I18n.locale = ::I18n.default_locale
60
+ end
61
+
62
+ should "Provide errors on creation" do
63
+ news = News.create :translations_attributes => [{:title => "Заголовок", :content => "Содержание", :locale => "ru"},
64
+ {:title => "Resent News", :content => "That is where the text goes", :locale => ""}]
65
+
66
+ assert news.new_record?
67
+
68
+ assert_equal ["Translations locale can't be blank"], news.errors.full_messages
69
+
70
+ news.translations.each do |t|
71
+ assert t.new_record?
72
+ end
73
+ end
74
+ end
75
+
76
+ context "Translatable instance" do
77
+ should "Respond to translatable methods" do
78
+ news = TranslatableNews.new
79
+
80
+ assert news.respond_to?(:title), "Title method is missing for TranslatableNews instance"
81
+ assert news.respond_to?(:content), "Content method is missing for TranslatableNews instance"
82
+ end
83
+
84
+ should "Respond to methods related to origin" do
85
+ news = TranslatableNews.new
86
+
87
+ assert news.respond_to?(:locale), "Locale method is missing for TranslatableNews instance"
88
+ assert news.respond_to?(:origin_id), "Origin methods is missing for TranslatableNews instance"
89
+ assert news.respond_to?(:origin), "Origin methods is missing for TranslatableNews instance"
90
+ end
91
+ end
92
+
93
+ context "Creation with translation" do
94
+ should "Assign to origin" do
95
+ news = News.create
96
+ t_news = TranslatableNews.create :title => "Заголовок", :content => "Содержание", :locale => "ru", :origin_id => news.id
97
+
98
+ assert t_news.persisted?
99
+
100
+ t_news = TranslatableNews.last
101
+ assert_equal news.id, t_news.origin_id
102
+ assert_equal "Заголовок", t_news.title
103
+ assert_equal "Содержание", t_news.content
104
+ assert_equal "ru", t_news.locale
105
+ end
106
+
107
+ should "Create translation on origin creation" do
108
+ news = News.create :translations_attributes => [{ :title => "Заголовок", :content => "Содержание", :locale => "ru"}]
109
+
110
+ assert news.persisted?
111
+
112
+ t_news = TranslatableNews.last
113
+ assert_equal news.id, t_news.origin_id.to_i
114
+ assert_equal "Заголовок", t_news.title
115
+ assert_equal "Содержание", t_news.content
116
+ assert_equal "ru", t_news.locale
117
+ end
118
+
119
+ should "Create multiple translations" do
120
+ news = News.create :translations_attributes => [{ :title => "Заголовок", :content => "Содержание", :locale => "ru"},
121
+ {:title => "Resent News", :content => "That is where the text goes", :locale => "en"}]
122
+
123
+ assert news.persisted?
124
+
125
+ t_news = TranslatableNews.first
126
+ assert_equal news.id, t_news.origin_id.to_i
127
+ assert_equal "Заголовок", t_news.title
128
+ assert_equal "Содержание", t_news.content
129
+ assert_equal "ru", t_news.locale
130
+
131
+ t_news = TranslatableNews.last
132
+ assert_equal news.id, t_news.origin_id.to_i
133
+ assert_equal "Resent News", t_news.title
134
+ assert_equal "That is where the text goes", t_news.content
135
+ assert_equal "en", t_news.locale
136
+ end
137
+ end
138
+
139
+ context "Current translation" do
140
+ should "Set default translation" do
141
+ news = News.create :translations_attributes => [{:title => "Заголовок", :content => "Содержание", :locale => "ru"},
142
+ {:title => "Resent News", :content => "That is where the text goes", :locale => "en"}]
143
+
144
+ assert news.persisted?
145
+
146
+ assert_equal "Resent News", news.title
147
+ assert_equal "That is where the text goes", news.content
148
+ end
149
+
150
+ should "Been set equal to current locale" do
151
+ news = News.create :translations_attributes => [{:title => "Заголовок", :content => "Содержание", :locale => "ru"},
152
+ {:title => "Resent News", :content => "That is where the text goes", :locale => "en"}]
153
+
154
+ assert news.persisted?
155
+
156
+ ::I18n.locale = :ru
157
+ assert_equal "Заголовок", news.title
158
+ assert_equal "Содержание", news.content
159
+ ::I18n.locale = ::I18n.default_locale
160
+ end
161
+
162
+ should "Not been set if unavailable" do
163
+ news = News.create :translations_attributes => [{:title => "Заголовок", :content => "Содержание", :locale => "ru"},
164
+ {:title => "Resent News", :content => "That is where the text goes", :locale => "en"}]
165
+
166
+ assert news.persisted?
167
+
168
+ ::I18n.locale = :de
169
+ assert_nil news.title
170
+ assert_nil news.content
171
+ end
172
+
173
+ should "Be be switched on locale switching" do
174
+ news = News.create :translations_attributes => [{:title => "Resent News", :content => "That is where the text goes", :locale => "en"}]
175
+
176
+ assert news.persisted?
177
+
178
+ t_news = news.translations.create :title => "Заголовок", :content => "Содержание",:locale => "ru"
179
+ assert t_news.persisted?
180
+
181
+ ::I18n.locale = ::I18n.default_locale
182
+
183
+ assert_equal "Resent News", news.title
184
+ assert_equal "That is where the text goes", news.content
185
+
186
+ ::I18n.locale = :ru
187
+
188
+ assert_equal "Заголовок", news.title
189
+ assert_equal "Содержание", news.content
190
+ end
191
+ end
192
+
193
+ should "Add translation to existing record" do
194
+ news = News.create :translations_attributes => [{:title => "Resent News", :content => "That is where the text goes", :locale => "en"}]
195
+
196
+ assert news.persisted?
197
+
198
+ t_news = news.translations.create :title => "Заголовок", :content => "Содержание",:locale => "ru"
199
+
200
+ assert t_news.persisted?
201
+ assert t_news.persisted?
202
+ end
203
+
204
+ should "Define validations" do
205
+ post = Post.create :translations_attributes => [{:title => "Заголовок",:content => "Содержание", :locale => "ru"},
206
+ {:title => "Resent Post", :content => "That is where the text goes", :locale => "en"}]
207
+ assert post.persisted?, "Message had errors: #{post.errors.inspect}"
208
+
209
+ post = Post.create :translations_attributes => [{:content => "Содержание", :locale => "ru"},
210
+ {:title => "Resent Post 2", :content => "That is where the text goes", :locale => "en"}]
211
+
212
+ assert post.new_record?, "Message had errors: #{post.errors.full_messages.inspect}"
213
+ assert_equal ["Translations title can't be blank"], post.errors.full_messages
214
+
215
+ post = Post.create :translations_attributes => [{:title => "Заголовок 2", :locale => "ru"},
216
+ {:title => "Resent Post 3", :content => "That is where the text goes", :locale => "en"}]
217
+
218
+ assert post.new_record?, "Message had errors: #{post.errors.full_messages.inspect}"
219
+ assert_equal ["Translations content can't be blank"], post.errors.full_messages
220
+
221
+ post = Post.create :translations_attributes => [{:title => "Заголовок", :content => "Содержание", :locale => "ru"},
222
+ {:title => "Resent Post 3", :content => "That is where the text goes", :locale => "en"}]
223
+
224
+ assert post.new_record?, "Message had errors: #{post.errors.full_messages.inspect}"
225
+ assert_equal ["Translations title has already been taken"], post.errors.full_messages
226
+ end
227
+
228
+ def test_origin_is_owerwrittent
229
+ post = Post.create :translations_attributes => [{:title => "Заголовок",:content => "Содержание", :locale => "ru"},
230
+ {:title => "Resent Post", :content => "That is where the text goes", :locale => "en"}]
231
+ assert post.persisted?, "Message had errors: #{post.errors.inspect}"
232
+
233
+ assert_not_equal post.object_id, post.translations.first.post.object_id
234
+ end
235
+
236
+ should "Accept aliases for fileds" do
237
+ post = Post.create :translations_attributes => [{:title => "Заголовок",:content => "Содержание", :locale => "ru"},
238
+ {:title => "Resent Post", :content => "That is where the text goes", :locale => "en"}]
239
+ assert post.persisted?, "Message had errors: #{post.errors.inspect}"
240
+
241
+ assert_equal "Resent Post", post.translated_title
242
+
243
+ ::I18n.locale = :ru
244
+ assert_equal "Заголовок", post.translated_title
245
+ ::I18n.locale = ::I18n.default_locale
246
+ end
247
+
248
+ context "Mass assigment" do
249
+ should "Be available to mass assigment by default" do
250
+ tp = TranslatableNews.new( :title => "Resent News", :content => "That is where the text goes", :locale => "en", :origin_id => 1)
251
+
252
+ assert_equal "Resent News", tp.title
253
+ assert_equal "That is where the text goes", tp.content
254
+ assert_equal "en", tp.locale
255
+ assert_equal 1, tp.origin_id
256
+ end
257
+
258
+ should "Protect internal fields on desire" do
259
+ tm = TranslatedMessage.new( :title => "Resent Post", :content => "That is where the text goes", :locale => "en", :message_id => 1)
260
+
261
+ assert_equal "Resent Post", tm.title
262
+ assert_equal "That is where the text goes", tm.content
263
+ assert_equal nil, tm.locale
264
+ assert_equal nil, tm.message_id
265
+ end
266
+
267
+ should "Allow multiple assigment rules" do
268
+ tm = TranslatedMessage.new( {:title => "Resent Post", :content => "That is where the text goes", :locale => "en", :message_id => 1}, :as => :editor)
269
+
270
+ assert_equal "Resent Post", tm.title
271
+ assert_equal "That is where the text goes", tm.content
272
+ assert_equal "en", tm.locale
273
+ assert_equal nil, tm.message_id
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,97 @@
1
+ require "test_helper"
2
+ require "rails"
3
+ require 'rails/generators'
4
+ require "generators/translatable/model_generator"
5
+
6
+ class ModelGeneratorTest < Rails::Generators::TestCase
7
+ tests Translatable::Generators::ModelGenerator
8
+ destination File.expand_path("../../../tmp", __FILE__)
9
+ setup :prepare_destination
10
+ teardown :cleanup_destination_root
11
+
12
+ should "Create required files with default options" do
13
+ run_generator %w(article title:string content:string)
14
+ assert_file "app/models/article.rb", <<CONTENT
15
+ class Article < ActiveRecord::Base
16
+ translatable do
17
+ translatable :title, :presence => true#, :uniqueness => true
18
+ translatable :content, :presence => true#, :uniqueness => true
19
+ #translatable_model 'TranslatedArticle'
20
+ #translatable_origin :article
21
+ #translatable_locale :locale
22
+ end
23
+ end
24
+ CONTENT
25
+ assert_migration "db/migrate/create_articles.rb", <<CONTENT
26
+ class CreateArticles < ActiveRecord::Migration
27
+ def change
28
+ create_table :articles do |t|
29
+
30
+ t.timestamps
31
+ end
32
+ end
33
+ end
34
+ CONTENT
35
+ end
36
+
37
+ should "Create required files with special options" do
38
+ run_generator %w(article title:string content:string --translated_model=ArticleTranslation --origin=post --locale=language)
39
+ assert_file "app/models/article.rb", <<CONTENT
40
+ class Article < ActiveRecord::Base
41
+ translatable do
42
+ translatable :title, :presence => true#, :uniqueness => true
43
+ translatable :content, :presence => true#, :uniqueness => true
44
+ translatable_model 'ArticleTranslation'
45
+ translatable_origin :post
46
+ translatable_locale :language
47
+ end
48
+ end
49
+ CONTENT
50
+ assert_migration "db/migrate/create_articles.rb", <<CONTENT
51
+ class CreateArticles < ActiveRecord::Migration
52
+ def change
53
+ create_table :articles do |t|
54
+
55
+ t.timestamps
56
+ end
57
+ end
58
+ end
59
+ CONTENT
60
+ end
61
+
62
+ should "Inject into existing class" do
63
+ create_model_file
64
+ run_generator %w(article title:string content:string)
65
+
66
+ assert_file "app/models/article.rb", <<CONTENT
67
+ class Article < ActiveRecord::Base
68
+ translatable do
69
+ translatable :title, :presence => true#, :uniqueness => true
70
+ translatable :content, :presence => true#, :uniqueness => true
71
+ #translatable_model 'TranslatedArticle'
72
+ #translatable_origin :article
73
+ #translatable_locale :locale
74
+ end
75
+ attr_accessor :created_at, :updated_at
76
+ end
77
+ CONTENT
78
+ assert_no_migration "db/migrate/create_articles.rb"
79
+ end
80
+
81
+ protected
82
+
83
+ def create_model_file
84
+ FileUtils.mkdir_p File.join(destination_root, "app", "models")
85
+ f = File.open(File.join(destination_root, "app", "models", "article.rb"), "w+")
86
+ f.write <<CONTENT
87
+ class Article < ActiveRecord::Base
88
+ attr_accessor :created_at, :updated_at
89
+ end
90
+ CONTENT
91
+ f.close
92
+ end
93
+
94
+ def cleanup_destination_root
95
+ FileUtils.rm_rf destination_root
96
+ end
97
+ end