use per-language instead of per-scope translation files

refs FOO-2720
refs DE-1022

Currently, translations are compiled on a per-scope level. The primary problem by doing this is that each generated scope file contains the translations for all languages, leading users to needing to download translations for languages they will never use.

Instead, we now generate a single translations file per language. This file is loaded and cached by the browser at the beginning of page load.

[change-merged]
[build-registry-path=jenkins/canvas-lms/de-1022]

Change-Id: I64b0d054b04e3d81bb7263650481d1d3fe9a4868
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/284285
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Ahmad Amireh <ahmad@instructure.com>
QA-Review: Ahmad Amireh <ahmad@instructure.com>
Product-Review: Ahmad Amireh <ahmad@instructure.com>
This commit is contained in:
Aaron Ogata 2022-02-16 08:12:16 -08:00
parent d3ba157e43
commit 1e8f45e3ed
11 changed files with 252 additions and 98 deletions

3
.gitignore vendored
View File

@ -40,7 +40,8 @@ mkmf.log
npm-debug.log
/public/dist/
/public/doc/api/
/public/javascripts/translations/
/public/javascripts/translations/*
!/public/javascripts/translations/_core_en.js
/storybook-static/
/tmp/*
!/tmp/.keep

View File

@ -21,27 +21,52 @@ module I18nTasks
module Utils
CORE_KEYS = %i[date time number datetime support].freeze
# From https://stackoverflow.com/a/64981412
def self.deep_compact(hash)
res_hash = hash.map do |key, value|
value = deep_compact(value) if value.is_a?(Hash)
value = nil if [{}, []].include?(value)
[key, value]
# From https://stackoverflow.com/questions/4364891
def self.flat_keys_to_nested(hash)
hash.each_with_object({}) do |(key,value), all|
key_parts = key.split('.').map!(&:to_sym)
leaf = key_parts[0...-1].inject(all) { |h, k| h[k] ||= {} }
leaf[key_parts.last] = value
end
res_hash.to_h.compact
end
def self.dump_js(translations)
<<~JS.gsub(/^ {8}/, "")
// this file was auto-generated by rake i18n:generate_js.
// you probably shouldn't edit it directly
import mergeI18nTranslations from '@canvas/i18n/mergeI18nTranslations.js';
def self.partition_hash(hash)
true_hash = {}
false_hash = {}
// we use JSON.parse here instead of just loading it as a javascript object literal
// because according to https://v8.dev/blog/cost-of-javascript-2019#json that is faster
mergeI18nTranslations(JSON.parse(#{deep_compact(translations).to_ordered.to_json.inspect}));
hash.each_with_object([true_hash, false_hash]) { |(k, v), (true_hash, false_hash)|
yield(k, v) ? true_hash[k] = v : false_hash[k] = v
}
[true_hash, false_hash]
end
def self.eager_translations_js(locale, root_translations)
return nil unless root_translations.count > 0
<<~JS
setRootTranslations("#{locale}", function() { return #{root_translations.to_ordered.to_json} })
JS
end
def self.lazy_translations_js(locale, scope, root_translations, nested_translations)
return nil unless root_translations.count > 0 || nested_translations.count > 0
root_translations_arg = root_translations.count > 0 ?
"function() { return #{root_translations.to_ordered.to_json} }" :
"null"
nested_translations_arg = nested_translations.count > 0 ?
"function() { return #{nested_translations.to_ordered.to_json} }" :
"null"
<<~JS
setLazyTranslations(
"#{locale}",
"#{scope}",
#{root_translations_arg},
#{nested_translations_arg}
)
JS
end
end

View File

@ -136,32 +136,104 @@ namespace :i18n do
end
file_translations = JSON.parse(File.read("config/locales/generated/js_bundles.json"))
dump_translations = lambda do |translation_name, translations|
file = "public/javascripts/translations/#{translation_name}.js"
content = I18nTasks::Utils.dump_js(translations)
dump_translations = lambda do |filename, content|
file = "public/javascripts/translations/#{filename}.js"
if !File.exist?(file) || File.read(file) != content
File.open(file, "w") { |f| f.write content }
end
end
file_translations.each do |scope, keys|
translations = {}
locales.each do |locale|
translations.update flat_translations.slice(*keys.map { |k| k.gsub(/\A/, "#{locale}.") })
translation_for_key = lambda do |locale, key|
try_locales = [locale.to_s, locale.to_s.split('-')[0], 'en'].uniq
try_locales.each do |try_locale|
return [key, flat_translations["#{try_locale}.#{key}"]] if flat_translations.has_key?("#{try_locale}.#{key}")
end
dump_translations.call(scope, translations.expand_keys)
# puts "key #{key} not found for locale #{locale} or possible fallbacks #{try_locales.to_sentence}"
nil
end
# in addition to getting the non-en stuff into each scope_file, we need to get the core
# formats and stuff for all languages (en included) into the common scope_file
core_translations = I18n.available_locales.each_with_object({}) do |locale, h1|
h1[locale.to_s] = all_translations[locale].slice(*I18nTasks::Utils::CORE_KEYS)
# Compute common metadata for all locales
key_counts = {}
scopes = {}
unique_key_scopes = {}
file_translations.map do |scope, keys|
stripped_scope = scope.split('.')[0]
keys.each do |key|
key_counts[key] = (key_counts[key] || 0) + 1
unique_key_scopes[key] = stripped_scope
scopes[stripped_scope] = true
scopes[key.split('.')[0]] = true if key.count('.') > 0
end
end
# defaults to be used in environments where the translations aren't loaded:
dump_translations.call("_core_en", core_translations.slice("en"))
# a set of the core keys for all supported locales:
dump_translations.call("_core", core_translations)
flat_translations.map do |key, translation|
I18nTasks::Utils::CORE_KEYS.map do |scope|
stripped_key = key.split(".", 2)[1] # remove the language prefix
next unless stripped_key.start_with?(scope.to_s)
key_counts[stripped_key] = (key_counts[stripped_key] || 0) + 1
unique_key_scopes[stripped_key] = scope
scopes[scope.to_s] = true
end
end
# 1. Find all of the keys that are used more than once, they will be stored at the root-level and always loaded.
# 2. Find the best translation for the given key.
common_keys, unique_keys = I18nTasks::Utils.partition_hash(key_counts) { |k, v| v > 1 }
puts "Common Keys: #{common_keys.count}"
puts "Unique Keys: #{unique_keys.count}"
eager_common_keys, lazy_common_keys = I18nTasks::Utils.partition_hash(common_keys) { |k, v| k.count('.') == 0 }
lazy_unique_root_keys, lazy_unique_nested_keys = I18nTasks::Utils.partition_hash(unique_keys) { |k, v| k.count('.') == 0 }
# 3. All common, non-nested translations will be loaded immediately.
# 4. All other translations will be loaded when their scope is first accessed.
eager_root_keys = eager_common_keys
lazy_nested_keys = lazy_common_keys.merge(lazy_unique_nested_keys)
lazy_root_keys = lazy_unique_root_keys
lazy_root_keys_by_scope = lazy_root_keys.each_with_object(Hash.new { |hash, k| hash[k] = [] }) { |(k, v), obj| obj[unique_key_scopes[k]] << k }
puts "Eager-Loaded Keys: #{eager_root_keys.count}"
puts "Lazy-Loaded Root Keys: #{lazy_root_keys.count}"
puts "Lazy-Loaded Nested Keys: #{lazy_nested_keys.count}"
base_translations_js = "import { setRootTranslations, setLazyTranslations } from '@canvas/i18n/mergeI18nTranslations.js';"
locales.each do |locale|
puts "Generating JS for #{locale}"
eager_root_translations = eager_root_keys.filter_map { |k, v| translation_for_key.call(locale, k) }.to_h
lazy_nested_translations_by_scope = I18nTasks::Utils.flat_keys_to_nested(lazy_nested_keys.filter_map { |k, v| translation_for_key.call(locale, k) }.to_h)
lazy_root_translations_by_scope = lazy_root_keys_by_scope.map { |scope, keys| [scope, keys.filter_map { |k| translation_for_key.call(locale, k) }.to_h] }.to_h
common_translations_js = I18nTasks::Utils.eager_translations_js(locale, eager_root_translations)
translations_js = scopes.map do |scope, _unused|
lazy_root_translations = lazy_root_translations_by_scope.fetch(scope, {})
lazy_scoped_translations = lazy_nested_translations_by_scope.fetch(scope.to_sym, {})
I18nTasks::Utils.lazy_translations_js(locale, scope, lazy_root_translations, lazy_scoped_translations)
end
dump_translations.call locale, [base_translations_js, common_translations_js, *translations_js].compact.join("\n\n")
if locale == :en
puts "Generating Default JS"
core_translations_js = I18nTasks::Utils::CORE_KEYS.map do |scope|
lazy_scoped_translations = lazy_nested_translations_by_scope.fetch(scope.to_sym, {})
I18nTasks::Utils.lazy_translations_js(locale, scope, {}, lazy_scoped_translations)
end
dump_translations.call '_core_en', [base_translations_js, core_translations_js].compact.join("\n\n")
end
end
end
desc "Generate the pseudo-translation file lolz"

View File

@ -1,7 +1,40 @@
// this file was auto-generated by rake i18n:generate_js.
// you probably shouldn't edit it directly
import mergeI18nTranslations from '@canvas/i18n/mergeI18nTranslations.js';
import { setRootTranslations, setLazyTranslations } from '@canvas/i18n/mergeI18nTranslations.js';
// we use JSON.parse here instead of just loading it as a javascript object literal
// because according to https://v8.dev/blog/cost-of-javascript-2019#json that is faster
mergeI18nTranslations(JSON.parse("{\"en\":{\"date\":{\"abbr_day_names\":[\"Sun\",\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\"],\"abbr_month_names\":[null,\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"],\"datepicker\":{\"column_headings\":[\"Su\",\"Mo\",\"Tu\",\"We\",\"Th\",\"Fr\",\"Sa\"]},\"day_names\":[\"Sunday\",\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\"],\"days\":{\"today\":\"Today\",\"today_lower\":\"today\",\"tomorrow\":\"Tomorrow\",\"yesterday\":\"Yesterday\"},\"formats\":{\"date_at_time\":\"%b %-d at %l:%M%P\",\"default\":\"%Y-%m-%d\",\"full\":\"%b %-d, %Y %-l:%M%P\",\"full_with_weekday\":\"%a %b %-d, %Y %-l:%M%P\",\"long\":\"%B %-d, %Y\",\"long_with_weekday\":\"%A, %B %-d\",\"medium\":\"%b %-d, %Y\",\"medium_month\":\"%b %Y\",\"medium_with_weekday\":\"%a %b %-d, %Y\",\"short\":\"%b %-d\",\"short_month\":\"%b\",\"short_weekday\":\"%a\",\"short_with_weekday\":\"%a, %b %-d\",\"weekday\":\"%A\"},\"month_names\":[null,\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],\"order\":[\"year\",\"month\",\"day\"]},\"datetime\":{\"distance_in_words\":{\"about_x_hours\":{\"one\":\"about 1 hour\",\"other\":\"about %{count} hours\"},\"about_x_months\":{\"one\":\"about 1 month\",\"other\":\"about %{count} months\"},\"about_x_years\":{\"one\":\"about 1 year\",\"other\":\"about %{count} years\"},\"almost_x_years\":{\"one\":\"almost 1 year\",\"other\":\"almost %{count} years\"},\"half_a_minute\":\"half a minute\",\"less_than_x_minutes\":{\"one\":\"less than a minute\",\"other\":\"less than %{count} minutes\"},\"less_than_x_seconds\":{\"one\":\"less than 1 second\",\"other\":\"less than %{count} seconds\"},\"over_x_years\":{\"one\":\"over 1 year\",\"other\":\"over %{count} years\"},\"x_days\":{\"one\":\"1 day\",\"other\":\"%{count} days\"},\"x_minutes\":{\"one\":\"1 minute\",\"other\":\"%{count} minutes\"},\"x_months\":{\"one\":\"1 month\",\"other\":\"%{count} months\"},\"x_seconds\":{\"one\":\"1 second\",\"other\":\"%{count} seconds\"}},\"prompts\":{\"day\":\"Day\",\"hour\":\"Hour\",\"minute\":\"Minute\",\"month\":\"Month\",\"second\":\"Seconds\",\"year\":\"Year\"}},\"number\":{\"currency\":{\"format\":{\"delimiter\":\",\",\"format\":\"%u%n\",\"precision\":2,\"separator\":\".\",\"significant\":false,\"strip_insignificant_zeros\":false,\"unit\":\"$\"}},\"format\":{\"delimiter\":\",\",\"precision\":3,\"separator\":\".\",\"significant\":false,\"strip_insignificant_zeros\":false},\"human\":{\"decimal_units\":{\"format\":\"%n %u\",\"units\":{\"billion\":\"Billion\",\"million\":\"Million\",\"quadrillion\":\"Quadrillion\",\"thousand\":\"Thousand\",\"trillion\":\"Trillion\",\"unit\":\"\"}},\"format\":{\"delimiter\":\"\",\"precision\":3,\"significant\":true,\"strip_insignificant_zeros\":true},\"storage_units\":{\"format\":\"%n %u\",\"units\":{\"byte\":{\"one\":\"Byte\",\"other\":\"Bytes\"},\"eb\":\"EB\",\"gb\":\"GB\",\"kb\":\"KB\",\"mb\":\"MB\",\"pb\":\"PB\",\"tb\":\"TB\"}}},\"nth\":{\"ordinalized\":{},\"ordinals\":{}},\"percentage\":{\"format\":{\"delimiter\":\"\",\"format\":\"%n%\"}},\"precision\":{\"format\":{\"delimiter\":\"\"}}},\"support\":{\"array\":{\"last_word_connector\":\", and \",\"or\":{\"last_word_connector\":\", or \",\"two_words_connector\":\" or \"},\"two_words_connector\":\" and \",\"words_connector\":\", \"},\"help_menu\":{\"community_support_description\":\"Interact with and get assistance from your peers.\",\"community_support_forums\":\"Community Support Forums\",\"global_support_desk\":\"NetAcad Support\",\"global_support_desk_description\":\"Our ASCs are your first line of support, and can connect you with our Global Support Desk for issues that require additional assistance.\",\"networking_academy_description\":\"View Cisco answers to the most commonly asked questions.\",\"networking_academy_faqs\":\"Networking Academy FAQs\",\"student_support\":\"NetAcad Support - Student\",\"student_support_description\":\"Your Instructor should be your first point of contact. They can answer your Networking Academy questions or contact the support desk for you. You can contact your instructor by using the Inbox feature.\",\"support_desk_livechat\":\"Support Desk Live Chat\"}},\"time\":{\"am\":\"am\",\"formats\":{\"default\":\"%a, %d %b %Y %H:%M:%S %z\",\"long\":\"%B %d, %Y %H:%M\",\"short\":\"%d %b %H:%M\",\"tiny\":\"%l:%M%P\",\"tiny_on_the_hour\":\"%l%P\"},\"pm\":\"pm\"}}}"));
setLazyTranslations(
"en",
"date",
null,
function() { return {"abbr_day_names":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"abbr_month_names":[null,"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"datepicker":{"column_headings":["Su","Mo","Tu","We","Th","Fr","Sa"]},"day_names":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"days":{"today":"Today","today_lower":"today","tomorrow":"Tomorrow","yesterday":"Yesterday"},"formats":{"date_at_time":"%b %-d at %l:%M%P","default":"%Y-%m-%d","full":"%b %-d, %Y %-l:%M%P","full_with_weekday":"%a %b %-d, %Y %-l:%M%P","long":"%B %-d, %Y","long_with_weekday":"%A, %B %-d","medium":"%b %-d, %Y","medium_month":"%b %Y","medium_with_weekday":"%a %b %-d, %Y","short":"%b %-d","short_month":"%b","short_weekday":"%a","short_with_weekday":"%a, %b %-d","weekday":"%A"},"month_names":[null,"January","February","March","April","May","June","July","August","September","October","November","December"],"order":["year","month","day"]} }
)
setLazyTranslations(
"en",
"time",
null,
function() { return {"am":"am","formats":{"default":"%a, %d %b %Y %H:%M:%S %z","long":"%B %d, %Y %H:%M","short":"%d %b %H:%M","tiny":"%l:%M%P","tiny_on_the_hour":"%l%P"},"pm":"pm"} }
)
setLazyTranslations(
"en",
"number",
null,
function() { return {"currency":{"format":{"delimiter":",","format":"%u%n","precision":2,"separator":".","significant":false,"strip_insignificant_zeros":false,"unit":"$"}},"format":{"delimiter":",","precision":3,"separator":".","significant":false,"strip_insignificant_zeros":false},"human":{"decimal_units":{"format":"%n %u","units":{"billion":"Billion","million":"Million","quadrillion":"Quadrillion","thousand":"Thousand","trillion":"Trillion","unit":""}},"format":{"delimiter":"","precision":3,"significant":true,"strip_insignificant_zeros":true},"storage_units":{"format":"%n %u","units":{"byte":{"one":"Byte","other":"Bytes"},"eb":"EB","gb":"GB","kb":"KB","mb":"MB","pb":"PB","tb":"TB"}}},"nth":{"ordinalized":{},"ordinals":{}},"percentage":{"format":{"delimiter":"","format":"%n%"}},"precision":{"format":{"delimiter":""}}} }
)
setLazyTranslations(
"en",
"datetime",
null,
function() { return {"distance_in_words":{"about_x_hours":{"one":"about 1 hour","other":"about %{count} hours"},"about_x_months":{"one":"about 1 month","other":"about %{count} months"},"about_x_years":{"one":"about 1 year","other":"about %{count} years"},"almost_x_years":{"one":"almost 1 year","other":"almost %{count} years"},"half_a_minute":"half a minute","less_than_x_minutes":{"one":"less than a minute","other":"less than %{count} minutes"},"less_than_x_seconds":{"one":"less than 1 second","other":"less than %{count} seconds"},"over_x_years":{"one":"over 1 year","other":"over %{count} years"},"x_days":{"one":"1 day","other":"%{count} days"},"x_minutes":{"one":"1 minute","other":"%{count} minutes"},"x_months":{"one":"1 month","other":"%{count} months"},"x_seconds":{"one":"1 second","other":"%{count} seconds"}},"prompts":{"day":"Day","hour":"Hour","minute":"Minute","month":"Month","second":"Seconds","year":"Year"}} }
)
setLazyTranslations(
"en",
"support",
null,
function() { return {"array":{"last_word_connector":", and ","or":{"last_word_connector":", or ","two_words_connector":" or "},"two_words_connector":" and ","words_connector":", "},"help_menu":{"community_support_description":"Interact with and get assistance from your peers.","community_support_forums":"Community Support Forums","global_support_desk":"NetAcad Support","global_support_desk_description":"Our ASCs are your first line of support, and can connect you with our Global Support Desk for issues that require additional assistance.","networking_academy_description":"View Cisco answers to the most commonly asked questions.","networking_academy_faqs":"Networking Academy FAQs","student_support":"NetAcad Support - Student","student_support_description":"Your Instructor should be your first point of contact. They can answer your Networking Academy questions or contact the support desk for you. You can contact your instructor by using the Inbox feature.","support_desk_livechat":"Support Desk Live Chat"}} }
)

View File

@ -20,15 +20,23 @@
describe I18n do
context "_core_en.js" do
# HINT: if this spec fails, run `rake i18n:generate_js`...
# it probably means you added a format or a new language
it "is up-to-date" do
skip("Rails 6.0 specific") unless CANVAS_RAILS6_0
translations = { "en" => I18n.backend.send(:translations)[:en].slice(*I18nTasks::Utils::CORE_KEYS) }
# HINT: if this spec fails, run `rake i18n:generate_js`...
# it probably means you added a format or a new language
expect(File.read("public/javascripts/translations/_core_en.js")).to eq(
I18nTasks::Utils.dump_js(translations)
)
file_contents = File.read("public/javascripts/translations/_core_en.js")
translations = I18n.backend.send(:translations)[:en].slice(*I18nTasks::Utils::CORE_KEYS)
# It's really slow to actually re-generate this file through the full path due to the
# computations that need to happen so let's do the next best thing and just check that
# the expected scopes exist and are rendered correctly.
translations.each do |scope, scope_translations|
expected_translations_js = I18nTasks::Utils.lazy_translations_js('en', scope, {}, scope_translations)
expect(file_contents).to include(expected_translations_js)
end
end
end

View File

@ -44,30 +44,8 @@ module.exports.pitch = function(remainingRequest, precedingRequest, data) {
scopeName = scopeName.replace(/-/, '_')
}
// in development, we can save bandwidth and build time by not bothering
// to include translation artifacts during the build.
// RAILS_LOAD_ALL_LOCALES: '1' or 'true' to enable
// RAILS_LOAD_ALL_LOCALES: '0' to disable in production mode
const shouldTranslate = (
process.env.RAILS_LOAD_ALL_LOCALES === '1' ||
process.env.RAILS_LOAD_ALL_LOCALES === 'true' ||
process.env.RAILS_LOAD_ALL_LOCALES !== '0' && (
process.env.RAILS_ENV == 'production' ||
process.env.NODE_ENV == 'production'
)
)
const translationDependency = shouldTranslate
? `
import 'translations/${scopeName}';
import 'translations/_core';
`
: `
import 'translations/_core_en';
`
const scopedJavascript = `
import I18n from '@canvas/i18n/i18nObj';
${translationDependency}
export default I18n.scoped('${scopeName}');
`

View File

@ -37,7 +37,11 @@ try {
moment().locale(ENV.MOMENT_LOCALE)
configureDateTimeMomentParser()
configureDateTime()
window.addEventListener('canvasReadyStateChange', function({ detail }) {
if(detail === 'localeFiles') {
configureDateTime()
}
})
enableDTNPI()
async function setupSentry() {

View File

@ -48,6 +48,7 @@ if (!ENV.LOCALE && ENV.LOCALES instanceof Array) ENV.LOCALE = ENV.LOCALES[0]
const readinessTargets = [
['asyncInitializers', false],
['deferredBundles', false],
['localeFiles', false],
['localePolyfills', false]
]
const advanceReadiness = target => {
@ -60,6 +61,7 @@ const advanceReadiness = target => {
}
entry[1] = true
window.dispatchEvent(new CustomEvent('canvasReadyStateChange', { detail: target }))
if (readinessTargets.every(x => x[1])) {
window.canvasReadyState = 'complete'
@ -138,7 +140,6 @@ function afterDocumentReady() {
require.include('./features/navigation_header')
if (!window.bundles) window.bundles = []
window.bundles.push = loadBundle
// If you add to this be sure there is support for it in the intl-polyfills package!
const intlSubsystemsInUse = ['DateTimeFormat', 'RelativeTimeFormat', 'NumberFormat']
@ -159,6 +160,9 @@ function noNativeSupport(sys) {
}
async function maybePolyfillLocaleThenGo() {
await import(`../public/javascripts/translations/${ENV.LOCALE}`)
advanceReadiness('localeFiles')
// If any Intl subsystem has no native support for the current locale, start
// trying to polyfill that locale from @formatjs. Note that this (possibly slow)
// process only executes at all if polyfilling was detected to be necessary.
@ -178,10 +182,13 @@ async function maybePolyfillLocaleThenGo() {
}
/* eslint-enable no-console */
}
// After possible polyfilling has completed, now we can start evaluating any
// queueud JS bundles, arrange for tasks to run after the document is fully ready,
// and advance the readiness state.
advanceReadiness('localePolyfills')
window.bundles.push = loadBundle
window.bundles.forEach(loadBundle)
ready(afterDocumentReady)
}

