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!)
- Neil Barnwell’s comment on the top voted answer to Schema for a multilanguage database
- Gabi Solomon’s option (4) at this blog post on Multilanguage database design approach
Copyright
Copyright © 2009 Martin Gamsjaeger (snusnu). See LICENSE for details.