Thursday, June 16, 2011

Database Localization in Rails

A problem arises when you use I18N plugins (or your own code) to localize web sites; the data derived from your database is not localized. You have several options:
  1. You can give up on international users.
  2. You can use another plugin to 'Globalize' your database.
  3. You can do it yourself.
Here is a short and rather simple way to do it.


First of all, create a table to hold the values you need to localize (such as name, notes, etc).
That table should have a field like 'locale' (or an id pointing to a list of available locales). This way you avoid adding new columns for every language you have to support in the future.
Of course, that table should contain a reference to the id of its main data (i.e. the table you want to localize). For convention we use 'local_entities' to localize a table of 'entities'.

Then you go to the model class you wish to localize and add the following:

class Entity < ActiveRecord::Base
  cattr_accessor :locale 
  has_many :local_entities

'locale' is now a class variable you can set from any controller simply. For best results, use in a before filter in your application controller.

before_filter :set_locale
  
  def set_locale
    Entity.locale = session[:locale] || 'en-US'
  end


You now have to ensure that you localizations are fetched every time you call Entity.find. Of course there is considerable oversizing of requested data (if you have that many locales to support and a large dataset to return) but the performance hit is not that huge using :include. Why? Because having to fetch localization for every record being displayed can affect performance much more.

This works best for one or two alternative languages being supported on large datasets. More elegant code can be written to further optimize the data fetched, if you find any, please tell us.

def self.find(*args)
  self.with_scope(:find => {:order => :name, :include => 'local_entities'}) {
    super
  } 
end

def localname
  local = self.local_entities.first(:conditions => "locale_code = '#{locale}'")
  return (local.nil?) ? self.name : local.name
end

def localnotes
  local = self.local_entities.first(:conditions => "locale_code = '#{locale}'") 
  return (local.nil?) ? self.notes : local.notes
end

The above functions return localized strings if they are defined and fall back to the original (default) language if none is found. This guarantees at least some valid string to be displayed.

So, just use entity_instance.name when you need to get the default name and entity_instance.localname when you need a safe means of displaying localized text.

Enjoy.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.