View File

@ -16,7 +16,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import mergeI18nTranslations from '../mergeI18nTranslations'
import { setRootTranslations, setLazyTranslations } from '../mergeI18nTranslations'
import i18nObj from '../i18nObj'
describe('mergeI18nTranslations', () => {
@ -33,19 +33,38 @@ describe('mergeI18nTranslations', () => {
})
it('merges onto i18n.translations', () => {
const newStrings = {
ar: {someKey: 'arabic value'},
en: {someKey: 'english value'}
}
mergeI18nTranslations(newStrings)
expect(i18nObj.translations).toEqual(newStrings)
const rootTranslations = { rootKeyA: 'rootKeyValueA' }
setRootTranslations('en', () => rootTranslations)
expect(i18nObj.translations.en).toBe(rootTranslations)
})
it('overwrites the key that is there', () => {
i18nObj.translations.en.anotherKey = 'original value'
mergeI18nTranslations({
en: {anotherKey: 'new value'}
})
expect(i18nObj.translations.en.anotherKey).toEqual('new value')
it('creates a getter that when accessed, creates new root translations', () => {
const rootTranslations = { rootKeyA: 'rootKeyValueA' }
const lazyRootTranslations = { lazyRootKeyA: 'lazyRootKeyValueA' }
setRootTranslations('en', () => rootTranslations)
setLazyTranslations('en', 'myScope', () => lazyRootTranslations)
expect(i18nObj.translations.en.rootKeyA).toBeDefined()
expect(i18nObj.translations.en.lazyRootKeyA).toBeUndefined()
i18nObj.translations.en.myScope // SIDE EFFECT: Invoke Getter
expect(i18nObj.translations.en.rootKeyA).toBeDefined()
expect(i18nObj.translations.en.lazyRootKeyA).toBeDefined()
})
it('creates a getter that when accessed, memoizes the scoped translations', () => {
const rootTranslations = { rootKeyA: 'rootKeyValueA' }
const lazyScopedTranslations = { lazyScopedKeyA: 'lazyScopedKeyValueA' }
setRootTranslations('en', () => rootTranslations)
setLazyTranslations('en', 'myScope', null, () => lazyScopedTranslations)
expect(Object.getOwnPropertyDescriptor(i18nObj.translations.en, 'myScope').value).toBeUndefined()
i18nObj.translations.en.myScope // SIDE EFFECT: Invoke Getter
expect(Object.getOwnPropertyDescriptor(i18nObj.translations.en, 'myScope').value).toBeDefined()
expect(i18nObj.translations.en.myScope.lazyScopedKeyA).toBeDefined()
})
})

View File

@ -349,8 +349,10 @@ if (window.ENV && window.ENV.lolcalize) {
}
I18n.scoped = (scope, callback) => {
const preloadLocale = window.ENV && window.ENV.LOCALE ? window.ENV.LOCALE : 'en'
const i18n_scope = new I18n.scope(scope)
if (callback) callback(i18n_scope)
I18n.translations[preloadLocale] && I18n.translations[preloadLocale][scope.split('.')[0]]; // SIDE EFFECT: Actually Load Translations (incl. Root Keys)
return i18n_scope
}
class Scope {

View File

@ -18,21 +18,26 @@
import I18n from './i18nObj'
// this is like $.extend(true, destination, source) but much faster and it mutates
function fastMerge(destination, source) {
const keys = Object.keys(source)
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
const val = source[key]
if (typeof destination[key] === 'object') {
fastMerge(destination[key], val)
} else {
destination[key] = val
}
}
return destination
export function setRootTranslations(locale, cb) {
I18n.translations[locale] = cb()
}
export default function mergeI18nTranslations(newStrings) {
fastMerge(I18n.translations, newStrings)
export function setLazyTranslations(locale, scope, cbRoot, cbScope) {
const localeTranslations = I18n.translations[locale]
Object.defineProperty(localeTranslations, scope, {
configurable: true,
enumerable: true,
get: function() {
Object.assign(localeTranslations, cbRoot && cbRoot())
Object.defineProperty(localeTranslations, scope, {
configurable: false,
enumerable: true,
value: cbScope ? cbScope() : {},
writable: false,
})
return localeTranslations[scope]
}
})
}