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 localizable 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, :required => true, :unique => true, :unique_index => true
  property :name, String, :required => true

  # 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 localizable models.


class Item

  include DataMapper::Resource
  
  property :id, Serial
  
  is :localizable do        # same as is :localizable, :accept_nested_attributes => true 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 :model option like so (note that this isn’t specced yet).


DataMapper::Model.is :localizable, :model => 'ItemLocalization'

Furthermore, the above Item will automatically have the following instance methods defined.


#item_translations_attributes
#item_translations_attributes=

# and handy aliases for the above

#translations_attributes
#translations_attributes=

These are generated by dm-accepts_nested_attributes and allow for easy manipulation of the localizable properties from say forms in a web application. For more information on working with nested attributes, have a look at the documentation at the README for dm-accepts_nested_attributes

Of course you can turn this behavior off by specifying the is :localizable, :accept_nested_attributes => false do .. end

The resulting model you get when calling Item.is(:localizable) looks like this:


class ItemTranslation

  include DataMapper::Resource
  
  property :id,          Serial
  
  property :item_id,     Integer, :required => true, :unique_index => :unique_languages
  property :language_id, Integer, :required => true, :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_model
  
  # -------------------
  #   class level API
  # -------------------
  
  # list all available languages for Items
  def self.available_languages
    Language.all :id => translation_model.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_model.all.size
  end
  
  # returns a list of symbols reflecting all localizable property names of this resource
  def localizable_properties
    translation_model.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


  # ----------------------------------------
  #   added by dm-accepts_nested_attributes
  # ----------------------------------------
  
  
  def item_translations_attributes
    # ...
  end
  
  def item_translations_attributes=(attributes_or_attributes_collection)
    # ...
  end

  # and handy aliases for the above

  alias :translations_attributes  :item_translations_attributes
  alias :translations_attributes= :item_translations_attributes
  
  
  # 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.