Skip to content

Commit

Permalink
Raise invalid files in test, ensure file only loads for one locale
Browse files Browse the repository at this point in the history
  • Loading branch information
paarthmadan committed Feb 2, 2022
1 parent 6847747 commit 8a633b8
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 33 deletions.
67 changes: 44 additions & 23 deletions lib/i18n/backend/lazy_loadable.rb
Expand Up @@ -36,7 +36,7 @@ module Backend
#
# The backend has two working modes: lazy_load and eager_load.
#
# We recommend enabling this to true in test environments only.
# Note: This backend should only be enabled in in test environments!
# When the mode is set to false, the backend behaves exactly like the
# Simple backend, with an additional check that the paths being loaded
# abide by the format. If paths can't be matched to the format, an error is raised.
Expand All @@ -48,7 +48,7 @@ module Backend
#
# I18n.backend = I18n::Backend::LazyLoadable.new(lazy_load: true)
#
# # In other environments, such as Prod and CI
# # In other environments, such as production and CI
#
# I18n.backend = I18n::Backend::LazyLoadable.new(lazy_load: false) # default
#
Expand Down Expand Up @@ -95,16 +95,6 @@ def eager_load!
end
end

# Select all files from I18n load path that belong to current locale.
# These files must start with the locale identifier (ie. "en", "pt-BR"),
# followed by an "_" demarcation to separate proceeding text.
def filenames_for_current_locale
I18n.load_path.flatten.select do |path|
LocaleExtractor.locale_from_path(path) == I18n.locale &&
supported_extension?(path)
end
end

# Parse the load path and extract all locales.
def available_locales
if lazy_load?
Expand All @@ -116,16 +106,18 @@ def available_locales

protected


# Load translations from files that belong to the current locale.
def init_translations
if lazy_load?
load_translations(filenames_for_current_locale)
file_errors = if lazy_load?
initialized_locales[I18n.locale] = true
load_translations_and_collect_file_errors(filenames_for_current_locale)
else
super
filenames_named_incorrectly = I18n.load_path.reject { |path| file_named_correctly?(path) }
raise InvalidFilenames.new(filenames_named_incorrectly) unless filenames_named_incorrectly.empty?
@initialized = true
load_translations_and_collect_file_errors(I18n.load_path)
end

raise InvalidFilenames.new(file_errors) unless file_errors.empty?
end

def initialized_locales
Expand All @@ -138,15 +130,44 @@ def lazy_load?
@lazy_load
end

SUPPORTED_EXTENSIONS = [".yml", ".yaml", ".po", ".json", ".rb"].freeze
class FilenameIncorrect < StandardError
def initialize(file, expected_locale, unexpected_locales)
super "#{file} can only load translations for \"#{expected_locale}\". Found translations for: #{unexpected_locales}."
end
end

# Loads each file supplied and asserts that the file only loads
# translations as expected by the name. The method returns a list of
# errors corresponding to offending files.
def load_translations_and_collect_file_errors(files)
errors = []

load_translations(files) do |file, loaded_translations|
assert_file_named_correctly!(file, loaded_translations)
rescue FilenameIncorrect => e
errors << e
end

def supported_extension?(path)
path.end_with?(*SUPPORTED_EXTENSIONS)
errors
end

def file_named_correctly?(path)
extracted_locale = LocaleExtractor.locale_from_path(path)
available_locales.include?(extracted_locale)
# Select all files from I18n load path that belong to current locale.
# These files must start with the locale identifier (ie. "en", "pt-BR"),
# followed by an "_" demarcation to separate proceeding text.
def filenames_for_current_locale
I18n.load_path.flatten.select do |path|
LocaleExtractor.locale_from_path(path) == I18n.locale
end
end

# Checks if a filename is named in correspondence to the translations it loaded.
# The locale extracted from the path must be the single locale loaded in the translations.
def assert_file_named_correctly!(file, translations)
loaded_locales = translations.keys.map(&:to_sym)
expected_locale = LocaleExtractor.locale_from_path(file)
unexpected_locales = loaded_locales.reject { |locale| locale == expected_locale }

raise FilenameIncorrect.new(file, expected_locale, unexpected_locales) unless unexpected_locales.empty?
end
end
end
Expand Down
24 changes: 20 additions & 4 deletions lib/i18n/exceptions.rb
Expand Up @@ -121,10 +121,26 @@ def initialize(method, backend_klass)
end

class InvalidFilenames < ArgumentError
attr_reader :invalid_filenames

