-
-
Notifications
You must be signed in to change notification settings - Fork 411
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #612 from Shopify/pm/lazy-loadable-backend
LazyLoadable Backend
- Loading branch information
Showing
8 changed files
with
479 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
# frozen_string_literal: true | ||
|
||
module I18n | ||
module Backend | ||
# Backend that lazy loads translations based on the current locale. This | ||
# implementation avoids loading all translations up front. Instead, it only | ||
# loads the translations that belong to the current locale. This offers a | ||
# performance incentive in local development and test environments for | ||
# applications with many translations for many different locales. It's | ||
# particularly useful when the application only refers to a single locales' | ||
# translations at a time (ex. A Rails workload). The implementation | ||
# identifies which translation files from the load path belong to the | ||
# current locale by pattern matching against their path name. | ||
# | ||
# Specifically, a translation file is considered to belong to a locale if: | ||
# a) the filename is in the I18n load path | ||
# b) the filename ends in a supported extension (ie. .yml, .json, .po, .rb) | ||
# c) the filename starts with the locale identifier | ||
# d) the locale identifier and optional proceeding text is separated by an underscore, ie. "_". | ||
# | ||
# Examples: | ||
# Valid files that will be selected by this backend: | ||
# | ||
# "files/locales/en_translation.yml" (Selected for locale "en") | ||
# "files/locales/fr.po" (Selected for locale "fr") | ||
# | ||
# Invalid files that won't be selected by this backend: | ||
# | ||
# "files/locales/translation-file" | ||
# "files/locales/en-translation.unsupported" | ||
# "files/locales/french/translation.yml" | ||
# "files/locales/fr/translation.yml" | ||
# | ||
# The implementation uses this assumption to defer the loading of | ||
# translation files until the current locale actually requires them. | ||
# | ||
# The backend has two working modes: lazy_load and eager_load. | ||
# | ||
# Note: This backend should only be enabled 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. | ||
# | ||
# You can configure lazy loaded backends through the initializer or backends | ||
# accessor: | ||
# | ||
# # In test environments | ||
# | ||
# I18n.backend = I18n::Backend::LazyLoadable.new(lazy_load: true) | ||
# | ||
# # In other environments, such as production and CI | ||
# | ||
# I18n.backend = I18n::Backend::LazyLoadable.new(lazy_load: false) # default | ||
# | ||
class LocaleExtractor | ||
class << self | ||
def locale_from_path(path) | ||
name = File.basename(path, ".*") | ||
locale = name.split("_").first | ||
locale.to_sym unless locale.nil? | ||
end | ||
end | ||
end | ||
|
||
class LazyLoadable < Simple | ||
def initialize(lazy_load: false) | ||
@lazy_load = lazy_load | ||
end | ||
|
||
# Returns whether the current locale is initialized. | ||
def initialized? | ||
if lazy_load? | ||
initialized_locales[I18n.locale] | ||
else | ||
super | ||
end | ||
end | ||
|
||
# Clean up translations and uninitialize all locales. | ||
def reload! | ||
if lazy_load? | ||
@initialized_locales = nil | ||
@translations = nil | ||
else | ||
super | ||
end | ||
end | ||
|
||
# Eager loading is not supported in the lazy context. | ||
def eager_load! | ||
if lazy_load? | ||
raise UnsupportedMethod.new(__method__, self.class, "Cannot eager load translations because backend was configured with lazy_load: true.") | ||
else | ||
super | ||
end | ||
end | ||
|
||
# Parse the load path and extract all locales. | ||
def available_locales | ||
if lazy_load? | ||
I18n.load_path.map { |path| LocaleExtractor.locale_from_path(path) } | ||
else | ||
super | ||
end | ||
end | ||
|
||
def lookup(locale, key, scope = [], options = EMPTY_HASH) | ||
if lazy_load? | ||
I18n.with_locale(locale) do | ||
super | ||
end | ||
else | ||
super | ||
end | ||
end | ||
|
||
protected | ||
|
||
|
||
# Load translations from files that belong to the current locale. | ||
def init_translations | ||
file_errors = if lazy_load? | ||
initialized_locales[I18n.locale] = true | ||
load_translations_and_collect_file_errors(filenames_for_current_locale) | ||
else | ||
@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 | ||
@initialized_locales ||= Hash.new(false) | ||
end | ||
|
||
private | ||
|
||
def lazy_load? | ||
@lazy_load | ||
end | ||
|
||
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 | ||
|
||
errors | ||
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 | ||
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 | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
require 'test_helper' | ||
|
||
class I18nLazyLoadableBackendApiTest < I18n::TestCase | ||
def setup | ||
I18n.backend = I18n::Backend::LazyLoadable.new | ||
super | ||
end | ||
|
||
include I18n::Tests::Basics | ||
include I18n::Tests::Defaults | ||
include I18n::Tests::Interpolation | ||
include I18n::Tests::Link | ||
include I18n::Tests::Lookup | ||
include I18n::Tests::Pluralization | ||
include I18n::Tests::Procs | ||
include I18n::Tests::Localization::Date | ||
include I18n::Tests::Localization::DateTime | ||
include I18n::Tests::Localization::Time | ||
include I18n::Tests::Localization::Procs | ||
|
||
test "make sure we use the LazyLoadable backend" do | ||
assert_equal I18n::Backend::LazyLoadable, I18n.backend.class | ||
end | ||
end |
Oops, something went wrong.