From 1b22262ce2fc97f1dd10f631d65210f5514a22a6 Mon Sep 17 00:00:00 2001 From: Patrick Hulce Date: Tue, 10 Jul 2018 13:25:01 -0700 Subject: [PATCH] core(i18n): initial utility library --- lighthouse-cli/cli-flags.js | 5 + .../render-blocking-resources.js | 34 +++-- lighthouse-core/audits/metrics/interactive.js | 18 ++- lighthouse-core/index.js | 2 + lighthouse-core/lib/i18n.js | 99 +++++++++++++++ lighthouse-core/lib/locales/en-US.js | 40 ++++++ lighthouse-core/lib/locales/en-XA.js | 40 ++++++ lighthouse-core/lib/locales/index.js | 10 ++ .../scripts/i18n/collect-strings.js | 118 ++++++++++++++++++ lighthouse-core/test/results/sample_v2.json | 9 +- lighthouse-extension/gulpfile.js | 1 + package.json | 7 +- typings/externs.d.ts | 3 + typings/intl-messageformat-parser/index.d.ts | 10 ++ yarn.lock | 18 +++ 15 files changed, 388 insertions(+), 26 deletions(-) create mode 100644 lighthouse-core/lib/i18n.js create mode 100644 lighthouse-core/lib/locales/en-US.js create mode 100644 lighthouse-core/lib/locales/en-XA.js create mode 100644 lighthouse-core/lib/locales/index.js create mode 100644 lighthouse-core/scripts/i18n/collect-strings.js create mode 100644 typings/intl-messageformat-parser/index.d.ts diff --git a/lighthouse-cli/cli-flags.js b/lighthouse-cli/cli-flags.js index d0d7b5345f2d..3c4e3345fa47 100644 --- a/lighthouse-cli/cli-flags.js +++ b/lighthouse-cli/cli-flags.js @@ -64,6 +64,7 @@ function getFlags(manualArgv) { ], 'Configuration:') .describe({ + 'locale': 'The locale/language the report should be formatted in', 'enable-error-reporting': 'Enables error reporting, overriding any saved preference. --no-enable-error-reporting will do the opposite. More: https://git.io/vFFTO', 'blocked-url-patterns': 'Block any network requests to the specified URL patterns', @@ -118,6 +119,10 @@ function getFlags(manualArgv) { 'disable-storage-reset', 'disable-device-emulation', 'save-assets', 'list-all-audits', 'list-trace-categories', 'view', 'verbose', 'quiet', 'help', ]) + .choices('locale', [ + 'en-US', // English + 'en-XA', // Accented English, good for testing + ]) .choices('output', printer.getValidOutputOptions()) .choices('throttling-method', ['devtools', 'provided', 'simulate']) .choices('preset', ['full', 'perf', 'mixed-content']) diff --git a/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js b/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js index 65511d93e707..e92c120b0be6 100644 --- a/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js +++ b/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js @@ -10,6 +10,7 @@ 'use strict'; const Audit = require('../audit'); +const i18nUtils = require('../../lib/i18n'); const BaseNode = require('../../lib/dependency-graph/base-node'); const ByteEfficiencyAudit = require('./byte-efficiency-audit'); const UnusedCSS = require('./unused-css-rules'); @@ -25,6 +26,19 @@ const NetworkRequest = require('../../lib/network-request'); // to possibly be non-blocking (and they have minimal impact anyway). const MINIMUM_WASTED_MS = 50; +const UIStrings = { + title: 'Eliminate render-blocking resources', + description: 'Resources are blocking the first paint of your page. Consider ' + + 'delivering critical JS/CSS inline and deferring all non-critical ' + + 'JS/styles. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/blocking-resources).', + displayValue: `{itemCount, plural, + one {1 resource} + other {# resources} + } delayed first paint by {timeInMs, number, milliseconds} ms`, +}; + +const i18n = i18nUtils.createStringFormatter(__filename, UIStrings); + /** * Given a simulation's nodeTimings, return an object with the nodes/timing keyed by network URL * @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings @@ -52,12 +66,9 @@ class RenderBlockingResources extends Audit { static get meta() { return { id: 'render-blocking-resources', - title: 'Eliminate render-blocking resources', + title: i18n(UIStrings.title), scoreDisplayMode: Audit.SCORING_MODES.NUMERIC, - description: - 'Resources are blocking the first paint of your page. Consider ' + - 'delivering critical JS/CSS inline and deferring all non-critical ' + - 'JS/styles. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/blocking-resources).', + description: i18n(UIStrings.description), // This audit also looks at CSSUsage but has a graceful fallback if it failed, so do not mark // it as a "requiredArtifact". // TODO: look into adding an `optionalArtifacts` property that captures this @@ -198,17 +209,15 @@ class RenderBlockingResources extends Audit { const {results, wastedMs} = await RenderBlockingResources.computeResults(artifacts, context); let displayValue = ''; - if (results.length > 1) { - displayValue = `${results.length} resources delayed first paint by ${wastedMs}ms`; - } else if (results.length === 1) { - displayValue = `${results.length} resource delayed first paint by ${wastedMs}ms`; + if (results.length > 0) { + displayValue = i18n(UIStrings.displayValue, {timeInMs: wastedMs, itemCount: results.length}); } /** @type {LH.Result.Audit.OpportunityDetails['headings']} */ const headings = [ - {key: 'url', valueType: 'url', label: 'URL'}, - {key: 'totalBytes', valueType: 'bytes', label: 'Size (KB)'}, - {key: 'wastedMs', valueType: 'timespanMs', label: 'Download Time (ms)'}, + {key: 'url', valueType: 'url', label: i18n(i18nUtils.UIStrings.columnURL)}, + {key: 'totalBytes', valueType: 'bytes', label: i18n(i18nUtils.UIStrings.columnSize)}, + {key: 'wastedMs', valueType: 'timespanMs', label: i18n(i18nUtils.UIStrings.columnWastedTime)}, ]; const details = Audit.makeOpportunityDetails(headings, results, wastedMs); @@ -223,3 +232,4 @@ class RenderBlockingResources extends Audit { } module.exports = RenderBlockingResources; +module.exports.UIStrings = UIStrings; diff --git a/lighthouse-core/audits/metrics/interactive.js b/lighthouse-core/audits/metrics/interactive.js index 3300082db495..3557e7b3b4d0 100644 --- a/lighthouse-core/audits/metrics/interactive.js +++ b/lighthouse-core/audits/metrics/interactive.js @@ -6,7 +6,15 @@ 'use strict'; const Audit = require('../audit'); -const Util = require('../../report/html/renderer/util'); +const i18nUtils = require('../../lib/i18n'); + +const UIStrings = { + title: 'Time to Interactive', + description: 'Interactive marks the time at which the page is fully interactive. ' + + '[Learn more](https://developers.google.com/web/tools/lighthouse/audits/consistently-interactive).', +}; + +const i18n = i18nUtils.createStringFormatter(__filename, UIStrings); /** * @fileoverview This audit identifies the time the page is "consistently interactive". @@ -21,9 +29,8 @@ class InteractiveMetric extends Audit { static get meta() { return { id: 'interactive', - title: 'Time to Interactive', - description: 'Interactive marks the time at which the page is fully interactive. ' + - '[Learn more](https://developers.google.com/web/tools/lighthouse/audits/consistently-interactive).', + title: i18n(UIStrings.title), + description: i18n(UIStrings.description), scoreDisplayMode: Audit.SCORING_MODES.NUMERIC, requiredArtifacts: ['traces', 'devtoolsLogs'], }; @@ -69,7 +76,7 @@ class InteractiveMetric extends Audit { context.options.scoreMedian ), rawValue: timeInMs, - displayValue: [Util.MS_DISPLAY_VALUE, timeInMs], + displayValue: i18n(i18nUtils.UIStrings.ms, {timeInMs}), extendedInfo: { value: extendedInfo, }, @@ -78,3 +85,4 @@ class InteractiveMetric extends Audit { } module.exports = InteractiveMetric; +module.exports.UIStrings = UIStrings; diff --git a/lighthouse-core/index.js b/lighthouse-core/index.js index 7b91a8b0dce7..063d40399b89 100644 --- a/lighthouse-core/index.js +++ b/lighthouse-core/index.js @@ -8,6 +8,7 @@ const Runner = require('./runner'); const log = require('lighthouse-logger'); const ChromeProtocol = require('./gather/connections/cri.js'); +const i18n = require('./lib/i18n'); const Config = require('./config/config'); /* @@ -34,6 +35,7 @@ const Config = require('./config/config'); async function lighthouse(url, flags, configJSON) { // TODO(bckenny): figure out Flags types. flags = flags || /** @type {LH.Flags} */ ({}); + i18n.setLocale(flags.locale); // set logging preferences, assume quiet flags.logLevel = flags.logLevel || 'error'; diff --git a/lighthouse-core/lib/i18n.js b/lighthouse-core/lib/i18n.js new file mode 100644 index 000000000000..16ab4f839905 --- /dev/null +++ b/lighthouse-core/lib/i18n.js @@ -0,0 +1,99 @@ +/** + * @license Copyright 2018 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'; + +const path = require('path'); +const MessageFormat = require('intl-messageformat').default; +const MessageParser = require('intl-messageformat-parser'); +const LOCALES = require('./locales'); + +let locale = MessageFormat.defaultLocale; + +const LH_ROOT = path.join(__dirname, '../../'); + +try { + // Node usually doesn't come with the locales we want built-in, so load the polyfill. + // In browser environments, we won't need the polyfill, and this will throw so wrap in try/catch. + + // @ts-ignore + const IntlPolyfill = require('intl'); + // @ts-ignore + Intl.NumberFormat = IntlPolyfill.NumberFormat; + // @ts-ignore + Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat; +} catch (_) {} + +const UIStrings = { + ms: '{timeInMs, number, milliseconds} ms', + columnURL: 'URL', + columnSize: 'Size (KB)', + columnWastedTime: 'Potential Savings (ms)', +}; + +const formats = { + number: { + milliseconds: { + maximumFractionDigits: 0, + }, + }, +}; + +/** + * @param {string} msg + * @param {Record} values + */ +function preprocessMessageValues(msg, values) { + const parsed = MessageParser.parse(msg); + // Round all milliseconds to 10s place + parsed.elements + .filter(el => el.format && el.format.style === 'milliseconds') + .forEach(el => (values[el.id] = Math.round(values[el.id] / 10) * 10)); + + // Replace all the bytes with KB + parsed.elements + .filter(el => el.format && el.format.style === 'bytes') + .forEach(el => (values[el.id] = values[el.id] / 1024)); +} + +module.exports = { + UIStrings, + /** + * @param {string} filename + * @param {Record} fileStrings + */ + createStringFormatter(filename, fileStrings) { + const mergedStrings = {...UIStrings, ...fileStrings}; + + /** @param {string} msg @param {*} [values] */ + const formatFn = (msg, values) => { + const keyname = Object.keys(mergedStrings).find(key => mergedStrings[key] === msg); + if (!keyname) throw new Error(`Could not locate: ${msg}`); + preprocessMessageValues(msg, values); + + const filenameToLookup = keyname in UIStrings ? __filename : filename; + const lookupKey = path.relative(LH_ROOT, filenameToLookup) + '!#' + keyname; + const localeStrings = LOCALES[locale] || {}; + const localeString = localeStrings[lookupKey] && localeStrings[lookupKey].message; + // fallback to the original english message if we couldn't find a message in the specified locale + // better to have an english message than no message at all, in some number cases it won't even matter + const messageForMessageFormat = localeString || msg; + // when using accented english, force the use of a different locale for number formatting + const localeForMessageFormat = locale === 'en-XA' ? 'de-DE' : locale; + + const formatter = new MessageFormat(messageForMessageFormat, localeForMessageFormat, formats); + return formatter.format(values); + }; + + return formatFn; + }, + /** + * @param {LH.Locale} [newLocale] + */ + setLocale(newLocale) { + if (typeof newLocale === 'undefined') return; + locale = newLocale; + }, +}; diff --git a/lighthouse-core/lib/locales/en-US.js b/lighthouse-core/lib/locales/en-US.js new file mode 100644 index 000000000000..90fcac7f576c --- /dev/null +++ b/lighthouse-core/lib/locales/en-US.js @@ -0,0 +1,40 @@ +/** +* @license Copyright 2018 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'; + +// THIS FILE IS AUTO-GENERATED, DO NOT MODIFY + +/* eslint-disable */ + +module.exports = { + "lighthouse-core/audits/byte-efficiency/render-blocking-resources.js!#title": { + "message": "Eliminate render-blocking resources" + }, + "lighthouse-core/audits/byte-efficiency/render-blocking-resources.js!#description": { + "message": "Resources are blocking the first paint of your page. Consider delivering critical JS/CSS inline and deferring all non-critical JS/styles. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/blocking-resources)." + }, + "lighthouse-core/audits/byte-efficiency/render-blocking-resources.js!#displayValue": { + "message": "{itemCount, plural,\n one {1 resource}\n other {# resources}\n } delayed first paint by {timeInMs, number, milliseconds} ms" + }, + "lighthouse-core/audits/metrics/interactive.js!#title": { + "message": "Time to Interactive" + }, + "lighthouse-core/audits/metrics/interactive.js!#description": { + "message": "Interactive marks the time at which the page is fully interactive. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/consistently-interactive)." + }, + "lighthouse-core/lib/i18n.js!#ms": { + "message": "{timeInMs, number, milliseconds} ms" + }, + "lighthouse-core/lib/i18n.js!#columnURL": { + "message": "URL" + }, + "lighthouse-core/lib/i18n.js!#columnSize": { + "message": "Size (KB)" + }, + "lighthouse-core/lib/i18n.js!#columnWastedTime": { + "message": "Potential Savings (ms)" + } +}; diff --git a/lighthouse-core/lib/locales/en-XA.js b/lighthouse-core/lib/locales/en-XA.js new file mode 100644 index 000000000000..8e58dc9b1b42 --- /dev/null +++ b/lighthouse-core/lib/locales/en-XA.js @@ -0,0 +1,40 @@ +/** +* @license Copyright 2018 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'; + +// THIS FILE IS AUTO-GENERATED, DO NOT MODIFY + +/* eslint-disable */ + +module.exports = { + "lighthouse-core/audits/byte-efficiency/render-blocking-resources.js!#title": { + "message": "Êĺîḿîńât́ê ŕêńd̂ér̂-b́l̂óĉḱîńĝ ŕêśôúr̂ćêś" + }, + "lighthouse-core/audits/byte-efficiency/render-blocking-resources.js!#description": { + "message": "R̂éŝóûŕĉéŝ ár̂é b̂ĺôćk̂ín̂ǵ t̂h́ê f́îŕŝt́ p̂áîńt̂ óf̂ ýôúr̂ ṕâǵê. Ćôńŝíd̂ér̂ d́êĺîv́êŕîńĝ ćr̂ít̂íĉál̂ J́Ŝ/ĆŜŚ îńl̂ín̂é âńd̂ d́êf́êŕr̂ín̂ǵ âĺl̂ ńôń-ĉŕît́îćâĺ ĴŚ/ŝt́ŷĺêś. [L̂éâŕn̂ ḿôŕê](h́t̂t́p̂ś://d̂év̂él̂óp̂ér̂ś.ĝóôǵl̂é.ĉóm̂/ẃêb́/t̂óôĺŝ/ĺîǵĥt́ĥóûśê/áûd́ît́ŝ/b́l̂óĉḱîńĝ-ŕêśôúr̂ćêś)." + }, + "lighthouse-core/audits/byte-efficiency/render-blocking-resources.js!#displayValue": { + "message": "{itemCount, plural,\n one {1 resource}\n other {# resources}\n } d̂él̂áŷéd̂ f́îŕŝt́ p̂áîńt̂ b́ŷ {timeInMs, number, milliseconds} ḿŝ" + }, + "lighthouse-core/audits/metrics/interactive.js!#title": { + "message": "T̂ím̂é t̂ó Îńt̂ér̂áĉt́îv́ê" + }, + "lighthouse-core/audits/metrics/interactive.js!#description": { + "message": "Îńt̂ér̂áĉt́îv́ê ḿâŕk̂ś t̂h́ê t́îḿê át̂ ẃĥíĉh́ t̂h́ê ṕâǵê íŝ f́ûĺl̂ý îńt̂ér̂áĉt́îv́ê. [Ĺêár̂ń m̂ór̂é](ĥt́t̂ṕŝ://d́êv́êĺôṕêŕŝ.ǵôóĝĺê.ćôḿ/ŵéb̂/t́ôól̂ś/l̂íĝh́t̂h́ôúŝé/âúd̂ít̂ś/ĉón̂śîśt̂én̂t́l̂ý-îńt̂ér̂áĉt́îv́ê)." + }, + "lighthouse-core/lib/i18n.js!#ms": { + "message": "{timeInMs, number, milliseconds} m̂ś" + }, + "lighthouse-core/lib/i18n.js!#columnURL": { + "message": "ÛŔL̂" + }, + "lighthouse-core/lib/i18n.js!#columnSize": { + "message": "Ŝíẑé (K̂B́)" + }, + "lighthouse-core/lib/i18n.js!#columnWastedTime": { + "message": "P̂ót̂én̂t́îál̂ Śâv́îńĝś (m̂ś)" + } +}; diff --git a/lighthouse-core/lib/locales/index.js b/lighthouse-core/lib/locales/index.js new file mode 100644 index 000000000000..0f63a5cac16c --- /dev/null +++ b/lighthouse-core/lib/locales/index.js @@ -0,0 +1,10 @@ +/** + * @license Copyright 2018 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'; + +module.exports = { + 'en-XA': require('./en-XA'), +}; diff --git a/lighthouse-core/scripts/i18n/collect-strings.js b/lighthouse-core/scripts/i18n/collect-strings.js new file mode 100644 index 000000000000..aa96507584cc --- /dev/null +++ b/lighthouse-core/scripts/i18n/collect-strings.js @@ -0,0 +1,118 @@ +#!/usr/bin/env node +/** + * @license Copyright 2018 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'; + +/* eslint-disable no-console, max-len */ + +const fs = require('fs'); +const path = require('path'); + +const LH_ROOT = path.join(__dirname, '../../../'); + +const ignoredPathComponents = [ + '/.git', + '/scripts', + '/node_modules', + '/renderer', + '/test/', + '-test.js', +]; + +const localeHeaderContent = `/** +* @license Copyright 2018 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'; + +// THIS FILE IS AUTO-GENERATED, DO NOT MODIFY + +/* eslint-disable */ + +`; + +/** + * @param {string} dir + * @param {Record} strings + */ +function collectAllStringsInDir(dir, strings = {}) { + for (const name of fs.readdirSync(dir)) { + const fullPath = path.join(dir, name); + const relativePath = path.relative(LH_ROOT, fullPath); + if (ignoredPathComponents.some(p => fullPath.includes(p))) continue; + + if (fs.statSync(fullPath).isDirectory()) { + collectAllStringsInDir(fullPath, strings); + } else { + if (name.endsWith('.js')) { + console.log('Collecting from', relativePath); + const mod = require(fullPath); + if (!mod.UIStrings) continue; + for (const [key, value] of Object.entries(mod.UIStrings)) { + strings[`${relativePath}!#${key}`] = value; + } + } + } + } + + return strings; +} + +/** + * @param {Record} strings + */ +function createPsuedoLocaleStrings(strings) { + const psuedoLocalizedStrings = {}; + for (const [key, string] of Object.entries(strings)) { + const psuedoLocalizedString = []; + let braceCount = 0; + let useHatForAccentMark = true; + for (let i = 0; i < string.length; i++) { + const char = string.substr(i, 1); + psuedoLocalizedString.push(char); + // Don't touch the characters inside braces + if (char === '{') braceCount++; + else if (char === '}') braceCount--; + else if (braceCount === 0) { + if (/[a-z]/i.test(char)) { + psuedoLocalizedString.push(useHatForAccentMark ? `\u0302` : `\u0301`); + useHatForAccentMark = !useHatForAccentMark; + } + } + } + + psuedoLocalizedStrings[key] = psuedoLocalizedString.join(''); + } + + return psuedoLocalizedStrings; +} + +/** + * @param {LH.Locale} locale + * @param {Record} strings + */ +function writeStringsToLocaleFormat(locale, strings) { + const fullPath = path.join(LH_ROOT, 'lighthouse-core/lib/locales/', `${locale}.js`); + const outputObject = {}; + for (const [key, message] of Object.entries(strings)) { + outputObject[key] = {message}; + } + + const outputString = localeHeaderContent + ` + module.exports = ${JSON.stringify(outputObject, null, 2)}; + `.trim() + '\n'; + + fs.writeFileSync(fullPath, outputString); +} + +const strings = collectAllStringsInDir(path.join(LH_ROOT, 'lighthouse-core')); +const psuedoLocalizedStrings = createPsuedoLocaleStrings(strings); +console.log('Collected!'); + +writeStringsToLocaleFormat('en-US', strings); +writeStringsToLocaleFormat('en-XA', psuedoLocalizedStrings); +console.log('Written to disk!'); diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json index 4a535323d242..1ce89c32e650 100644 --- a/lighthouse-core/test/results/sample_v2.json +++ b/lighthouse-core/test/results/sample_v2.json @@ -276,10 +276,7 @@ "score": 0.78, "scoreDisplayMode": "numeric", "rawValue": 4927.278, - "displayValue": [ - "%10d ms", - 4927.278 - ] + "displayValue": "4,930 ms" }, "user-timings": { "id": "user-timings", @@ -1923,7 +1920,7 @@ "score": 0.46, "scoreDisplayMode": "numeric", "rawValue": 1129, - "displayValue": "5 resources delayed first paint by 1129ms", + "displayValue": "5 resources delayed first paint by 1,130 ms", "details": { "type": "opportunity", "headings": [ @@ -1940,7 +1937,7 @@ { "key": "wastedMs", "valueType": "timespanMs", - "label": "Download Time (ms)" + "label": "Potential Savings (ms)" } ], "items": [ diff --git a/lighthouse-extension/gulpfile.js b/lighthouse-extension/gulpfile.js index 646f6c610dc2..20ed862e37f6 100644 --- a/lighthouse-extension/gulpfile.js +++ b/lighthouse-extension/gulpfile.js @@ -125,6 +125,7 @@ gulp.task('browserify-lighthouse', () => { // scripts will need some additional transforms, ignores and requires… bundle.ignore('source-map') .ignore('debug/node') + .ignore('intl') .ignore('raven') .ignore('mkdirp') .ignore('rimraf') diff --git a/package.json b/package.json index a395a51124c4..56533022ef16 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,7 @@ "build-viewer": "cd ./lighthouse-viewer && yarn build", "clean": "rimraf *.report.html *.report.dom.html *.report.json *.screenshots.html *.screenshots.json *.devtoolslog.json *.trace.json || true", "lint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe . || eslint .", - "smoke": "node lighthouse-cli/test/smokehouse/run-smoke.js", - "debug": "node --inspect-brk ./lighthouse-cli/index.js", "start": "node ./lighthouse-cli/index.js", "test": "yarn diff:sample-json && yarn lint --quiet && yarn unit && yarn type-check", @@ -51,7 +49,6 @@ "coverage:smoke": "nyc yarn smoke && nyc report --reporter html", "coveralls": "cat unit-coverage.lcov | coveralls", "codecov": "codecov -f unit-coverage.lcov -F unit && codecov -f smoke-coverage.lcov -F smoke", - "devtools": "bash lighthouse-core/scripts/roll-to-devtools.sh", "compile-devtools": "bash lighthouse-core/scripts/compile-against-devtools.sh", "chrome": "node lighthouse-core/scripts/manual-chrome-launcher.js", @@ -74,6 +71,7 @@ "@types/configstore": "^2.1.1", "@types/css-font-loading-module": "^0.0.2", "@types/inquirer": "^0.0.35", + "@types/intl-messageformat": "^1.3.0", "@types/jpeg-js": "^0.3.0", "@types/lodash.isequal": "^4.5.2", "@types/node": "*", @@ -96,6 +94,7 @@ "gulp": "^3.9.1", "gulp-replace": "^0.5.4", "gulp-util": "^3.0.7", + "intl": "^1.2.5", "jest": "^23.2.0", "jsdom": "^9.12.0", "mocha": "^3.2.0", @@ -118,6 +117,8 @@ "esprima": "^4.0.0", "http-link-header": "^0.8.0", "inquirer": "^3.3.0", + "intl-messageformat": "^2.2.0", + "intl-messageformat-parser": "^1.4.0", "jpeg-js": "0.1.2", "js-library-detector": "^4.3.1", "lighthouse-logger": "^1.0.0", diff --git a/typings/externs.d.ts b/typings/externs.d.ts index 8e04d0dbfab8..c2d57840b69a 100644 --- a/typings/externs.d.ts +++ b/typings/externs.d.ts @@ -52,10 +52,13 @@ declare global { cpuSlowdownMultiplier?: number } + export type Locale = 'en-US'|'en-XA'; + export type OutputMode = 'json' | 'html' | 'csv'; interface SharedFlagsSettings { output?: OutputMode|OutputMode[]; + locale?: Locale; maxWaitForLoad?: number; blockedUrlPatterns?: string[] | null; additionalTraceCategories?: string | null; diff --git a/typings/intl-messageformat-parser/index.d.ts b/typings/intl-messageformat-parser/index.d.ts new file mode 100644 index 000000000000..0db5d1fed74b --- /dev/null +++ b/typings/intl-messageformat-parser/index.d.ts @@ -0,0 +1,10 @@ +declare module 'intl-messageformat-parser' { + interface Element { + type: 'messageTextElement'|'argumentElement'; + id: string + value?: string + format?: null | {type: string; style?: string}; + } + function parse(message: string): {elements: Element[]}; + export {parse}; +} diff --git a/yarn.lock b/yarn.lock index 6e8c0da55e8f..aaeb04969b25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,6 +55,10 @@ "@types/rx" "*" "@types/through" "*" +"@types/intl-messageformat@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@types/intl-messageformat/-/intl-messageformat-1.3.0.tgz#6af7144802b13d62ade9ad68f8420cdb33b75916" + "@types/jpeg-js@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@types/jpeg-js/-/jpeg-js-0.3.0.tgz#5971f0aa50900194e9565711c265c10027385e89" @@ -2961,6 +2965,20 @@ interpret@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c" +intl-messageformat-parser@1.4.0, intl-messageformat-parser@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-1.4.0.tgz#b43d45a97468cadbe44331d74bb1e8dea44fc075" + +intl-messageformat@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.2.0.tgz#345bcd46de630b7683330c2e52177ff5eab484fc" + dependencies: + intl-messageformat-parser "1.4.0" + +intl@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/intl/-/intl-1.2.5.tgz#82244a2190c4e419f8371f5aa34daa3420e2abde" + invariant@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.1.tgz#b097010547668c7e337028ebe816ebe36c8a8d54"