def initialize(invalid_filenames)
super "Locales cannot be extracted from the following paths: #{invalid_filenames}"
NUMBER_OF_ERRORS_SHOWN = 20
def initialize(file_errors)
super <<~MSG
Found #{file_errors.count} error(s).
The first #{[file_errors.count, NUMBER_OF_ERRORS_SHOWN].min} error(s):
#{file_errors.map(&:message).first(NUMBER_OF_ERRORS_SHOWN).join("\n")}
To use the LazyLoadable backend:
1. Filenames must start with the locale.
2. An underscore must separate the locale with any optional text that follows.
3. The file must only contain translation data for the single locale.
Example:
"/config/locales/fr.yml" which contains:
```yml
fr:
dog:
chien
```
MSG
end
end
end
4 changes: 2 additions & 2 deletions lib/i18n/tests/basics.rb
Expand Up @@ -9,7 +9,7 @@ def teardown
I18n.backend.store_translations('de', :foo => 'bar')
I18n.backend.store_translations('en', :foo => 'foo')

assert_equal I18n.backend.available_locales, I18n.available_locales
assert_equal I18n.available_locales, I18n.backend.available_locales
end

test "available_locales can be set to something else independently from the actual locale data" do
Expand All @@ -23,7 +23,7 @@ def teardown
assert_equal [:foo, :bar], I18n.available_locales

I18n.available_locales = nil
assert_equal I18n.backend.available_locales, I18n.available_locales
assert_equal I18n.available_locales, I18n.backend.available_locales
end

test "available_locales memoizes when set explicitely" do
Expand Down
48 changes: 44 additions & 4 deletions test/backend/lazy_loadable_test.rb
Expand Up @@ -73,13 +73,12 @@ def setup
end
end

test "lazy mode: loads translations from files that start with current locale identifier or contain identifier in path components, and end with a supported extension" do
test "lazy mode: loads translations from files that start with current locale identifier" do
with_lazy_mode do
file_contents = { en: { alice: "bob" } }.to_yaml

invalid_files = [
{ filename: ['translation', '.yml'] }, # No locale identifier
{ filename: ['en_translation', '.unsupported'] }, # Unsupported extension
{ filename: ['translation', '.unsupported'] }, # No locale identifier and unsupported extension
]

Expand All @@ -104,13 +103,52 @@ def setup
end
end

test "lazy mode: files with unsupported extensions raise UnknownFileType error" do
with_lazy_mode do
file_contents = { en: { alice: "bob" } }.to_yaml
filename = ['en_translation', '.unsupported'] # Correct locale identifier, but unsupported extension

with_translation_file_in_load_path(filename, nil, file_contents) do
assert_raises(I18n::UnknownFileType) { I18n.t("foo.bar") }
end
end
end

test "lazy mode: #available_locales returns all locales available from load path irrespective of current locale" do
with_lazy_mode do
I18n.with_locale(:en) { assert_equal [:en, :fr], @backend.available_locales }
I18n.with_locale(:fr) { assert_equal [:en, :fr], @backend.available_locales }
end
end

test "lazy mode: raises error if translations loaded don't correspond to locale extracted from filename" do
filename = ["en_", ".yml"]
file_contents = { fr: { dog: "chien" } }.to_yaml

with_lazy_mode do
with_translation_file_in_load_path(filename, nil, file_contents) do |file_path|
exception = assert_raises(I18n::InvalidFilenames) { I18n.t("foo.bar") }

expected_message = /#{Regexp.escape(file_path)} can only load translations for "en"\. Found translations for: \[\:fr\]/
assert_match expected_message, exception.message
end
end
end

test "lazy mode: raises error if translations for more than one locale are loaded from a single file" do
filename = ["en_", ".yml"]
file_contents = { en: { alice: "bob" }, fr: { dog: "chien" }, de: { cat: 'katze' } }.to_yaml

with_lazy_mode do
with_translation_file_in_load_path(filename, nil, file_contents) do |file_path|
exception = assert_raises(I18n::InvalidFilenames) { I18n.t("foo.bar") }

expected_message = /#{Regexp.escape(file_path)} can only load translations for "en"\. Found translations for: \[\:fr\, \:de\]/
assert_match expected_message, exception.message
end
end
end

test "eager mode: load all translations, irrespective of locale" do
with_eager_mode do
@backend.reload!
Expand All @@ -128,9 +166,11 @@ def setup

contents = { de: { cat: 'katze' } }.to_yaml

with_translation_file_in_load_path(['translation', '.yml'], nil, contents) do |file_path|
with_translation_file_in_load_path(['fr_translation', '.yml'], nil, contents) do |file_path|
exception = assert_raises(I18n::InvalidFilenames) { I18n.t("foo.bar") }
assert_equal "Locales cannot be extracted from the following paths: #{[file_path]}", exception.message

expected_message = /#{Regexp.escape(file_path)} can only load translations for "fr"\. Found translations for: \[\:de\]/
assert_match expected_message, exception.message
end
end
end
Expand Down

0 comments on commit 8a633b8

Please sign in to comment.