module ActiveTranslation extend ActiveSupport::Concern module QueryMethods class WhereChain < ::ActiveRecord::QueryMethods::WhereChain def not(opts, *rest) if parsed = @scope.clone.parse_translated_conditions(opts) @scope.join_translations.where.not(parsed, *rest) else super end end end def count(*args) if (select_values.present?) spawn.except(:select).count(*args) else super(*args) end end def where(opts = :chain, *rest) if opts == :chain WhereChain.new(spawn) elsif parsed = parse_translated_conditions(opts) join_translations(super(parsed, *rest)) else super end end def exists?(conditions = :none) if parsed = parse_translated_conditions(conditions) with_translation.exists?(parsed) else super end end def parse_translated_conditions(opts) if opts.is_a?(Hash) && respond_to?(:translated_attribute_names) && (keys = opts.symbolize_keys.keys & translated_attribute_names).present? opts = opts.dup keys.each { |key| opts[translated_column_name(key)] = opts.delete(key) || opts.delete(key.to_s) } opts end end def join_translations(relation = self) relation.with_translation end end module ClassMethods def init_translated_attributes return if defined? translated_attribute_names class_attribute :translated_attribute_names self.translated_attribute_names = [] end def translated_column_name(name) "#{name}_translations.text" end def with_translated_attribute(name, value, locales = nil) #locales ||= [:uk] #with_translation.where( # translated_column_name(name) => value, # translated_column_name(:locale) => Array(locales).map(&:to_s) #) with_translation.where(translated_column_name(name) => value) end def with_translation with_this_translation(I18n.locale) end def attr_translatable!(*attr_names) @presence = true attr_translatable(*attr_names) end def attr_translatable(*attr_names) init_translated_attributes attr_names-= translated_attribute_names if attr_names.present? # self.instance_eval %{attr_accessible :#{attr_names.join(', :')}} attr_names.each do |attr_name| build_attribute_extras(attr_name) create_attr_accessor(attr_name) validate_attr(attr_name) if @presence translated_attribute_names << attr_name after_save do phrase_container = send("#{attr_name}_phrase_container") if phrase_container if phrase_container.changed? phrase_container.save else phrase_container.phrase.save if phrase_container.phrase end end end end rebuild_translations_scope() end @presence = false end private # Override the default relation method in order to return a subclass # of ActiveRecord::Relation with custom finder methods for translated # attributes. def relation super.extending!(QueryMethods) end def build_attribute_extras(attr_name) phrase_container_name = "#{attr_name}_phrase_container" phrase_name = "#{attr_name}_phrase" translations_name = "#{attr_name}_translations" has_one phrase_container_name.to_sym, -> { where attribute_name: attr_name }, as: :translatable, class_name: 'PhraseContainer', dependent: :destroy has_one phrase_name.to_sym, class_name: 'Phrase', through: phrase_container_name.to_sym, source: :phrase has_many translations_name.to_sym, class_name: 'Translation', through: phrase_name.to_sym, source: :translations (@translations_columns ||= []) << "#{translations_name}.text as #{attr_name}, #{translations_name}.locale as #{attr_name}_locale" (@join_expressions ||= []) << %{ left join phrase_containers #{phrase_container_name} on #{phrase_container_name}.translatable_id=#{table_name}.id and #{phrase_container_name}.translatable_type='#{model_name}' and #{phrase_container_name}.attribute_name='#{attr_name}' left join phrases #{phrase_name} on #{phrase_name}.id=#{phrase_container_name}.phrase_id left join translations #{translations_name} on #{translations_name}.phrase_id=#{phrase_name}.id }.delete("\n").squeeze(' ') end def rebuild_translations_scope scope :with_translations, -> { joins(@join_expressions.join(' ')).select("#{table_name}.*, #{@translations_columns.join(',')}") } scope :with_this_translation, ->(locale) { expressions = @join_expressions.map do |expression| "#{expression} and #{expression.scan(/on\s+(\w+)\./).last.first}.locale='#{locale}'" end joins(expressions.join(' ')).select("#{table_name}.*, #{@translations_columns.join(',')}") } end def create_attr_accessor(attr_name) phrase_container_name = "#{attr_name}_phrase_container".to_sym define_method(:"#{attr_name}=") do |value| @locale ||= I18n.locale.to_s container = self.send(phrase_container_name) if container.nil? if new_record? self.send("build_#{phrase_container_name}".to_sym, locale: @locale, text: value) else self.send("create_#{phrase_container_name}".to_sym, locale: @locale, text: value) end else container.locale, container.text = @locale, value end self.updated_at = current_time_from_proper_timezone @locale = nil end define_method(attr_name) do |*args| value = nil phrase_container = send(phrase_container_name) if phrase_container params = args && (args[0].is_a? Hash) ? args[0] : {} params[:locale] = @locale if @locale value = phrase_container.text(params) end @locale = nil value end alias_method :"#{attr_name}_before_type_cast", attr_name end def validate_attr(attr_name) validate do attr = send attr_name if attr.nil? or attr.empty? error_text = I18n.t('activerecord.errors.messages.blank') errors.add attr_name, error_text I18n.available_locales.each {|l| errors.add "#{attr_name}_#{l}", error_text} end end end end def reload(*args) super end def method_missing(m, *args, &block) match_data = m.to_s.match /^(\w+)_(\w\w\w?)(=?)$/ if match_data @locale = match_data[2] send(match_data[1]+match_data[3], args[0]) else super end end def attributes if defined? translated_attribute_names translated_attributes = translated_attribute_names.inject({}) do |translated_attributes, attr_name| translated_attributes.merge(attr_name.to_s => send(attr_name)) end super.merge(translated_attributes) else super end end end ActiveRecord::Base.include(ActiveTranslation)