From 820be731aa9fb0b39f48ef433175f133b70ec838 Mon Sep 17 00:00:00 2001 From: Paarth Madan Date: Wed, 8 Dec 2021 16:10:36 -0500 Subject: [PATCH] Only deep_symbolize_keys when needed When loading certain translations from file, they can be parsed into their Symbol representation. It is wasteful to traverse the entire object graph in these cases. --- lib/i18n/backend/base.rb | 33 ++++++++++++++++++++++++++------- lib/i18n/backend/gettext.rb | 2 +- lib/i18n/backend/simple.rb | 2 +- test/backend/simple_test.rb | 14 ++++++++++++++ test/test_helper.rb | 4 ++-- 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/lib/i18n/backend/base.rb b/lib/i18n/backend/base.rb index 3b8f3c2f..c9d377fb 100644 --- a/lib/i18n/backend/base.rb +++ b/lib/i18n/backend/base.rb @@ -216,6 +216,22 @@ def deep_interpolate(locale, data, values = EMPTY_HASH) end end + class LocaleDataDecorator < SimpleDelegator + def initialize(*) + super + @names_symbolized = false + end + + def mark_keys_as_symbolized! + @names_symbolized = true + self + end + + def keys_symbolized? + @names_symbolized + end + end + # Loads a single translations file by delegating to #load_rb or # #load_yml depending on the file extension and directly merges the # data to the existing translations. Raises I18n::UnknownFileType @@ -223,17 +239,18 @@ def deep_interpolate(locale, data, values = EMPTY_HASH) def load_file(filename) type = File.extname(filename).tr('.', '').downcase raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true) - data = send(:"load_#{type}", filename) - unless data.is_a?(Hash) + locale_data = send(:"load_#{type}", filename) + unless locale_data.__getobj__.is_a?(Hash) raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not') end - data.each { |locale, d| store_translations(locale, d || {}) } + locale_data.each { |locale, d| store_translations(locale, d || {}, skip_symbolize_keys: locale_data.keys_symbolized?) } end # Loads a plain Ruby translations file. eval'ing the file must yield # a Hash containing translation data with locales as toplevel keys. def load_rb(filename) - eval(IO.read(filename), binding, filename) + translations = eval(IO.read(filename), binding, filename) + LocaleDataDecorator.new(translations) end # Loads a YAML translations file. The data must have locales as @@ -241,9 +258,9 @@ def load_rb(filename) def load_yml(filename) begin if YAML.respond_to?(:unsafe_load_file) # Psych 4.0 way - YAML.unsafe_load_file(filename) + LocaleDataDecorator.new(YAML.unsafe_load_file(filename)) else - YAML.load_file(filename) + LocaleDataDecorator.new(YAML.load_file(filename)) end rescue TypeError, ScriptError, StandardError => e raise InvalidLocaleData.new(filename, e.inspect) @@ -255,7 +272,9 @@ def load_yml(filename) # toplevel keys. def load_json(filename) begin - ::JSON.parse(File.read(filename), symbolize_names: true, freeze: true) + LocaleDataDecorator.new( + ::JSON.parse(File.read(filename), symbolize_names: true, freeze: true), + ).mark_keys_as_symbolized! rescue TypeError, StandardError => e raise InvalidLocaleData.new(filename, e.inspect) end diff --git a/lib/i18n/backend/gettext.rb b/lib/i18n/backend/gettext.rb index 72d20f06..61b193c4 100644 --- a/lib/i18n/backend/gettext.rb +++ b/lib/i18n/backend/gettext.rb @@ -43,7 +43,7 @@ def set_comment(msgid_or_sym, comment) def load_po(filename) locale = ::File.basename(filename, '.po').to_sym data = normalize(locale, parse(filename)) - { locale => data } + Backend::Base::LocaleDataDecorator.new({ locale => data }) end def parse(filename) diff --git a/lib/i18n/backend/simple.rb b/lib/i18n/backend/simple.rb index 0dde82d7..4e75c23f 100644 --- a/lib/i18n/backend/simple.rb +++ b/lib/i18n/backend/simple.rb @@ -40,7 +40,7 @@ def store_translations(locale, data, options = EMPTY_HASH) end locale = locale.to_sym translations[locale] ||= Concurrent::Hash.new - data = data.deep_symbolize_keys + data = data.deep_symbolize_keys unless options.fetch(:skip_symbolize_keys, false) translations[locale].deep_merge!(data) end diff --git a/test/backend/simple_test.rb b/test/backend/simple_test.rb index cbf3541a..32fc1ea5 100644 --- a/test/backend/simple_test.rb +++ b/test/backend/simple_test.rb @@ -140,6 +140,20 @@ def setup assert_equal 'foo', I18n.t(:'1') end + test "simple store_translations: store translations doesn't deep symbolize keys if skip_symbolize_keys is true" do + data = { :foo => {'bar' => 'barfr', 'baz' => 'bazfr'} } + + # symbolized by default + store_translations(:fr, data) + assert_equal Hash[:foo, {:bar => 'barfr', :baz => 'bazfr'}], translations[:fr] + + I18n.backend.reload! + + # not deep symbolized when configured + store_translations(:fr, data, skip_symbolize_keys: true) + assert_equal Hash[:foo, {'bar' => 'barfr', 'baz' => 'bazfr'}], translations[:fr] + end + # reloading translations test "simple reload_translations: unloads translations" do diff --git a/test/test_helper.rb b/test/test_helper.rb index 1f4217e5..1b8daba3 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -45,8 +45,8 @@ def translations I18n.backend.instance_variable_get(:@translations) end - def store_translations(locale, data) - I18n.backend.store_translations(locale, data) + def store_translations(locale, data, options = I18n::EMPTY_HASH) + I18n.backend.store_translations(locale, data, options) end def locales_dir