# iain.nl  

Translating Columns

Written on

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.