Insert language polyfills at page load time
Refs FOO-1891 flag=none Canvas officially supports a fixed set of languages and locales. Unfortunately, not all browsers support all of those (the worst offender oddly enough is Google Chrome). Fortunately, polyfills for almost all of the Canvas supported languages are available via a polyfill package. This commit introduces a function that asynchronously loads the polyfill for a requested language. It does nothing if it sees there is already native support in the browser for that language. The function can be imported and run to dynamically switch in and out the polyfills as necessary, but since Canvas never changes ENV.LOCALE except at page load time, it should suffice to call it at front end initialization time; this was added to ui/index.js as a part of its "readiness" checks. The missing one (that NO browser supports) is Haitian Creole; we will have to probably build our own locale data file for that one. This takes care of polyfilling DateTimeFormat, NumberFormat, and RelativeTimeFormat. We can always add other Intl systems if the need arises. Test plan: * Be on Chrome (which does not support the Welsh language) * In the browser console, verify no native support for Welsh: Intl.DateTimeFormat.supportedLocalesOf(['cy']) ... should return an empty array * Go into your user settings on Canvas and change your language to Welsh (Cymraeg). Reload the page to make it take effect. Now your settings page should appear in Welsh. * In the browser console, verify that dates and times can be formatted correctly in Welsh (thanks to the polyfill): Intl.DateTimeFormat('cy', { dateStyle: 'long', timeStyle: 'long' }).format(new Date()) * That should display a date and time in Welsh (most prominently seen in the name of the month). Change-Id: I40632344ba1d8679aba1a976fcb55af97636be4b Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/267359 Reviewed-by: Ahmad Amireh <ahmad@instructure.com> Product-Review: Charley Kline <ckline@instructure.com> Product-Review: Ahmad Amireh <ahmad@instructure.com> QA-Review: Ahmad Amireh <ahmad@instructure.com> Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
This commit is contained in:
parent
285890e783
commit
bb5ced7951
|
@ -60,7 +60,7 @@ module.exports = {
|
|||
// This just reflects how big the 'main' entry is at the time of writing. Every
|
||||
// time we get it smaller we should change this to the new smaller number so it
|
||||
// only goes down over time instead of growing bigger over time
|
||||
maxEntrypointSize: 1200000,
|
||||
maxEntrypointSize: 1230000,
|
||||
// This is how big our biggest js bundles are at the time of writing. We should
|
||||
// first work to attack the things in `thingsWeKnowAreWayTooBig` so we can start
|
||||
// tracking them too. Then, as we work to get all chunks smaller, we should change
|
||||
|
|
|
@ -121,6 +121,7 @@
|
|||
"immer": "^3",
|
||||
"immutability-helper": "^3",
|
||||
"immutable": "^3.8.2",
|
||||
"intl-polyfills": "^1",
|
||||
"is-valid-domain": "^0.0.11",
|
||||
"jquery": "https://github.com/instructure/jquery.git#1.7.2-with-AMD-and-CommonJS",
|
||||
"jquery-getscrollbarwidth": "^1.0.0",
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// All of these only implement the en-US locale, as do most of our tests anyway.
|
||||
|
||||
// Now that Canvas is on Node 14, there is at least some support for the ICU
|
||||
// functionality. In case it is not 100% complete, though, this skeleton will
|
||||
// remain in case we need to add any Intl polyfills in the future. Hopefully
|
||||
|
@ -26,3 +24,126 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 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')
|
||||
return import('@formatjs/intl-datetimeformat/add-all-tz')
|
||||
},
|
||||
NumberFormat: () => import('@formatjs/intl-numberformat/polyfill'),
|
||||
RelativeTimeFormat: () => import('@formatjs/intl-relativetimeformat/polyfill')
|
||||
}
|
||||
|
||||
const localeImports = {
|
||||
DateTimeFormat: l => import(`@formatjs/intl-datetimeformat/locale-data/${l}`),
|
||||
NumberFormat: l => import(`@formatjs/intl-numberformat/locale-data/${l}`),
|
||||
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')
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// 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.
|
||||
|
||||
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') {
|
||||
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, then that locale does not
|
||||
// exist and we have no choice but to 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)
|
||||
}
|
||||
}
|
||||
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.
|
||||
// Returns a Promise that resolves to an array of the result objects
|
||||
// (see above) for each subsystem.
|
||||
export function loadAllLocalePolyfills(locale) {
|
||||
return Promise.all([
|
||||
loadDateTimeFormatPolyfill(locale),
|
||||
loadNumberFormatPolyfill(locale),
|
||||
loadRelativeTimeFormatPolyfill(locale)
|
||||
])
|
||||
}
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
"name": "intl-polyfills",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"author": "neme",
|
||||
"main": "./index.js"
|
||||
"author": "Charley Kline, ckline@instructure.com",
|
||||
"main": "./index.js",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-datetimeformat": "^4",
|
||||
"@formatjs/intl-numberformat": "^7",
|
||||
"@formatjs/intl-relativetimeformat": "^9"
|
||||
}
|
||||
}
|
||||
|
|
63
ui/index.js
63
ui/index.js
|
@ -16,9 +16,9 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import './boot/initializers/setWebpackCdnHost.js'
|
||||
import './boot/initializers/setWebpackCdnHost'
|
||||
import '@canvas/jquery/jquery.instructure_jquery_patches' // this needs to be before anything else that requires jQuery
|
||||
import './boot/index.js'
|
||||
import './boot'
|
||||
|
||||
// true modules that we use in this file
|
||||
import $ from 'jquery'
|
||||
|
@ -35,24 +35,24 @@ import loadBundle from 'bundles-generated'
|
|||
import 'translations/_core_en'
|
||||
import '@canvas/jquery/jquery.ajaxJSON'
|
||||
import '@canvas/forms/jquery/jquery.instructure_forms'
|
||||
import './boot/initializers/ajax_errors.js'
|
||||
import './boot/initializers/activateKeyClicks.js'
|
||||
import './boot/initializers/activateTooltips.js'
|
||||
import './boot/initializers/ajax_errors'
|
||||
import './boot/initializers/activateKeyClicks'
|
||||
import './boot/initializers/activateTooltips'
|
||||
|
||||
window.canvasReadyState = 'loading'
|
||||
window.dispatchEvent(new CustomEvent('canvasReadyStateChange'))
|
||||
|
||||
const readinessTargets = [
|
||||
['asyncInitializers',false],
|
||||
['deferredBundles',false],
|
||||
['asyncInitializers', false],
|
||||
['deferredBundles', false],
|
||||
['localePolyfills', false]
|
||||
]
|
||||
const advanceReadiness = target => {
|
||||
const entry = readinessTargets.find(x => x[0] === target)
|
||||
|
||||
if (!entry) {
|
||||
throw new Error(`Invalid readiness target -- "${target}"`)
|
||||
}
|
||||
else if (entry[1]) {
|
||||
} else if (entry[1]) {
|
||||
throw new Error(`Target already marked ready -- "${target}"`)
|
||||
}
|
||||
|
||||
|
@ -77,25 +77,23 @@ window.bundles.forEach(loadBundle)
|
|||
|
||||
if (ENV.csp)
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
import('./boot/initializers/setupCSP.js').then(({default: setupCSP}) =>
|
||||
setupCSP(window.document)
|
||||
)
|
||||
if (ENV.INCOMPLETE_REGISTRATION) import('./boot/initializers/warnOnIncompleteRegistration.js')
|
||||
if (ENV.badge_counts) import('./boot/initializers/showBadgeCounts.js')
|
||||
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.js').then(({default: helpDialog}) => helpDialog.open())
|
||||
import('./boot/initializers/enableHelpDialog').then(({default: helpDialog}) => helpDialog.open())
|
||||
})
|
||||
|
||||
// Backbone routes
|
||||
$('body').on(
|
||||
'click',
|
||||
'[data-pushstate]',
|
||||
preventDefault(function() {
|
||||
preventDefault(function () {
|
||||
Backbone.history.navigate($(this).attr('href'), true)
|
||||
})
|
||||
)
|
||||
|
@ -107,23 +105,38 @@ if (
|
|||
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()
|
||||
}
|
||||
)
|
||||
import('./features/new_user_tutorial/index').then(({default: initializeNewUserTutorials}) => {
|
||||
initializeNewUserTutorials()
|
||||
})
|
||||
}
|
||||
|
||||
;(window.requestIdleCallback || window.setTimeout)(() => {
|
||||
import('./boot/initializers/runOnEveryPageButDontBlockAnythingElse.js').then(() =>
|
||||
// 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)
|
||||
})
|
||||
.then(() => advanceReadiness('localePolyfills'))
|
||||
/* eslint-enable no-console */
|
||||
|
||||
ready(() => {
|
||||
Promise.all(
|
||||
(window.deferredBundles || []).map(loadBundle)
|
||||
).then(() => advanceReadiness('deferredBundles'))
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
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
|
||||
|
|
31
yarn.lock
31
yarn.lock
|
@ -1346,6 +1346,37 @@
|
|||
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==
|
||||
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==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.9.4"
|
||||
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==
|
||||
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"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@gulp-sourcemaps/identity-map@1.X":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/identity-map/-/identity-map-1.0.2.tgz#1e6fe5d8027b1f285dc0d31762f566bccd73d5a9"
|
||||
|
|
Loading…
Reference in New Issue