beatrichartz/babylonia

View on GitHub
lib/babylonia/class_methods.rb

Summary

Maintainability
A
1 hr
Test Coverage
require 'yaml'
require 'i18n'

# Let your users translate their content into their languages without additional tables or columns in your tables
# @author Beat Richartz
# @version 0.2.0
# @since 0.0.1
#
module Babylonia
  
  # Helper Methods for locales
  # @author Beat Richartz
  # @version 0.0.2
  # @since 0.0.1
  #
  module HelperMethods
    # Return the currently active locale for the object
    # @return [Symbol] The currently active locale
    #
    def locale
      evaluate_localization_option!(:locale)
    end
    
    # Return the default locale for the object
    # @return [Symbol] The currently active locale
    #
    def default_locale
      evaluate_localization_option!(:default_locale)
    end
    
    # Return if the object falls back on translations
    # @return [Boolean] if the translations fall back to the default locale
    #
    def locale_fallback?
      evaluate_localization_option!(:fallback)
    end
    
    # Return the missing translation placeholder
    # @return [String] The missing translation placeholder
    #
    def missing_translation_placeholder field
      evaluate_localization_option!(:placeholder, field)
    end
    
    # Return if a translation in the language is stored in all translated fields
    # @return [Boolean] True if a translation is stored
    #
    def has_locale? locale
      locales.include?(locale.to_sym)
    end
    
    # Return all the available locales
    # @return [Array] An array of symbols of all available locales
    # 
    def available_locales
      evaluate_localization_option!(:available_locales)
    end
    
    # Return if a locale is theoretically available in all translated fields
    # @return [Boolean] True if the language is available
    #
    def has_available_locale? locale
      available_locales.include?(locale.to_sym)
    end
    
    protected
    
    def translation_nil_or_empty? translation
      translation.nil? or translation.empty?
    end
    
    def dump_translation_locale_hash hash
      YAML.dump(hash.delete_if{|k,v| translation_nil_or_empty?(v) })
    end
    
    def fallback_to_default_locale!(hash, translation, options)
      if translation_nil_or_empty?(translation) and options[:fallback] and locale_fallback?
        hash[default_locale]
      else
        translation
      end
    end
  end
  
  # Method missing implementation for virtual attributes
  # @author Beat Richartz
  # @version 0.0.2
  # @since 0.0.1
  #
  module VirtualAttributes
    # Define method missing to be able to access a language directly
    # Enables to call a language virtual attribute directly
    # @note Since the virtual attribute is called directly, there is no fallback on this unless you set it to true
    # @example Call a getter directly
    #   object.field_de #=> 'DEUTSCH'
    # @example Call a setter directly
    #   object.field_de = 'DEUTSCH'
    # @example Call an untranslated field
    #   object.field_it #=> nil
    #
    # @example Call a field with fallback
    #   object.field_it(fallback: true)
    def method_missing meth, *args, &block
      if parts = extract_locale_method_parts(meth)
        parts[2] ? send(parts[0] + parts[2].to_s, { parts[1].to_sym => args.first }) : send(parts[0], parts[1].to_sym, args.first || {})
      else
        super(meth, *args, &block)
      end
    end
    
    private
    
    def extract_locale_method_parts meth
      if (parts = meth.to_s.match(/\A(\w+)_(\w+)(=)?\z/).to_a[1..3]) && localized?(parts[0]) && has_available_locale?(parts[1])
        parts
      end
    end
  end
  
  # Class methods to extend a class with in order to make it able to handle translatable attributes
  #
  module ClassMethods
    private
    
    def install_locale_field_getter_for(field)
      # Alias method chain the field to a translated value
      # @param [Symbol] locale Pass a locale to get the field translation in this specific locale
      # @param [Boolean] fallback Whether a fallback should be used, defaults to true
      # @return [String, NilClass] Either the string with the translation, the fallback, the placeholder or nil
      # @example Call a field in italian
      #   your_instance.field(:it) #=> "Translation" 
      #
      define_method :"#{field}_translated" do |l=nil, options={fallback: true}|
        field_hash    = send(:"#{field}_hash")
        translation   = field_hash[l || locale]
        translation   = fallback_to_default_locale!(field_hash, translation, options)
        translation_nil_or_empty?(translation) ? missing_translation_placeholder(field) : translation
      end
      alias_method :"#{field}_raw", field
      alias_method field, :"#{field}_translated"
    end
    
    def install_locale_field_hash_for(field)
      # Return the translated values as a hash
      # @return [Hash] The hash with all the translations stored in the field
      #
      define_method :"#{field}_hash" do
        field_content = send(:"#{field}_raw")
        field_content.is_a?(String) ? YAML.load(field_content) : {}
      end
    end
    
    def install_locale_field_setter_for(field)
      # Set the field to a value. This either takes a string or a hash
      # If given a String, the current locale is set to this value
      # If given a Hash, the hash is merged into the current translation hash, and empty values are purged
      # @param [String, Hash] data The data to set either the current language translation or all translations to
      # @example Set the translation for the current locale
      #   your_object.field = "TRANSLATION"
      # @example Set the translation and delete italian
      #   your_object.field = {de: 'DEUTSCH', it: ''}
      #
      define_method :"#{field}_translated=" do |data|
        current_hash = send(:"#{field}_hash")
        
        if data.is_a?(String)
          current_hash.merge! locale => data
        elsif data.is_a?(Hash)
          data.delete_if{|k,v| !has_available_locale?(k) }
          current_hash.merge! data
        end
        
        send(:"#{field}_raw=", dump_translation_locale_hash(current_hash))
      end
      alias_method :"#{field}_raw=", :"#{field}="
      alias_method :"#{field}=", :"#{field}_translated="
    end
    
    def install_virtual_locale_attributes_via_method_missing

    end
    
    def install_localized_helper_for(fields)
      # Return if an attribute is localized
      # @return [Boolean] True if the attribute is localized
      # 
      define_method :localized? do |attr|
        fields.include?(attr.to_sym)
      end
    end
    
    def install_locales_helper_for(fields)
      # Return languages stored in all translated fields
      # @return [Array] An array containing all languages stored
      #
      define_method :locales do
        first_field_locales = send(:"#{fields.first}_hash").keys
        fields.inject(first_field_locales){|o, f| o & send(:"#{f}_hash").keys }
      end
    end
    
    def install_locale_options_evaluation(options)
      # Evaluate a localization option
      #
      define_method :evaluate_localization_option! do |option, field=nil, recursion=false|
        o = options[option]
        if o.is_a?(Proc) && field
          o.call self, field
        elsif o.is_a?(Proc)
          o.call self
        elsif o.is_a?(Symbol) && (recursion || !evaluate_localization_option!(:available_locales, nil, true).include?(o))
          send o
        else
          o
        end
      end
    end
    
    def evaluate_locale_options_for(fields)
      options                     = fields.last.is_a?(Hash) ? fields.pop : {}
      options[:locale]            ||= lambda { |r| I18n.locale }
      options[:default_locale]    ||= lambda { |r| I18n.default_locale }
      options[:available_locales] ||= lambda { |r| I18n.available_locales }
      options[:fallback]          = true if options[:fallback].nil?
      options
    end
    
    # Main class method ob Babylonia. Use to make attributes translatable
    # @param [Symbol] fields the attributes to translate
    # @param [Hash] options The options for translation
    # @option [Symbol, Proc, String] locale The current locale - can be either a symbol that will be sent to the instance, a locale symbol that is included in available_locales or a proc that will get called with the instance. Defaults to I18n.locale at the time of use
    # @option [Symbol, Proc, String] default_locale The fallback / default locale - can be either a symbol that will be sent to the instance, a locale symbol that is included in available_locales, a proc that will get called with the instance
    # @option [Symbol, Proc, Array] available_locales The available locales - can be either a symbol that will be sent to the instance, a proc that will get called with the instance, or an Array of symbols of available locales. Defaults to I18n.available_locales at the time of use.
    # @option [Boolean] fallback Wheter to fallback to the default locale or not
    # @option [String] placeholder The placeholder to use for missing translations
    #
    public
    
    def build_babylonian_tower_on(*fields)
      options = evaluate_locale_options_for(fields)
      
      fields.each do |field|
        install_locale_field_getter_for(field)
        install_locale_field_setter_for(field)
        install_locale_field_hash_for(field)
      end
      
      include HelperMethods
      install_localized_helper_for(fields)
      install_locales_helper_for(fields)
      
      include VirtualAttributes
      install_locale_options_evaluation(options)
    end
  end
end