module Lolita
module Translation
module SingletonMethods
# Provides ability to add the translations for the model using delegate pattern.
# Uses has_many association to the ModelNameTranslation.
#
# For example you have model Article with attributes title and text.
# You want that attributes title and text to be translated.
# For this reason you need to generate new model ArticleTranslation.
# In migration you need to add:
#
# create_table :article_translations do |t|
# t.references :article, :null => false
# t.string :locale, :length => 2, :null => false
# t.string :name, :null => false
# end
#
# add_index :articles, [:article_id, :locale], :unique => true, :name => 'unique_locale_for_article_id'
#
# And in the Article model:
#
# translations :title, :text
#
# This will adds:
#
# * named_scope (translated) and has_many association to the Article model
# * locale presence validation to the ArticleTranslation model.
#
# Notice: if you want to have validates_presence_of :article, you should use :inverse_of.
# Support this by yourself. Better is always to use artile.translations.build() method.
#
# For more information please read API. Feel free to write me an email to:
# dmitry.polushkin@gmail.com.
#
# ===
#
# You also can pass attributes and options to the translations class method:
#
# translations :title, :text, :fallback => true, :writer => true, :nil => nil
#
# ===
#
# Configuration options:
#
# * :fallback - if translation for the current locale not found.
# By default true.
# Uses algorithm of fallback:
# 0) current translation (using I18n.locale);
# 1) default locale (using I18n.default_locale);
# 2) :nil value (see :nil configuration option)
# * :reader - add reader attributes to the model and delegate them
# to the translation model columns. Add's fallback if it is set to true.
# * :writer - add writer attributes to the model and assign them
# to the translation model attributes.
# * :nil - when reader cant find string, it returns by default an
# empty string. If you want to change this setting for example to nil,
# add :nil => nil
#
# ===
#
# When you are using :writer option, you can create translations using
# update_attributes method. For example:
#
# Article.create!
# Article.update_attributes(:title => 'title', :text => 'text')
#
# ===
#
# translated named_scope is useful when you want to find only those
# records that are translated to a specific locale.
# For example if you want to find all Articles that is translated to an english
# language, you can write: Article.translated(:en)
#
# has_translation?(locale) method, that returns true if object's model
# have a translation for a specified locale
#
# translation(locale) method finds translation with specified locale.
#
# all_translations method that returns all possible translations in
# ordered hash (useful when creating forms with nested attributes).
def translations *attrs
@translations_config ||= Lolita::Translation::Configuration.new(self,*attrs)
if block_given?
yield @translations_config
end
@translations_config
end
end
class Configuration
attr_reader :klass, :options, :translation_class_name, :translation_class,:attrs
def initialize(base_class, *attrs)
@klass = base_class
@attrs = attrs
initialize_options
include_modules
initialize_default_attributes
extend_klass
extend_translation_class
end
private
def extend_klass
config = self
@klass.class_eval do
class_attribute :has_translations_options
self.has_translations_options = config.options
class_attribute :columns_has_translations
self.columns_has_translations = (columns rescue []).collect{|col| col if config.attrs.include?(col.name.to_sym)}.compact
class_attribute :translation_attrs
self.translation_attrs = config.attrs
has_many :translations, :class_name => config.translation_class_name, :foreign_key => config.translation_class.master_id, :dependent => :destroy
accepts_nested_attributes_for :translations, :allow_destroy => true, :reject_if => proc { |attributes| columns_has_translations.collect{|col| attributes[col.name].blank? ? nil : 1}.compact.empty? }
scope :translated, lambda { |locale|
where("#{translation_class.table_name}.locale = ?", locale.to_s).joins(:translations)
}
class << self
alias_method_chain :find, :translations
end
end
override_readers
end
def extend_translation_class
translation_class.belongs_to @klass.name.demodulize.underscore.to_sym
translation_class.validates_presence_of :locale
translation_class.validates_uniqueness_of :locale, :scope => translation_class.master_id
end
def override_readers
if options[:reader]
attrs.each do |name|
override_reader(name)
end
end
end
def override_reader(name)
@klass.send :define_method, name do
unless ::I18n.default_locale == ::I18n.locale
translation = self.translation(::I18n.locale)
if translation.nil?
if has_translations_options[:fallback]
(self[name].nil? || self[name].blank?) ? has_translations_options[:nil] : self[name].set_origins(self,name) else
has_translations_options[:nil]
end
else
if @return_raw_data
(self[name].nil? || self[name].blank?) ? has_translations_options[:nil] : self[name].set_origins(self,name)
else
value = translation.send(name) and value.set_origins(self,name)
end
end
else
(self[name].nil? || self[name].blank?) ? has_translations_options[:nil] : self[name].set_origins(self,name)
end
end
end
def initialize_default_attributes
@translation_class_name = "#{@klass.name}Translation"
@translation_class = Lolita::Translation::TranslationModel.new(self).klass
end
def include_modules
@klass.send(:include, Lolita::Translation::InstanceMethods)
@klass.extend(Lolita::Translation::ClassMethods)
end
def initialize_options
@options = {
:fallback => true,
:reader => true,
:writer => false,
:nil => ''
}.merge(@attrs.extract_options!)
@options.assert_valid_keys([:fallback, :reader, :writer, :nil,:table_name])
end
end
end
end