diff --git a/lighthouse-core/lib/i18n/i18n.js b/lighthouse-core/lib/i18n/i18n.js index fea132ec615e..a7160013e649 100644 --- a/lighthouse-core/lib/i18n/i18n.js +++ b/lighthouse-core/lib/i18n/i18n.js @@ -21,22 +21,26 @@ const MESSAGE_INSTANCE_ID_REGEX = /(.* \| .*) # (\d+)$/; const MESSAGE_INSTANCE_ID_QUICK_REGEX = / # \d+$/; (() => { - // Node usually doesn't come with the locales we want built-in, so load the polyfill if we can. + // Node without full-icu doesn't come with the locales we want built-in. Load the polyfill if needed. + // See https://nodejs.org/api/intl.html#intl_options_for_building_node_js - try { - // @ts-ignore - const IntlPolyfill = require('intl'); - // In browser environments where we don't need the polyfill, this won't exist - if (!IntlPolyfill.NumberFormat) return; + // Conditionally polyfills itself. Bundler removes this dep, so this will be a no-op in browsers. + // @ts-ignore + require('intl-pluralrules'); + // @ts-ignore + const IntlPolyfill = require('intl'); + + // The bundler also removes this dep, so there's nothing to do if it's empty. + if (!IntlPolyfill.NumberFormat) return; + + // Check if global implementation supports a minimum set of locales. + const minimumLocales = ['en', 'es', 'ru', 'zh']; + const supportedLocales = Intl.NumberFormat.supportedLocalesOf(minimumLocales); + + if (supportedLocales.length !== minimumLocales.length) { Intl.NumberFormat = IntlPolyfill.NumberFormat; Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat; - - // Intl.PluralRules polyfilled on require(). - // @ts-ignore - require('intl-pluralrules'); - } catch (_) { - log.warn('i18n', 'Failed to install `intl` polyfill'); } })(); diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/util.js index 902cfc4cb68b..aa16df28a58e 100644 --- a/lighthouse-core/report/html/renderer/util.js +++ b/lighthouse-core/report/html/renderer/util.js @@ -192,7 +192,7 @@ class Util { */ static formatNumber(number, granularity = 0.1) { const coarseValue = Math.round(number / granularity) * granularity; - return coarseValue.toLocaleString(Util.numberDateLocale); + return Util.numberFormatter.format(coarseValue); } /** @@ -201,8 +201,7 @@ class Util { * @return {string} */ static formatBytesToKB(size, granularity = 0.1) { - const kbs = (Math.round(size / 1024 / granularity) * granularity) - .toLocaleString(Util.numberDateLocale); + const kbs = Util.numberFormatter.format(Math.round(size / 1024 / granularity) * granularity); return `${kbs}${NBSP}KB`; } @@ -213,7 +212,7 @@ class Util { */ static formatMilliseconds(ms, granularity = 10) { const coarseTime = Math.round(ms / granularity) * granularity; - return `${coarseTime.toLocaleString(Util.numberDateLocale)}${NBSP}ms`; + return `${Util.numberFormatter.format(coarseTime)}${NBSP}ms`; } /** @@ -223,7 +222,7 @@ class Util { */ static formatSeconds(ms, granularity = 0.1) { const coarseTime = Math.round(ms / 1000 / granularity) * granularity; - return `${coarseTime.toLocaleString(Util.numberDateLocale)}${NBSP}s`; + return `${Util.numberFormatter.format(coarseTime)}${NBSP}s`; } /** @@ -556,10 +555,11 @@ class Util { * @param {LH.Locale} locale */ static setNumberDateLocale(locale) { - Util.numberDateLocale = locale; - // When testing, use a locale with more exciting numeric formatting - if (Util.numberDateLocale === 'en-XA') Util.numberDateLocale = 'de'; + if (locale === 'en-XA') locale = 'de'; + + Util.numberDateLocale = locale; + Util.numberFormatter = new Intl.NumberFormat(locale); } /** @@ -618,6 +618,12 @@ class Util { */ Util.numberDateLocale = 'en'; +/** + * This value stays in sync with Util.numberDateLocale. + * @type {Intl.NumberFormat} + */ +Util.numberFormatter = new Intl.NumberFormat(Util.numberDateLocale); + /** * Report-renderer-specific strings. * @type {LH.I18NRendererStrings} diff --git a/lighthouse-core/test/lib/i18n/swap-locale-test.js b/lighthouse-core/test/lib/i18n/swap-locale-test.js index 9a73a7e8198a..289f405912cb 100644 --- a/lighthouse-core/test/lib/i18n/swap-locale-test.js +++ b/lighthouse-core/test/lib/i18n/swap-locale-test.js @@ -26,6 +26,12 @@ describe('swap-locale', () => { // Renderer formatted strings expect(lhrEn.i18n.rendererFormattedStrings.labDataTitle).toEqual('Lab Data'); expect(lhrEs.i18n.rendererFormattedStrings.labDataTitle).toEqual('Datos de prueba'); + + // Formatted numbers in placeholders. + expect(lhrEn.audits['render-blocking-resources'].displayValue) + .toEqual('Potential savings of 1,130 ms'); + expect(lhrEs.audits['render-blocking-resources'].displayValue) + .toEqual('Ahorro potencial de 1.130 ms'); }); it('can roundtrip back to english correctly', () => { diff --git a/lighthouse-core/test/report/html/renderer/util-test.js b/lighthouse-core/test/report/html/renderer/util-test.js index 774b64795b97..19009fe1af0e 100644 --- a/lighthouse-core/test/report/html/renderer/util-test.js +++ b/lighthouse-core/test/report/html/renderer/util-test.js @@ -9,6 +9,11 @@ const assert = require('assert'); const Util = require('../../../../report/html/renderer/util.js'); const sampleResult = require('../../../results/sample_v2.json'); +// Require i18n to make sure Intl is polyfilled in Node without full-icu for testing. +// When Util is run in a browser, Intl will be supplied natively (IE11+). +// eslint-disable-next-line no-unused-vars +const i18n = require('../../../../lib/i18n/i18n.js'); + const NBSP = '\xa0'; /* eslint-env jest */ @@ -47,25 +52,40 @@ describe('util helpers', () => { assert.equal(Util.formatDuration(28 * 60 * 60 * 1000 + 5000), `1${NBSP}d 4${NBSP}h 5${NBSP}s`); }); - // TODO: need ICU support in node on Travis/Appveyor - it.skip('formats based on locale', () => { + it('formats numbers based on locale', () => { + // Requires full-icu or Intl polyfill. const number = 12346.858558; const originalLocale = Util.numberDateLocale; Util.setNumberDateLocale('de'); assert.strictEqual(Util.formatNumber(number), '12.346,9'); - Util.setNumberDateLocale(originalLocale); // reset + assert.strictEqual(Util.formatBytesToKB(number), `12,1${NBSP}KB`); + assert.strictEqual(Util.formatMilliseconds(number), `12.350${NBSP}ms`); + assert.strictEqual(Util.formatSeconds(number), `12,3${NBSP}s`); + + Util.setNumberDateLocale(originalLocale); // reset locale assert.strictEqual(Util.formatNumber(number), '12,346.9'); + assert.strictEqual(Util.formatBytesToKB(number), `12.1${NBSP}KB`); + assert.strictEqual(Util.formatMilliseconds(number), `12,350${NBSP}ms`); + assert.strictEqual(Util.formatSeconds(number), `12.3${NBSP}s`); }); - it.skip('uses decimal comma with en-XA test locale', () => { + it('uses decimal comma with en-XA test locale', () => { + // Requires full-icu or Intl polyfill. const number = 12346.858558; const originalLocale = Util.numberDateLocale; Util.setNumberDateLocale('en-XA'); assert.strictEqual(Util.formatNumber(number), '12.346,9'); - Util.setNumberDateLocale(originalLocale); // reset + assert.strictEqual(Util.formatBytesToKB(number), `12,1${NBSP}KB`); + assert.strictEqual(Util.formatMilliseconds(number), `12.350${NBSP}ms`); + assert.strictEqual(Util.formatSeconds(number), `12,3${NBSP}s`); + + Util.setNumberDateLocale(originalLocale); // reset locale assert.strictEqual(Util.formatNumber(number), '12,346.9'); + assert.strictEqual(Util.formatBytesToKB(number), `12.1${NBSP}KB`); + assert.strictEqual(Util.formatMilliseconds(number), `12,350${NBSP}ms`); + assert.strictEqual(Util.formatSeconds(number), `12.3${NBSP}s`); }); it('calculates a score ratings', () => {