core(i18n): initial utility library (#5644)

This commit is contained in:
Patrick Hulce 2018-07-17 16:35:18 -07:00 коммит произвёл Paul Irish
Родитель e94be2cb49
Коммит 589162b696
16 изменённых файлов: 354 добавлений и 23 удалений

Просмотреть файл

@ -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'])

Просмотреть файл

@ -10,6 +10,7 @@
'use strict';
const Audit = require('../audit');
const i18n = 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 str_ = i18n.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: str_(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: str_(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 = str_(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: str_(i18n.UIStrings.columnURL)},
{key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnSize)},
{key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedTime)},
];
const details = Audit.makeOpportunityDetails(headings, results, wastedMs);
@ -223,3 +232,4 @@ class RenderBlockingResources extends Audit {
}
module.exports = RenderBlockingResources;
module.exports.UIStrings = UIStrings;

Просмотреть файл

@ -6,7 +6,15 @@
'use strict';
const Audit = require('../audit');
const Util = require('../../report/html/renderer/util');
const i18n = 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 str_ = i18n.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: str_(UIStrings.title),
description: str_(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: str_(i18n.UIStrings.ms, {timeInMs}),
extendedInfo: {
value: extendedInfo,
},
@ -78,3 +85,4 @@ class InteractiveMetric extends Audit {
}
module.exports = InteractiveMetric;
module.exports.UIStrings = UIStrings;

Просмотреть файл

@ -39,6 +39,7 @@ const defaultSettings = {
// the following settings have no defaults but we still want ensure that `key in settings`
// in config will work in a typechecked way
locale: null, // default determined by the intl library
blockedUrlPatterns: null,
additionalTraceCategories: null,
extraHeaders: null,

Просмотреть файл

@ -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';

Просмотреть файл

@ -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}\xa0ms',
columnURL: 'URL',
columnSize: 'Size (KB)',
columnWastedTime: 'Potential Savings (ms)',
};
const formats = {
number: {
milliseconds: {
maximumFractionDigits: 0,
},
},
};
/**
* @param {string} msg
* @param {Record<string, *>} 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<string, string>} 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|null} [newLocale]
*/
setLocale(newLocale) {
if (!newLocale) return;
locale = newLocale;
},
};

Просмотреть файл

@ -0,0 +1,29 @@
{
"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)"
}
}

Просмотреть файл

@ -0,0 +1,29 @@
{
"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̂ś)"
}
}

Просмотреть файл

@ -0,0 +1,11 @@
/**
* @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.
*/
// @ts-nocheck
'use strict';
module.exports = {
'en-XA': require('./en-XA.json'),
};

Просмотреть файл

@ -0,0 +1,103 @@
#!/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',
];
/**
* @param {string} dir
* @param {Record<string, string>} 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<string, string>} 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<string, string>} strings
*/
function writeStringsToLocaleFormat(locale, strings) {
const fullPath = path.join(LH_ROOT, `lighthouse-core/lib/locales/${locale}.json`);
const output = {};
for (const [key, message] of Object.entries(strings)) {
output[key] = {message};
}
fs.writeFileSync(fullPath, JSON.stringify(output, null, 2));
}
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!');

Просмотреть файл

@ -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": [
@ -2774,6 +2771,7 @@
"gatherMode": false,
"disableStorageReset": false,
"disableDeviceEmulation": false,
"locale": null,
"blockedUrlPatterns": null,
"additionalTraceCategories": null,
"extraHeaders": null,

Просмотреть файл

@ -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')

Просмотреть файл

@ -74,6 +74,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 +97,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 +120,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",

3
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 | null;
maxWaitForLoad?: number;
blockedUrlPatterns?: string[] | null;
additionalTraceCategories?: string | null;

10
typings/intl-messageformat-parser/index.d.ts поставляемый Normal file
Просмотреть файл

@ -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};
}

Просмотреть файл

@ -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"