formtastic/formtastic

View on GitHub
lib/formtastic/localizer.rb

Summary

Maintainability
C
1 day
Test Coverage
# frozen_string_literal: true
module Formtastic
  # Implementation for looking up localized values within Formtastic using I18n, if no
  # explicit value (like the `:label` option) is set and I18n-lookups are enabled in the
  # configuration.
  #
  # You can subclass this to implement your own Localizer, and configure Formtastic to use this
  # localizer with:
  #
  #   Formtastic::FormBuilder.i18n_localizer
  #
  # Enabled/disable i18n lookups completely with:
  #
  #   Formtastic::FormBuilder.i18n_lookups_by_default = true/false
  #
  # Lookup priority:
  #
  #   'formtastic.%{type}.%{model}.%{action}.%{attribute}'
  #   'formtastic.%{type}.%{model}.%{attribute}'
  #   'formtastic.%{type}.%{attribute}'
  #
  # Example:
  #
  #   'formtastic.labels.post.edit.title'
  #   'formtastic.labels.post.title'
  #   'formtastic.labels.title'
  class Localizer
    class Cache
      def get(key)
        cache[key]
      end

      def has_key?(key)
        cache.has_key?(key)
      end

      def set(key, result)
        cache[key] = result
      end

      def cache
        @cache ||= {}
      end

      def clear!
        cache.clear
      end
    end

    attr_accessor :builder

    def self.cache
      @cache ||= Cache.new
    end

    def initialize(current_builder)
      self.builder = current_builder
    end

    def localize(key, value, type, options = {}) # @private
      key = value if value.is_a?(::Symbol)

      if value.is_a?(::String)
        escape_html_entities(value)
      else
        use_i18n = value.nil? ? i18n_lookups_by_default : (value != false)
        use_cache = i18n_cache_lookups
        cache = self.class.cache

        if use_i18n
          model_name, nested_model_name  = normalize_model_name(builder.model_name.underscore)

          action_name = builder.template.params[:action].to_s rescue ''
          attribute_name = key.to_s

          # look in the cache first
          if use_cache
            cache_key = [::I18n.locale, action_name, model_name, nested_model_name, attribute_name, key, value, type, options]
            return cache.get(cache_key) if cache.has_key?(cache_key)
          end

          defaults = Formtastic::I18n::SCOPES.reject do |i18n_scope|
            nested_model_name.nil? && i18n_scope.match(/nested_model/)
          end.collect do |i18n_scope|
            i18n_path = i18n_scope.dup
            i18n_path.gsub!('%{action}', action_name)
            i18n_path.gsub!('%{model}', model_name)
            i18n_path.gsub!('%{nested_model}', nested_model_name) unless nested_model_name.nil?
            i18n_path.gsub!('%{attribute}', attribute_name)
            i18n_path.tr!('..', '.')
            i18n_path.to_sym
          end
          defaults << ''

          defaults.uniq!

          default_key = defaults.shift
          i18n_value = Formtastic::I18n.t(default_key,
            options.merge(:default => defaults, :scope => type.to_s.pluralize.to_sym))
          i18n_value = i18n_value.is_a?(::String) ? i18n_value : nil
          if i18n_value.blank? && type == :label
            # This is effectively what Rails label helper does for i18n lookup
            options[:scope] = [:helpers, type]
            options[:default] = defaults
            i18n_value = ::I18n.t(default_key, **options)
          end

          # save the result to the cache
          result = (i18n_value.is_a?(::String) && i18n_value.present?) ? escape_html_entities(i18n_value) : nil
          cache.set(cache_key, result) if use_cache
          result
        end
      end
    end

    protected

    def normalize_model_name(name)
      if name =~ /(.+)\[(.+)\]/
        # Nested builder case with :post rather than @post
        # TODO: check if this is no longer required with a minimum of Rails 4.1
        [$1, $2]
      elsif builder.respond_to?(:options) && builder.options.key?(:as)
        [builder.options[:as].to_s]
      elsif builder.respond_to?(:options) && builder.options.key?(:parent_builder)
        # Rails 3.0+ nested builder work-around case, where :parent_builder is provided by f.semantic_form_for
        [builder.options[:parent_builder].object_name.to_s, name]
      else
        # Non-nested case
        [name]
      end
    end

    def escape_html_entities(string) # @private
      if (builder.escape_html_entities_in_hints_and_labels) ||
         (self.respond_to?(:escape_html_entities_in_hints_and_labels) && escape_html_entities_in_hints_and_labels)
        string = builder.template.escape_once(string) unless string.respond_to?(:html_safe?) && string.html_safe? == true # Accept html_safe flag as indicator to skip escaping
      end
      string
    end

    def i18n_lookups_by_default
      builder.i18n_lookups_by_default
    end

    def i18n_cache_lookups
      builder.i18n_cache_lookups
    end

  end
end