lib/i18n/backend/fallbacks.rb



# frozen_string_literal: true

# I18n locale fallbacks are useful when you want your application to use
# translations from other locales when translations for the current locale are
# missing. E.g. you might want to use :en translations when translations in
# your applications main locale :de are missing.
#
# To enable locale fallbacks you can simply include the Fallbacks module to
# the Simple backend - or whatever other backend you are using:
#
#   I18n::Backend::Simple.include(I18n::Backend::Fallbacks)
module I18n
  @@fallbacks = nil

  class << self
    # Returns the current fallbacks implementation. Defaults to +I18n::Locale::Fallbacks+.
    def fallbacks
      @@fallbacks ||= I18n::Locale::Fallbacks.new
      Thread.current[:i18n_fallbacks] || @@fallbacks
    end

    # Sets the current fallbacks implementation. Use this to set a different fallbacks implementation.
    def fallbacks=(fallbacks)
      @@fallbacks = fallbacks.is_a?(Array) ? I18n::Locale::Fallbacks.new(fallbacks) : fallbacks
      Thread.current[:i18n_fallbacks] = @@fallbacks
    end
  end

  module Backend
    module Fallbacks
      # Overwrites the Base backend translate method so that it will try each
      # locale given by I18n.fallbacks for the given locale. E.g. for the
      # locale :"de-DE" it might try the locales :"de-DE", :de and :en
      # (depends on the fallbacks implementation) until it finds a result with
      # the given options. If it does not find any result for any of the
      # locales it will then throw MissingTranslation as usual.
      #
      # The default option takes precedence over fallback locales only when
      # it's a Symbol. When the default contains a String, Proc or Hash
      # it is evaluated last after all the fallback locales have been tried.
      def translate(locale, key, options = EMPTY_HASH)
        return super unless options.fetch(:fallback, true)
        return super if options[:fallback_in_progress]
        default = extract_non_symbol_default!(options) if options[:default]

        fallback_options = options.merge(:fallback_in_progress => true, fallback_original_locale: locale)
        I18n.fallbacks[locale].each do |fallback|
          begin
            catch(:exception) do
              result = super(fallback, key, fallback_options)
              unless result.nil?
                on_fallback(locale, fallback, key, options) if locale.to_s != fallback.to_s
                return result
              end
            end
          rescue I18n::InvalidLocale
            # we do nothing when the locale is invalid, as this is a fallback anyways.
          end
        end

        return if options.key?(:default) && options[:default].nil?

        return super(locale, nil, options.merge(:default => default)) if default
        throw(:exception, I18n::MissingTranslation.new(locale, key, options))
      end

      def resolve_entry(locale, object, subject, options = EMPTY_HASH)
        return subject if options[:resolve] == false
        result = catch(:exception) do
          options.delete(:fallback_in_progress) if options.key?(:fallback_in_progress)

          case subject
          when Symbol
            I18n.translate(subject, **options.merge(:locale => options[:fallback_original_locale], :throw => true))
          when Proc
            date_or_time = options.delete(:object) || object
            resolve_entry(options[:fallback_original_locale], object, subject.call(date_or_time, **options))
          else
            subject
          end
        end
        result unless result.is_a?(MissingTranslation)
      end

      def extract_non_symbol_default!(options)
        defaults = [options[:default]].flatten
        first_non_symbol_default = defaults.detect{|default| !default.is_a?(Symbol)}
        if first_non_symbol_default
          options[:default] = defaults[0, defaults.index(first_non_symbol_default)]
        end
        return first_non_symbol_default
      end

      def exists?(locale, key, options = EMPTY_HASH)
        return super unless options.fetch(:fallback, true)
        I18n.fallbacks[locale].each do |fallback|
          begin
            return true if super(fallback, key)
          rescue I18n::InvalidLocale
            # we do nothing when the locale is invalid, as this is a fallback anyways.
          end
        end

        false
      end

      private

        # Overwrite on_fallback to add specified logic when the fallback succeeds.
        def on_fallback(_original_locale, _fallback_locale, _key, _options)
          nil
        end
    end
  end
end