Skip to content

Commit

Permalink
feat: drop Intl polyfill, polyfill ListFormat & Unit Number Format
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Framework now requires a minimum of Safari 10.3, as the base "Intl" object is no longer polyfilled
  • Loading branch information
ephys committed Mar 12, 2020
1 parent 8da81de commit 344742e
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 97 deletions.
34 changes: 13 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -58,8 +58,10 @@
"@babel/preset-env": "^7.8.4",
"@babel/preset-react": "^7.8.3",
"@babel/runtime": "^7.8.4",
"@formatjs/intl-listformat": "^1.4.1",
"@formatjs/intl-pluralrules": "^1.5.2",
"@formatjs/intl-relativetimeformat": "^4.5.9",
"@formatjs/intl-unified-numberformat": "^3.2.0",
"@hapi/accept": "^4.0.0",
"@hapi/joi": "^16.1.7",
"@hot-loader/react-dom": "^16.11.0",
Expand Down Expand Up @@ -88,13 +90,11 @@
"fs-extra": "^8.1.0",
"get-port": "^5.1.1",
"glob": "^7.1.5",
"global": "^4.4.0",
"history": "^4.10.1",
"html-webpack-plugin": "^3.2.0",
"http-proxy": "^1.18.0",
"image-webpack-loader": "^6.0.0",
"inquirer": "^7.0.4",
"intl": "^1.2.5",
"ip": "^1.1.5",
"json-loader": "^0.5.7",
"lodash": "^4.17.15",
Expand Down
4 changes: 2 additions & 2 deletions src/framework/common/debug.js
@@ -1,6 +1,6 @@
import global from 'global';
import globalThis from '../../shared/globalThis';

const debug = {};
global.rjsDebug = debug;
globalThis.rjsDebug = debug;

export default debug;
10 changes: 10 additions & 0 deletions src/framework/common/i18n/_intl-plural-locales.js
@@ -0,0 +1,10 @@

// this file requires and installs all locales for intl-pluralrules
// as those locales are tiny, individual lazy-loading produces more code than just loading them individually
installAll(require.context('@formatjs/intl-pluralrules/dist/locale-data', true, /\.json$/));

function installAll(r) {
for (const key of r.keys()) {
Intl.RelativeTimeFormat.__addLocaleData(r(key));
}
}
165 changes: 95 additions & 70 deletions src/framework/common/i18n/_native-intl.js
@@ -1,130 +1,155 @@
// @flow

import global from 'global';
import globalThis from '../../../shared/globalThis';
import { getFileName, getLocaleBestFit, runBundleLoader } from './_locale-utils';

// Node should always have these available, no need for polyfilling
// TODO: listFormat

