From 53746cc18c8d6c484046483de7494b4fe339a2bf Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 27 Oct 2023 17:35:07 -0700 Subject: [PATCH] clients(extension): add locale selector (#15574) --- build/build-extension.js | 5 ++ clients/extension/popup.html | 7 ++ clients/extension/scripts/popup.js | 75 +++++++++++++++++++ .../extension/scripts/settings-controller.js | 5 +- clients/extension/styles/lighthouse.css | 11 ++- viewer/app/src/viewer-ui-features.js | 5 +- 6 files changed, 102 insertions(+), 6 deletions(-) diff --git a/build/build-extension.js b/build/build-extension.js index bd15a525b6e1..65318fd622ce 100644 --- a/build/build-extension.js +++ b/build/build-extension.js @@ -27,6 +27,10 @@ const packagePath = `${distDir}/../extension-${browserBrand}-package`; const manifestVersion = readJson(`${sourceDir}/manifest.json`).version; async function buildEntryPoint() { + const locales = fs.readdirSync(`${LH_ROOT}/shared/localization/locales`) + .filter(f => !f.includes('.ctc.json')) + .map(f => f.replace('.json', '')) + .filter(locale => !['en-XA', 'en-XL', 'ar-XB'].includes(locale)); await esbuild.build({ entryPoints: [`${sourceDir}/scripts/${sourceName}`], outfile: `${distDir}/scripts/${distName}`, @@ -38,6 +42,7 @@ async function buildEntryPoint() { plugins.bulkLoader([ plugins.partialLoaders.replaceText({ '___BROWSER_BRAND___': browserBrand, + '__LOCALES__': JSON.stringify(locales), }), ]), ], diff --git a/clients/extension/popup.html b/clients/extension/popup.html index 030469633018..b574108e3d14 100644 --- a/clients/extension/popup.html +++ b/clients/extension/popup.html @@ -53,6 +53,13 @@

Categories

+ +
+

Locale

+ +
diff --git a/clients/extension/scripts/popup.js b/clients/extension/scripts/popup.js index 9bdc0c662851..016c435bc06b 100644 --- a/clients/extension/scripts/popup.js +++ b/clients/extension/scripts/popup.js @@ -9,6 +9,8 @@ import * as SettingsController from './settings-controller.js'; // Replaced with 'chrome' or 'firefox' in the build script. /** @type {string} */ const BROWSER_BRAND = '___BROWSER_BRAND___'; +/** @type {string[]} */ +const LOCALES = JSON.parse('__LOCALES__'); const CHROME_STRINGS = { localhostErrorMessage: 'Use DevTools to audit pages on localhost.', @@ -100,6 +102,7 @@ function onGenerateReportButtonClick(backend, url, settings) { for (const category of settings.selectedCategories) { apiUrl.searchParams.append('category', category); } + apiUrl.searchParams.append('hl', settings.locale); } else { apiUrl = new URL('https://googlechrome.github.io/lighthouse/viewer/'); apiUrl.searchParams.append('psiurl', url); @@ -107,6 +110,7 @@ function onGenerateReportButtonClick(backend, url, settings) { for (const category of settings.selectedCategories) { apiUrl.searchParams.append('category', category); } + apiUrl.searchParams.append('locale', settings.locale); } apiUrl.searchParams.append('utm_source', 'lh-chrome-ext'); window.open(apiUrl.href); @@ -146,6 +150,74 @@ function generateBackendOptionsList(settings) { optionsCategoriesList.append(frag); } +/** + * From third_party/devtools-frontend/src/front_end/core/i18n/i18nImpl.ts + * + * Returns a string of the form: + * "German (Austria) - Deutsch (Österreich)" + * where the former locale representation is written in the currently enabled DevTools + * locale and the latter locale representation is written in the locale of `localeString`. + * + * Should the two locales match (i.e. have the same language) then the latter locale + * representation is written in English. + * + * @param {string} localeString + * @param {string} currentLocale + * @return {string} + */ +function getLocalizedLanguageRegion(localeString, currentLocale) { + const locale = new Intl.Locale(localeString); + const localLanguage = locale.language || 'en'; + const localBaseName = locale.baseName || 'en-US'; + const devtoolsLoc = new Intl.Locale(currentLocale); + const targetLanguage = localLanguage === devtoolsLoc.language ? 'en' : localBaseName; + const languageInCurrentLocale = + new Intl.DisplayNames([currentLocale], {type: 'language'}).of(localLanguage); + const languageInTargetLocale = + new Intl.DisplayNames([targetLanguage], {type: 'language'}).of(localLanguage); + + let wrappedRegionInCurrentLocale = ''; + let wrappedRegionInTargetLocale = ''; + + if (locale.region) { + const regionInCurrentLocale = + new Intl.DisplayNames([currentLocale], {type: 'region', style: 'short'}).of(locale.region); + const regionInTargetLocale = + new Intl.DisplayNames([targetLanguage], {type: 'region', style: 'short'}).of(locale.region); + wrappedRegionInCurrentLocale = ` (${regionInCurrentLocale})`; + wrappedRegionInTargetLocale = ` (${regionInTargetLocale})`; + } + + const lhs = languageInCurrentLocale + wrappedRegionInCurrentLocale; + const rhs = languageInTargetLocale + wrappedRegionInTargetLocale; + if (lhs === rhs) { + return lhs; + } + + return `${lhs} - ${rhs}`; +} + +/** + * Generates a document fragment containing a list of locale options. + * @param {SettingsController.Settings} settings + */ +function generateLocaleOptionsList(settings) { + const frag = document.createDocumentFragment(); + + LOCALES.forEach(locale => { + const optionEl = document.createElement('option'); + optionEl.textContent = getLocalizedLanguageRegion(locale, navigator.language); + optionEl.value = locale; + if (settings.locale === locale) { + optionEl.selected = true; + } + frag.append(optionEl); + }); + + const optionsLocalesList = find('.options__locales'); + optionsLocalesList.append(frag); +} + /** * @param {SettingsController.Settings} settings */ @@ -168,12 +240,14 @@ function readSettingsFromDomAndPersist() { const optionsEl = find('.section--options'); // Save settings when options page is closed. const backend = find('.options__backend input:checked').value; + const locale = find('select.options__locales').value; const checkboxes = optionsEl.querySelectorAll('.options__categories input:checked'); const selectedCategories = Array.from(checkboxes).map(input => input.value); const device = find('input[name="device"]:checked').value; const settings = { backend, + locale, selectedCategories, device, }; @@ -238,6 +312,7 @@ async function initPopup() { // Generate checkboxes from saved settings. generateBackendOptionsList(settings); generateCategoryOptionsList(settings); + generateLocaleOptionsList(settings); configureVisibleSettings(settings); const selectedDeviceEl = find(`.options__device input[value="${settings.device}"]`); selectedDeviceEl.checked = true; diff --git a/clients/extension/scripts/settings-controller.js b/clients/extension/scripts/settings-controller.js index b92aecf31d05..98d574c5e7d3 100644 --- a/clients/extension/scripts/settings-controller.js +++ b/clients/extension/scripts/settings-controller.js @@ -30,7 +30,7 @@ const DEFAULT_CATEGORIES = [{ title: 'PWA', }]; -/** @typedef {{backend: string, selectedCategories: string[], device: string}} Settings */ +/** @typedef {{backend: string, selectedCategories: string[], device: string, locale: string}} Settings */ const STORAGE_KEYS = { Categories: 'lighthouse_audits', @@ -61,6 +61,8 @@ function saveSettings(settings) { // Stash backend setting. storage[STORAGE_KEYS.Settings].backend = settings.backend; + storage[STORAGE_KEYS.Settings].locale = settings.locale; + // Save object to chrome local storage. chrome.storage.local.set(storage); } @@ -94,6 +96,7 @@ function loadSettings() { resolve({ backend: savedSettings.backend ?? 'psi', device: savedSettings.device, + locale: savedSettings.locale ?? navigator.language, selectedCategories: Object.keys(savedCategories).filter(cat => savedCategories[cat]), }); }); diff --git a/clients/extension/styles/lighthouse.css b/clients/extension/styles/lighthouse.css index 85e6e15bab64..e76c08b1f531 100644 --- a/clients/extension/styles/lighthouse.css +++ b/clients/extension/styles/lighthouse.css @@ -33,7 +33,7 @@ html, body { padding: 0; margin: 0; - width: 350px; + width: 450px; background-color: var(--color-bg); color: #212121; font-family: var(--report-font-family); @@ -105,6 +105,9 @@ main { padding-bottom: 10px; } +.options__group { + width: 50%; +} .options__group:first-child { width: 100%; } @@ -158,8 +161,12 @@ main { margin-right: 8px; } -.options__list { +.options__list, .options__select { padding: 0; margin: 0; list-style-type: none; } + +.options__group--locales { + width: 100%; +} diff --git a/viewer/app/src/viewer-ui-features.js b/viewer/app/src/viewer-ui-features.js index 6526f7a0f8d0..7731064bb69a 100644 --- a/viewer/app/src/viewer-ui-features.js +++ b/viewer/app/src/viewer-ui-features.js @@ -45,9 +45,8 @@ export class ViewerUIFeatures extends ReportUIFeatures { saveGistItem.setAttribute('disabled', 'true'); } - this._getI18nModule().then(async (i18nModule) => { - const locales = /** @type {LH.Locale[]} */ ( - await i18nModule.format.getCanonicalLocales()); + this._getI18nModule().then(i18nModule => { + const locales = /** @type {LH.Locale[]} */ (i18nModule.format.getCanonicalLocales()); this._swapLocales.enable(locales); }).catch(err => console.error(err)); }