Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

report: remove Util.UIStrings mutation, add I18n renderer class #10153

Merged
merged 13 commits into from
Jan 7, 2020
15 changes: 8 additions & 7 deletions lighthouse-core/audits/dobetterweb/dom-size.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
'use strict';

const Audit = require('../audit.js');
const Util = require('../../report/html/renderer/util.js');
const i18n = require('../../lib/i18n/i18n.js');
const I18n = require('../../report/html/renderer/i18n.js');
const i18n_ = require('../../lib/i18n/i18n.js');

const UIStrings = {
/** Title of a diagnostic audit that provides detail on the size of the web page's DOM. The size of a DOM is characterized by the total number of DOM elements and greatest DOM depth. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
Expand Down Expand Up @@ -44,8 +44,7 @@ const UIStrings = {
statisticDOMWidth: 'Maximum Child Elements',
};

const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);

const str_ = i18n_.createMessageInstanceIdFn(__filename, UIStrings);

class DOMSize extends Audit {
/**
Expand Down Expand Up @@ -97,28 +96,30 @@ class DOMSize extends Audit {
{key: 'value', itemType: 'numeric', text: str_(UIStrings.columnValue)},
];

const i18n = new I18n(context.settings.locale);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a bummer these won't get into the localeLog, maybe we file a separate issue to track that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no idea what this means :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry I meant icuMessagePaths

when we create i18n'd strings using str we get to track it in the lhr so we can use swap-locale later, it'd be great if these usages could take advantage of that

Copy link
Collaborator Author

@connorjclark connorjclark Dec 31, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah right. an additional challenge - besides figuring out how to sink these paths to a central place - the swap locale script only works for UIStrings, not for value formatting.

Instead, I think we shouldn't format in the audit at all. These values should be raw numbers - and all the numeric detail types should be formatted by the report renderer (odd that this isn't the case now ...). That way there's nothing to swap.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does that make sense?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh sorry! yes!

I think we shouldn't format in the audit at all. These values should be raw numbers - and all the numeric detail types should be formatted by the report renderer (odd that this isn't the case now ...). That way there's nothing to swap.

Agreed I think there was even a todo I put in one of my PRs during review on this topic that I've totally forgotten to merge...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool.. so I'll leave the changes in the audits as they are now. It's still an improvement. We can follow up on formatting in just the renderer later.

I'll fix those tests now..


/** @type {LH.Audit.Details.Table['items']} */
const items = [
{
statistic: str_(UIStrings.statisticDOMElements),
element: '',
value: Util.formatNumber(stats.totalBodyElements),
value: i18n.formatNumber(stats.totalBodyElements),
},
{
statistic: str_(UIStrings.statisticDOMDepth),
element: {
type: 'code',
value: stats.depth.snippet,
},
value: Util.formatNumber(stats.depth.max),
value: i18n.formatNumber(stats.depth.max),
},
{
statistic: str_(UIStrings.statisticDOMWidth),
element: {
type: 'code',
value: stats.width.snippet,
},
value: Util.formatNumber(stats.width.max),
value: i18n.formatNumber(stats.width.max),
},
];

Expand Down
5 changes: 3 additions & 2 deletions lighthouse-core/audits/mixed-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

const Audit = require('./audit.js');
const URL = require('../lib/url-shim.js');
const Util = require('../report/html/renderer/util.js');
const I18n = require('../report/html/renderer/i18n.js');
const NetworkRecords = require('../computed/network-records.js');

/**
Expand Down Expand Up @@ -126,7 +126,8 @@ class MixedContent extends Audit {
upgradeableResources.push(resource);
}

const displayValue = `${Util.formatNumber(upgradeableResources.length)}
const i18n = new I18n(context.settings.locale);
const displayValue = `${i18n.formatNumber(upgradeableResources.length)}
${upgradeableResources.length === 1 ? 'request' : 'requests'}`;

/** @type {LH.Audit.Details.Table['headings']} */
Expand Down
6 changes: 4 additions & 2 deletions lighthouse-core/audits/predictive-perf.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
'use strict';

const Audit = require('./audit.js');
const Util = require('../report/html/renderer/util.js');
const I18n = require('../report/html/renderer/i18n.js');

const LanternFcp = require('../computed/metrics/lantern-first-contentful-paint.js');
const LanternFmp = require('../computed/metrics/lantern-first-meaningful-paint.js');
Expand Down Expand Up @@ -92,10 +92,12 @@ class PredictivePerf extends Audit {
SCORING_MEDIAN
);

const i18n = new I18n(context.settings.locale);

return {
score,
numericValue: values.roughEstimateOfTTI,
displayValue: Util.formatMilliseconds(values.roughEstimateOfTTI),
displayValue: i18n.formatMilliseconds(values.roughEstimateOfTTI),
details: {
type: 'debugdata',
// TODO: Consider not nesting values under `items`.
Expand Down
3 changes: 2 additions & 1 deletion lighthouse-core/lib/i18n/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,14 +337,15 @@ function getRendererFormattedStrings(locale) {
if (!localeMessages) throw new Error(`Unsupported locale '${locale}'`);

const icuMessageIds = Object.keys(localeMessages).filter(f => f.includes('core/report/html/'));
/** @type {LH.I18NRendererStrings} */
/** @type {Record<string, string>} */
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
const strings = {};
for (const icuMessageId of icuMessageIds) {
const [filename, varName] = icuMessageId.split(' | ');
if (!filename.endsWith('util.js')) throw new Error(`Unexpected message: ${icuMessageId}`);
strings[varName] = localeMessages[icuMessageId].message;
}

// @ts-ignore
return strings;
}

Expand Down
1 change: 1 addition & 0 deletions lighthouse-core/report/html/html-report-assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const REPORT_JAVASCRIPT = [
fs.readFileSync(__dirname + '/renderer/performance-category-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/pwa-category-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/i18n.js', 'utf8'),
].join(';\n');
const REPORT_CSS = fs.readFileSync(__dirname + '/report-styles.css', 'utf8');
const REPORT_TEMPLATES = fs.readFileSync(__dirname + '/templates.html', 'utf8');
Expand Down
20 changes: 11 additions & 9 deletions lighthouse-core/report/html/renderer/category-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ class CategoryRenderer {
*/
get _clumpTitles() {
return {
warning: Util.UIStrings.warningAuditsGroupTitle,
manual: Util.UIStrings.manualAuditsGroupTitle,
passed: Util.UIStrings.passedAuditsGroupTitle,
notApplicable: Util.UIStrings.notApplicableAuditsGroupTitle,
warning: Util.i18n.strings.warningAuditsGroupTitle,
manual: Util.i18n.strings.manualAuditsGroupTitle,
passed: Util.i18n.strings.passedAuditsGroupTitle,
notApplicable: Util.i18n.strings.notApplicableAuditsGroupTitle,
};
}

Expand All @@ -68,6 +68,7 @@ class CategoryRenderer {
* @return {Element}
*/
populateAuditValues(audit, tmpl) {
const strings = Util.i18n.strings;
const auditEl = this.dom.find('.lh-audit', tmpl);
auditEl.id = audit.result.id;
const scoreDisplayMode = audit.result.scoreDisplayMode;
Expand Down Expand Up @@ -115,10 +116,11 @@ class CategoryRenderer {
if (audit.result.scoreDisplayMode === 'error') {
auditEl.classList.add(`lh-audit--error`);
const textEl = this.dom.find('.lh-audit__display-text', auditEl);
textEl.textContent = Util.UIStrings.errorLabel;
textEl.textContent = strings.errorLabel;
textEl.classList.add('tooltip-boundary');
const tooltip = this.dom.createChildOf(textEl, 'div', 'tooltip tooltip--error');
tooltip.textContent = audit.result.errorMessage || Util.UIStrings.errorMissingAuditInfo;
tooltip.textContent =
audit.result.errorMessage || strings.errorMissingAuditInfo;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't this line get shorter? must be a leftover newline from earlier version :)

} else if (audit.result.explanation) {
const explEl = this.dom.createChildOf(titleEl, 'div', 'lh-audit-explanation');
explEl.textContent = audit.result.explanation;
Expand All @@ -128,7 +130,7 @@ class CategoryRenderer {

// Add list of warnings or singular warning
const warningsEl = this.dom.createChildOf(titleEl, 'div', 'lh-warnings');
this.dom.createChildOf(warningsEl, 'span').textContent = Util.UIStrings.warningHeader;
this.dom.createChildOf(warningsEl, 'span').textContent = strings.warningHeader;
if (warnings.length === 1) {
warningsEl.appendChild(this.dom.document().createTextNode(warnings.join('')));
} else {
Expand Down Expand Up @@ -287,7 +289,7 @@ class CategoryRenderer {

const summaryInnerEl = this.dom.find('.lh-audit-group__summary', clumpElement);
const chevronEl = summaryInnerEl.appendChild(this._createChevron());
chevronEl.title = Util.UIStrings.auditGroupExpandTooltip;
chevronEl.title = Util.i18n.strings.auditGroupExpandTooltip;

const headerEl = this.dom.find('.lh-audit-group__header', clumpElement);
const title = this._clumpTitles[clumpId];
Expand Down Expand Up @@ -345,7 +347,7 @@ class CategoryRenderer {
percentageEl.textContent = scoreOutOf100.toString();
if (category.score === null) {
percentageEl.textContent = '?';
percentageEl.title = Util.UIStrings.errorLabel;
percentageEl.title = Util.i18n.strings.errorLabel;
}

this.dom.find('.lh-gauge__label', tmpl).textContent = category.title;
Expand Down
10 changes: 5 additions & 5 deletions lighthouse-core/report/html/renderer/crc-details-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ class CriticalRequestChainRenderer {
if (!segment.hasChildren) {
const {startTime, endTime, transferSize} = segment.node.request;
const span = dom.createElement('span', 'crc-node__chain-duration');
span.textContent = ' - ' + Util.formatMilliseconds((endTime - startTime) * 1000) + ', ';
span.textContent = ' - ' + Util.i18n.formatMilliseconds((endTime - startTime) * 1000) + ', ';
const span2 = dom.createElement('span', 'crc-node__chain-duration');
span2.textContent = Util.formatBytesToKB(transferSize, 0.01);
span2.textContent = Util.i18n.formatBytesToKB(transferSize, 0.01);

treevalEl.appendChild(span);
treevalEl.appendChild(span2);
Expand Down Expand Up @@ -172,11 +172,11 @@ class CriticalRequestChainRenderer {
const containerEl = dom.find('.lh-crc', tmpl);

// Fill in top summary.
dom.find('.crc-initial-nav', tmpl).textContent = Util.UIStrings.crcInitialNavigation;
dom.find('.crc-initial-nav', tmpl).textContent = Util.i18n.strings.crcInitialNavigation;
dom.find('.lh-crc__longest_duration_label', tmpl).textContent =
Util.UIStrings.crcLongestDurationLabel;
Util.i18n.strings.crcLongestDurationLabel;
dom.find('.lh-crc__longest_duration', tmpl).textContent =
Util.formatMilliseconds(details.longestChain.duration);
Util.i18n.formatMilliseconds(details.longestChain.duration);

// Construct visual tree.
const root = CRCRenderer.initTree(details.chains);
Expand Down
7 changes: 3 additions & 4 deletions lighthouse-core/report/html/renderer/details-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ class DetailsRenderer {
* @param {DOM} dom
*/
constructor(dom) {
/** @type {DOM} */
this._dom = dom;
/** @type {ParentNode} */
this._templateContext; // eslint-disable-line no-unused-expressions
Expand Down Expand Up @@ -76,7 +75,7 @@ class DetailsRenderer {
*/
_renderBytes(details) {
// TODO: handle displayUnit once we have something other than 'kb'
const value = Util.formatBytesToKB(details.value, details.granularity);
const value = Util.i18n.formatBytesToKB(details.value, details.granularity);
return this._renderText(value);
}

Expand All @@ -85,9 +84,9 @@ class DetailsRenderer {
* @return {Element}
*/
_renderMilliseconds(details) {
let value = Util.formatMilliseconds(details.value, details.granularity);
let value = Util.i18n.formatMilliseconds(details.value, details.granularity);
if (details.displayUnit === 'duration') {
value = Util.formatDuration(details.value);
value = Util.i18n.formatDuration(details.value);
}

return this._renderText(value);
Expand Down
157 changes: 157 additions & 0 deletions lighthouse-core/report/html/renderer/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* @license
* Copyright 2019 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';

/* globals self, URL */

// Not named `NBSP` because that creates a duplicate identifier (util.js).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😬, on of these days the grimaces will add up to a build step for renderer :)

const NBSP2 = '\xa0';

class I18n {
/**
* @param {LH.Locale} locale
*/
constructor(locale) {
this.setNumberDateLocale(locale);
this._numberDateLocale = locale;
this._numberFormatter = new Intl.NumberFormat(locale);
}

/**
* @param {LH.I18NRendererStrings} strings
*/
setStrings(strings) {
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
this._strings = strings;
}

get strings() {
return this._strings;
}

/**
* Format number.
* @param {number} number
* @param {number=} granularity Number of decimal places to include. Defaults to 0.1.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove granularity in most of these functions.

We never send a granularity, and sending it as a decimal, instead of the number of digits you want to be significant seems like a bad API.

Copy link
Collaborator Author

@connorjclark connorjclark Jan 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't want to change functionality in this pr. this change would at least require changing many tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the unit-tests use granularity except 1. I'm fine if it's a follow up/not done, just seems like old YAGNI.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, i assumed.

detailsRenderer uses this a bit.

* @return {string}
*/
formatNumber(number, granularity = 0.1) {
const coarseValue = Math.round(number / granularity) * granularity;
return this._numberFormatter.format(coarseValue);
}

/**
* @param {number} size
* @param {number=} granularity Controls how coarse the displayed value is, defaults to .01
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
* @return {string}
*/
formatBytesToKB(size, granularity = 0.1) {
const kbs = this._numberFormatter.format(Math.round(size / 1024 / granularity) * granularity);
return `${kbs}${NBSP2}KB`;
}

/**
* @param {number} ms
* @param {number=} granularity Controls how coarse the displayed value is, defaults to 10
* @return {string}
*/
formatMilliseconds(ms, granularity = 10) {
const coarseTime = Math.round(ms / granularity) * granularity;
return `${this._numberFormatter.format(coarseTime)}${NBSP2}ms`;
}

/**
* @param {number} ms
* @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
* @return {string}
*/
formatSeconds(ms, granularity = 0.1) {
const coarseTime = Math.round(ms / 1000 / granularity) * granularity;
return `${this._numberFormatter.format(coarseTime)}${NBSP2}s`;
}

/**
* 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',
};
let formatter = new Intl.DateTimeFormat(this._numberDateLocale, options);

// Force UTC if runtime timezone could not be detected.
// See https://github.com/GoogleChrome/lighthouse/issues/1056
const tz = formatter.resolvedOptions().timeZone;
if (!tz || tz.toLowerCase() === 'etc/unknown') {
options.timeZone = 'UTC';
formatter = new Intl.DateTimeFormat(this._numberDateLocale, 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) {
let timeInSeconds = timeInMilliseconds / 1000;
if (Math.round(timeInSeconds) === 0) {
return 'None';
}

/** @type {Array<string>} */
const parts = [];
const unitLabels = /** @type {Object<string, number>} */ ({
d: 60 * 60 * 24,
h: 60 * 60,
m: 60,
s: 1,
});

Object.keys(unitLabels).forEach(label => {
const unit = unitLabels[label];
const numberOfUnits = Math.floor(timeInSeconds / unit);
if (numberOfUnits > 0) {
timeInSeconds -= numberOfUnits * unit;
parts.push(`${numberOfUnits}\xa0${label}`);
}
});

return parts.join(' ');
}

/**
* Set the locale to be used for I18n's number and date formatting functions.
* @param {LH.Locale} locale
*/
setNumberDateLocale(locale) {
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
// When testing, use a locale with more exciting numeric formatting
if (locale === 'en-XA') locale = 'de';

this._numberDateLocale = locale;
this._numberFormatter = new Intl.NumberFormat(locale);
}
}

if (typeof module !== 'undefined' && module.exports) {
module.exports = I18n;
} else {
self.I18n = I18n;
}
Loading