translatable 0.2.4 → 0.3.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.
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