lighthouse 8.6.0-dev.20211013 → 8.6.0-dev.20211017

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@
7
7
  import {createContext, FunctionComponent} from 'preact';
8
8
  import {useContext, useMemo} from 'preact/hooks';
9
9
 
10
+ import {formatMessage} from '../../../shared/localization/format';
10
11
  import {I18n} from '../../../report/renderer/i18n';
11
12
  import {UIStrings} from './ui-strings';
12
13
  import {useLocale} from '../util';
@@ -25,6 +26,13 @@ export function useUIStrings() {
25
26
  return i18n.strings;
26
27
  }
27
28
 
29
+ export function useStringFormatter() {
30
+ const locale = useLocale();
31
+ return (str: string, values?: Record<string, string|number>) => {
32
+ return formatMessage(str, values, locale);
33
+ };
34
+ }
35
+
28
36
  export const I18nProvider: FunctionComponent = ({children}) => {
29
37
  const locale = useLocale();
30
38
  const i18n = useMemo(() => new I18n(locale, {
@@ -57,6 +57,30 @@ export const UIStrings = {
57
57
  ratingFail: 'Poor',
58
58
  /** Rating indicating that a report category rating could not be calculated because of an error. */
59
59
  ratingError: 'Error',
60
+ /**
61
+ * @description Label indicating the number of Lighthouse reports that evaluate a web page as it loads.
62
+ * @example {2} numNavigation
63
+ */
64
+ navigationReportCount: `{numNavigation, plural,
65
+ =1 {{numNavigation} navigation report}
66
+ other {{numNavigation} navigation reports}
67
+ }`,
68
+ /**
69
+ * @description Label indicating the number of Lighthouse reports that evaluate a web page over a period of time.
70
+ * @example {2} numTimespan
71
+ */
72
+ timespanReportCount: `{numTimespan, plural,
73
+ =1 {{numTimespan} timespan report}
74
+ other {{numTimespan} timespan reports}
75
+ }`,
76
+ /**
77
+ * @description Label indicating the number of Lighthouse reports that evaluate a web page at a single point in time.
78
+ * @example {2} numSnapshot
79
+ */
80
+ snapshotReportCount: `{numSnapshot, plural,
81
+ =1 {{numSnapshot} snapshot report}
82
+ other {{numSnapshot} snapshot reports}
83
+ }`,
60
84
  /** Label for a button that saves a Lighthouse report to disk. */
61
85
  save: 'Save',
62
86
  /** Label for a button that toggles the help modal with explanations on how to interpret the Lighthouse flow report. */
@@ -83,4 +107,28 @@ export const UIStrings = {
83
107
  helpUseCaseSnapshot1: 'Find accessibility issues in single page applications or complex forms.',
84
108
  /** Example use case for how Lighthouse can be applied in practice. Appears in a list with other examples. */
85
109
  helpUseCaseSnapshot2: 'Evaluate best practices of menus and UI elements hidden behind interaction.',
110
+ /**
111
+ * @description Label indicating the number of Lighthouse audits that passed.
112
+ * @example {2} numPassed
113
+ */
114
+ passedAuditCount: `{numPassed, plural,
115
+ =1 {{numPassed} audit passed}
116
+ other {{numPassed} audits passed}
117
+ }`,
118
+ /**
119
+ * @description Label indicating the number of Lighthouse audits that are possible to pass for a page.
120
+ * @example {2} numPassableAudits
121
+ */
122
+ passableAuditCount: `{numPassableAudits, plural,
123
+ =1 {{numPassableAudits} passable audit}
124
+ other {{numPassableAudits} passable audits}
125
+ }`,
126
+ /**
127
+ * @description Label indicating the number of Lighthouse audits that are informative.
128
+ * @example {2} numInformative
129
+ */
130
+ informativeAuditCount: `{numInformative, plural,
131
+ =1 {{numInformative} informative audit}
132
+ other {{numInformative} informative audits}
133
+ }`,
86
134
  };
@@ -9,7 +9,7 @@ import {FunctionComponent} from 'preact';
9
9
  import {Util} from '../../../report/renderer/util';
10
10
  import {Separator} from '../common';
11
11
  import {CategoryScore} from '../wrappers/category-score';
12
- import {useUIStrings} from '../i18n/i18n';
12
+ import {useStringFormatter, useUIStrings} from '../i18n/i18n';
13
13
 
14
14
  import type {UIStringsType} from '../i18n/ui-strings';
15
15
 
@@ -35,6 +35,7 @@ export const SummaryTooltip: FunctionComponent<{
35
35
  gatherMode: LH.Result.GatherMode
36
36
  }> = ({category, gatherMode}) => {
37
37
  const strings = useUIStrings();
38
+ const str_ = useStringFormatter();
38
39
  const {
39
40
  numPassed,
40
41
  numPassableAudits,
@@ -69,18 +70,14 @@ export const SummaryTooltip: FunctionComponent<{
69
70
  }
70
71
  </div>
71
72
  <div className="SummaryTooltip__fraction">
72
- {
73
- // TODO(FLOW-I18N): Placeholder format.
74
- `${numPassed} audits passed / ${numPassableAudits} passable audits`
75
- }
73
+ <span>{str_(strings.passedAuditCount, {numPassed})}</span>
74
+ <span> / </span>
75
+ <span>{str_(strings.passableAuditCount, {numPassableAudits})}</span>
76
76
  </div>
77
- {
78
- // TODO(FLOW-I18N): Placeholder format.
79
- numInformative ?
80
- <div className="SummaryTooltip__informative">
81
- {`${numInformative} informative audits`}
82
- </div> :
83
- null
77
+ {numInformative !== 0 &&
78
+ <div className="SummaryTooltip__informative">
79
+ {str_(strings.informativeAuditCount, {numInformative})}
80
+ </div>
84
81
  }
85
82
  </div>
86
83
  );
@@ -11,7 +11,7 @@ import {FlowSegment, FlowStepThumbnail, Separator} from '../common';
11
11
  import {getModeDescription, useFlowResult} from '../util';
12
12
  import {Util} from '../../../report/renderer/util';
13
13
  import {SummaryCategory} from './category';
14
- import {useUIStrings} from '../i18n/i18n';
14
+ import {useStringFormatter, useUIStrings} from '../i18n/i18n';
15
15
 
16
16
  const DISPLAYED_CATEGORIES = ['performance', 'accessibility', 'best-practices', 'seo'];
17
17
  const THUMBNAIL_WIDTH = 50;
@@ -108,6 +108,7 @@ const SummaryFlow: FunctionComponent = () => {
108
108
  export const SummaryHeader: FunctionComponent = () => {
109
109
  const flowResult = useFlowResult();
110
110
  const strings = useUIStrings();
111
+ const str_ = useStringFormatter();
111
112
 
112
113
  let numNavigation = 0;
113
114
  let numTimespan = 0;
@@ -126,11 +127,10 @@ export const SummaryHeader: FunctionComponent = () => {
126
127
  }
127
128
  }
128
129
 
129
- // TODO(FLOW-I18N): Placeholder format.
130
130
  const subtitleCounts = [];
131
- if (numNavigation) subtitleCounts.push(`${numNavigation} navigation reports`);
132
- if (numTimespan) subtitleCounts.push(`${numTimespan} timespan reports`);
133
- if (numSnapshot) subtitleCounts.push(`${numSnapshot} snapshot reports`);
131
+ if (numNavigation) subtitleCounts.push(str_(strings.navigationReportCount, {numNavigation}));
132
+ if (numTimespan) subtitleCounts.push(str_(strings.timespanReportCount, {numTimespan}));
133
+ if (numSnapshot) subtitleCounts.push(str_(strings.snapshotReportCount, {numSnapshot}));
134
134
  const subtitle = subtitleCounts.join(' · ');
135
135
 
136
136
  return (
@@ -44,7 +44,8 @@ describe('SummaryTooltip', () => {
44
44
 
45
45
  expect(root.getByText('Average')).toBeTruthy();
46
46
  expect(() => root.getByText(/^[0-9]+$/)).toThrow();
47
- expect(root.getByText('2 audits passed / 3 passable audits')).toBeTruthy();
47
+ expect(root.getByText('2 audits passed')).toBeTruthy();
48
+ expect(root.getByText('3 passable audits')).toBeTruthy();
48
49
  });
49
50
 
50
51
  it('renders tooltip without rating', async () => {
@@ -65,7 +66,8 @@ describe('SummaryTooltip', () => {
65
66
 
66
67
  expect(() => root.getByText(/^(Average|Good|Poor)$/)).toThrow();
67
68
  expect(() => root.getByText(/^[0-9]+$/)).toThrow();
68
- expect(root.getByText('2 audits passed / 3 passable audits')).toBeTruthy();
69
+ expect(root.getByText('2 audits passed')).toBeTruthy();
70
+ expect(root.getByText('3 passable audits')).toBeTruthy();
69
71
  });
70
72
 
71
73
  it('renders scored category tooltip with score', async () => {
@@ -86,7 +88,8 @@ describe('SummaryTooltip', () => {
86
88
 
87
89
  expect(root.getByText('Good')).toBeTruthy();
88
90
  expect(root.getByText('100')).toBeTruthy();
89
- expect(root.getByText('2 audits passed / 3 passable audits')).toBeTruthy();
91
+ expect(root.getByText('2 audits passed')).toBeTruthy();
92
+ expect(root.getByText('3 passable audits')).toBeTruthy();
90
93
  });
91
94
 
92
95
  it('renders informative audit count if any', async () => {
@@ -107,7 +110,8 @@ describe('SummaryTooltip', () => {
107
110
 
108
111
  expect(root.getByText('Good')).toBeTruthy();
109
112
  expect(root.getByText('100')).toBeTruthy();
110
- expect(root.getByText('2 audits passed / 2 passable audits')).toBeTruthy();
111
- expect(root.getByText('1 informative audits')).toBeTruthy();
113
+ expect(root.getByText('2 audits passed')).toBeTruthy();
114
+ expect(root.getByText('2 passable audits')).toBeTruthy();
115
+ expect(root.getByText('1 informative audit')).toBeTruthy();
112
116
  });
113
117
  });
@@ -34,7 +34,7 @@ describe('SummaryHeader', () => {
34
34
  const lhrCounts = root.getByText(/·/);
35
35
  expect(root.getByText('Summary')).toBeTruthy();
36
36
  expect(lhrCounts.textContent).toEqual(
37
- '2 navigation reports · 1 timespan reports · 1 snapshot reports'
37
+ '2 navigation reports · 1 timespan report · 1 snapshot report'
38
38
  );
39
39
  });
40
40
  });
@@ -14,6 +14,7 @@
14
14
  "references": [
15
15
  {"path": "../types/lhr/"},
16
16
  {"path": "../report"},
17
+ {"path": "../shared"},
17
18
  ],
18
19
  "include": [
19
20
  "**/*.js",
@@ -283,6 +283,7 @@ function resolveSettings(settingsJson = {}, overrides = undefined) {
283
283
  // If a locale is requested in flags or settings, use it. A typical CLI run will not have one,
284
284
  // however `lookupLocale` will always determine which of our supported locales to use (falling
285
285
  // back if necessary).
286
+ // TODO: could do more work to sniff out the user's locale
286
287
  const locale = i18n.lookupLocale((overrides && overrides.locale) || settingsJson.locale);
287
288
 
288
289
  // Fill in missing settings with defaults
@@ -12,9 +12,11 @@ const lookupClosestLocale = require('lookup-closest-locale');
12
12
  const {getAvailableLocales} = require('../../../shared/localization/format.js');
13
13
  const log = require('lighthouse-logger');
14
14
  const {LH_ROOT} = require('../../../root.js');
15
- const {isIcuMessage, _formatMessage} = require('../../../shared/localization/format.js');
16
-
17
- const DEFAULT_LOCALE = 'en';
15
+ const {
16
+ isIcuMessage,
17
+ formatMessage,
18
+ DEFAULT_LOCALE,
19
+ } = require('../../../shared/localization/format.js');
18
20
 
19
21
  const UIStrings = {
20
22
  /** Used to show the duration in milliseconds that something lasted. The `{timeInMs}` placeholder will be replaced with the time duration, shown in milliseconds (e.g. 63 ms) */
@@ -118,23 +120,32 @@ const UIStrings = {
118
120
  * - supported locales in Intl formatters
119
121
  *
120
122
  * If `locale` isn't provided or one could not be found, DEFAULT_LOCALE is returned.
123
+ *
124
+ * By default any of the locales Lighthouse has strings for can be returned, but this
125
+ * can be overriden with `possibleLocales`, useful e.g. when Lighthouse is bundled and
126
+ * only DEFAULT_LOCALE is available, but `possibleLocales` can be used to select a
127
+ * locale available to be downloaded on demand.
121
128
  * @param {string|string[]=} locales
129
+ * @param {Array<string>=} possibleLocales
122
130
  * @return {LH.Locale}
123
131
  */
124
- function lookupLocale(locales) {
125
- // If Node was built with `--with-intl=none`, `Intl` won't exist.
132
+ function lookupLocale(locales, possibleLocales) {
133
+ // TODO: lookupLocale may need to be split into two functions, one that canonicalizes
134
+ // locales and one that looks up the best locale filename for a given locale.
135
+ // e.g. `en-IE` is canonical, but uses `en-GB.json`. See TODO in locales.js
136
+
126
137
  if (typeof Intl !== 'object') {
138
+ // If Node was built with `--with-intl=none`, `Intl` won't exist.
127
139
  throw new Error('Lighthouse must be run in Node with `Intl` support. See https://nodejs.org/api/intl.html for help');
128
140
  }
129
141
 
130
- // TODO: could do more work to sniff out the user's locale
131
142
  const canonicalLocales = Intl.getCanonicalLocales(locales);
132
143
 
133
144
  // Filter by what's available in this runtime.
134
145
  const availableLocales = Intl.NumberFormat.supportedLocalesOf(canonicalLocales);
135
146
 
136
147
  // Get available locales and transform into object to match `lookupClosestLocale`'s API.
137
- const localesWithMessages = getAvailableLocales();
148
+ const localesWithMessages = possibleLocales || getAvailableLocales();
138
149
  const localesWithmessagesObj = /** @type {Record<LH.Locale, LhlMessages>} */ (
139
150
  Object.fromEntries(localesWithMessages.map(l => [l, {}])));
140
151
 
@@ -182,7 +193,7 @@ function createIcuMessageFn(filename, fileStrings) {
182
193
  return {
183
194
  i18nId,
184
195
  values,
185
- formattedDefault: _formatMessage(message, values, DEFAULT_LOCALE),
196
+ formattedDefault: formatMessage(message, values, DEFAULT_LOCALE),
186
197
  };
187
198
  };
188
199
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "8.6.0-dev.20211013",
3
+ "version": "8.6.0-dev.20211017",
4
4
  "description": "Automated auditing, performance metrics, and best practices for the web.",
5
5
  "main": "./lighthouse-core/index.js",
6
6
  "bin": {
@@ -13,6 +13,8 @@ const {isObjectOfUnknownValues, isObjectOrArrayOfUnknownValues} = require('../ty
13
13
  /** Contains available locales with messages. May be an empty object if bundled. */
14
14
  const LOCALE_MESSAGES = require('./locales.js');
15
15
 
16
+ const DEFAULT_LOCALE = 'en-US';
17
+
16
18
  /**
17
19
  * The locale tags for the localized messages available to Lighthouse on disk.
18
20
  * When bundled, these will be inlined by brfs.
@@ -169,7 +171,7 @@ function _preformatValues(messageFormatter, values, lhlMessage) {
169
171
  * @param {LH.Locale} locale
170
172
  * @return {string}
171
173
  */
172
- function _formatMessage(message, values = {}, locale) {
174
+ function formatMessage(message, values = {}, locale) {
173
175
  // When using accented english, force the use of a different locale for number formatting.
174
176
  const localeForMessageFormat = (locale === 'en-XA' || locale === 'en-XL') ? 'de-DE' : locale;
175
177
 
@@ -189,19 +191,21 @@ function _formatMessage(message, values = {}, locale) {
189
191
  * @return {string}
190
192
  */
191
193
  function _localizeIcuMessage(icuMessage, locale) {
192
- const localeMessages = LOCALE_MESSAGES[locale];
193
- if (!localeMessages) throw new Error(`Unsupported locale '${locale}'`);
194
+ const localeMessages = _getLocaleMessages(locale);
194
195
  const localeMessage = localeMessages[icuMessage.i18nId];
195
196
 
196
- // Fall back to the default (usually the original english message) if we couldn't find a
197
- // message in the specified locale. This could be because of string drift between
198
- // Lighthouse versions or because new strings haven't been updated yet. Better to have
199
- // an english message than no message at all; in some cases it won't even matter.
197
+ // Use the DEFAULT_LOCALE fallback (usually the original english message) if we couldn't
198
+ // find a message in the specified locale. Possible reasons:
199
+ // - string drift between Lighthouse versions
200
+ // - in a bundle stripped of locale files but running in the DEFAULT_LOCALE
201
+ // - new strings haven't been updated yet in a local dev run
202
+ // Better to have an english message than no message at all; in some cases it
203
+ // won't even matter.
200
204
  if (!localeMessage) {
201
205
  return icuMessage.formattedDefault;
202
206
  }
203
207
 
204
- return _formatMessage(localeMessage.message, icuMessage.values, locale);
208
+ return formatMessage(localeMessage.message, icuMessage.values, locale);
205
209
  }
206
210
 
207
211
  /**
@@ -209,9 +213,10 @@ function _localizeIcuMessage(icuMessage, locale) {
209
213
  * @return {Record<string, string>}
210
214
  */
211
215
  function getRendererFormattedStrings(locale) {
212
- const localeMessages = LOCALE_MESSAGES[locale];
213
- if (!localeMessages) throw new Error(`Unsupported locale '${locale}'`);
216
+ const localeMessages = _getLocaleMessages(locale);
214
217
 
218
+ // If `localeMessages` is empty in the bundled and DEFAULT_LOCALE case, this
219
+ // will be empty and the report will fall back to the util UIStrings for these.
215
220
  const icuMessageIds = Object.keys(localeMessages).filter(f => f.startsWith('report/'));
216
221
  /** @type {Record<string, string>} */
217
222
  const strings = {};
@@ -349,12 +354,38 @@ function replaceIcuMessages(inputObject, locale) {
349
354
  return icuMessagePaths;
350
355
  }
351
356
 
357
+ /**
358
+ * Returns the locale messages for the given `locale`, if they exist.
359
+ * Throws if an unsupported locale.
360
+ *
361
+ * NOTE: If DEFAULT_LOCALE is requested and this is inside a bundle with locale
362
+ * messages stripped, an empty object will be returned. Default fallbacks will need to handle that case.
363
+ * @param {LH.Locale} locale
364
+ * @return {import('./locales').LhlMessages}
365
+ */
366
+ function _getLocaleMessages(locale) {
367
+ const localeMessages = LOCALE_MESSAGES[locale];
368
+ if (!localeMessages) {
369
+ if (locale === DEFAULT_LOCALE) {
370
+ // If the default locale isn't in LOCALE_MESSAGES, this is likely executing
371
+ // in a bundle. Let the caller use the fallbacks available.
372
+ return {};
373
+ }
374
+ throw new Error(`Unsupported locale '${locale}'`);
375
+ }
376
+
377
+ return localeMessages;
378
+ }
379
+
352
380
  /**
353
381
  * Returns whether the `requestedLocale` can be used.
354
382
  * @param {LH.Locale} requestedLocale
355
383
  * @return {boolean}
356
384
  */
357
385
  function hasLocale(requestedLocale) {
386
+ // The default locale is always supported through `IcuMessage.formattedDefault`.
387
+ if (requestedLocale === DEFAULT_LOCALE) return true;
388
+
358
389
  const hasIntlSupport = Intl.NumberFormat.supportedLocalesOf([requestedLocale]).length > 0;
359
390
  const hasMessages = Boolean(LOCALE_MESSAGES[requestedLocale]);
360
391
 
@@ -362,8 +393,9 @@ function hasLocale(requestedLocale) {
362
393
  }
363
394
 
364
395
  /**
365
- * Returns a list of canonical locales (each of which may have aliases, but those would
366
- * only show in getAvailableLocales)
396
+ * Returns a list of canonical locales, as defined by the existent message files.
397
+ * In practice, each of these may have aliases in the full list returned by
398
+ * `getAvailableLocales()`.
367
399
  * TODO: create a CanonicalLocale type
368
400
  * @return {Array<string>}
369
401
  */
@@ -375,13 +407,13 @@ function getCanonicalLocales() {
375
407
  * Returns a list of available locales.
376
408
  * - if full build, this includes all canonical locales, aliases, and any locale added
377
409
  * via `registerLocaleData`.
378
- * - if bundled and locale messages have been stripped (locales.js shimmed), this includes no
379
- * locales (perhaps available in a separate bundle), and perhaps any locales
380
- * from `registerLocaleData`.
410
+ * - if bundled and locale messages have been stripped (locales.js shimmed), this includes
411
+ * only DEFAULT_LOCALE and any locales from `registerLocaleData`.
381
412
  * @return {Array<LH.Locale>}
382
413
  */
383
414
  function getAvailableLocales() {
384
- return /** @type {Array<LH.Locale>} */ (Object.keys(LOCALE_MESSAGES).sort());
415
+ const localesWithMessages = new Set([...Object.keys(LOCALE_MESSAGES), DEFAULT_LOCALE]);
416
+ return /** @type {Array<LH.Locale>} */ ([...localesWithMessages].sort());
385
417
  }
386
418
 
387
419
  /**
@@ -407,6 +439,7 @@ function getIcuMessageIdParts(i18nMessageId) {
407
439
  }
408
440
 
409
441
  module.exports = {
442
+ DEFAULT_LOCALE,
410
443
  _formatPathAsString,
411
444
  collectAllCustomElementsFromICU,
412
445
  isIcuMessage,
@@ -415,7 +448,7 @@ module.exports = {
415
448
  replaceIcuMessages,
416
449
  hasLocale,
417
450
  registerLocaleData,
418
- _formatMessage,
451
+ formatMessage,
419
452
  getIcuMessageIdParts,
420
453
  getAvailableLocales,
421
454
  getCanonicalLocales,
@@ -59,6 +59,9 @@
59
59
  "flow-report/src/i18n/ui-strings.js | helpUseCaseTimespan2": {
60
60
  "message": "Discover performance opportunities to improve the experience for long-lived pages and single-page applications."
61
61
  },
62
+ "flow-report/src/i18n/ui-strings.js | informativeAuditCount": {
63
+ "message": "{numInformative, plural,\n =1 {{numInformative} informative audit}\n other {{numInformative} informative audits}\n }"
64
+ },
62
65
  "flow-report/src/i18n/ui-strings.js | mobile": {
63
66
  "message": "Mobile"
64
67
  },
@@ -71,6 +74,15 @@
71
74
  "flow-report/src/i18n/ui-strings.js | navigationReport": {
72
75
  "message": "Navigation report"
73
76
  },
77
+ "flow-report/src/i18n/ui-strings.js | navigationReportCount": {
78
+ "message": "{numNavigation, plural,\n =1 {{numNavigation} navigation report}\n other {{numNavigation} navigation reports}\n }"
79
+ },
80
+ "flow-report/src/i18n/ui-strings.js | passableAuditCount": {
81
+ "message": "{numPassableAudits, plural,\n =1 {{numPassableAudits} passable audit}\n other {{numPassableAudits} passable audits}\n }"
82
+ },
83
+ "flow-report/src/i18n/ui-strings.js | passedAuditCount": {
84
+ "message": "{numPassed, plural,\n =1 {{numPassed} audit passed}\n other {{numPassed} audits passed}\n }"
85
+ },
74
86
  "flow-report/src/i18n/ui-strings.js | ratingAverage": {
75
87
  "message": "Average"
76
88
  },
@@ -95,6 +107,9 @@
95
107
  "flow-report/src/i18n/ui-strings.js | snapshotReport": {
96
108
  "message": "Snapshot report"
97
109
  },
110
+ "flow-report/src/i18n/ui-strings.js | snapshotReportCount": {
111
+ "message": "{numSnapshot, plural,\n =1 {{numSnapshot} snapshot report}\n other {{numSnapshot} snapshot reports}\n }"
112
+ },
98
113
  "flow-report/src/i18n/ui-strings.js | summary": {
99
114
  "message": "Summary"
100
115
  },
@@ -107,6 +122,9 @@
107
122
  "flow-report/src/i18n/ui-strings.js | timespanReport": {
108
123
  "message": "Timespan report"
109
124
  },
125
+ "flow-report/src/i18n/ui-strings.js | timespanReportCount": {
126
+ "message": "{numTimespan, plural,\n =1 {{numTimespan} timespan report}\n other {{numTimespan} timespan reports}\n }"
127
+ },
110
128
  "flow-report/src/i18n/ui-strings.js | title": {
111
129
  "message": "Lighthouse User Flow Report"
112
130
  },
@@ -59,6 +59,9 @@
59
59
  "flow-report/src/i18n/ui-strings.js | helpUseCaseTimespan2": {
60
60
  "message": "D̂íŝćôv́êŕ p̂ér̂f́ôŕm̂án̂ćê óp̂ṕôŕt̂ún̂ít̂íêś t̂ó îḿp̂ŕôv́ê t́ĥé êx́p̂ér̂íêńĉé f̂ór̂ ĺôńĝ-ĺîv́êd́ p̂áĝéŝ án̂d́ ŝín̂ǵl̂é-p̂áĝé âṕp̂ĺîćât́îón̂ś."
61
61
  },
62
+ "flow-report/src/i18n/ui-strings.js | informativeAuditCount": {
63
+ "message": "{numInformative, plural,\n =1 {{numInformative} îńf̂ór̂ḿât́îv́ê áûd́ît́}\n other {{numInformative} îńf̂ór̂ḿât́îv́ê áûd́ît́ŝ}\n }"
64
+ },
62
65
  "flow-report/src/i18n/ui-strings.js | mobile": {
63
66
  "message": "M̂ób̂íl̂é"
64
67
  },
@@ -71,6 +74,15 @@
71
74
  "flow-report/src/i18n/ui-strings.js | navigationReport": {
72
75
  "message": "N̂áv̂íĝát̂íôń r̂ép̂ór̂t́"
73
76
  },
77
+ "flow-report/src/i18n/ui-strings.js | navigationReportCount": {
78
+ "message": "{numNavigation, plural,\n =1 {{numNavigation} n̂áv̂íĝát̂íôń r̂ép̂ór̂t́}\n other {{numNavigation} n̂áv̂íĝát̂íôń r̂ép̂ór̂t́ŝ}\n }"
79
+ },
80
+ "flow-report/src/i18n/ui-strings.js | passableAuditCount": {
81
+ "message": "{numPassableAudits, plural,\n =1 {{numPassableAudits} p̂áŝśâb́l̂é âúd̂ít̂}\n other {{numPassableAudits} ṕâśŝáb̂ĺê áûd́ît́ŝ}\n }"
82
+ },
83
+ "flow-report/src/i18n/ui-strings.js | passedAuditCount": {
84
+ "message": "{numPassed, plural,\n =1 {{numPassed} âúd̂ít̂ ṕâśŝéd̂}\n other {{numPassed} áûd́ît́ŝ ṕâśŝéd̂}\n }"
85
+ },
74
86
  "flow-report/src/i18n/ui-strings.js | ratingAverage": {
75
87
  "message": "Âv́êŕâǵê"
76
88
  },
@@ -95,6 +107,9 @@
95
107
  "flow-report/src/i18n/ui-strings.js | snapshotReport": {
96
108
  "message": "Ŝńâṕŝh́ôt́ r̂ép̂ór̂t́"
97
109
  },
110
+ "flow-report/src/i18n/ui-strings.js | snapshotReportCount": {
111
+ "message": "{numSnapshot, plural,\n =1 {{numSnapshot} ŝńâṕŝh́ôt́ r̂ép̂ór̂t́}\n other {{numSnapshot} ŝńâṕŝh́ôt́ r̂ép̂ór̂t́ŝ}\n }"
112
+ },
98
113
  "flow-report/src/i18n/ui-strings.js | summary": {
99
114
  "message": "Ŝúm̂ḿâŕŷ"
100
115
  },
@@ -107,6 +122,9 @@
107
122
  "flow-report/src/i18n/ui-strings.js | timespanReport": {
108
123
  "message": "T̂ím̂éŝṕâń r̂ép̂ór̂t́"
109
124
  },
125
+ "flow-report/src/i18n/ui-strings.js | timespanReportCount": {
126
+ "message": "{numTimespan, plural,\n =1 {{numTimespan} t̂ím̂éŝṕâń r̂ép̂ór̂t́}\n other {{numTimespan} t̂ím̂éŝṕâń r̂ép̂ór̂t́ŝ}\n }"
127
+ },
110
128
  "flow-report/src/i18n/ui-strings.js | title": {
111
129
  "message": "L̂íĝh́t̂h́ôúŝé Ûśêŕ F̂ĺôẃ R̂ép̂ór̂t́"
112
130
  },
@@ -12,6 +12,12 @@
12
12
  * Google locale inheritance rules: https://goto.google.com/ccssm
13
13
  * CLDR language aliases: https://www.unicode.org/cldr/charts/latest/supplemental/aliases.html
14
14
  * CLDR locale inheritance: https://github.com/unicode-cldr/cldr-core/blob/master/supplemental/parentLocales.json
15
+ *
16
+ * For Lighthouse bundles that shouldn't include locale data, the recommended pattern
17
+ * is to replace the default export of this file with `{}` so that no locale messages
18
+ * are included. Strings will work normally through the IcuMessage.formattedDefault
19
+ * fallback, and locale messages can be added on demand (e.g. dynamically fetched)
20
+ * through `format.registerLocaleData()`.
15
21
  */
16
22
 
17
23
  // TODO(paulirish): Centralize locale inheritance (combining this & i18n.lookupLocale()), adopt cldr parentLocale rules.
@@ -9,10 +9,54 @@ const path = require('path');
9
9
 
10
10
  const format = require('../../localization/format.js');
11
11
  const i18n = require('../../../lighthouse-core/lib/i18n/i18n.js');
12
+ const constants = require('../../../lighthouse-core/config/constants.js');
13
+ const locales = require('../../localization/locales.js');
12
14
 
13
15
  /* eslint-env jest */
14
16
 
15
17
  describe('format', () => {
18
+ describe('DEFAULT_LOCALE', () => {
19
+ it('is the same as the default config locale', () => {
20
+ expect(format.DEFAULT_LOCALE).toBe(constants.defaultSettings.locale);
21
+ });
22
+ });
23
+
24
+ describe('#getAvailableLocales', () => {
25
+ it('has all the available locales', () => {
26
+ const availableLocales = format.getAvailableLocales();
27
+ for (const locale of ['en', 'es', 'ru', 'zh']) {
28
+ expect(availableLocales).toContain(locale);
29
+ }
30
+
31
+ const rawLocales = Object.keys(locales).sort();
32
+ expect(availableLocales.sort()).toEqual(rawLocales);
33
+ });
34
+
35
+ it('contains the default locale', () => {
36
+ expect(format.getAvailableLocales()).toContain(format.DEFAULT_LOCALE);
37
+ });
38
+ });
39
+
40
+ describe('#getCanonicalLocales', () => {
41
+ it('contains some canonical locales', () => {
42
+ const canonicalLocales = format.getCanonicalLocales();
43
+ for (const locale of ['en-US', 'es', 'ru', 'zh']) {
44
+ expect(canonicalLocales).toContain(locale);
45
+ }
46
+ });
47
+
48
+ it('is a subset of the available locales', () => {
49
+ const canonicalLocales = format.getCanonicalLocales();
50
+ const availableLocales = format.getAvailableLocales();
51
+
52
+ for (const canonicalLocale of canonicalLocales) {
53
+ expect(availableLocales).toContain(canonicalLocale);
54
+ }
55
+
56
+ expect(canonicalLocales.length).toBeLessThan(availableLocales.length);
57
+ });
58
+ });
59
+
16
60
  describe('#_formatPathAsString', () => {
17
61
  it('handles simple paths', () => {
18
62
  expect(format._formatPathAsString(['foo'])).toBe('foo');