glebm/i18n-tasks

View on GitHub
lib/i18n/tasks/missing_keys.rb

Summary

Maintainability
A
2 hrs
Test Coverage
A
99%
# frozen_string_literal: true

require 'set'
module I18n::Tasks
  module MissingKeys # rubocop:disable Metrics/ModuleLength
    MISSING_TYPES = %w[
      used
      diff
      plural
    ].freeze

    def self.missing_keys_types
      @missing_keys_types ||= MISSING_TYPES
    end

    def missing_keys_types
      MissingKeys.missing_keys_types
    end

    # @param types [:used, :diff, :plural] all if `nil`.
    # @return [Siblings]
    def missing_keys(locales: nil, types: nil, base_locale: nil)
      locales ||= self.locales
      types   ||= missing_keys_types
      base = base_locale || self.base_locale
      types.inject(empty_forest) do |f, type|
        f.merge! send(:"missing_#{type}_forest", locales, base)
      end
    end

    def eq_base_keys(opts = {})
      locales = Array(opts[:locales]).presence || self.locales
      (locales - [base_locale]).inject(empty_forest) do |tree, locale|
        tree.merge! equal_values_tree(locale, base_locale)
      end
    end

    def missing_diff_forest(locales, base = base_locale)
      tree = empty_forest
      # present in base but not locale
      (locales - [base]).each do |locale|
        tree.merge! missing_diff_tree(locale, base)
      end
      if locales.include?(base)
        # present in locale but not base
        (self.locales - [base]).each do |locale|
          tree.merge! missing_diff_tree(base, locale)
        end
      end
      tree
    end

    def missing_used_forest(locales, _base = base_locale)
      locales.inject(empty_forest) do |forest, locale|
        forest.merge! missing_used_tree(locale)
      end
    end

    def missing_plural_forest(locales, _base = base_locale)
      locales.each_with_object(empty_forest) do |locale, forest|
        required_keys = required_plural_keys_for_locale(locale)
        next if required_keys.empty?

        tree = empty_forest
        plural_nodes data[locale] do |node|
          children = node.children
          present_keys = Set.new(children.map { |c| c.key.to_sym })
          next if ignore_key?(node.full_key(root: false), :missing)
          next if present_keys.superset?(required_keys)

          tree[node.full_key] = node.derive(
            value: children.to_hash,
            children: nil,
            data: node.data.merge(missing_keys: (required_keys - present_keys).to_a)
          )
        end
        tree.set_root_key!(locale, type: :missing_plural)
        forest.merge!(tree)
      end
    end

    def required_plural_keys_for_locale(locale)
      @plural_keys_for_locale ||= {}
      return @plural_keys_for_locale[locale] if @plural_keys_for_locale.key?(locale)

      @plural_keys_for_locale[locale] = plural_keys_for_locale(locale)
    end

    # Loads rails-i18n pluralization config for the given locale.
    def load_rails_i18n_pluralization!(locale)
      path = File.join(Gem::Specification.find_by_name('rails-i18n').gem_dir, 'rails', 'pluralization', "#{locale}.rb")
      eval(File.read(path), binding, path) # rubocop:disable Security/Eval
    end

    # keys present in compared_to, but not in locale
    def missing_diff_tree(locale, compared_to = base_locale)
      data[compared_to].select_keys do |key, _node|
        locale_key_missing? locale, depluralize_key(key, compared_to)
      end.set_root_key!(locale, type: :missing_diff).keys do |_key, node|
        # change path and locale to base
        data = { locale: locale, missing_diff_locale: node.data[:locale] }
        if node.data.key?(:path)
          data[:path] = LocalePathname.replace_locale(node.data[:path], node.data[:locale], locale)
        end
        node.data.update data
      end
    end

    # keys used in the code missing translations in locale
    def missing_used_tree(locale)
      used_tree(strict: true).select_keys do |key, _node|
        locale_key_missing?(locale, key)
      end.set_root_key!(locale, type: :missing_used)
    end

    def equal_values_tree(locale, compare_to = base_locale)
      base = data[compare_to].first.children
      data[locale].select_keys(root: false) do |key, node|
        other_node = base[key]
        other_node && !node.reference? && node.value == other_node.value && !ignore_key?(key, :eq_base, locale)
      end.set_root_key!(locale, type: :eq_base)
    end

    def locale_key_missing?(locale, key)
      !key_value?(key, locale) && !external_key?(key, locale) && !ignore_key?(key, :missing)
    end

    # @param [::I18n::Tasks::Data::Tree::Siblings] forest
    # @yield [::I18n::Tasks::Data::Tree::Node]
    # @yieldreturn [Boolean] whether to collapse the node
    def collapse_same_key_in_locales!(forest)
      locales_and_node_by_key = {}
      to_remove               = []
      forest.each do |root|
        locale = root.key
        root.keys do |key, node|
          next unless yield node

          if locales_and_node_by_key.key?(key)
            locales_and_node_by_key[key][0] << locale
          else
            locales_and_node_by_key[key] = [[locale], node]
          end
          to_remove << node
        end
      end
      forest.remove_nodes_and_emptied_ancestors! to_remove
      locales_and_node_by_key.each_with_object({}) do |(key, (locales, node)), inv|
        (inv[locales.sort.join('+')] ||= []) << [key, node]
      end.map do |locales, keys_nodes|
        keys_nodes.each do |(key, node)|
          forest["#{locales}.#{key}"] = node
        end
      end
      forest
    end

    private

    def plural_keys_for_locale(locale)
      configuration = load_rails_i18n_pluralization!(locale)
      if configuration[locale.to_sym].nil?
        alternate_locale = alternate_locale_from(locale)
        return Set.new if configuration[alternate_locale.to_sym].nil?

        return set_from_rails_i18n_pluralization(configuration, alternate_locale)
      end
      set_from_rails_i18n_pluralization(configuration, locale)
    rescue SystemCallError, IOError
      Set.new
    end

    def alternate_locale_from(locale)
      re = /(\w{2})-*(\w{2,3})*/
      match = locale.match(re)
      language_code = match[1]
      country_code = match[2]
      "#{language_code}-#{country_code.upcase}"
    end

    def set_from_rails_i18n_pluralization(configuration, locale)
      Set.new(configuration[locale.to_sym][:i18n][:plural][:keys])
    end
  end
end