h1. dm-is-localizable Datamapper support for localization of (user entered) content in multilanguage applications h3. 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 h4. Advantages * Proper normalization and referential integrity * Ease in adding a new language (add row to xxx_translations) * Easy to query * Columns keep their names h4. Disadvantages (not really if you think about it) * One table for every resource that needs translations h3. 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. <pre> <code> 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 </code> </pre> Once you have this model in place, you can start defining your _localizable models_. <pre> <code> 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 </code> </pre> 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). <pre> <code> DataMapper::Model.is :localizable, :model => 'ItemLocalization' </code> </pre> Furthermore, the above @Item@ will automatically have the following instance methods defined. <pre> <code> #item_translations_attributes #item_translations_attributes= # and handy aliases for the above #translations_attributes #translations_attributes= </code> </pre> These are generated by "dm-accepts_nested_attributes":http://github.com/snusnu/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":http://github.com/snusnu/dm-accepts_nested_attributes for "dm-accepts_nested_attributes":http://github.com/snusnu/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: <pre> <code> 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 </code> </pre> Furthermore, the following API gets defined on the @Item@ class: <pre> <code> 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 </code> </pre> h3. Inspired by (thx guys!) * Neil Barnwell's comment on the top voted answer to "Schema for a multilanguage database":http://stackoverflow.com/questions/316780/schema-for-a-multilanguage-database * Gabi Solomon's option (4) at this blog post on "Multilanguage database design approach":http://www.gsdesign.ro/blog/multilanguage-database-design-approach/ h3. Copyright Copyright (c) 2009 Martin Gamsjaeger (snusnu). See "LICENSE":http://github.com/snusnu/dm-is-localizable/tree/master/LICENSE for details.