translate_columns 1.1.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/.gitignore +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +36 -0
- data/LICENSE +16 -0
- data/README.rdoc +262 -0
- data/Rakefile +23 -0
- data/VERSION +1 -0
- data/init.rb +3 -0
- data/install.rb +1 -0
- data/lib/translate_columns.rb +228 -0
- data/tasks/translate_columns_tasks.rake +4 -0
- data/test/database.yml +4 -0
- data/test/fixtures/document.rb +11 -0
- data/test/fixtures/document_translation.rb +3 -0
- data/test/fixtures/document_translations.yml +14 -0
- data/test/fixtures/documents.yml +13 -0
- data/test/fixtures/schema.rb +18 -0
- data/test/lib/activerecord_connector.rb +6 -0
- data/test/lib/activerecord_test_helper.rb +10 -0
- data/test/translate_columns_test.rb +153 -0
- data/translate_columns.gemspec +28 -0
- metadata +131 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.swp
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
translate_columns (1.1.0)
|
5
|
+
activerecord (~> 3.0.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activemodel (3.0.3)
|
11
|
+
activesupport (= 3.0.3)
|
12
|
+
builder (~> 2.1.2)
|
13
|
+
i18n (~> 0.4)
|
14
|
+
activerecord (3.0.3)
|
15
|
+
activemodel (= 3.0.3)
|
16
|
+
activesupport (= 3.0.3)
|
17
|
+
arel (~> 2.0.2)
|
18
|
+
tzinfo (~> 0.3.23)
|
19
|
+
activesupport (3.0.3)
|
20
|
+
arel (2.0.7)
|
21
|
+
builder (2.1.2)
|
22
|
+
i18n (0.5.0)
|
23
|
+
mocha (0.9.10)
|
24
|
+
rake
|
25
|
+
rake (0.8.7)
|
26
|
+
sqlite3 (1.3.3)
|
27
|
+
tzinfo (0.3.24)
|
28
|
+
|
29
|
+
PLATFORMS
|
30
|
+
ruby
|
31
|
+
|
32
|
+
DEPENDENCIES
|
33
|
+
activerecord (~> 3.0.0)
|
34
|
+
mocha (~> 0.9.3)
|
35
|
+
sqlite3 (~> 1.3.3)
|
36
|
+
translate_columns!
|
data/LICENSE
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
Copyright (c) 2007 Samuel Lown
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
4
|
+
and associated documentation files (the “Software”), to deal in the Software without restriction,
|
5
|
+
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
6
|
+
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
|
7
|
+
is furnished to do so, subject to the following conditions:
|
8
|
+
|
9
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
10
|
+
substantial portions of the Software.
|
11
|
+
|
12
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
13
|
+
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
14
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
15
|
+
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
16
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,262 @@
|
|
1
|
+
= Translate Columns Plugin
|
2
|
+
|
3
|
+
Copyright (c) 2007-2011 Samuel Lown <me (AT) samlown.com>
|
4
|
+
|
5
|
+
This Plugin is released under the MIT license, as Rails itself. Please see the
|
6
|
+
attached LICENSE file for further details.
|
7
|
+
|
8
|
+
This document and plugin should be considered a work in progress until further
|
9
|
+
notice!
|
10
|
+
|
11
|
+
== Introduction
|
12
|
+
|
13
|
+
The aim of the Translate Columns plugin is to aid the normally difficult task
|
14
|
+
of supporting multiple languages in the models. It provides a near transparent
|
15
|
+
interface to the data contained in the models and their translations such that
|
16
|
+
your current controllers, views and models only need to be modified slightly
|
17
|
+
to support multiple languages in a scalable fashion.
|
18
|
+
|
19
|
+
If you already have your rails app set up and functioning, using translate
|
20
|
+
columns will not require any major refactoring of your code (unless you're
|
21
|
+
really unlucky), and can be simply added. Indeed, the plugin was written to be
|
22
|
+
added to an existing application.
|
23
|
+
|
24
|
+
== Updates
|
25
|
+
|
26
|
+
=== v1.1 - 21st January 2011
|
27
|
+
|
28
|
+
- Now only supports ActiveRecord 3
|
29
|
+
- Converted to gem
|
30
|
+
- +include TranslateColumns+ now required on a per model basis
|
31
|
+
|
32
|
+
=== 24th September 2009
|
33
|
+
|
34
|
+
- Added support for setting the locale variable on the parent, which disables translations. See below.
|
35
|
+
|
36
|
+
=== 23rd September 2009
|
37
|
+
|
38
|
+
- Testing finally added (hope to add more tests soon)
|
39
|
+
- Validations now only performed by the parent model
|
40
|
+
- Changed namespace to TranslateColumns (as opposed to Translate::Columns)
|
41
|
+
- Parent model's +locale+ variable changed to +translation_locale+ so that it can be added as a column/attribute if needed.
|
42
|
+
|
43
|
+
=== 20th May 2009
|
44
|
+
|
45
|
+
- Finally got round to moving to github
|
46
|
+
- Added support for the rails 2.2 I18n stuff (WIN!)
|
47
|
+
|
48
|
+
*WARNING* Translate Columns will now only work with Rails 3 as a gem. For older
|
49
|
+
2.3 projects, use as a plugin with the v_1.0 release tag.
|
50
|
+
|
51
|
+
== Architecture
|
52
|
+
|
53
|
+
Translate columns while simple, does require a specific architecture. The basic
|
54
|
+
idea is that each of your models has an associated model that defines the
|
55
|
+
translations. An ASCII ERM that uses an example primary class called document
|
56
|
+
follows:
|
57
|
+
|
58
|
+
____________ _______________________
|
59
|
+
| | 1 * | |
|
60
|
+
| Document |---------------| DocumentTranslation |
|
61
|
+
|____________| |_______________________|
|
62
|
+
|
63
|
+
|
64
|
+
The data contained by these entities may be similar to the following:
|
65
|
+
|
66
|
+
Document:
|
67
|
+
|
68
|
+
| Column | Type |
|
69
|
+
-------------------------
|
70
|
+
| id | integer |
|
71
|
+
| name | string |
|
72
|
+
| title | string |
|
73
|
+
| sub_title | string |
|
74
|
+
| body | text |
|
75
|
+
| created_on | datetime |
|
76
|
+
| updated_on | datetime |
|
77
|
+
|
78
|
+
DocumentTranslation:
|
79
|
+
|
80
|
+
| Column | Type |
|
81
|
+
--------------------------
|
82
|
+
| id | integer |
|
83
|
+
| document_id | integer |
|
84
|
+
| locale | string |
|
85
|
+
| title | string |
|
86
|
+
| sub_title | string |
|
87
|
+
| body | text |
|
88
|
+
|
89
|
+
In Rails, thsee models would be defined as follows:
|
90
|
+
|
91
|
+
class Document < ActiveRecord::Base
|
92
|
+
has_many :translations, :class_name => 'DocumentTranslation'
|
93
|
+
end
|
94
|
+
|
95
|
+
class DocumentTranslation < ActiveRecord::Base
|
96
|
+
belongs_to :document
|
97
|
+
end
|
98
|
+
|
99
|
+
Each DocumentTranslation belongs to a Document and defines the locale of the
|
100
|
+
translation and only those fields that require a translation. If you really
|
101
|
+
wanted to, a composite key could be used on the document_id and the locale,
|
102
|
+
as these should always uniquely identify the translation.
|
103
|
+
|
104
|
+
In previous versions of translate_columns a Locale model and associations was
|
105
|
+
used to determine the language of a translation, this is no longer required
|
106
|
+
with the new Rails 2.2 I18n code and simple string for the locale code
|
107
|
+
of your choice can be used instead.
|
108
|
+
|
109
|
+
IMPORTANT: Default locale. In order for this setup to work, there must be a
|
110
|
+
single, pre-defined locale for the default data, this is the data contained
|
111
|
+
in the 'Document' entity and will be used whenever we're operating in default
|
112
|
+
mode, or if there is no translation available. It is essential that this
|
113
|
+
default locale *never* change during the lifetime of your application,
|
114
|
+
otherwise you'll end up with a mess.
|
115
|
+
|
116
|
+
The Document's translations association uses the :class_name option to name the
|
117
|
+
correct class. Aside from saving on typing, this is an essential requirement
|
118
|
+
of the translate_columns plugin. (At least until I get chance to add an option
|
119
|
+
to allow for different names.)
|
120
|
+
|
121
|
+
== Installation
|
122
|
+
|
123
|
+
Assuming you've read the above and understand the basic requirements, the
|
124
|
+
plugin can now be installed and setup.
|
125
|
+
|
126
|
+
The latest details and updates are available on the github repository:
|
127
|
+
|
128
|
+
http://github.com/samlown/translate_columns
|
129
|
+
|
130
|
+
To install plugin, use the standard rails plugin install method:
|
131
|
+
|
132
|
+
./script/plugin install git://github.com/samlown/translate_columns.git
|
133
|
+
|
134
|
+
There are no more installation steps, and the plugin does not install any extra
|
135
|
+
files or customise the setup. To uninstall, simply remove the directory.
|
136
|
+
|
137
|
+
== Setup
|
138
|
+
|
139
|
+
Now for the hard part :-) Re-using the example above for documents, to use the
|
140
|
+
plugin modify the model so that it looks like the following:
|
141
|
+
|
142
|
+
class Document < ActiveRecord::Base
|
143
|
+
include TranslateColumns
|
144
|
+
has_many :translations, :class_name => 'DocumentTranslation'
|
145
|
+
translate_columns :title, :sub_title, :body
|
146
|
+
end
|
147
|
+
|
148
|
+
I'm working on getting it so that you don't need to specify the columns
|
149
|
+
manually, but it is not yet ready.
|
150
|
+
|
151
|
+
In earlier versions you'd need to mess around with a Locale class but thanks to
|
152
|
+
the Rails I18n extension, this is no longer necessary.
|
153
|
+
|
154
|
+
== Upgrading
|
155
|
+
|
156
|
+
If you're using a realy old version of Translate Columns, then you'll need to perform an
|
157
|
+
upgrade and migration to use the fabulous new I18n Rails code. Fortunately, this is
|
158
|
+
very easy to do.
|
159
|
+
|
160
|
+
To upgrade, remove and previous entries to your Locale class in you translation models
|
161
|
+
and generate a migration to convert the local_id column into a string. Something like
|
162
|
+
the following will surfice.
|
163
|
+
|
164
|
+
class UpgradeTranslationModels < ActiveRecord::Migration
|
165
|
+
def self.up
|
166
|
+
alter_column :product_translations, :locale_id, :string, :length => 10
|
167
|
+
rename_column :product_translations, :locale_id, :locale
|
168
|
+
end
|
169
|
+
|
170
|
+
def self.down
|
171
|
+
rename_column :product_translations, :locale, :locale_id
|
172
|
+
alter_column :product_translations, :locale_id, :integer
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
After ensuring you're using the I18n.locale calls throughout your application, it
|
177
|
+
should work fine.
|
178
|
+
|
179
|
+
== Usage
|
180
|
+
|
181
|
+
The idea here is that you forget about the fact your models can be translated
|
182
|
+
and just use the app as normal. Indeed, if you don't set a locale, you
|
183
|
+
won't even notice the plugin is there.
|
184
|
+
|
185
|
+
Here's a really basic example of what we can do on the console.
|
186
|
+
|
187
|
+
>> I18n.locale = I18n.default_locale # First try default language
|
188
|
+
=> :en
|
189
|
+
>> doc = Document.find(:first)
|
190
|
+
-- output hidden --
|
191
|
+
>> doc.title
|
192
|
+
=> "Sample Document" # title in english
|
193
|
+
>> I18n.locale = 'es' # set to other language
|
194
|
+
=> "es"
|
195
|
+
>> doc = Document.find(:first) # Reload to avoid caching problems!
|
196
|
+
-- output hidden --
|
197
|
+
>> doc.title
|
198
|
+
=> "Titulo español" # Title now in spanish
|
199
|
+
>> doc.title_default
|
200
|
+
=> "Sample Document" # original field data
|
201
|
+
>> doc.title = "Nuevo Título Español"
|
202
|
+
=> "Nuevo Título Español"
|
203
|
+
>> doc.save # set the title and save
|
204
|
+
=> true
|
205
|
+
>> I18n.locale = 'en'
|
206
|
+
=> "en" # return to english
|
207
|
+
>> doc = Document.find(:first)
|
208
|
+
-- output hidden --
|
209
|
+
>> doc.title
|
210
|
+
=> "Sample Document"
|
211
|
+
|
212
|
+
As can be seen, just by setting the locale we are able to edit the data
|
213
|
+
without having to worry about the details.
|
214
|
+
|
215
|
+
The current version also has support for disabling translations by giving the
|
216
|
+
parent object a +locale+ field and setting it to something. This is actually a
|
217
|
+
very powerful feature as it allows new objects to be created under a specific locale
|
218
|
+
and filtered as such. A typical example would be a blog where most of the posts
|
219
|
+
you'd like to be translated into several languages, but occaisionly some posts will only
|
220
|
+
be relevant for a specific region:
|
221
|
+
|
222
|
+
>> I18n.locale = I18n.default_locale
|
223
|
+
>> post = Post.new(:title => "Example") # WIN
|
224
|
+
>>
|
225
|
+
>> I18n.locale = 'es'
|
226
|
+
>> post = Post.new(:title => "Ejemplo") # FAIL
|
227
|
+
TranslateColumns::MissingParent: Cannot create translations without a stored parent
|
228
|
+
>>
|
229
|
+
>> post = Post.new(:locale => 'es', :title => "Ejemplo") # WIN
|
230
|
+
>>
|
231
|
+
>> # Provide posts, with either a translation of for the current locale
|
232
|
+
>> posts = Post.paginate(:conditions => ['posts.locale IS NULL OR posts.locale = ?', I18n.locale.to_s])
|
233
|
+
|
234
|
+
A useful +named_scope+ could be as follows:
|
235
|
+
|
236
|
+
class Post < ActiveRecord::Base
|
237
|
+
# ... translate columns stuff ...
|
238
|
+
named_scope :for_current_locale, :conditions => ['posts.locale IS NULL OR posts.locale = ?', I18n.locale.to_s]
|
239
|
+
end
|
240
|
+
|
241
|
+
@posts = Post.for_current_locale.paginate
|
242
|
+
|
243
|
+
Changing locale of an object after it has been created will cause its translations to be ignored, but
|
244
|
+
by emptying the locale value the translations should work as before.
|
245
|
+
Of course, if you don't want this funcionality simply do not add a locale attribute or method to
|
246
|
+
the parent model.
|
247
|
+
|
248
|
+
|
249
|
+
== How it works
|
250
|
+
|
251
|
+
The plugin overrides the default attribute accessor functions and automatically
|
252
|
+
uses the 'translations' association to find the request fields. It also
|
253
|
+
provides a new method that extends the original method name to access
|
254
|
+
the original values.
|
255
|
+
|
256
|
+
== Todos / Bugs
|
257
|
+
|
258
|
+
* Caching - Using a basic rails setup, everything should work fine, however
|
259
|
+
if you have a more complex caching setup strange things might happen.
|
260
|
+
Please mail me if you have any problems!
|
261
|
+
|
262
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc 'Test the translate_columns plugin.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.libs << 'test/lib'
|
12
|
+
t.pattern = 'test/*_test.rb'
|
13
|
+
t.verbose = true
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Generate documentation for the translate_columns plugin.'
|
17
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
18
|
+
rdoc.rdoc_dir = 'rdoc'
|
19
|
+
rdoc.title = 'TranslateColumns'
|
20
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
21
|
+
rdoc.rdoc_files.include('README')
|
22
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
23
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.1.0
|
data/init.rb
ADDED
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,228 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# TranslateColumns
|
4
|
+
#
|
5
|
+
# Copyright (c)2007-2011 Samuel Lown <me@samlown.com>
|
6
|
+
#
|
7
|
+
module TranslateColumns
|
8
|
+
|
9
|
+
class MissingParent < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.included(base)
|
13
|
+
base.extend(ClassMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
# methods used in the class definition
|
17
|
+
module ClassMethods
|
18
|
+
|
19
|
+
# Read the provided list of symbols as column names and
|
20
|
+
# generate methods for each to access translated versions.
|
21
|
+
#
|
22
|
+
# Possible options, after the columns, include:
|
23
|
+
#
|
24
|
+
# * :locale_field - Name of the field in the parents translation table
|
25
|
+
# of the locale. This defaults to 'locale'.
|
26
|
+
#
|
27
|
+
def translate_columns( *options )
|
28
|
+
|
29
|
+
locale_field = 'locale'
|
30
|
+
|
31
|
+
columns = [ ]
|
32
|
+
if ! options.is_a? Array
|
33
|
+
raise "Provided parameter to translate_columns is not an array!"
|
34
|
+
end
|
35
|
+
# extract all the options
|
36
|
+
options.each do | opt |
|
37
|
+
if opt.is_a? Symbol
|
38
|
+
columns << opt
|
39
|
+
elsif opt.is_a? Hash
|
40
|
+
# Override the locale class if set.
|
41
|
+
locale_field = opt[:locale_field]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
define_method 'columns_to_translate' do
|
46
|
+
columns.collect{ |c| c.to_s }
|
47
|
+
end
|
48
|
+
|
49
|
+
# set the instance Methods first
|
50
|
+
include TranslateColumns::InstanceMethods
|
51
|
+
|
52
|
+
# Rails magic to override the normal save process
|
53
|
+
alias_method_chain :save, :translation
|
54
|
+
alias_method_chain :save!, :translation
|
55
|
+
alias_method_chain :attributes=, :locale
|
56
|
+
|
57
|
+
# Generate a module containing methods that override access
|
58
|
+
# to the ActiveRecord methods.
|
59
|
+
# This dynamic module is then included in the parent such that
|
60
|
+
# the super method will function correctly.
|
61
|
+
mod = Module.new do | m |
|
62
|
+
|
63
|
+
columns.each do | column |
|
64
|
+
|
65
|
+
next if ['id', locale_field].include?(column.to_s)
|
66
|
+
|
67
|
+
# This is strange, so allow me to explain:
|
68
|
+
# We define access to the original method and its super,
|
69
|
+
# a normal "alias" can't find the super which is the method
|
70
|
+
# created by ActionBase.
|
71
|
+
# The Alias_method function takes a copy, and retains the
|
72
|
+
# ability to call the parent with the same name.
|
73
|
+
# Finally, the method is overwritten to support translation.
|
74
|
+
#
|
75
|
+
# All this is to avoid defining parameters for the overwritten
|
76
|
+
# accessor which normally doesn't have them.
|
77
|
+
# (Warnings are produced on execution when a metaprogrammed
|
78
|
+
# function is called without parameters and its expecting them)
|
79
|
+
#
|
80
|
+
# Sam Lown (2007-01-17) dev at samlown dot com
|
81
|
+
define_method(column) do
|
82
|
+
# This super should call the missing_method method in ActiveRecord.
|
83
|
+
super()
|
84
|
+
end
|
85
|
+
|
86
|
+
alias_method("#{column}_before_translation", column)
|
87
|
+
|
88
|
+
# overwrite accessor to read
|
89
|
+
define_method("#{column}") do
|
90
|
+
if translation and ! translation.send(column).blank?
|
91
|
+
translation.send(column)
|
92
|
+
else
|
93
|
+
super()
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
define_method("#{column}_before_type_cast") do
|
98
|
+
if (translation)
|
99
|
+
translation.send("#{column}_before_type_cast")
|
100
|
+
else
|
101
|
+
super
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
define_method("#{column}=") do |value|
|
106
|
+
# translation object must have already been set up for this to work!
|
107
|
+
if (translation)
|
108
|
+
translation.send("#{column}=",value)
|
109
|
+
else
|
110
|
+
super( value )
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end # dynamic module
|
116
|
+
|
117
|
+
# include the anonymous module so that the "super" method
|
118
|
+
# will work correctly in the child!
|
119
|
+
include mod
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
# Methods that are specific to the current class
|
125
|
+
# and only called when translate_columns is used
|
126
|
+
module InstanceMethods
|
127
|
+
|
128
|
+
# Provide the locale which is currently in use with the object or the current global locale.
|
129
|
+
# If the default is in use, always return nil.
|
130
|
+
def translation_locale
|
131
|
+
locale = @translation_locale || I18n.locale.to_s
|
132
|
+
locale == I18n.default_locale.to_s ? nil : locale
|
133
|
+
end
|
134
|
+
|
135
|
+
# Setting the locale will always enable translation.
|
136
|
+
# If set to nil the global locale is used.
|
137
|
+
def translation_locale=(locale)
|
138
|
+
enable_translation
|
139
|
+
# TODO some checks for available translations would be nice.
|
140
|
+
# I18n.available_locales only available as standard with rails 2.3
|
141
|
+
@translation_locale = locale.to_s.empty? ? nil : locale.to_s
|
142
|
+
end
|
143
|
+
|
144
|
+
# Do not allow translations!
|
145
|
+
def disable_translation
|
146
|
+
@disable_translation = true
|
147
|
+
end
|
148
|
+
def enable_translation
|
149
|
+
@disable_translation = false
|
150
|
+
end
|
151
|
+
|
152
|
+
# Important check to see if the parent has a locale method.
|
153
|
+
# If so, translations should be disabled if it is set to something!
|
154
|
+
def has_locale_value?
|
155
|
+
respond_to?(:locale) && !self.locale.to_s.empty?
|
156
|
+
end
|
157
|
+
|
158
|
+
# determine if the conditions are set for a translation to be used
|
159
|
+
def translation_enabled?
|
160
|
+
(!@disable_translation && translation_locale) and !has_locale_value?
|
161
|
+
end
|
162
|
+
|
163
|
+
# Provide a translation object based on the parent and the translation_locale
|
164
|
+
# current value.
|
165
|
+
def translation
|
166
|
+
if translation_enabled?
|
167
|
+
if !@translation || (@translation.locale != translation_locale)
|
168
|
+
raise MissingParent, "Cannot create translations without a stored parent" if new_record?
|
169
|
+
# try to find translation or build a new one
|
170
|
+
@translation = translations.find_by_locale(translation_locale) || translations.build(:locale => translation_locale)
|
171
|
+
end
|
172
|
+
@translation
|
173
|
+
else
|
174
|
+
nil
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# As this is included in a mixin, a "super" call from inside the
|
179
|
+
# child (inheriting) class will infact look here before looking to
|
180
|
+
# ActiveRecord for the real 'save'. This method should therefore
|
181
|
+
# be safely overridden if needed.
|
182
|
+
#
|
183
|
+
# Assumes validation enabled in ActiveRecord and performs validation
|
184
|
+
# before saving. This means the base records validation checks will always
|
185
|
+
# be used.
|
186
|
+
#
|
187
|
+
def save_with_translation(*args)
|
188
|
+
perform_validation = args.is_a?(Hash) ? args[:validate] : args
|
189
|
+
if perform_validation && valid? || !perform_validation
|
190
|
+
translation.save(*args) if (translation)
|
191
|
+
disable_translation
|
192
|
+
save_without_translation(*args)
|
193
|
+
enable_translation
|
194
|
+
true
|
195
|
+
else
|
196
|
+
false
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def save_with_translation!
|
201
|
+
if valid?
|
202
|
+
translation.save! if (translation)
|
203
|
+
disable_translation
|
204
|
+
save_without_translation!
|
205
|
+
enable_translation
|
206
|
+
else
|
207
|
+
raise ActiveRecord::RecordInvalid.new(self)
|
208
|
+
end
|
209
|
+
rescue
|
210
|
+
enable_translation
|
211
|
+
raise
|
212
|
+
end
|
213
|
+
|
214
|
+
# Override the default mass assignment method so that the locale variable is always
|
215
|
+
# given preference.
|
216
|
+
def attributes_with_locale=(new_attributes, guard_protected_attributes = true)
|
217
|
+
return if new_attributes.nil?
|
218
|
+
attributes = new_attributes.dup
|
219
|
+
attributes.stringify_keys!
|
220
|
+
|
221
|
+
attributes = sanitize_for_mass_assignment(attributes) if guard_protected_attributes
|
222
|
+
send(:locale=, attributes["locale"]) if attributes.has_key?("locale") and respond_to?(:locale=)
|
223
|
+
|
224
|
+
send(:attributes_without_locale=, attributes, guard_protected_attributes)
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
end
|
data/test/database.yml
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
class Document < ActiveRecord::Base
|
2
|
+
include TranslateColumns
|
3
|
+
|
4
|
+
has_many :translations, :class_name => 'DocumentTranslation'
|
5
|
+
translate_columns :title, :body
|
6
|
+
|
7
|
+
validates_presence_of :title
|
8
|
+
validates_length_of :title, :within => 3..200
|
9
|
+
|
10
|
+
validates_length_of :body, :within => 3..500
|
11
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
document1:
|
2
|
+
id: 1
|
3
|
+
title: Test Document Number 1
|
4
|
+
body: This is a test document with some kind of body.
|
5
|
+
published_at: "2009-09-23 21:53:04"
|
6
|
+
|
7
|
+
document2:
|
8
|
+
id: 2
|
9
|
+
title: Test Document Number 2
|
10
|
+
locale: en
|
11
|
+
body: This is a second test document with some random content for the body.
|
12
|
+
published_at: "2009-09-23 21:53:46"
|
13
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
create_table "documents", :force => true do |t|
|
3
|
+
t.column "locale", :string, :length => 8
|
4
|
+
t.column "title", :string
|
5
|
+
t.column "body", :text
|
6
|
+
t.column "published_at", :datetime
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
create_table "document_translations", :force => true do |t|
|
11
|
+
t.references "document"
|
12
|
+
t.string :locale
|
13
|
+
t.column "title", :string
|
14
|
+
t.column "body", :text
|
15
|
+
t.timestamps
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,10 @@
|
|
1
|
+
|
2
|
+
require 'activerecord_connector'
|
3
|
+
require File.join(File.dirname(__FILE__), '../fixtures/schema.rb')
|
4
|
+
|
5
|
+
module ActiverecordTestHelper
|
6
|
+
FIXTURES_PATH = File.join(File.dirname(__FILE__), '/../fixtures')
|
7
|
+
dep = defined?(ActiveSupport::Dependencies) ? ActiveSupport::Dependencies : ::Dependencies
|
8
|
+
dep.autoload_paths.unshift FIXTURES_PATH
|
9
|
+
end
|
10
|
+
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'test/unit'
|
5
|
+
require 'mocha'
|
6
|
+
|
7
|
+
require 'activerecord_test_helper'
|
8
|
+
require 'translate_columns'
|
9
|
+
|
10
|
+
class TranslateColumnsTest < Test::Unit::TestCase
|
11
|
+
|
12
|
+
include ActiverecordTestHelper
|
13
|
+
|
14
|
+
def setup
|
15
|
+
@docs = Fixtures.create_fixtures(FIXTURES_PATH, ['documents', 'document_translations'])
|
16
|
+
end
|
17
|
+
|
18
|
+
def teardown
|
19
|
+
Fixtures.reset_cache
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_basic_document_fields
|
23
|
+
doc = Document.find(:first)
|
24
|
+
assert_equal "Test Document Number 1", doc.title, "Document not found!"
|
25
|
+
assert_not_nil doc.body, "Empty document body"
|
26
|
+
assert_not_nil doc.published_at, "Missing published date"
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_basic_document_fields_for_default_locale
|
30
|
+
I18n.locale = "en"
|
31
|
+
doc = Document.find(:first)
|
32
|
+
assert_equal "Test Document Number 1", doc.title, "Document not found!"
|
33
|
+
assert_not_nil doc.body, "Empty document body"
|
34
|
+
assert_not_nil doc.published_at, "Missing published date"
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_count_translations
|
38
|
+
doc = Document.find(:first)
|
39
|
+
assert_equal 2, doc.translations.count, "Count doesn't match!"
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_basic_document_fields_for_spanish
|
43
|
+
I18n.locale = "es"
|
44
|
+
doc = Document.find(:first)
|
45
|
+
assert_equal "Este es el titulo de un documento en Espa\303\261ol", doc.title, "Document not found!"
|
46
|
+
assert_equal "Nada", doc.body, "Different document body"
|
47
|
+
assert_not_nil doc.published_at, "Missing published date"
|
48
|
+
assert_equal "Test Document Number 1", doc.title_before_translation
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_missing_fields_resort_to_original
|
52
|
+
I18n.locale = 'fr'
|
53
|
+
doc = Document.find(:first)
|
54
|
+
assert_equal "Un title en francais", doc.title
|
55
|
+
assert_match /body/, doc.body
|
56
|
+
assert doc.body_before_type_cast.to_s.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_switching_languages_for_reading
|
60
|
+
I18n.locale = I18n.default_locale
|
61
|
+
doc1 = Document.find(:first)
|
62
|
+
assert_equal "Test Document Number 1", doc1.title
|
63
|
+
I18n.locale = 'es'
|
64
|
+
doc2 = Document.find(:first)
|
65
|
+
assert_equal doc1.title, doc2.title
|
66
|
+
assert_not_equal "Test Document Number 1", doc1.title
|
67
|
+
I18n.locale = 'en'
|
68
|
+
assert_equal doc1.title, doc2.title
|
69
|
+
assert_equal "Test Document Number 1", doc1.title
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_setting_fields_in_default_language
|
73
|
+
time_now = Time.now
|
74
|
+
I18n.locale = I18n.default_locale
|
75
|
+
doc1 = Document.find(:first)
|
76
|
+
doc1.title = "A new title"
|
77
|
+
doc1.published_at = time_now
|
78
|
+
assert doc1.save, "Unable to save document"
|
79
|
+
assert_equal "A new title", doc1.title
|
80
|
+
# Now change language
|
81
|
+
I18n.locale = 'es'
|
82
|
+
doc1 = Document.find(:first)
|
83
|
+
assert_not_equal "A new title", doc1.title
|
84
|
+
assert_equal time_now.to_s, doc1.published_at.to_s
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_saving_changes_in_translations
|
88
|
+
time_now = Time.now
|
89
|
+
I18n.locale = 'es'
|
90
|
+
doc1 = Document.find(:first)
|
91
|
+
doc1.title = "Un nuevo título"
|
92
|
+
doc1.published_at = time_now
|
93
|
+
assert doc1.save
|
94
|
+
I18n.locale = I18n.default_locale
|
95
|
+
doc1 = Document.find(:first)
|
96
|
+
assert_not_equal "Un nuevo título", doc1.title
|
97
|
+
assert_equal time_now.to_s, doc1.published_at.to_s
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_creating_new_documents
|
101
|
+
I18n.locale = I18n.default_locale
|
102
|
+
doc = Document.new(:title => "A new document", :body => "The Body")
|
103
|
+
assert doc.save
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_creating_new_documents_under_locale_fails
|
107
|
+
I18n.locale = 'es'
|
108
|
+
assert_raise TranslateColumns::MissingParent do
|
109
|
+
Document.new(:title => "Un nuevo documento", :body => 'El cuerpo')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_failed_validations
|
114
|
+
I18n.locale = I18n.default_locale
|
115
|
+
doc = Document.find(:first)
|
116
|
+
doc.title = "a"
|
117
|
+
assert !doc.save
|
118
|
+
assert !doc.errors[:title].empty?
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_failed_validations_on_translation
|
122
|
+
I18n.locale = 'es'
|
123
|
+
doc = Document.find(:first)
|
124
|
+
doc.title = "a"
|
125
|
+
assert !doc.save
|
126
|
+
assert !doc.errors[:title].empty?
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_locale_attribute_detection
|
130
|
+
doc = Document.find(:first)
|
131
|
+
assert !doc.has_locale_value?
|
132
|
+
doc.locale = "en"
|
133
|
+
assert doc.has_locale_value?
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_locale_attribute_detection_without_attribute
|
137
|
+
doc = Document.find(:first)
|
138
|
+
doc.locale = "en"
|
139
|
+
doc.stubs(:respond_to?).with(:locale).returns(false)
|
140
|
+
assert !doc.has_locale_value?
|
141
|
+
end
|
142
|
+
|
143
|
+
def test_create_new_document_with_specific_locale
|
144
|
+
I18n.locale = 'es'
|
145
|
+
doc = nil
|
146
|
+
assert_nothing_thrown do
|
147
|
+
doc = Document.new(:locale => 'es', :title => "A new document", :body => "Test Body")
|
148
|
+
end
|
149
|
+
assert doc.locale, 'es'
|
150
|
+
assert doc.save
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{translate_columns}
|
5
|
+
s.version = File.read(File.join(File.dirname(__FILE__), 'VERSION')).strip
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new("> 1.3.5") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Sam Lown"]
|
9
|
+
s.date = %q{2011-01-21}
|
10
|
+
s.description = %q{Automatically translate ActiveRecord columns using a second model containing the translations.}
|
11
|
+
s.email = %q{me@samlown.com}
|
12
|
+
s.extra_rdoc_files = [
|
13
|
+
"LICENSE",
|
14
|
+
"README.rdoc"
|
15
|
+
]
|
16
|
+
s.homepage = %q{http://github.com/samlown/translate_columns}
|
17
|
+
s.rubygems_version = %q{1.3.7}
|
18
|
+
s.summary = %q{Use fields from other translation models easily}
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.require_paths = ["lib"]
|
23
|
+
|
24
|
+
s.add_dependency("activerecord", "~> 3.0.0")
|
25
|
+
s.add_development_dependency(%q<mocha>, "~> 0.9.3")
|
26
|
+
s.add_development_dependency(%q<sqlite3>, "~> 1.3.3")
|
27
|
+
end
|
28
|
+
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: translate_columns
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 1.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Sam Lown
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-01-21 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: activerecord
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 3
|
30
|
+
- 0
|
31
|
+
- 0
|
32
|
+
version: 3.0.0
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: mocha
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
- 9
|
46
|
+
- 3
|
47
|
+
version: 0.9.3
|
48
|
+
type: :development
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: sqlite3
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ~>
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
segments:
|
59
|
+
- 1
|
60
|
+
- 3
|
61
|
+
- 3
|
62
|
+
version: 1.3.3
|
63
|
+
type: :development
|
64
|
+
version_requirements: *id003
|
65
|
+
description: Automatically translate ActiveRecord columns using a second model containing the translations.
|
66
|
+
email: me@samlown.com
|
67
|
+
executables: []
|
68
|
+
|
69
|
+
extensions: []
|
70
|
+
|
71
|
+
extra_rdoc_files:
|
72
|
+
- LICENSE
|
73
|
+
- README.rdoc
|
74
|
+
files:
|
75
|
+
- .gitignore
|
76
|
+
- Gemfile
|
77
|
+
- Gemfile.lock
|
78
|
+
- LICENSE
|
79
|
+
- README.rdoc
|
80
|
+
- Rakefile
|
81
|
+
- VERSION
|
82
|
+
- init.rb
|
83
|
+
- install.rb
|
84
|
+
- lib/translate_columns.rb
|
85
|
+
- tasks/translate_columns_tasks.rake
|
86
|
+
- test/database.yml
|
87
|
+
- test/fixtures/document.rb
|
88
|
+
- test/fixtures/document_translation.rb
|
89
|
+
- test/fixtures/document_translations.yml
|
90
|
+
- test/fixtures/documents.yml
|
91
|
+
- test/fixtures/schema.rb
|
92
|
+
- test/lib/activerecord_connector.rb
|
93
|
+
- test/lib/activerecord_test_helper.rb
|
94
|
+
- test/translate_columns_test.rb
|
95
|
+
- translate_columns.gemspec
|
96
|
+
has_rdoc: true
|
97
|
+
homepage: http://github.com/samlown/translate_columns
|
98
|
+
licenses: []
|
99
|
+
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
segments:
|
111
|
+
- 0
|
112
|
+
version: "0"
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ">"
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
segments:
|
119
|
+
- 1
|
120
|
+
- 3
|
121
|
+
- 5
|
122
|
+
version: 1.3.5
|
123
|
+
requirements: []
|
124
|
+
|
125
|
+
rubyforge_project:
|
126
|
+
rubygems_version: 1.3.7
|
127
|
+
signing_key:
|
128
|
+
specification_version: 3
|
129
|
+
summary: Use fields from other translation models easily
|
130
|
+
test_files: []
|
131
|
+
|