Repository: GoogleChrome/lighthouse
Pull request #5713 (2018-07-24 01:27:36)

core(i18n): export rendererFormattedStrings
brings i18n strings to the renderer! this tackles most of what we need for performance category. strategy is what was outlined in #5655, strings are located at `lhr.i18n.rendererFormattedStrings` which the renderer uses to replace its own UIStrings in `util.js`

the `${n} audits` is definitely going to be a problem we're going to have to figure out, but for performance it only shows up for the "passed audits" section and none of the others which isn't the worst thing to live without at first if we need to


Patch:
diff --git a/lighthouse-core/lib/i18n.js b/lighthouse-core/lib/i18n.js
index 75a6f5bd7..bb18a8814 100644
--- a/lighthouse-core/lib/i18n.js
+++ b/lighthouse-core/lib/i18n.js
@@ -131,6 +131,22 @@ function getDefaultLocale() {
   return 'en-US';
 }
 
+/**
+ * @param {LH.Locale} locale
+ * @return {LH.I18NRendererStrings}
+ */
+function getRendererFormattedStrings(locale) {
+  const icuMessageIds = Object.keys(LOCALES[locale]).filter(f => f.includes('core/report/html/'));
+  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] = LOCALES[locale][icuMessageId].message;
+  }
+
+  return strings;
+}
+
 /**
  * @param {string} filename
  * @param {Record<string, string>} fileStrings
@@ -208,13 +224,14 @@ function replaceIcuMessageInstanceIds(lhr, locale) {
 
   const icuMessagePaths = {};
   replaceInObject(lhr, icuMessagePaths);
-  lhr.i18n = {icuMessagePaths};
+  return icuMessagePaths;
 }
 
 module.exports = {
   _formatPathAsString,
   UIStrings,
   getDefaultLocale,
+  getRendererFormattedStrings,
   createMessageInstanceIdFn,
   replaceIcuMessageInstanceIds,
 };
diff --git a/lighthouse-core/lib/locales/en-US.json b/lighthouse-core/lib/locales/en-US.json
index ef621e482..49ce49628 100644
--- a/lighthouse-core/lib/locales/en-US.json
+++ b/lighthouse-core/lib/locales/en-US.json
@@ -43,5 +43,41 @@
   },
   "lighthouse-core/lib/i18n.js | displayValueWastedMs": {
     "message": "Potential savings of {wastedMs, number, milliseconds} ms"
+  },
+  "lighthouse-core/report/html/renderer/util.js | varianceDisclaimer": {
+    "message": "Values are estimated and may vary."
+  },
+  "lighthouse-core/report/html/renderer/util.js | opportunityResourceColumnLabel": {
+    "message": "Resource to optimize"
+  },
+  "lighthouse-core/report/html/renderer/util.js | opportunitySavingsColumnLabel": {
+    "message": "Estimated Savings"
+  },
+  "lighthouse-core/report/html/renderer/util.js | errorMissingAuditInfo": {
+    "message": "Report error: no audit information"
+  },
+  "lighthouse-core/report/html/renderer/util.js | errorLabel": {
+    "message": "Error!"
+  },
+  "lighthouse-core/report/html/renderer/util.js | warningHeader": {
+    "message": "Warnings: "
+  },
+  "lighthouse-core/report/html/renderer/util.js | auditGroupExpandTooltip": {
+    "message": "Show audits"
+  },
+  "lighthouse-core/report/html/renderer/util.js | passedAuditsGroupTitle": {
+    "message": "Passed audits"
+  },
+  "lighthouse-core/report/html/renderer/util.js | notApplicableAuditsGroupTitle": {
+    "message": "Not applicable"
+  },
+  "lighthouse-core/report/html/renderer/util.js | manualAuditsGroupTitle": {
+    "message": "Additional items to manually check"
+  },
+  "lighthouse-core/report/html/renderer/util.js | toplevelWarningsMessage": {
+    "message": "There were issues affecting this run of Lighthouse:"
+  },
+  "lighthouse-core/report/html/renderer/util.js | scorescaleLabel": {
+    "message": "Score scale:"
   }
 }
diff --git a/lighthouse-core/lib/locales/en-XA.json b/lighthouse-core/lib/locales/en-XA.json
index a14708926..3693b8ba1 100644
--- a/lighthouse-core/lib/locales/en-XA.json
+++ b/lighthouse-core/lib/locales/en-XA.json
@@ -43,5 +43,41 @@
   },
   "lighthouse-core/lib/i18n.js | displayValueWastedMs": {
     "message": "P̂ót̂én̂t́îál̂ śâv́îńĝś ôf́ {wastedMs, number, milliseconds} m̂ś"
+  },
+  "lighthouse-core/report/html/renderer/util.js | varianceDisclaimer": {
+    "message": "V̂ál̂úêś âŕê éŝt́îḿât́êd́ âńd̂ ḿâý v̂ár̂ý."
+  },
+  "lighthouse-core/report/html/renderer/util.js | opportunityResourceColumnLabel": {
+    "message": "R̂éŝóûŕĉé t̂ó ôṕt̂ím̂íẑé"
+  },
+  "lighthouse-core/report/html/renderer/util.js | opportunitySavingsColumnLabel": {
+    "message": "Êśt̂ím̂át̂éd̂ Śâv́îńĝś"
+  },
+  "lighthouse-core/report/html/renderer/util.js | errorMissingAuditInfo": {
+    "message": "R̂ép̂ór̂t́ êŕr̂ór̂: ńô áûd́ît́ îńf̂ór̂ḿât́îón̂"
+  },
+  "lighthouse-core/report/html/renderer/util.js | errorLabel": {
+    "message": "Êŕr̂ór̂!"
+  },
+  "lighthouse-core/report/html/renderer/util.js | warningHeader": {
+    "message": "Ŵár̂ńîńĝś: "
+  },
+  "lighthouse-core/report/html/renderer/util.js | auditGroupExpandTooltip": {
+    "message": "Ŝh́ôẃ âúd̂ít̂ś"
+  },
+  "lighthouse-core/report/html/renderer/util.js | passedAuditsGroupTitle": {
+    "message": "P̂áŝśêd́ âúd̂ít̂ś"
+  },
+  "lighthouse-core/report/html/renderer/util.js | notApplicableAuditsGroupTitle": {
+    "message": "N̂ót̂ áp̂ṕl̂íĉáb̂ĺê"
+  },
+  "lighthouse-core/report/html/renderer/util.js | manualAuditsGroupTitle": {
+    "message": "Âd́d̂ít̂íôńâĺ ît́êḿŝ t́ô ḿâńûál̂ĺŷ ćĥéĉḱ"
+  },
+  "lighthouse-core/report/html/renderer/util.js | toplevelWarningsMessage": {
+    "message": "T̂h́êŕê ẃêŕê íŝśûéŝ áf̂f́êćt̂ín̂ǵ t̂h́îś r̂ún̂ óf̂ Ĺîǵĥt́ĥóûśê:"
+  },
+  "lighthouse-core/report/html/renderer/util.js | scorescaleLabel": {
+    "message": "Ŝćôŕê śĉál̂é:"
   }
 }
diff --git a/lighthouse-core/report/html/renderer/category-renderer.js b/lighthouse-core/report/html/renderer/category-renderer.js
index 4daf750cb..869792536 100644
--- a/lighthouse-core/report/html/renderer/category-renderer.js
+++ b/lighthouse-core/report/html/renderer/category-renderer.js
@@ -75,10 +75,10 @@ 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 = 'Error!';
+      textEl.textContent = Util.UIStrings.errorLabel;
       textEl.classList.add('tooltip-boundary');
       const tooltip = this.dom.createChildOf(textEl, 'div', 'tooltip tooltip--error');
-      tooltip.textContent = audit.result.errorMessage || 'Report error: no audit information';
+      tooltip.textContent = audit.result.errorMessage || Util.UIStrings.errorMissingAuditInfo;
     } else if (audit.result.explanation) {
       const explEl = this.dom.createChildOf(titleEl, 'div', 'lh-audit-explanation');
       explEl.textContent = audit.result.explanation;
@@ -89,9 +89,9 @@ class CategoryRenderer {
     // Add list of warnings or singular warning
     const warningsEl = this.dom.createChildOf(titleEl, 'div', 'lh-warnings');
     if (warnings.length === 1) {
-      warningsEl.textContent = `Warning: ${warnings.join('')}`;
+      warningsEl.textContent = `${Util.UIStrings.warningHeader} ${warnings.join('')}`;
     } else {
-      warningsEl.textContent = 'Warnings: ';
+      warningsEl.textContent = Util.UIStrings.warningHeader;
       const warningsUl = this.dom.createChildOf(warningsEl, 'ul');
       for (const warning of warnings) {
         const item = this.dom.createChildOf(warningsUl, 'li');
@@ -158,7 +158,7 @@ class CategoryRenderer {
     const itemCountEl = this.dom.createChildOf(summmaryEl, 'div', 'lh-audit-group__itemcount');
     if (expandable) {
       const chevronEl = summmaryEl.appendChild(this._createChevron());
-      chevronEl.title = 'Show audits';
+      chevronEl.title = Util.UIStrings.auditGroupExpandTooltip;
     }
 
     if (group.description) {
@@ -169,6 +169,7 @@ class CategoryRenderer {
     headerEl.textContent = group.title;
 
     if (opts.itemCount) {
+      // TODO(i18n): support multiple locales here
       itemCountEl.textContent = `${opts.itemCount} audits`;
     }
     return groupEl;
@@ -211,7 +212,7 @@ class CategoryRenderer {
    */
   renderPassedAuditsSection(elements) {
     const passedElem = this.renderAuditGroup({
-      title: `Passed audits`,
+      title: Util.UIStrings.passedAuditsGroupTitle,
     }, {expandable: true, itemCount: this._getTotalAuditsLength(elements)});
     passedElem.classList.add('lh-passed-audits');
     elements.forEach(elem => passedElem.appendChild(elem));
@@ -224,7 +225,7 @@ class CategoryRenderer {
    */
   _renderNotApplicableAuditsSection(elements) {
     const notApplicableElem = this.renderAuditGroup({
-      title: `Not applicable`,
+      title: Util.UIStrings.notApplicableAuditsGroupTitle,
     }, {expandable: true, itemCount: this._getTotalAuditsLength(elements)});
     notApplicableElem.classList.add('lh-audit-group--not-applicable');
     elements.forEach(elem => notApplicableElem.appendChild(elem));
@@ -237,7 +238,7 @@ class CategoryRenderer {
    * @return {Element}
    */
   _renderManualAudits(manualAudits, manualDescription) {
-    const group = {title: 'Additional items to manually check', description: manualDescription};
+    const group = {title: Util.UIStrings.manualAuditsGroupTitle, description: manualDescription};
     const auditGroupElem = this.renderAuditGroup(group,
         {expandable: true, itemCount: manualAudits.length});
     auditGroupElem.classList.add('lh-audit-group--manual');
@@ -282,7 +283,7 @@ class CategoryRenderer {
     percentageEl.textContent = scoreOutOf100.toString();
     if (category.score === null) {
       percentageEl.textContent = '?';
-      percentageEl.title = 'Errors occurred while auditing';
+      percentageEl.title = Util.UIStrings.errorLabel;
     }
 
     this.dom.find('.lh-gauge__label', tmpl).textContent = category.title;
diff --git a/lighthouse-core/report/html/renderer/performance-category-renderer.js b/lighthouse-core/report/html/renderer/performance-category-renderer.js
index 3b25314a0..bcbd70978 100644
--- a/lighthouse-core/report/html/renderer/performance-category-renderer.js
+++ b/lighthouse-core/report/html/renderer/performance-category-renderer.js
@@ -128,7 +128,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
     });
     const estValuesEl = this.dom.createChildOf(metricsColumn2El, 'div',
         'lh-metrics__disclaimer lh-metrics__disclaimer');
-    estValuesEl.textContent = 'Values are estimated and may vary.';
+    estValuesEl.textContent = Util.UIStrings.varianceDisclaimer;
 
     metricAuditsEl.classList.add('lh-audit-group--metrics');
     element.appendChild(metricAuditsEl);
@@ -156,6 +156,12 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
       const scale = Math.max(Math.ceil(maxWaste / 1000) * 1000, minimumScale);
       const groupEl = this.renderAuditGroup(groups['load-opportunities'], {expandable: false});
       const tmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity-header', this.templateContext);
+
+      this.dom.find('.lh-load-opportunity__col--one', tmpl).textContent =
+        Util.UIStrings.opportunityResourceColumnLabel;
+      this.dom.find('.lh-load-opportunity__col--two', tmpl).textContent =
+        Util.UIStrings.opportunitySavingsColumnLabel;
+
       const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
       groupEl.appendChild(headerEl);
       opportunityAudits.forEach((item, i) =>
diff --git a/lighthouse-core/report/html/renderer/report-renderer.js b/lighthouse-core/report/html/renderer/report-renderer.js
index de990d259..3ad4925e7 100644
--- a/lighthouse-core/report/html/renderer/report-renderer.js
+++ b/lighthouse-core/report/html/renderer/report-renderer.js
@@ -35,6 +35,11 @@ class ReportRenderer {
   renderReport(report, container) {
     // If any mutations happen to the report within the renderers, we want the original object untouched
     const clone = /** @type {LH.ReportResult} */ (JSON.parse(JSON.stringify(report)));
+    // Mutate the UIStrings if necessary (while saving originals)
+    const clonedStrings = JSON.parse(JSON.stringify(Util.UIStrings));
+    if (clone.i18n && clone.i18n.rendererFormattedStrings) {
+      ReportRenderer.updateAllUIStrings(clone.i18n.rendererFormattedStrings);
+    }
 
     // TODO(phulce): we all agree this is technical debt we should fix
     if (typeof clone.categories !== 'object') throw new Error('No categories provided.');
@@ -43,6 +48,10 @@ class ReportRenderer {
 
     container.textContent = ''; // Remove previous report.
     container.appendChild(this._renderReport(clone));
+
+    // put the UIStrings back into original state
+    ReportRenderer.updateAllUIStrings(clonedStrings);
+
     return /** @type {Element} **/ (container);
   }
 
@@ -126,6 +135,9 @@ class ReportRenderer {
     }
 
     const container = this._dom.cloneTemplate('#tmpl-lh-warnings--toplevel', this._templateContext);
+    const message = this._dom.find('.lh-warnings__msg', container);
+    message.textContent = Util.UIStrings.toplevelWarningsMessage;
+
     const warnings = this._dom.find('ul', container);
     for (const warningString of report.runWarnings) {
       const warning = warnings.appendChild(this._dom.createElement('li'));
@@ -187,6 +199,8 @@ class ReportRenderer {
 
     if (scoreHeader) {
       const scoreScale = this._dom.cloneTemplate('#tmpl-lh-scorescale', this._templateContext);
+      this._dom.find('.lh-scorescale-label', scoreScale).textContent =
+        Util.UIStrings.scorescaleLabel;
       scoresContainer.appendChild(scoreHeader);
       scoresContainer.appendChild(scoreScale);
     }
@@ -213,8 +227,21 @@ class ReportRenderer {
       });
     }
   }
+
+  /**
+   * @param {LH.I18NRendererStrings} rendererFormattedStrings
+   */
+  static updateAllUIStrings(rendererFormattedStrings) {
+    // TODO(i18n): don't mutate these here but on the LHR and pass that around everywhere
+    for (const [key, value] of Object.entries(rendererFormattedStrings)) {
+      Util.UIStrings[key] = value;
+    }
+  }
 }
 
+/** @type {LH.I18NRendererStrings} */
+ReportRenderer._UIStringsStash = {};
+
 if (typeof module !== 'undefined' && module.exports) {
   module.exports = ReportRenderer;
 } else {
diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/util.js
index 87fc3a739..0bdc24390 100644
--- a/lighthouse-core/report/html/renderer/util.js
+++ b/lighthouse-core/report/html/renderer/util.js
@@ -378,6 +378,23 @@ class Util {
   }
 }
 
+Util.UIStrings = {
+  varianceDisclaimer: 'Values are estimated and may vary.',
+  opportunityResourceColumnLabel: 'Resource to optimize',
+  opportunitySavingsColumnLabel: 'Estimated Savings',
+
+  errorMissingAuditInfo: 'Report error: no audit information',
+  errorLabel: 'Error!',
+  warningHeader: 'Warnings: ',
+  auditGroupExpandTooltip: 'Show audits',
+  passedAuditsGroupTitle: 'Passed audits',
+  notApplicableAuditsGroupTitle: 'Not applicable',
+  manualAuditsGroupTitle: 'Additional items to manually check',
+
+  toplevelWarningsMessage: 'There were issues affecting this run of Lighthouse:',
+  scorescaleLabel: 'Score scale:',
+};
+
 if (typeof module !== 'undefined' && module.exports) {
   module.exports = Util;
 } else {
diff --git a/lighthouse-core/report/html/templates.html b/lighthouse-core/report/html/templates.html
index 770432f86..087ad79f0 100644
--- a/lighthouse-core/report/html/templates.html
+++ b/lighthouse-core/report/html/templates.html
@@ -1,7 +1,7 @@
 <!-- Lighthouse run warnings -->
 <template id="tmpl-lh-warnings--toplevel">
   <div class="lh-warnings lh-warnings--toplevel">
-    <strong>There were issues affecting this run of Lighthouse:</strong>
+    <strong class="lh-warnings__msg"></strong>
     <ul></ul>
   </div>
 </template>
@@ -9,7 +9,7 @@
 <!-- Lighthouse score scale -->
 <template id="tmpl-lh-scorescale">
   <div class="lh-scorescale">
-    <span class="lh-scorescale-label">Score scale:</span>
+    <span class="lh-scorescale-label"></span>
     <span class="lh-scorescale-range lh-scorescale-range--fail">0-44</span>
     <span class="lh-scorescale-range lh-scorescale-range--average">45-74</span>
     <span class="lh-scorescale-range lh-scorescale-range--pass">75-100</span>
@@ -90,12 +90,8 @@
 <!-- Lighthouse perf opportunity header -->
 <template id="tmpl-lh-opportunity-header">
   <div class="lh-load-opportunity__header lh-load-opportunity__cols">
-    <div class="lh-load-opportunity__col lh-load-opportunity__col--one">
-      Resource to optimize
-    </div>
-    <div class="lh-load-opportunity__col lh-load-opportunity__col--two">
-      Estimated Savings
-    </div>
+    <div class="lh-load-opportunity__col lh-load-opportunity__col--one"></div>
+    <div class="lh-load-opportunity__col lh-load-opportunity__col--two"></div>
   </div>
 </template>
 
@@ -394,6 +390,7 @@
     <div class="lh-export">
       <button class="report-icon report-icon--share lh-export__button" title="Export report"></button>
       <div class="lh-export__dropdown">
+        <!-- TODO(i18n): localize export dropdown -->
         <a href="#" class="report-icon report-icon--print" data-action="print-summary">Print Summary</a>
         <a href="#" class="report-icon report-icon--print" data-action="print-expanded">Print Expanded</a>
         <a href="#" class="report-icon report-icon--copy" data-action="copy">Copy JSON</a>
@@ -436,6 +433,7 @@
     }
   </style>
   <footer class="lh-footer">
+    <!-- TODO(i18n): localize runtime settings -->
     <div class="lh-env">
       <div class="lh-env__title">Runtime settings</div>
       <ul class="lh-env__items">
@@ -605,6 +603,7 @@
       }
     </style>
     <div>
+      <!-- TODO(i18n): remove CRC sentences or localize -->
       Longest chain: <b class="lh-crc__longest_duration"><!-- fill me: longestChain.duration --></b>
       over <b class="lh-crc__longest_length"><!-- fill me: longestChain.length --></b> requests, totalling
       <b class="lh-crc__longest_transfersize"><!-- fill me: longestChain.length --></b>
diff --git a/lighthouse-core/runner.js b/lighthouse-core/runner.js
index d11111baf..249421fe8 100644
--- a/lighthouse-core/runner.js
+++ b/lighthouse-core/runner.js
@@ -137,7 +137,10 @@ class Runner {
         timing: {total: Date.now() - startTime},
       };
 
-      i18n.replaceIcuMessageInstanceIds(lhr, settings.locale);
+      lhr.i18n = {
+        rendererFormattedStrings: i18n.getRendererFormattedStrings(settings.locale),
+        icuMessagePaths: i18n.replaceIcuMessageInstanceIds(lhr, settings.locale),
+      };
 
       const report = generateReport(lhr, settings.output);
       return {lhr, artifacts, report};
diff --git a/lighthouse-core/scripts/i18n/collect-strings.js b/lighthouse-core/scripts/i18n/collect-strings.js
index ce8afc382..ed780c47e 100644
--- a/lighthouse-core/scripts/i18n/collect-strings.js
+++ b/lighthouse-core/scripts/i18n/collect-strings.js
@@ -17,9 +17,9 @@ const ignoredPathComponents = [
   '/.git',
   '/scripts',
   '/node_modules',
-  '/renderer',
   '/test/',
   '-test.js',
+  '-renderer.js',
 ];
 
 /**
diff --git a/typings/lhr.d.ts b/typings/lhr.d.ts
index a86b974dd..c9480cb64 100644
--- a/typings/lhr.d.ts
+++ b/typings/lhr.d.ts
@@ -9,7 +9,11 @@ declare global {
     export type I18NMessageEntry = string | {path: string, values: any};
 
     export interface I18NMessages {
-      [templateID: string]: I18NMessageEntry[];
+      [icuMessageId: string]: I18NMessageEntry[];
+    }
+
+    export interface I18NRendererStrings {
+      [varName: string]: string;
     }
 
     /**
@@ -42,7 +46,7 @@ declare global {
       /** Execution timings for the Lighthouse run */
       timing: {total: number, [t: string]: number};
       /** The record of all formatted string locations in the LHR and their corresponding source values. */
-      i18n?: {icuMessagePaths: I18NMessages};
+      i18n?: {rendererFormattedStrings: I18NRendererStrings, icuMessagePaths: I18NMessages};
     }
 
     // Result namespace


Test patch:
diff --git a/lighthouse-core/test/lib/i18n-test.js b/lighthouse-core/test/lib/i18n-test.js
index 300bded7a..4de140dda 100644
--- a/lighthouse-core/test/lib/i18n-test.js
+++ b/lighthouse-core/test/lib/i18n-test.js
@@ -48,10 +48,17 @@ describe('i18n', () => {
       const reference = templateID + ' # 0';
       const lhr = {audits: {'fake-audit': {title: reference}}};
 
-      i18n.replaceIcuMessageInstanceIds(lhr, 'en-US');
+      const icuMessagePaths = i18n.replaceIcuMessageInstanceIds(lhr, 'en-US');
       expect(lhr.audits['fake-audit'].title).toBe('use me!');
-      expect(lhr.i18n.icuMessagePaths).toEqual({
+      expect(icuMessagePaths).toEqual({
         [templateID]: [{path: 'audits[fake-audit].title', values: {x: 1}}]});
     });
   });
+
+  describe('#getRendererFormattedStrings', () => {
+    it('returns icu messages in the specified locale', () => {
+      const strings = i18n.getRendererFormattedStrings('en-XA');
+      expect(strings.passedAuditsGroupTitle).toEqual('P̂áŝśêd́ âúd̂ít̂ś');
+    });
+  });
 });
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index b87617f1d..7dc2528d2 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -3430,6 +3430,20 @@
     }
   },
   "i18n": {
+    "rendererFormattedStrings": {
+      "varianceDisclaimer": "Values are estimated and may vary.",
+      "opportunityResourceColumnLabel": "Resource to optimize",
+      "opportunitySavingsColumnLabel": "Estimated Savings",
+      "errorMissingAuditInfo": "Report error: no audit information",
+      "errorLabel": "Error!",
+      "warningHeader": "Warnings: ",
+      "auditGroupExpandTooltip": "Show audits",
+      "passedAuditsGroupTitle": "Passed audits",
+      "notApplicableAuditsGroupTitle": "Not applicable",
+      "manualAuditsGroupTitle": "Additional items to manually check",
+      "toplevelWarningsMessage": "There were issues affecting this run of Lighthouse:",
+      "scorescaleLabel": "Score scale:"
+    },
     "icuMessagePaths": {
       "lighthouse-core/audits/metrics/interactive.js | title": [
         "audits.interactive.title"