lib/lolita-translation/has_translations.rb in lolita-translation-0.0.4 vs lib/lolita-translation/has_translations.rb in lolita-translation-0.1.0

- old
+ new

@@ -1,295 +1,299 @@ -require 'stringio' - -class ActiveRecord::Base - # 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: - # - # * <tt>:fallback</tt> - 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 <tt>:nil</tt> configuration option) - # * <tt>:reader</tt> - add reader attributes to the model and delegate them - # to the translation model columns. Add's fallback if it is set to true. - # * <tt>:writer</tt> - add writer attributes to the model and assign them - # to the translation model attributes. - # * <tt>:nil</tt> - 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 <tt>:writer</tt> option, you can create translations using - # update_attributes method. For example: - # - # Article.create! - # Article.update_attributes(:title => 'title', :text => 'text') - # - # === - # - # <tt>translated</tt> 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) - # - # <tt>has_translation?(locale)</tt> method, that returns true if object's model - # have a translation for a specified locale - # - # <tt>translation(locale)</tt> method finds translation with specified locale. - # - # <tt>all_translations</tt> method that returns all possible translations in - # ordered hash (useful when creating forms with nested attributes). - def self.translations(*attrs) - options = { - :fallback => true, - :reader => true, - :writer => false, - :nil => '' - }.merge(attrs.extract_options!) - options.assert_valid_keys([:fallback, :reader, :writer, :nil]) - - class << self - # adds :translations to :includes if current locale differs from default - #FIXME is this enough with find or need to create chain for find_last, find_first and others? - alias_method(:find_without_translations, :find) unless method_defined?(:find_without_translations) - def find(*args) - if args[0].kind_of?(Hash) - args[0][:include] ||= [] - args[0][:include] << :translations - end unless I18n.locale == I18n.default_locale - find_without_translations(*args) - end - # Defines given class recursively - # Example: - # create_class('Cms::Text::Page', Object, ActiveRecord::Base) - # => Cms::Text::Page - def create_class(class_name, parent, superclass, &block) - first,*other = class_name.split("::") - if other.empty? - klass = Class.new superclass, &block - parent.const_set(first, klass) - else - klass = Class.new - parent = unless parent.const_defined?(first) - parent.const_set(first, klass) - else - first.constantize - end - create_class(other.join('::'), parent, superclass, &block) - end - end - # defines "ModelNameTranslation" if it's not defined manualy - def define_translation_class name, attrs - klass = name.constantize rescue nil - unless klass - klass = create_class(name, Object, ActiveRecord::Base) do - # set's real table name - set_table_name name.sub('Translation','').constantize.table_name.singularize + "_translations" - cattr_accessor :translate_attrs, :master_id - # override validate to vaidate only translate fields from master Class - def validate - item = self.class.name.sub('Translation','').constantize.new(self.attributes.clone.delete_if{|k,_| !self.class.translate_attrs.include?(k.to_sym)}) - was_table_name = item.class.table_name - item.class.set_table_name self.class.table_name - item.valid? rescue - self.class.translate_attrs.each do |attr| - errors_on_attr = item.errors.on(attr) - self.errors.add(attr,errors_on_attr) if errors_on_attr - end - item.class.set_table_name was_table_name - end - extend TranslationClassMethods - end - klass.translate_attrs = attrs - else - unless klass.respond_to?(:translate_attrs) - klass.send(:cattr_accessor, :translate_attrs, :master_id) - klass.send(:extend,TranslationClassMethods) - klass.translate_attrs = attrs - end - end - - klass.extract_master_id(name) - klass - end - # creates translation table and adds missing fields - # So at first add the "translations :name, :desc" in your model - # then put YourModel.sync_translation_table! in db/seed.rb and run "rake db:seed" - # Later adding more fields in translations array, just run agin "rake db:seed" - # If you want to remove fields do it manualy, it's safer - def sync_translation_table! - out = StringIO.new - $stdout = out - translations_class = reflections[:translations].class_name.constantize - translations_table = translations_class.table_name - unless ActiveRecord::Migration::table_exists?(translations_table) - ActiveRecord::Migration.create_table translations_table do |t| - t.integer translations_class.master_id, :null => false - t.string :locale, :null => false, :limit => 5 - columns_has_translations.each do |col| - t.send(col.type,col.name) - end - end - ActiveRecord::Migration.add_index translations_table, [translations_class.master_id, :locale], :unique => true - translations_class.reset_column_information - else - changes = false - columns_has_translations.each do |col| - unless translations_class.columns_hash.has_key?(col.name) - ActiveRecord::Migration.add_column(translations_table, col.name, col.type) - changes = true - end - end - translations_class.reset_column_information if changes - end - $stdout = STDOUT - end - end - - translation_class_name = "#{self.name}Translation" - translation_class = self.define_translation_class(translation_class_name, attrs) - belongs_to = self.name.demodulize.underscore.to_sym - - write_inheritable_attribute :has_translations_options, options - class_inheritable_reader :has_translations_options - - write_inheritable_attribute :columns_has_translations, (columns rescue []).collect{|col| col if attrs.include?(col.name.to_sym)}.compact - class_inheritable_reader :columns_has_translations - - # forces given locale - # I18n.locale = :lv - # a = Article.find 18 - # a.title - # => "LV title" - # a.in(:en).title - # => "EN title" - def in locale - locale.to_sym == I18n.default_locale ? self : find_translation(locale) - end - - def find_or_build_translation(*args) - locale = args.first.to_s - build = args.second.present? - find_translation(locale) || (build ? self.translations.build(:locale => locale) : self.translations.new(:locale => locale)) - end - - def translation(locale) - find_translation(locale.to_s) - end - - def all_translations - t = I18n.available_locales.map do |locale| - [locale, find_or_build_translation(locale)] - end - ActiveSupport::OrderedHash[t] - end - - def has_translation?(locale) - return true if locale == I18n.default_locale - find_translation(locale).present? - end - - # if object is new, then nested slaves ar built for all available locales - def build_nested_translations - if (I18n.available_locales.size - 1) > self.translations.size - I18n.available_locales.clone.delete_if{|l| l == I18n.default_locale}.each do |l| - options = {:locale => l.to_s} - options[self.class.reflections[:translations].class_name.constantize.master_id] = self.id unless self.new_record? - self.translations.build(options) unless self.translations.map(&:locale).include?(l.to_s) - end - end - end - - if options[:reader] - attrs.each do |name| - 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 - end - - has_many :translations, :class_name => translation_class_name, :foreign_key => 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? } - translation_class.belongs_to belongs_to - translation_class.validates_presence_of :locale - translation_class.validates_uniqueness_of :locale, :scope => translation_class.master_id - - # Workaround to support Rails 2 - scope_method = if ActiveRecord::VERSION::MAJOR < 3 then :named_scope else :scope end - - send scope_method, :translated, lambda { |locale| {:conditions => ["#{translation_class.table_name}.locale = ?", locale.to_s], :joins => :translations} } - - #private is no good - - def find_translation(locale) - locale = locale.to_s - translations.detect { |t| t.locale == locale } - end - end -end - -module TranslationClassMethods - # sets real master_id it's aware of STI - def extract_master_id name - master_class = name.sub('Translation','').constantize - #FIXME why need to check superclass ? - class_name = master_class.name #!master_class.superclass.abstract_class? ? master_class.superclass.name : master_class.name - self.master_id = :"#{class_name.demodulize.underscore}_id" - end +require 'stringio' + +class ActiveRecord::Base + # 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: + # + # * <tt>:fallback</tt> - 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 <tt>:nil</tt> configuration option) + # * <tt>:reader</tt> - add reader attributes to the model and delegate them + # to the translation model columns. Add's fallback if it is set to true. + # * <tt>:writer</tt> - add writer attributes to the model and assign them + # to the translation model attributes. + # * <tt>:nil</tt> - 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 <tt>:writer</tt> option, you can create translations using + # update_attributes method. For example: + # + # Article.create! + # Article.update_attributes(:title => 'title', :text => 'text') + # + # === + # + # <tt>translated</tt> 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) + # + # <tt>has_translation?(locale)</tt> method, that returns true if object's model + # have a translation for a specified locale + # + # <tt>translation(locale)</tt> method finds translation with specified locale. + # + # <tt>all_translations</tt> method that returns all possible translations in + # ordered hash (useful when creating forms with nested attributes). + def self.translations(*attrs) + options = { + :fallback => true, + :reader => true, + :writer => false, + :nil => '' + }.merge(attrs.extract_options!) + options.assert_valid_keys([:fallback, :reader, :writer, :nil]) + + class << self + # adds :translations to :includes if current locale differs from default + #FIXME is this enough with find or need to create chain for find_last, find_first and others? + alias_method(:find_without_translations, :find) unless method_defined?(:find_without_translations) + def find(*args) + if args[0].kind_of?(Hash) + args[0][:include] ||= [] + args[0][:include] << :translations + end unless I18n.locale == I18n.default_locale + find_without_translations(*args) + end + # Defines given class recursively + # Example: + # create_class('Cms::Text::Page', Object, ActiveRecord::Base) + # => Cms::Text::Page + def create_class(class_name, parent, superclass, &block) + first,*other = class_name.split("::") + if other.empty? + klass = Class.new superclass, &block + parent.const_set(first, klass) + else + klass = Class.new + parent = unless parent.const_defined?(first) + parent.const_set(first, klass) + else + first.constantize + end + create_class(other.join('::'), parent, superclass, &block) + end + end + # defines "ModelNameTranslation" if it's not defined manualy + def define_translation_class name, attrs + klass = name.constantize rescue nil + unless klass + klass = create_class(name, Object, ActiveRecord::Base) do + # set's real table name + set_table_name name.sub('Translation','').constantize.table_name.singularize + "_translations" + cattr_accessor :translate_attrs, :master_id + # override validate to vaidate only translate fields from master Class + def validate + item = self.class.name.sub('Translation','').constantize.new(self.attributes.clone.delete_if{|k,_| !self.class.translate_attrs.include?(k.to_sym)}) + was_table_name = item.class.table_name + item.class.set_table_name self.class.table_name + item.valid? rescue + self.class.translate_attrs.each do |attr| + errors_on_attr = item.errors.on(attr) + self.errors.add(attr,errors_on_attr) if errors_on_attr + end + item.class.set_table_name was_table_name + end + extend TranslationClassMethods + end + klass.translate_attrs = attrs + else + unless klass.respond_to?(:translate_attrs) + klass.send(:cattr_accessor, :translate_attrs, :master_id) + klass.send(:extend,TranslationClassMethods) + klass.translate_attrs = attrs + end + end + + klass.extract_master_id(name) + klass + end + # creates translation table and adds missing fields + # So at first add the "translations :name, :desc" in your model + # then put YourModel.sync_translation_table! in db/seed.rb and run "rake db:seed" + # Later adding more fields in translations array, just run agin "rake db:seed" + # If you want to remove fields do it manualy, it's safer + def sync_translation_table! + out = StringIO.new + $stdout = out + translations_class = reflections[:translations].class_name.constantize + translations_table = translations_class.table_name + unless ActiveRecord::Migration::table_exists?(translations_table) + ActiveRecord::Migration.create_table translations_table do |t| + t.integer translations_class.master_id, :null => false + t.string :locale, :null => false, :limit => 5 + columns_has_translations.each do |col| + t.send(col.type,col.name) + end + end + ActiveRecord::Migration.add_index translations_table, [translations_class.master_id, :locale], :unique => true + translations_class.reset_column_information + else + changes = false + columns_has_translations.each do |col| + unless translations_class.columns_hash.has_key?(col.name) + ActiveRecord::Migration.add_column(translations_table, col.name, col.type) + changes = true + end + end + translations_class.reset_column_information if changes + end + $stdout = STDOUT + end + end + + translation_class_name = "#{self.name}Translation" + translation_class = self.define_translation_class(translation_class_name, attrs) + belongs_to = self.name.demodulize.underscore.to_sym + + write_inheritable_attribute :has_translations_options, options + class_inheritable_reader :has_translations_options + + write_inheritable_attribute :columns_has_translations, (columns rescue []).collect{|col| col if attrs.include?(col.name.to_sym)}.compact + class_inheritable_reader :columns_has_translations + + # forces given locale + # I18n.locale = :lv + # a = Article.find 18 + # a.title + # => "LV title" + # a.in(:en).title + # => "EN title" + def in locale + locale.to_sym == I18n.default_locale ? self : find_translation(locale) + end + + def find_or_build_translation(*args) + locale = args.first.to_s + build = args.second.present? + find_translation(locale) || (build ? self.translations.build(:locale => locale) : self.translations.new(:locale => locale)) + end + + def translation(locale) + find_translation(locale.to_s) + end + + def all_translations + t = I18n.available_locales.map do |locale| + [locale, find_or_build_translation(locale)] + end + ActiveSupport::OrderedHash[t] + end + + def has_translation?(locale) + return true if locale == I18n.default_locale + find_translation(locale).present? + end + + # if object is new, then nested slaves ar built for all available locales + def build_nested_translations + if (I18n.available_locales.size - 1) > self.translations.size + I18n.available_locales.clone.delete_if{|l| l == I18n.default_locale}.each do |l| + options = {:locale => l.to_s} + options[self.class.reflections[:translations].class_name.constantize.master_id] = self.id unless self.new_record? + self.translations.build(options) unless self.translations.map(&:locale).include?(l.to_s) + end + end + end + + if options[:reader] + attrs.each do |name| + 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 + end + + @translation_attrs = attrs + def self.translation_attrs + @translation_attrs + end + has_many :translations, :class_name => translation_class_name, :foreign_key => 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? } + translation_class.belongs_to belongs_to + translation_class.validates_presence_of :locale + translation_class.validates_uniqueness_of :locale, :scope => translation_class.master_id + + # Workaround to support Rails 2 + scope_method = if ActiveRecord::VERSION::MAJOR < 3 then :named_scope else :scope end + + send scope_method, :translated, lambda { |locale| {:conditions => ["#{translation_class.table_name}.locale = ?", locale.to_s], :joins => :translations} } + + #private is no good + + def find_translation(locale) + locale = locale.to_s + translations.detect { |t| t.locale == locale } + end + end +end + +module TranslationClassMethods + # sets real master_id it's aware of STI + def extract_master_id name + master_class = name.sub('Translation','').constantize + #FIXME why need to check superclass ? + class_name = master_class.name #!master_class.superclass.abstract_class? ? master_class.superclass.name : master_class.name + self.master_id = :"#{class_name.demodulize.underscore}_id" + end end \ No newline at end of file