dm-is-localizable

Datamapper support for localization of (user entered) content in multilanguage applications

Schema

  • one xxx_translations table for every translatable resource
  • xxx_translations belongs_to the resource to translate
  • xxx_translations belongs_to a language
  • properties to be translated are defined in xxx_translations

Advantages

  • Proper normalization and referential integrity
  • Ease in adding a new language (add row to xxx_translations)
  • Easy to query
  • Columns keep their names

Disadvantages (not really if you think about it)

  • One table for every resource that needs translations

Example definition of a localized model

Currently, you need to define a Language model yourself, to get dm-is-localizable started. However, this is reasonably easy! If you do a rake install after you cloned the repo (I guess it won’t work if you do a simple gem install), it will print out the code for language.rb and will tell you where to put it.


class Language

  include DataMapper::Resource

  # properties

  property :id, Serial

  property :code, String, :nullable => false, :unique => true, :unique_index => true
  property :name, String, :nullable => false

  # locale string like 'en-US'
  validates_format :code, :with => /^[a-z]{2}-[A-Z]{2}$/


  def self.[](code)
    return nil if code.nil?
    first :code => code.to_s.gsub('_', '-')
  end

end

Once you have this model in place, you can start defining your localized models.


class Item

  include DataMapper::Resource
  
  property :id, Serial
  
  is :localizable do
    property :name, String
    property :desc, String
  end
  
end

The above Item model will define and thus be able to DataMapper.auto_migrate! the ItemTranslation model. The naming convention used here is "#{ClassToBeLocalized.name}Translation".

Preliminary support for changing this is available by using the :class_name option like so (note that this isn’t specced yet).


DataMapper::Model.is :localized, :class_name => 'ItemLocalization'

class ItemTranslation

  include DataMapper::Resource
  
  property :id,          Serial
  
  property :item_id,     Integer, :nullable => false, :unique_index => :unique_languages
  property :language_id, Integer, :nullable => false, :unique_index => :unique_languages
  
  property :name,        String
  property :desc,        String
  
  validates_is_unique :language_id, :scope => :item_id

  belongs_to :item
  belongs_to :language
  
end

Furthermore, the following API gets defined on the Item class:


class Item

  include DataMapper::Resource
  
  property :id, Serial
  
  is :localizable do
    property :name, String
    property :desc, String
  end
  
  # ----------------------------
  #   added by is :localizable
  # ----------------------------
  
  has n, :item_translations
  has n, :languages, :through => :item_translations
  
  # and a handy alias
  alias :translations :item_translations
  
  # helper to get at ItemTranslation
  class_inheritable_reader :translation_class
  
  # -------------------
  #   class level API
  # -------------------
  
  # list all available languages for Items
  def self.available_languages
    Language.all :id => translation_class.all.map { |t| t.language_id }.uniq
  end
  
  # the number of all available languages for the localizable model
  def self.nr_of_available_languages
    available_languages.size
  end
  
  # checks if all localizable resources are translated in all available languages
  def self.translations_complete?
    nr_of_available_languages * all.size == translation_class.all.size
  end
  
  # returns a list of symbols reflecting all localizable property names of this resource
  def localizable_properties
    translation_class.properties.map do |p|
      p.name
    end.select do |p|
      # exclude properties that are'nt localizable
      p != :id && p != :language_id && p != Extlib::Inflection.foreign_key(self.name).to_sym
    end
  end
  
  # ----------------------
  #   instance level API
  # ----------------------
  
  # list all available languages for this instance
  def available_languages
    Language.all :id => translations.map { |t| t.language_id }.uniq
  end
  
  # the number of all available languages for this instance
  def nr_of_available_languages
    available_languages.size
  end
  
  # checks if this instance is translated into all available languages for this model
  def translations_complete?
    self.class.nr_of_available_languages == translations.size
  end
  
  # translates the given attribute to the language identified by the given language_code
  def translate(attribute, language_code)
    if language = Language[language_code]
      t = translations.first(:language_id => language.id)
      t.respond_to?(attribute) ? t.send(attribute) : nil
    else
      nil
    end
  end
  
  # translates the :name property to the given language
  def name(language_code)
    translate(:name, language_code)
  end
  
  # translates the :desc property to the given language
  def desc(language_code)
    translate(:desc, language_code)
  end
  
  
  # TODO
  # more API to support common usecases (and i18n/l10n solutions)
  
end

Inspired by (thx guys!)

Copyright

Copyright © 2009 Martin Gamsjaeger (snusnu). See LICENSE for details.