fnando / i18n-js

It's a small library to provide the I18n translations on the Javascript. It comes with Rails support.
MIT License
3.75k stars 519 forks source link

Bug: `proc` rules should be cleared from exported JSON localization files #690

Closed oleksii-leonov closed 1 year ago

oleksii-leonov commented 1 year ago

Description

proc rules should be cleared from exported JSON.

In v4 you will get such strings in exported JSON files:

{
  "bg": {
    "i18n": {
      "plural": {
        "rule": "#<Proc:0x0000ffff8e7ed770 /usr/local/rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rails-i18n-7.0.6/lib/rails_i18n/common_pluralizations/one_other.rb:7 (lambda)>"
      }
    }
  }
}

Address of proc object (for example, Proc:0x0000ffff8e7ed770) will change at any export, so webpack or another build system will rebuild assets even when no actual change in localization happened.

Also, it exposes project internals to JSON that would be publicly available in builded assets and could be a minor security issue.

v3 have a special code to filter procs: https://github.com/fnando/i18n-js/blob/4b07b30e8b6f42c4415f4bb967e8e2986a0e2268/lib/i18n/js/utils.rb#L77

How to reproduce

Export localization with bundle exec i18n export in any Rails project with rails-i18n gem included. Proc: strings would be present in exported JSON.

What do you expect

As in v3, proc rules should be cleared from exported JSON.

What happened instead

proc rules not cleared from exported JSON.

Software:

oleksii-leonov commented 1 year ago

@fnando , I could add PR with a plugin that clears procs (we use it internally in our project), code is trivially extracted from v3. But it feels like procs filtering should be in the core (due to security concerns and general use-case).

# frozen_string_literal: true

module I18nJS
  require "i18n-js/plugin"

  class FilterProcsPlugin < I18nJS::Plugin
    module Utils
      # https://github.com/fnando/i18n-js/blob/4b07b30e8b6f42c4415f4bb967e8e2986a0e2268/lib/i18n/js/utils.rb#L77
      def self.deep_remove_procs(hash)
        hash.keys.each_with_object({}) do |key, seed|
          value = hash[key]
          next if value.is_a?(Proc)

          seed[key] = value.is_a?(Hash) ? deep_remove_procs(value) : value
        end
      end
    end

    def transform(translations:)
      return translations unless enabled?

      Utils.deep_remove_procs(translations)
    end

    def setup
      I18nJS::Schema.root_keys << config_key
    end

    def validate_schema
      valid_keys = %i[enabled]

      schema.expect_required_keys(keys: valid_keys, path: [config_key])
      schema.reject_extraneous_keys(keys: valid_keys, path: [config_key])
    end
  end
end