Redo the locale polyfiller using new features from @formatjs
Fixes FOO-2330 flag=none Canvas supports locales that not all browsers support, so we have to check for those cases and pull in a polyfilled version of the Intl functions that we use (DateTimeFormat, NumberFormat, and RelativeTimeFormat) when a locale is called for that the browser does not support. The first time the locale polyfiller was implemented, a lot of tests had to be done to see if there was an available polyfill for the given locale; this somehow broke Chrome after Version 92 so it was disabled for Chrome. However, latest versions of the @formatjs polyfillers are much smarter and can perform those checks and fallbacks themselves, which makes the Canvas polyfiller code way simpler and more stable. An additional issue was uncovered: when the locale is being polyfilled, NONE of the native locales can be accessed any more, so code relying on being able to get at, say, 'en-US' to do date arithmetic was failing. So when a polyfill loads, we save the browser's version as Native* (so Intl.DateTimeFormat --> Intl.NativeDateTimeFormat) so it can be accessed if needed (shouldn't be anywhere except in the ui/shared datetime support code). Another issue uncovered is that when a polyfill is necessary, it seems to take a long time... it can be SECONDS before it is fully loaded and its Promises resolve, at least in dev. Unfortunately a lot of front-end code can start up and begin to use Intl functions during that time, possibly with unpredictable results. The only real answer is to make the execution of immediate bundles, and the call to ready() to finish front-end initialization, wait pending the resolution of the Promise indicating that the locales have loaded. This is a bit unfortunate as it delays the execution of front-end code, but will only be noticeable in locales that need polyfilling because if a native locale is detected, the bundle loading and call to ready() will happen immediately as before. This commit does the following: - changes the Rails js_env to include LOCALES instead of LOCALE. LOCALES is a full list of locale strings in fallback order, so that if `sv-SE-x-k12` isn't found, it will try `sv-SE` and `sv-x-k12` and `sv` before finally falling back to `en`. The front-end startup code backfills ENV.LOCALE to ENV.LOCALES[0] for backward compatibility. - up-revs @formatjs to a more recent version that implements the check if a polyfill is available and leverages the LOCALES list to find an available polyfill. - Rewrites intl-polyfills to be smarter about detecting when a polyfill is necessary and whether it is possible. - Makes it easier to add new Intl subsystems to the polyfill code should they start getting used in the Canvas code somewhere. - Fixes up the helpers that relied upon the 'en-US' locale specifically. They now specifically check for the Native version of an Intl subsystem first. - Fixes up the changeTimezone helpers in ui/shared/datetime because they relied on the `en-US` locale always being available, which is not true when the locale has been polyfilled. That was kind of sketchy anyway because it relied on new Date() parsing a fairly free-form English date and time string; it now constructs an ISO8601 string which is much more supported as an argument to new Date(). Test plan: * Try some different locale scenarios and observe the behavior of Canvas in that language in general, and also specifically how it formats dates and times (this may be faster to do in console). * Pay particular attention to places where datepickers are used, specifically, create a new assignment and fiddle with the due dates down near the bottom. * Locale scenarios to try: a native locale like de or es -should just work a native locale with an extension like sv-x-k12 -should just work a polyfillable locale like cy or nn -should work with a console message saying it is polyfilled the one non-polyfillable locale: Kreyòl Ayisyen -should work via a fallback all the way to 'en' on console Change-Id: I93a1f2c3f0b3002747f564aba24c93d77244383e Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/284509 Reviewed-by: Ahmad Amireh <ahmad@instructure.com> QA-Review: Charley Kline <ckline@instructure.com> Product-Review: Charley Kline <ckline@instructure.com> Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
This commit is contained in:
parent
8af10493a2
commit
3af92969b2
|
@ -250,9 +250,9 @@ class ApplicationController < ActionController::Base
|
|||
@js_env[:ping_url] = polymorphic_url([:api_v1, @context, :ping]) if @context.is_a?(Course)
|
||||
@js_env[:TIMEZONE] = Time.zone.tzinfo.identifier unless @js_env[:TIMEZONE]
|
||||
@js_env[:CONTEXT_TIMEZONE] = @context.time_zone.tzinfo.identifier if !@js_env[:CONTEXT_TIMEZONE] && @context.respond_to?(:time_zone) && @context.time_zone.present?
|
||||
unless @js_env[:LOCALE]
|
||||
unless @js_env[:LOCALES]
|
||||
I18n.set_locale_with_localizer
|
||||
@js_env[:LOCALE] = I18n.locale.to_s
|
||||
@js_env[:LOCALES] = I18n.fallbacks[I18n.locale].map(&:to_s)
|
||||
@js_env[:BIGEASY_LOCALE] = I18n.bigeasy_locale
|
||||
@js_env[:FULLCALENDAR_LOCALE] = I18n.fullcalendar_locale
|
||||
@js_env[:MOMENT_LOCALE] = I18n.moment_locale
|
||||
|
|
|
@ -21,43 +21,31 @@
|
|||
// remain in case we need to add any Intl polyfills in the future. Hopefully
|
||||
// we will not ever have to.
|
||||
|
||||
import {shouldPolyfill as spfNF} from '@formatjs/intl-numberformat/should-polyfill'
|
||||
import {shouldPolyfill as spfDTF} from '@formatjs/intl-datetimeformat/should-polyfill'
|
||||
import {shouldPolyfill as spfRTF} from '@formatjs/intl-relativetimeformat/should-polyfill'
|
||||
|
||||
export function installIntlPolyfills() {
|
||||
if (typeof window.Intl === 'undefined') window.Intl = {}
|
||||
}
|
||||
|
||||
class LocaleLoadError extends Error {
|
||||
constructor(result, ...rest) {
|
||||
super(...rest)
|
||||
Error.captureStackTrace && Error.captureStackTrace(this, LocaleLoadError)
|
||||
this.name = 'LocaleLoadError'
|
||||
this.result = result
|
||||
}
|
||||
}
|
||||
|
||||
function chromeVersion() {
|
||||
const m = navigator.userAgent.match(new RegExp('Chrom(e|ium)/([0-9]+).'))
|
||||
return m ? parseInt(m[2], 10) : 0
|
||||
const shouldPolyfill = {
|
||||
NumberFormat: spfNF,
|
||||
DateTimeFormat: spfDTF,
|
||||
RelativeTimeFormat: spfRTF
|
||||
}
|
||||
|
||||
//
|
||||
// Intl polyfills for locale-specific output of Dates, Times, Numbers, and
|
||||
// Relative Times.
|
||||
//
|
||||
const nativeSubsystem = {
|
||||
DateTimeFormat: Intl.DateTimeFormat,
|
||||
NumberFormat: Intl.NumberFormat,
|
||||
RelativeTimeFormat: Intl.RelativeTimeFormat
|
||||
}
|
||||
|
||||
const polyfilledSubsystem = {}
|
||||
|
||||
const polyfillImports = {
|
||||
DateTimeFormat: async () => {
|
||||
await import('@formatjs/intl-datetimeformat/polyfill')
|
||||
await import('@formatjs/intl-datetimeformat/polyfill-force')
|
||||
return import('@formatjs/intl-datetimeformat/add-all-tz')
|
||||
},
|
||||
NumberFormat: () => import('@formatjs/intl-numberformat/polyfill'),
|
||||
RelativeTimeFormat: () => import('@formatjs/intl-relativetimeformat/polyfill')
|
||||
NumberFormat: () => import('@formatjs/intl-numberformat/polyfill-force'),
|
||||
RelativeTimeFormat: () => import('@formatjs/intl-relativetimeformat/polyfill-force')
|
||||
}
|
||||
|
||||
const localeImports = {
|
||||
|
@ -66,114 +54,63 @@ const localeImports = {
|
|||
RelativeTimeFormat: l => import(`@formatjs/intl-relativetimeformat/locale-data/${l}`)
|
||||
}
|
||||
|
||||
function reset(locale, subsystem) {
|
||||
Intl[subsystem] = nativeSubsystem[subsystem] // Check browser-native Intl sources first
|
||||
const localeNotNative = Intl[subsystem].supportedLocalesOf([locale]).length === 0
|
||||
return {subsystem, locale, source: localeNotNative ? 'polyfill' : 'native'}
|
||||
}
|
||||
|
||||
// Mark a return result as a fallback locale
|
||||
const fallback = r => ({...r, source: 'fallback'})
|
||||
|
||||
// This utility is called when changing locales midstride (Canvas normally never
|
||||
// changes ENV.LOCALE except at a page load). It will if necessary dynamically load
|
||||
// language and locale support for locales not supported natively in the browser.
|
||||
// Called with the desired locale string and a string representing which Intl subsystem
|
||||
// is to be operated on ('DateTimeFormat', 'NumberFormat', or 'RelativeTimeFormat')
|
||||
// Check to see if there is native support in the specified Intl subsystem for
|
||||
// any of the locales given in the list (they are tried in order). If there is not
|
||||
// load an appropriate locale polyfill for the list of locales from @formatjs.
|
||||
//
|
||||
// Returns a Promise which will resolve to an object with properties:
|
||||
// subsystem - the Intl subsystem being (possibly) polyfilled
|
||||
// locale - the locale that was loaded
|
||||
// source - one of 'native' or 'polyfill', indicating whether the requested locale
|
||||
// locale is native to the browser or was loaded via a polyfill
|
||||
// Return value is a Promise which resolves to an hash with the following properties:
|
||||
//
|
||||
// If the polyfill fails to load, the language falls back to whatever the browser's native
|
||||
// language is (navigator.language), and the Promise rejects, returning a custom error
|
||||
// LocaleLoadError with the `message` explaining the failure and a `result` property which
|
||||
// is an object as described above, which will indicate the fallback state of the locale
|
||||
// after the failure causing the error to be thrown.
|
||||
// subsys - the subsystem being operated on
|
||||
// locale - the locale that was requested
|
||||
// loaded - the locale that was actually polyfilled (is missing, an error occurred)
|
||||
// source - how that locale is available ('native' or 'polyfill')
|
||||
// error - if an error occurred, contains the error message
|
||||
//
|
||||
// In most cases, if none of the locales in the list have either native support
|
||||
// nor can any of them be polyfilled, the subsystem will fall back to 'en' as a
|
||||
// locale (this is what the browser's native Intl would also do).
|
||||
//
|
||||
async function doPolyfill(givenLocales, subsys) {
|
||||
// 'en' is the final fallback, don't settle for that unless it's the only
|
||||
// available locale, in which case we do nothing.
|
||||
const locales = [...givenLocales]
|
||||
if (locales.length < 1 || (locales.length === 1 && locales[0] === 'en'))
|
||||
return {subsys, locale: 'en', source: 'native'}
|
||||
if (locales.slice(-1)[0] === 'en') locales.pop()
|
||||
|
||||
async function doPolyfill(locale, subsys) {
|
||||
// Reset back to the native browser Intl subsystem and see if the requested locale
|
||||
// is one of its supported ones. If not, we need to polyfill it. First import the
|
||||
// polyfilled subsystem itself. We can only do this once as that import is not
|
||||
// idempotent, which is why we save the polyfills and only do the import if there
|
||||
// is no saved one.
|
||||
const result = reset(locale, subsys)
|
||||
if (result.source === 'polyfill') {
|
||||
// Does the requested polyfill locale exist at all? If not, do not proceed,
|
||||
// it breaks some browser behavior around .toLocaleDateString() [???].
|
||||
try {
|
||||
await localeImports[subsys](locale)
|
||||
} catch (e) {
|
||||
throw new LocaleLoadError(
|
||||
{
|
||||
locale: navigator.language,
|
||||
subsystem: subsys,
|
||||
source: 'fallback'
|
||||
},
|
||||
e.message
|
||||
)
|
||||
}
|
||||
try {
|
||||
/* eslint-disable no-await-in-loop */ // it's actually fine in for-loops
|
||||
for (const locale of locales) {
|
||||
const native = Intl[subsys].supportedLocalesOf([locale])
|
||||
if (native.length > 0) return {subsys, locale: native[0], source: 'native'}
|
||||
|
||||
delete Intl[subsys]
|
||||
if (typeof polyfilledSubsystem[subsys] === 'undefined') {
|
||||
try {
|
||||
await polyfillImports[subsys]()
|
||||
polyfilledSubsystem[subsys] = Intl[subsys]
|
||||
} catch (e) {
|
||||
// restore native one and throw an error
|
||||
throw new LocaleLoadError(fallback(reset(navigator.language, subsys)), e.message)
|
||||
}
|
||||
} else {
|
||||
// Have already loaded the polyfill, just need to stuff it back in
|
||||
Intl[subsys] = polyfilledSubsystem[subsys]
|
||||
}
|
||||
|
||||
// Now load the specific locale... if it fails (it shouldn't because we
|
||||
// already checked this above!!), then fall back to the navigator language.
|
||||
// Note that loading a locale onto the polyfill *is* idempotent, so it
|
||||
// won't matter if we do this multiple times.
|
||||
try {
|
||||
await localeImports[subsys](locale)
|
||||
} catch (e) {
|
||||
throw new LocaleLoadError(fallback(reset(navigator.language, subsys)), e.message)
|
||||
const doable = shouldPolyfill[subsys](locale)
|
||||
if (!doable || doable === 'en') continue
|
||||
const origSubsys = Intl[subsys]
|
||||
await polyfillImports[subsys]()
|
||||
await localeImports[subsys](doable)
|
||||
Intl[`Native${subsys}`] = origSubsys
|
||||
return {subsys, locale, source: 'polyfill', loaded: doable}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
return {subsys, locale: locales[0], error: 'polyfill unavailable'}
|
||||
} catch (e) {
|
||||
return {subsys, locale: locales[0], error: e.message}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Convenience functions that load the Intl polyfill for each of the three
|
||||
// supported subsystems supported here (we could add more if needed)
|
||||
export function loadDateTimeFormatPolyfill(locale) {
|
||||
return doPolyfill(locale, 'DateTimeFormat')
|
||||
}
|
||||
|
||||
export function loadNumberFormatPolyfill(locale) {
|
||||
return doPolyfill(locale, 'NumberFormat')
|
||||
}
|
||||
|
||||
export function loadRelativeTimeFormatPolyfill(locale) {
|
||||
return doPolyfill(locale, 'RelativeTimeFormat')
|
||||
}
|
||||
|
||||
// Grand cru convenience function that (maybe) polyfills everything here.
|
||||
// (Possibly) load the Intl polyfill for each of the given subsystems,
|
||||
// for the best available locale in the given list.
|
||||
// Returns a Promise that resolves to an array of the result objects
|
||||
// (see above) for each subsystem.
|
||||
//
|
||||
// TEMPORARY PATCH (CNVS-53338) ... these polyfillers break certain date
|
||||
// and time Intl functions in recent versions of Chrome. For now, just
|
||||
// skip.
|
||||
export function loadAllLocalePolyfills(locale) {
|
||||
const ver = chromeVersion()
|
||||
if (ver >= 92) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(`Skipping language polyfills for Chrome ${ver}`)
|
||||
return null
|
||||
}
|
||||
return Promise.all([
|
||||
loadDateTimeFormatPolyfill(locale),
|
||||
loadNumberFormatPolyfill(locale),
|
||||
loadRelativeTimeFormatPolyfill(locale)
|
||||
])
|
||||
// It is an error for the subsystems array to contain the name of an
|
||||
// Intl subsystem that we are not prepared to polyfill.
|
||||
export function loadAllLocalePolyfills(locales, subsystems) {
|
||||
subsystems.forEach(sys => {
|
||||
if (!Object.keys(shouldPolyfill).includes(sys)) {
|
||||
throw new RangeError(`Intl subsystem ${sys} is not polyfillable!`)
|
||||
}
|
||||
})
|
||||
|
||||
return Promise.all(subsystems.map(sys => doPolyfill(locales, sys)))
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
"author": "Charley Kline, ckline@instructure.com",
|
||||
"main": "./index.js",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-datetimeformat": "^4",
|
||||
"@formatjs/intl-numberformat": "^7",
|
||||
"@formatjs/intl-relativetimeformat": "^9"
|
||||
"@formatjs/intl-datetimeformat": "^4.5",
|
||||
"@formatjs/intl-numberformat": "^7.4",
|
||||
"@formatjs/intl-relativetimeformat": "^9.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,10 +122,10 @@ RSpec.describe ApplicationController do
|
|||
expect(controller.js_env[:files_domain]).to eq "files.example.com"
|
||||
end
|
||||
|
||||
it "auto-sets timezone and locale" do
|
||||
it "auto-sets timezone and locales" do
|
||||
I18n.with_locale(:fr) do
|
||||
Time.use_zone("Alaska") do
|
||||
expect(@controller.js_env[:LOCALE]).to eq "fr"
|
||||
expect(@controller.js_env[:LOCALES]).to eq ["fr", "en"] # 'en' is always the last fallback
|
||||
expect(@controller.js_env[:BIGEASY_LOCALE]).to eq "fr_FR"
|
||||
expect(@controller.js_env[:FULLCALENDAR_LOCALE]).to eq "fr"
|
||||
expect(@controller.js_env[:MOMENT_LOCALE]).to eq "fr"
|
||||
|
|
|
@ -57,10 +57,9 @@ describe "calendar2" do
|
|||
event_dialog.find(".edit_assignment_option").click
|
||||
wait_for_ajaximations
|
||||
event_dialog.find("#assignment_title").send_keys("saturday assignment")
|
||||
event_dialog.find(".datetime_field").clear
|
||||
# take next week's monday and advance to saturday from the current date
|
||||
due_date = "Dec 26, 2015 at 8pm"
|
||||
event_dialog.find(".datetime_field").send_keys(due_date)
|
||||
replace_content(event_dialog.find(".datetime_field"), due_date)
|
||||
assignment_form = event_dialog.find("#edit_assignment_form")
|
||||
submit_form(assignment_form)
|
||||
wait_for_ajaximations
|
||||
|
|
179
ui/index.js
179
ui/index.js
|
@ -43,6 +43,9 @@ import './boot/initializers/injectAuthTokenIntoForms'
|
|||
window.canvasReadyState = 'loading'
|
||||
window.dispatchEvent(new CustomEvent('canvasReadyStateChange'))
|
||||
|
||||
// Backfill LOCALE from LOCALES
|
||||
if (!ENV.LOCALE && ENV.LOCALES instanceof Array) ENV.LOCALE = ENV.LOCALES[0]
|
||||
|
||||
const readinessTargets = [
|
||||
['asyncInitializers', false],
|
||||
['deferredBundles', false],
|
||||
|
@ -65,79 +68,11 @@ const advanceReadiness = target => {
|
|||
}
|
||||
}
|
||||
|
||||
// This is because most pages use this and by having it all in it's own chunk it makes webpack
|
||||
// split out a ton of stuff (like @instructure/ui-view) into multiple chunks because its chunking
|
||||
// algorithm decides that because that chunk would either be too small or it would cause more than
|
||||
// our maxAsyncRequests it should concat it into mutlple parents.
|
||||
require.include('./features/navigation_header')
|
||||
|
||||
if (!window.bundles) window.bundles = []
|
||||
window.bundles.push = loadBundle
|
||||
// process any queued ones
|
||||
window.bundles.forEach(loadBundle)
|
||||
|
||||
if (ENV.csp)
|
||||
function afterDocumentReady() {
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
import('./boot/initializers/setupCSP').then(({default: setupCSP}) => setupCSP(window.document))
|
||||
if (ENV.INCOMPLETE_REGISTRATION) import('./boot/initializers/warnOnIncompleteRegistration')
|
||||
if (ENV.badge_counts) import('./boot/initializers/showBadgeCounts')
|
||||
|
||||
$('html').removeClass('scripts-not-loaded')
|
||||
|
||||
$('.help_dialog_trigger').click(event => {
|
||||
event.preventDefault()
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
import('./boot/initializers/enableHelpDialog').then(({default: helpDialog}) => helpDialog.open())
|
||||
})
|
||||
|
||||
// Backbone routes
|
||||
$('body').on(
|
||||
'click',
|
||||
'[data-pushstate]',
|
||||
preventDefault(function () {
|
||||
Backbone.history.navigate($(this).attr('href'), true)
|
||||
})
|
||||
)
|
||||
|
||||
if (
|
||||
window.ENV.NEW_USER_TUTORIALS &&
|
||||
window.ENV.NEW_USER_TUTORIALS.is_enabled &&
|
||||
window.ENV.context_asset_string &&
|
||||
splitAssetString(window.ENV.context_asset_string)[0] === 'courses'
|
||||
) {
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
import('./features/new_user_tutorial/index').then(({default: initializeNewUserTutorials}) => {
|
||||
initializeNewUserTutorials()
|
||||
})
|
||||
}
|
||||
|
||||
;(window.requestIdleCallback || window.setTimeout)(() => {
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
import('./boot/initializers/runOnEveryPageButDontBlockAnythingElse').then(() =>
|
||||
advanceReadiness('asyncInitializers')
|
||||
)
|
||||
})
|
||||
|
||||
// Load Intl polyfills if necessary given the ENV.LOCALE. Advance the readiness
|
||||
// state whether that worked or not.
|
||||
|
||||
/* eslint-disable no-console */
|
||||
import('intl-polyfills')
|
||||
.then(im => im.loadAllLocalePolyfills(ENV.LOCALE))
|
||||
.catch(e => {
|
||||
console.error(
|
||||
`Problem loading locale polyfill for ${ENV.LOCALE}, falling back to ${e.result.locale}`
|
||||
)
|
||||
console.error(e.message)
|
||||
})
|
||||
.finally(() => advanceReadiness('localePolyfills'))
|
||||
/* eslint-enable no-console */
|
||||
|
||||
ready(() => {
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
Promise.all((window.deferredBundles || []).map(loadBundle)).then(() =>
|
||||
Promise.all((window.deferredBundles || []).map(loadBundle)).then(() => {
|
||||
advanceReadiness('deferredBundles')
|
||||
)
|
||||
})
|
||||
|
||||
// LS-1662: there are math equations on the page that
|
||||
// we don't see, so remain invisible and aren't
|
||||
|
@ -195,4 +130,106 @@ ready(() => {
|
|||
childList: true,
|
||||
subtree: true
|
||||
})
|
||||
}
|
||||
|
||||
// This is because most pages use this and by having it all in it's own chunk it makes webpack
|
||||
// split out a ton of stuff (like @instructure/ui-view) into multiple chunks because its chunking
|
||||
// algorithm decides that because that chunk would either be too small or it would cause more than
|
||||
// our maxAsyncRequests it should concat it into mutlple parents.
|
||||
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']
|
||||
|
||||
// Do we have native support in the given Intl subsystem for one of the current
|
||||
// locale fallbacks?
|
||||
function noNativeSupport(sys) {
|
||||
const locales = [...ENV.LOCALES]
|
||||
// 'en' is the final fallback, don't settle for that unless it's the only
|
||||
// available locale, in which case there is obviously native support.
|
||||
if (locales.length < 1 || (locales.length === 1 && locales[0] === 'en')) return false
|
||||
if (locales.slice(-1)[0] === 'en') locales.pop()
|
||||
for (const locale of locales) {
|
||||
const native = Intl[sys].supportedLocalesOf([locale])
|
||||
if (native.length > 0) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function maybePolyfillLocaleThenGo() {
|
||||
// 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.
|
||||
if (intlSubsystemsInUse.some(noNativeSupport)) {
|
||||
/* eslint-disable no-console */
|
||||
try {
|
||||
const im = await import('intl-polyfills')
|
||||
const result = await im.loadAllLocalePolyfills(ENV.LOCALES, intlSubsystemsInUse)
|
||||
result.forEach(r => {
|
||||
if (r.error)
|
||||
console.error(`${r.subsys} polyfill for locale "${r.locale}" failed: ${r.error}`)
|
||||
if (r.source === 'polyfill')
|
||||
console.info(`${r.subsys} polyfilled "${r.loaded}" for locale "${r.locale}"`)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(`Locale polyfill load failed: ${e.message}`)
|
||||
}
|
||||
/* 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.forEach(loadBundle)
|
||||
ready(afterDocumentReady)
|
||||
}
|
||||
|
||||
maybePolyfillLocaleThenGo().catch(e =>
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Front-end bundles did not successfully start! (${e.message})`)
|
||||
)
|
||||
|
||||
if (ENV.csp)
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
import('./boot/initializers/setupCSP').then(({default: setupCSP}) => setupCSP(window.document))
|
||||
if (ENV.INCOMPLETE_REGISTRATION) import('./boot/initializers/warnOnIncompleteRegistration')
|
||||
if (ENV.badge_counts) import('./boot/initializers/showBadgeCounts')
|
||||
|
||||
$('html').removeClass('scripts-not-loaded')
|
||||
|
||||
$('.help_dialog_trigger').click(event => {
|
||||
event.preventDefault()
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
import('./boot/initializers/enableHelpDialog').then(({default: helpDialog}) => helpDialog.open())
|
||||
})
|
||||
|
||||
// Backbone routes
|
||||
$('body').on(
|
||||
'click',
|
||||
'[data-pushstate]',
|
||||
preventDefault(function () {
|
||||
Backbone.history.navigate($(this).attr('href'), true)
|
||||
})
|
||||
)
|
||||
|
||||
if (
|
||||
window.ENV.NEW_USER_TUTORIALS &&
|
||||
window.ENV.NEW_USER_TUTORIALS.is_enabled &&
|
||||
window.ENV.context_asset_string &&
|
||||
splitAssetString(window.ENV.context_asset_string)[0] === 'courses'
|
||||
) {
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
import('./features/new_user_tutorial/index').then(({default: initializeNewUserTutorials}) => {
|
||||
initializeNewUserTutorials()
|
||||
})
|
||||
}
|
||||
|
||||
;(window.requestIdleCallback || window.setTimeout)(() => {
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
import('./boot/initializers/runOnEveryPageButDontBlockAnythingElse').then(() =>
|
||||
advanceReadiness('asyncInitializers')
|
||||
)
|
||||
})
|
||||
|
|
|
@ -122,12 +122,22 @@ describe('utcDateOffset::', () => {
|
|||
let testDate
|
||||
|
||||
it('detects the next UTC day', () => {
|
||||
testDate = new Date('2021-04-02T02:35:00.000Z') // April 2nd in UTC in the US
|
||||
testDate = new Date('2021-04-02T02:35:00.000Z') // ... is April 1st in the US
|
||||
expect(utcDateOffset(testDate, americaTZ)).toBe(1)
|
||||
})
|
||||
|
||||
it('detects the previous UTC day', () => {
|
||||
testDate = new Date('2021-04-01T18:45:00.000Z') // March 31st in UTC in Australia (4:15am local)
|
||||
testDate = new Date('2021-04-01T18:45:00.000Z') // ... is 4:15am April 2nd in Australia
|
||||
expect(utcDateOffset(testDate, australiaTZ)).toBe(-1)
|
||||
})
|
||||
|
||||
it('still works if the next UTC day is also the next month', () => {
|
||||
testDate = new Date('2022-04-01T01:00:00.000Z') // ... is March 31st in the US
|
||||
expect(utcDateOffset(testDate, americaTZ)).toBe(1)
|
||||
})
|
||||
|
||||
it('still works if the previous UTC day is also the previous month', () => {
|
||||
testDate = new Date('2022-03-31T22:00:00.000Z') // ... is April 1st in Australia
|
||||
expect(utcDateOffset(testDate, australiaTZ)).toBe(-1)
|
||||
})
|
||||
|
||||
|
|
|
@ -21,15 +21,32 @@ const MINS = 60 * 1000
|
|||
function formatObject(tz) {
|
||||
return {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
timeZone: tz
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE: Takes a Date object and a timezone, and constructs a string that
|
||||
// represents the given Date, represented in the given timezone, that we
|
||||
// know new Date() will be able to handle, of the form: YYYY-MM-DDThh:mm:ss
|
||||
function parseableDateString(date, tz) {
|
||||
// in case Intl has been polyfilled, we'll need the native one here because
|
||||
// @formatjs does not provide formatToParts()
|
||||
const DTF = Intl.NativeDateTimeFormat || Intl.DateTimeFormat
|
||||
const fmtr = new DTF('en-US', formatObject(tz))
|
||||
const r = fmtr
|
||||
.formatToParts(date)
|
||||
.reduce((acc, obj) => Object.assign(acc, {[obj.type]: obj.value}), {})
|
||||
let iso = `${r.year}-${r.month}-${r.day}T${r.hour}:${r.minute}:${r.second}`
|
||||
if (iso.length < 19) iso = '0' + iso // in case of a year before 1000
|
||||
return iso
|
||||
}
|
||||
|
||||
//
|
||||
// Takes a Date object and an object with two optional properties,
|
||||
// originTZ: an origin timezone
|
||||
|
@ -57,8 +74,8 @@ function utcTimeOffset(date, hereTZ = null) {
|
|||
return -date.getTimezoneOffset() * MINS
|
||||
}
|
||||
const jsDate = date instanceof Date ? date : new Date(date)
|
||||
const hereDate = new Date(jsDate.toLocaleString('en-US', formatObject(hereTZ)))
|
||||
const utcDate = new Date(jsDate.toLocaleString('en-US', formatObject('Etc/UTC')))
|
||||
const hereDate = new Date(parseableDateString(jsDate, hereTZ))
|
||||
const utcDate = new Date(parseableDateString(jsDate, 'Etc/UTC'))
|
||||
return hereDate.getTime() - utcDate.getTime()
|
||||
}
|
||||
|
||||
|
@ -68,9 +85,17 @@ function utcTimeOffset(date, hereTZ = null) {
|
|||
//
|
||||
function utcDateOffset(date, hereTZ) {
|
||||
const jsDate = date instanceof Date ? date : new Date(date)
|
||||
const here = jsDate.toLocaleString('en-US', {day: 'numeric', timeZone: hereTZ})
|
||||
const utc = jsDate.toLocaleString('en-US', {day: 'numeric', timeZone: 'Etc/UTC'})
|
||||
return parseInt(utc, 10) - parseInt(here, 10)
|
||||
const dayOfMonth = timeZone => {
|
||||
const DTF = Intl.NativeDateTimeFormat || Intl.DateTimeFormat
|
||||
const fmtr = new DTF('en-US', {day: 'numeric', timeZone})
|
||||
return parseInt(fmtr.format(jsDate), 10)
|
||||
}
|
||||
const here = dayOfMonth(hereTZ)
|
||||
const utc = dayOfMonth('Etc/UTC')
|
||||
let diff = utc - here
|
||||
if (diff < -1) diff = 1 // crossed a month going forwards
|
||||
if (diff > 1) diff = -1 // crossed a month going backwards
|
||||
return diff
|
||||
}
|
||||
|
||||
export {changeTimezone, utcTimeOffset, utcDateOffset}
|
||||
|
|
55
yarn.lock
55
yarn.lock
|
@ -1389,35 +1389,46 @@
|
|||
minimatch "^3.0.4"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@formatjs/ecma402-abstract@1.9.4":
|
||||
version "1.9.4"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.9.4.tgz#797ae6c407fb5a0d09023a60c86f19aca1958c5e"
|
||||
integrity sha512-ePJXI7tWC9PBxQxS7jtbkCLGVmpC8MH8n9Yjmg8dsh9wXK9svu7nAbq76Oiu5Zb+5GVkLkeTVerlSvHCbNImlA==
|
||||
"@formatjs/ecma402-abstract@1.11.2":
|
||||
version "1.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.2.tgz#7f01595e6985a28983aae26bede9b78b273fee3d"
|
||||
integrity sha512-qDgOL0vtfJ51cc0pRbFB/oXc4qDbamG22Z6h/QWy6FBxaQgppiy8JF0iYbmNO35cC8r88bQGsgfd/eM6/eTEQQ==
|
||||
dependencies:
|
||||
"@formatjs/intl-localematcher" "0.2.23"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@formatjs/intl-datetimeformat@^4.5":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-datetimeformat/-/intl-datetimeformat-4.5.1.tgz#588d5d9875bd19417873fdc67112456c3b28b2ff"
|
||||
integrity sha512-4Ncg6bC5X8QyA4jsyHCGyO84aLulqpKXbxrvsPF1Y4deMFcYBVzg43fT/Dbp6ZqfdViGJ3K/RTBD8XlAD8evIg==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.11.2"
|
||||
"@formatjs/intl-localematcher" "0.2.23"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@formatjs/intl-localematcher@0.2.23":
|
||||
version "0.2.23"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.23.tgz#5a0b1d81df1f392ecf37e556ca7040a7ec9f72e8"
|
||||
integrity sha512-oCe2TOciTtB1bEbJ85EvYrXQxD0epusmVJfJ7AduO0tlbXP42CmDIYIH2CZ+kP2GE+PTLQD1Hbt9kpOpl939MQ==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@formatjs/intl-datetimeformat@^4":
|
||||
version "4.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-datetimeformat/-/intl-datetimeformat-4.1.6.tgz#ce30d3907dfb229c79e023c03171da09b2611e36"
|
||||
integrity sha512-o4M/RzBNAC/LmOwAd3W/3bsCT7xl9luddNdX9x1uZroS/8Wx6M8sE/KMNDCSZogZtmMasCBvpNmuQoeMyBBASg==
|
||||
"@formatjs/intl-numberformat@^7.4":
|
||||
version "7.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-numberformat/-/intl-numberformat-7.4.1.tgz#41b452505e4e3864ddb5e5c5e176dd467bc00abd"
|
||||
integrity sha512-9F+n4fO/u9p5bvUHDZK5pvuVqFKjrWRe3SVf5D216IZcjGO8veeRPDPjja+YoRpNDwR0ITIyMlV+IdUHXmfwvA==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.9.4"
|
||||
"@formatjs/ecma402-abstract" "1.11.2"
|
||||
"@formatjs/intl-localematcher" "0.2.23"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@formatjs/intl-numberformat@^7":
|
||||
version "7.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-numberformat/-/intl-numberformat-7.1.5.tgz#8d1df875f7dfbd2ed8e1326fddb5f4dad3c66d11"
|
||||
integrity sha512-1U6IEp/1vf2XzfDjLWqJuQufRZY3/F9UfOCAVp14eYVb/+gZmG4Q3heJQZXOAKnnQl3YGG73Ez7UgFVo/rowGQ==
|
||||
"@formatjs/intl-relativetimeformat@^9.5":
|
||||
version "9.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-9.5.1.tgz#d00f2bf9cf9840da3c0e8b27a2ea26e12ecc376a"
|
||||
integrity sha512-A4kL6tTjcvVgyKH33BREBWBW2nPgTJSkhjKcTGfNHdmvWN8WAbDovRVOUZ6UmufRW7ZZfoDA8QT7sEOKuxKqyg==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.9.4"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@formatjs/intl-relativetimeformat@^9":
|
||||
version "9.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-9.1.7.tgz#9c5481b4d4ac39de3162c836cedef52feeae8a05"
|
||||
integrity sha512-uthmRnNg+agzulNxa4Em2Jbll1msbJdkb6CO8JrOg/CFHyXH9YPMaPjCxgzR2wdDeMjBjGnFhbTJZtqz5LN+Uw==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.9.4"
|
||||
"@formatjs/ecma402-abstract" "1.11.2"
|
||||
"@formatjs/intl-localematcher" "0.2.23"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@graphql-typed-document-node/core@^3.0.0":
|
||||
|
|
Loading…
Reference in New Issue