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:
parent
d3ba157e43
commit
1e8f45e3ed
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"}} }
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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}');
|
||||
`
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue