UNPKG

lighthouse

Version:

Automated auditing, performance metrics, and best practices for the web.

265 lines (235 loc) 7.8 kB
/** * @license * Copyright 2020 Google LLC * SPDX-License-Identifier: Apache-2.0 */ // Not named `NBSP` because that creates a duplicate identifier (util.js). const NBSP2 = '\xa0'; const KiB = 1024; const MiB = KiB * KiB; export class I18nFormatter { /** * @param {LH.Locale} locale */ constructor(locale) { // When testing, use a locale with more exciting numeric formatting. if (locale === 'en-XA') locale = 'de'; this._locale = locale; this._cachedNumberFormatters = new Map(); } /** * @param {number} number * @param {number|undefined} granularity * @param {Intl.NumberFormatOptions=} opts * @return {string} */ _formatNumberWithGranularity(number, granularity, opts = {}) { if (granularity !== undefined) { const log10 = -Math.log10(granularity); if (!Number.isInteger(log10)) { // eslint-disable-next-line no-console console.warn(`granularity of ${granularity} is invalid. Using 1 instead`); granularity = 1; } if (granularity < 1) { opts = {...opts}; opts.minimumFractionDigits = opts.maximumFractionDigits = Math.ceil(log10); } number = Math.round(number / granularity) * granularity; // Avoid displaying a negative value that rounds to zero as "0". if (Object.is(number, -0)) number = 0; } else if (Math.abs(number) < 0.0005) { // Also avoids "-0". number = 0; } let formatter; const cacheKey = [ opts.minimumFractionDigits, opts.maximumFractionDigits, opts.style, opts.unit, opts.unitDisplay, this._locale, ].join(''); formatter = this._cachedNumberFormatters.get(cacheKey); if (!formatter) { formatter = new Intl.NumberFormat(this._locale, opts); this._cachedNumberFormatters.set(cacheKey, formatter); } return formatter.format(number).replace(' ', NBSP2); } /** * Format number. * @param {number} number * @param {number=} granularity Controls how coarse the displayed value is. * If undefined, the number will be displayed as described * by the Intl defaults: tinyurl.com/7s67w5x7 * @return {string} */ formatNumber(number, granularity) { return this._formatNumberWithGranularity(number, granularity); } /** * Format integer. * Just like {@link formatNumber} but uses a granularity of 1, rounding to the nearest * whole number. * @param {number} number * @return {string} */ formatInteger(number) { return this._formatNumberWithGranularity(number, 1); } /** * Format percent. * @param {number} number 0–1 * @return {string} */ formatPercent(number) { return new Intl.NumberFormat(this._locale, {style: 'percent'}).format(number); } /** * @param {number} size * @param {number=} granularity Controls how coarse the displayed value is. * If undefined, the number will be displayed in full. * @return {string} */ formatBytesToKiB(size, granularity = undefined) { return this._formatNumberWithGranularity(size / KiB, granularity) + `${NBSP2}KiB`; } /** * @param {number} size * @param {number=} granularity Controls how coarse the displayed value is. * If undefined, the number will be displayed in full. * @return {string} */ formatBytesToMiB(size, granularity = undefined) { return this._formatNumberWithGranularity(size / MiB, granularity) + `${NBSP2}MiB`; } /** * @param {number} size * @param {number=} granularity Controls how coarse the displayed value is. * If undefined, the number will be displayed in full. * @return {string} */ formatBytes(size, granularity = 1) { return this._formatNumberWithGranularity(size, granularity, { style: 'unit', unit: 'byte', unitDisplay: 'long', }); } /** * @param {number} size * @param {number=} granularity Controls how coarse the displayed value is. * If undefined, the number will be displayed in full. * @return {string} */ formatBytesWithBestUnit(size, granularity = 0.1) { if (size >= MiB) return this.formatBytesToMiB(size, granularity); if (size >= KiB) return this.formatBytesToKiB(size, granularity); return this._formatNumberWithGranularity(size, granularity, { style: 'unit', unit: 'byte', unitDisplay: 'narrow', }); } /** * @param {number} size * @param {number=} granularity Controls how coarse the displayed value is. * If undefined, the number will be displayed in full. * @return {string} */ formatKbps(size, granularity = undefined) { return this._formatNumberWithGranularity(size, granularity, { style: 'unit', unit: 'kilobit-per-second', unitDisplay: 'short', }); } /** * @param {number} ms * @param {number=} granularity Controls how coarse the displayed value is. * If undefined, the number will be displayed in full. * @return {string} */ formatMilliseconds(ms, granularity = undefined) { return this._formatNumberWithGranularity(ms, granularity, { style: 'unit', unit: 'millisecond', unitDisplay: 'short', }); } /** * @param {number} ms * @param {number=} granularity Controls how coarse the displayed value is. * If undefined, the number will be displayed in full. * @return {string} */ formatSeconds(ms, granularity = undefined) { return this._formatNumberWithGranularity(ms / 1000, granularity, { style: 'unit', unit: 'second', unitDisplay: 'narrow', }); } /** * Format time. * @param {string} date * @return {string} */ formatDateTime(date) { /** @type {Intl.DateTimeFormatOptions} */ const options = { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'short', }; // Force UTC if runtime timezone could not be detected. // See https://github.com/GoogleChrome/lighthouse/issues/1056 // and https://github.com/GoogleChrome/lighthouse/pull/9822 let formatter; try { formatter = new Intl.DateTimeFormat(this._locale, options); } catch (err) { options.timeZone = 'UTC'; formatter = new Intl.DateTimeFormat(this._locale, options); } return formatter.format(new Date(date)); } /** * Converts a time in milliseconds into a duration string, i.e. `1d 2h 13m 52s` * @param {number} timeInMilliseconds * @return {string} */ formatDuration(timeInMilliseconds) { // There is a proposal for a Intl.DurationFormat. // https://github.com/tc39/proposal-intl-duration-format // Until then, we do things a bit more manually. let timeInSeconds = timeInMilliseconds / 1000; if (Math.round(timeInSeconds) === 0) { return 'None'; } /** @type {Array<string>} */ const parts = []; /** @type {Record<string, number>} */ const unitToSecondsPer = { day: 60 * 60 * 24, hour: 60 * 60, minute: 60, second: 1, }; Object.keys(unitToSecondsPer).forEach(unit => { const secondsPerUnit = unitToSecondsPer[unit]; const numberOfUnits = Math.floor(timeInSeconds / secondsPerUnit); if (numberOfUnits > 0) { timeInSeconds -= numberOfUnits * secondsPerUnit; const part = this._formatNumberWithGranularity(numberOfUnits, 1, { style: 'unit', unit, unitDisplay: 'narrow', }); parts.push(part); } }); return parts.join(' '); } }