Skip to content

Commit

Permalink
Only deep_symbolize_keys when needed
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
paarthmadan committed Dec 8, 2021
1 parent ad41bdc commit 820be73
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 11 deletions.
33 changes: 26 additions & 7 deletions lib/i18n/backend/base.rb
Expand Up @@ -216,34 +216,51 @@ 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
# for all other file extensions.
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
# toplevel keys.
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)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/i18n/backend/gettext.rb
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/i18n/backend/simple.rb
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions test/backend/simple_test.rb
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions test/test_helper.rb
Expand Up @@ -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
Expand Down

0 comments on commit 820be73

Please sign in to comment.