Translating Columns
- September 3rd, 2008
- Posted in Ruby on Rails . Tips
- Write comment
I made this into a plugin: translatable_columns.
Dmitry asked in the comments of my last post about translating ActiveRecord:
Can you write about how to use translated columns of database in rails? For example we have table named ‘blog’, and I want to translate it on several languages: fr, en, ru. How to do that?
And although I don’t think this is the way to go, I can of course demonstrate an easy way to do this, using I18n.
Here’s the table definition:
create_table :posts do |t| t.string :title_en, :title_nl, :title_fr, :title_de t.string :text_en, :text_nl, :text_fr, :text_de end
So what you’d want to do is read the currently selected locale and choose to write to the proper attribute depending on that.
class Post < ActiveRecord::Base def title self[column('title')] end def title=(str) self[column('title')] = str end private def column(name) column_name = "#{name}_#{I18n.locale.split('-').first}" self.class.column_names.include?(column_name) ? column_name.to_sym : "#{name}_#{I18n.default_locale.split('-').first.to_sym}" end end
Now, you can treat Post as if it had a normal title attribute, but it would save to the proper column. If you don’t have a column named for this attribute, it’ll save or get the value of the default_locale.
So for instance you can do this in your edit view:
<% form_for(@post) do |f| %> <%= f.text_field :title %> <% end %>
But when you have multiple columns that needs to be translated, even scattered through multiple models, it tends to be a boring and repeating business to add all those virtual attributes. So let’s do some meta-programming, and clean up models!
First, make a file in the RAILS_ROOT/lib directory, called load_translations.rb and put in this Ruby meta-programming goodness/madness:
module TranslatableColumns def translatable_columns(*columns) columns.each do |column| define_method column do self[self.class.column_translated(column)] end define_method "#{column}=" do |value| self[self.class.column_translated(column)] = value end end end def column_translated(name) column_name = "#{name}_#{I18n.locale.split('-').first}" self.column_names.include?(column_name) ? column_name.to_sym : "#{name}_#{I18n.default_locale.split('-').first.to_sym}" end end
Now all you have to to in the model is extend it with this module and specify which columns can be translated:
class Post < ActiveRecord::Base extend TranslatableColumns translatable_columns :title, :text end
Still I’m not really fond of this. I can’t find a good, sensible scenario where this would be the best option. I would rather go with an attribute called ‘locale’. So let’s look at that too.
./script/generate migration add_locale_to_post locale:string rake db:migrate
And add a named_scope to the models you want to be localized, like Post in this case, to get the proper locales and save it to whatever locale was selected at the moment, if it hasn’t already been set any other way (you might want to make the user able to choose it when entering the post).
class Post < ActiveRecord::Base named_scope :localized, { :conditions => { :locale => I18n.locale } } } before_save :store_locale private def store_locale self[:locale] ||= I18n.locale end end
Let’s meta-program this one as well!
Get the proper posts, just call Post.localized or Post.localized.find(params[:id]). Note that I’m not using any translatable columns now. Just use normal columns and create multiple posts if you want more than one language for a post (e.g. create a Dutch one and a French one).
As you can see, I’m not using the translating functionality of I18n here. I just use I18n to know which locale to choose.
@Iain Hecker: Thank you! Interesting post.
But why rails will not support this future in it’s own core? How do you think?
@Dmitry: Rails tries to be more lean… Only features that are really really handy and often used get into the rails core.
1) since the named_scope uses a value that may change during requests (I18n.locale) it should read:
named_scope :localized, lambda { { :conditions => { :locale => I18n.locale } } }
2) With your own example (Post.localized.find(params[:id])) you gave a good reason to use the first variant. To find the same Post in another language params[:id] would have to be different. Which means you need to find out this other id for linking to the post in another language (common task). Which sucks.
Have you looked into the Globalize2 preview app? It is constructing the database differently, more in the way recommended by the chapter in the beta version of Agile Development ed.3 in internationalization.