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