function getNumberDateTimeLocaleLoaders() {
function getUnitLocaleLoaders() {
// $FlowIgnore
return require.context('bundle-loader?lazy&name=p-intl-[name]!intl/locale-data/jsonp', true, /\.js$/);
return require.context('bundle-loader?lazy&name=p-intlunit-[name]!@formatjs/intl-unified-numberformat/dist/locale-data', true, /\.json$/);
}

function getRelativeTimeLocaleLoaders() {
// $FlowIgnore
return require.context('bundle-loader?lazy&name=p-intlrelative-[name]!@formatjs/intl-relativetimeformat/dist/locale-data', true, /\.js$/);
return require.context('bundle-loader?lazy&name=p-intlrelative-[name]!@formatjs/intl-relativetimeformat/dist/locale-data', true, /\.json$/);
}

function areNumberAndDateTimeSupported(locale: string): boolean {
const Intl = global.Intl;
function getListFormatLoaders() {
// $FlowIgnore
return require.context('bundle-loader?lazy&name=p-intllist-[name]!@formatjs/intl-listformat/dist/locale-data', true, /\.js$/);
}

if (!Intl) {
return false;
}
function isUnifiedNumberFormatSupported(locale: string): boolean {
return globalThis.Intl.NumberFormat.supportedLocalesOf([locale]).length !== 0;
}

if (!Intl.DateTimeFormat
|| !Intl.NumberFormat
|| !Intl.DateTimeFormat.supportedLocalesOf
|| !Intl.NumberFormat.supportedLocalesOf) {
return false;
}
const hasNativeUnifiedNumberFormat: boolean = (() => {
try {
new globalThis.Intl.NumberFormat('en-US', { style: 'unit', unit: 'meter' }).format(0);

if (Intl.DateTimeFormat.supportedLocalesOf([locale]).length === 0) {
return true;
} catch (e) {
return false;
}
})();

if (Intl.NumberFormat.supportedLocalesOf([locale]).length === 0) {
return false;
async function installUnifiedNumberFormat(localeName: string) {
// install polyfill if unified number format is not natively supported
if (hasNativeUnifiedNumberFormat) {
return;
}

return true;
}

function downloadNumberAndDateTime() {
return import(/* webpackChunkName: "p-intl" */ 'intl');
}

function installNumberAndDateTime(localeName: string) {
// load base DateTimeFormat / NumberFormat
const intlPromise = global.Intl ? Promise.resolve() : downloadNumberAndDateTime();
await import(/* webpackChunkName: "p-intlunit" */'@formatjs/intl-unified-numberformat/polyfill');

// load locale data
return intlPromise.then(() => {
if (areNumberAndDateTimeSupported(localeName)) {
return null;
}
if (isUnifiedNumberFormatSupported(localeName)) {
return;
}

// load locale best fit
// Note: this should never be called on non-webpack generated builds
const intlLocaleLoaders = getNumberDateTimeLocaleLoaders();
const availableIntlLocales: string[] = intlLocaleLoaders.keys().map(getFileName);
// load locale best fit
// Note: this should never be called on non-webpack generated builds
const intlLocaleLoaders = getUnitLocaleLoaders();
const availableIntlLocales: string[] = intlLocaleLoaders.keys().map(getFileName);

let actualLocale = getLocaleBestFit(localeName, availableIntlLocales);
if (actualLocale == null) {
console.error(`Could not fetch Intl locale '${localeName}'. Fallback to 'en'.`);
let actualLocale = getLocaleBestFit(localeName, availableIntlLocales);
if (actualLocale == null) {
console.error(`Could not fetch Unified Number Format locale '${localeName}'. Fallback to 'en'.`);

// TODO(DEFAULT_LOCALE): use default locale instead of 'en'
actualLocale = 'en';
}
// TODO(DEFAULT_LOCALE): use default locale instead of 'en'
actualLocale = 'en';
}

const loader = intlLocaleLoaders(`./${actualLocale}.js`);
const loader = intlLocaleLoaders(`./${actualLocale}.json`);
const localeModule = await runBundleLoader(loader);

return runBundleLoader(loader);
});
Intl.NumberFormat.__addLocaleData(localeModule);
}

function installPluralRules(localeName: string) {
const Intl = global.Intl;
async function installPluralRules(localeName: string) {
const Intl = globalThis.Intl;

if (Intl
&& Intl.PluralRules
if (Intl.PluralRules
&& Intl.PluralRules.supportedLocalesOf
&& Intl.PluralRules.supportedLocalesOf(localeName).length !== 0) {
return Promise.resolve();
return;
}

// the overhead generated by the lazy-loader of each individual locale
// is equivalent to the overhead of loading all locales
// so might as well only impact people without the polyfill.
return import(/* webpackChunkName: "p-intlplural" */ '@formatjs/intl-pluralrules/polyfill-locales');
await import(/* webpackChunkName: "p-intlplural" */ '@formatjs/intl-pluralrules/polyfill');
await import(/* webpackChunkName: "p-intlplural" */ './_intl-plural-locales');
}

function installRelativeTime(localeName: string) {
const Intl = global.Intl;
async function installRelativeTime(localeName: string) {
const Intl = globalThis.Intl;

if (Intl
&& Intl.RelativeTimeFormat
&& Intl.RelativeTimeFormat.supportedLocalesOf
&& Intl.RelativeTimeFormat.supportedLocalesOf(localeName).length !== 0) {
return Promise.resolve();
return;
}

await import(/* webpackChunkName: "p-intlrelative" */ '@formatjs/intl-relativetimeformat/polyfill');

// load locale best fit
// Note: this should never be called on non-webpack generated builds
const localeLoaders = getRelativeTimeLocaleLoaders();
const availableLocales: string[] = localeLoaders.keys().map(getFileName);

let actualLocale = getLocaleBestFit(localeName, availableLocales);
if (actualLocale == null) {
console.error(`Could not fetch Intl.RelativeTimeFormat locale '${localeName}'. Fallback to 'en'.`);

// TODO(DEFAULT_LOCALE): use default locale instead of 'en'
actualLocale = 'en';
}

const loader = localeLoaders(`./${actualLocale}.json`);

const localeModule = await runBundleLoader(loader);
Intl.RelativeTimeFormat.__addLocaleData(localeModule);
}

async function installListFormat(localeName: string) {
const Intl = globalThis.Intl;

if (Intl
&& Intl.ListFormat
&& Intl.ListFormat.supportedLocalesOf
&& Intl.ListFormat.supportedLocalesOf(localeName).length !== 0) {
return;
}

return import(/* webpackChunkName: "p-intlrelative" */ '@formatjs/intl-relativetimeformat/polyfill').then(() => {
await import(/* webpackChunkName: "p-intllist" */ '@formatjs/intl-listformat/polyfill');

// load locale best fit
// Note: this should never be called on non-webpack generated builds
const localeLoaders = getRelativeTimeLocaleLoaders();
const availableLocales: string[] = localeLoaders.keys().map(getFileName);
// load locale best fit
// Note: this should never be called on non-webpack generated builds
const localeLoaders = getListFormatLoaders();
const availableLocales: string[] = localeLoaders.keys().map(getFileName);

let actualLocale = getLocaleBestFit(localeName, availableLocales);
if (actualLocale == null) {
console.error(`Could not fetch Intl.RelativeTimeFormat locale '${localeName}'. Fallback to 'en'.`);
let actualLocale = getLocaleBestFit(localeName, availableLocales);
if (actualLocale == null) {
console.error(`Could not fetch Intl.ListFormat locale '${localeName}'. Fallback to 'en'.`);

// TODO(DEFAULT_LOCALE): use default locale instead of 'en'
actualLocale = 'en';
}
// TODO(DEFAULT_LOCALE): use default locale instead of 'en'
actualLocale = 'en';
}

const loader = localeLoaders(`./${actualLocale}.js`);
const loader = localeLoaders(`./${actualLocale}.js`);

return runBundleLoader(loader);
});
// js version as json version is not supported
await runBundleLoader(loader);
}

export function installIntlLocale(localeName: string): Promise<void> {

return Promise.all([
installNumberAndDateTime(localeName),
installUnifiedNumberFormat(localeName),
installPluralRules(localeName),
installRelativeTime(localeName),
installListFormat(localeName),
]).then(() => void 0);
}
7 changes: 5 additions & 2 deletions src/internals/webpack/features/react-intl.js
Expand Up @@ -36,9 +36,12 @@ export default class ReactIntlFeature extends BaseFeature {

const ignoreRegex = new RegExp(`\\.\\/(?!${supportedLocales.join('|')})[a-z0-9-_]+\\.js`, 'i');

webpackConfig.injectPlugins(new webpack.IgnorePlugin(ignoreRegex, /react-intl\/locale-data$/));
webpackConfig.injectPlugins(new webpack.IgnorePlugin(ignoreRegex, /intl\/locale-data\/jsonp$/));
// webpackConfig.injectPlugins(new webpack.IgnorePlugin(ignoreRegex, /react-intl\/locale-data$/));
// webpackConfig.injectPlugins(new webpack.IgnorePlugin(ignoreRegex, /intl\/locale-data\/jsonp$/));
webpackConfig.injectPlugins(new webpack.IgnorePlugin(ignoreRegex, /@formatjs\/intl-relativetimeformat\/dist\/locale-data$/));
webpackConfig.injectPlugins(new webpack.IgnorePlugin(ignoreRegex, /@formatjs\/intl-unified-numberformat\/dist\/locale-data$/));
webpackConfig.injectPlugins(new webpack.IgnorePlugin(ignoreRegex, /@formatjs\/intl-listformat\/dist\/locale-data$/));
webpackConfig.injectPlugins(new webpack.IgnorePlugin(ignoreRegex, /@formatjs\/intl-pluralrules\/dist\/locale-data$/));
}
}

Expand Down
21 changes: 21 additions & 0 deletions src/shared/globalThis.js
@@ -0,0 +1,21 @@
/* eslint-disable */

// https://mathiasbynens.be/notes/globalthis
const globalObject = (function () {
if (typeof globalThis === 'object') {
return globalThis;
}

Object.defineProperty(Object.prototype, '__magic__', {
get() {
return this;
},
configurable: true, // This makes it possible to `delete` the getter later.
});
const globalObject = __magic__; // get __magic__ from global object's prototype, which is Object.prototype
delete Object.prototype.__magic__;

return globalObject;
}());

export default globalObject;

0 comments on commit 344742e

Please sign in to comment.