diff --git a/front_end/core/i18n/DevToolsLocale.ts b/front_end/core/i18n/DevToolsLocale.ts index 7269c4d66d..5c2a6a0d5d 100644 --- a/front_end/core/i18n/DevToolsLocale.ts +++ b/front_end/core/i18n/DevToolsLocale.ts @@ -29,8 +29,11 @@ export type DevToolsLocaleCreationOptions = { */ export class DevToolsLocale { readonly locale: string; + readonly lookupClosestDevToolsLocale: (locale: string) => string; private constructor(data: DevToolsLocaleData) { + this.lookupClosestDevToolsLocale = data.lookupClosestDevToolsLocale; + // TODO(crbug.com/1163928): Use constant once setting actually exists. if (data.settingLanguage === 'browserLanguage') { this.locale = data.navigatorLanguage || 'en-US'; @@ -38,7 +41,7 @@ export class DevToolsLocale { this.locale = data.settingLanguage; } - this.locale = data.lookupClosestDevToolsLocale(this.locale); + this.locale = this.lookupClosestDevToolsLocale(this.locale); } static instance(opts: DevToolsLocaleCreationOptions = {create: false}): DevToolsLocale { @@ -57,4 +60,26 @@ export class DevToolsLocale { // overwrite the locale. (this.locale as DevToolsLocale['locale']) = 'en-US'; } + + /** + * Returns true iff DevTools supports the language of the passed locale. + * Note that it doesn't have to be a one-to-one match, e.g. if DevTools supports + * 'de', then passing 'de-AT' will return true. + */ + languageIsSupportedByDevTools(localeString: string): boolean { + return localeLanguagesMatch(localeString, this.lookupClosestDevToolsLocale(localeString)); + } +} + +/** + * Returns true iff the two locales have matching languages. This means the + * passing 'de-AT' and 'de-DE' will return true, while 'de-DE' and 'en' will + * return false. + */ +export function localeLanguagesMatch(localeString1: string, localeString2: string): boolean { + // @ts-ignore TODO(crbug.com/1163928) Wait for Intl support. + const locale1 = new Intl.Locale(localeString1); + // @ts-ignore TODO(crbug.com/1163928) Wait for Intl support. + const locale2 = new Intl.Locale(localeString2); + return locale1.language === locale2.language; } diff --git a/front_end/core/i18n/locales/en-US.json b/front_end/core/i18n/locales/en-US.json index e3900723f7..2c3f5d058e 100644 --- a/front_end/core/i18n/locales/en-US.json +++ b/front_end/core/i18n/locales/en-US.json @@ -11303,6 +11303,9 @@ "ui/legacy/InspectorView.ts | closeDrawer": { "message": "Close drawer" }, + "ui/legacy/InspectorView.ts | devToolsLanguageMissmatch": { + "message": "DevTools is now available in {PH1}!" + }, "ui/legacy/InspectorView.ts | moreTools": { "message": "More Tools" }, @@ -11318,6 +11321,12 @@ "ui/legacy/InspectorView.ts | reloadDevtools": { "message": "Reload DevTools" }, + "ui/legacy/InspectorView.ts | setToBrowserLanguage": { + "message": "Always match Chrome's language" + }, + "ui/legacy/InspectorView.ts | setToSpecificLanguage": { + "message": "Switch DevTools to {PH1}" + }, "ui/legacy/ListWidget.ts | addString": { "message": "Add" }, diff --git a/front_end/core/i18n/locales/en-XL.json b/front_end/core/i18n/locales/en-XL.json index 22406e56c2..01baff5351 100644 --- a/front_end/core/i18n/locales/en-XL.json +++ b/front_end/core/i18n/locales/en-XL.json @@ -11303,6 +11303,9 @@ "ui/legacy/InspectorView.ts | closeDrawer": { "message": "Ĉĺôśê d́r̂áŵér̂" }, + "ui/legacy/InspectorView.ts | devToolsLanguageMissmatch": { + "message": "D̂év̂T́ôól̂ś îś n̂óŵ áv̂áîĺâb́l̂é îń {PH1}!" + }, "ui/legacy/InspectorView.ts | moreTools": { "message": "M̂ór̂é T̂óôĺŝ" }, @@ -11318,6 +11321,12 @@ "ui/legacy/InspectorView.ts | reloadDevtools": { "message": "R̂él̂óâd́ D̂év̂T́ôól̂ś" }, + "ui/legacy/InspectorView.ts | setToBrowserLanguage": { + "message": "Âĺŵáŷś m̂át̂ćĥ Ćĥŕôḿê'ś l̂án̂ǵûáĝé" + }, + "ui/legacy/InspectorView.ts | setToSpecificLanguage": { + "message": "Ŝẃît́ĉh́ D̂év̂T́ôól̂ś t̂ó {PH1}" + }, "ui/legacy/ListWidget.ts | addString": { "message": "Âd́d̂" }, diff --git a/front_end/ui/legacy/InspectorView.ts b/front_end/ui/legacy/InspectorView.ts index 7e35cc5a66..69cdee1a33 100644 --- a/front_end/ui/legacy/InspectorView.ts +++ b/front_end/ui/legacy/InspectorView.ts @@ -79,6 +79,26 @@ const UIStrings = { *@description Text for context menu action to move a tab to the drawer */ moveToBottom: 'Move to bottom', + /** + * @description Text shown in a prompt to the user when DevTools is started and the + * currently selected DevTools locale does not match Chrome's locale. + * The placeholder is the current Chrome language. + * @example {German} PH1 + */ + devToolsLanguageMissmatch: 'DevTools is now available in {PH1}!', + /** + * @description An option the user can select when we notice that DevTools + * is configured with a different locale than Chrome. This option means DevTools will + * always try and display the DevTools UI in the same language as Chrome. + */ + setToBrowserLanguage: 'Always match Chrome\'s language', + /** + * @description An option the user can select when DevTools notices that DevTools + * is configured with a different locale than Chrome. This option means DevTools UI + * will be switched to the language specified in the placeholder. + * @example {German} PH1 + */ + setToSpecificLanguage: 'Switch DevTools to {PH1}', }; const str_ = i18n.i18n.registerUIStrings('ui/legacy/InspectorView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); @@ -157,6 +177,12 @@ export class InspectorView extends VBox implements ViewLocationResolver { const panelName = (event.data as string); this.showPanel(panelName); } + + if (shouldShowLocaleInfobar()) { + const infobar = createLocaleInfobar(); + infobar.setParentView(this); + this._attachInfobar(infobar); + } } static instance(opts: { @@ -371,17 +397,12 @@ export class InspectorView extends VBox implements ViewLocationResolver { { text: i18nString(UIStrings.reloadDevtools), highlight: true, - delegate: (): void => { - if (DockController.instance().canDock() && DockController.instance().dockSide() === State.Undocked) { - Host.InspectorFrontendHost.InspectorFrontendHostInstance.setIsDocked(true, function() {}); - } - Host.InspectorFrontendHost.InspectorFrontendHostInstance.reattach(() => window.location.reload()); - }, + delegate: (): void => reloadDevTools(), dismiss: false, }, ]); infobar.setParentView(this); - this._attachReloadRequiredInfobar(infobar); + this._attachInfobar(infobar); this._reloadRequiredInfobar = infobar; infobar.setCloseCallback(() => { delete this._reloadRequiredInfobar; @@ -389,14 +410,85 @@ export class InspectorView extends VBox implements ViewLocationResolver { } } - _attachReloadRequiredInfobar(infobar: Infobar): void { + _createInfoBarDiv(): void { if (!this._infoBarDiv) { this._infoBarDiv = (document.createElement('div') as HTMLDivElement); this._infoBarDiv.classList.add('flex-none'); this.contentElement.insertBefore(this._infoBarDiv, this.contentElement.firstChild); } - this._infoBarDiv.appendChild(infobar.element); } + + _attachInfobar(infobar: Infobar): void { + this._createInfoBarDiv(); + this._infoBarDiv?.appendChild(infobar.element); + } +} + +function getDisableLocaleInfoBarSetting(): Common.Settings.Setting { + return Common.Settings.Settings.instance().createSetting('disableLocaleInfoBar', false); +} + +function shouldShowLocaleInfobar(): boolean { + if (!Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.LOCALIZED_DEVTOOLS)) { + return false; + } + + if (getDisableLocaleInfoBarSetting().get()) { + return false; + } + + const languageSettingValue = Common.Settings.Settings.instance().moduleSetting('language').get(); + if (languageSettingValue === 'browserLanguage') { + return false; + } + + // When the selected DevTools locale differs from the locale of the browser UI, we want to notify + // users only once, that they have the opportunity to adjust DevTools locale to match Chrome's locale. + return !i18n.DevToolsLocale.localeLanguagesMatch(navigator.language, languageSettingValue) && + i18n.DevToolsLocale.DevToolsLocale.instance().languageIsSupportedByDevTools(navigator.language); +} + +function createLocaleInfobar(): Infobar { + const devtoolsLocale = i18n.DevToolsLocale.DevToolsLocale.instance(); + const closestSupportedLocale = devtoolsLocale.lookupClosestDevToolsLocale(navigator.language); + // @ts-ignore TODO(crbug.com/1163928) Wait for Intl support. + const locale = new Intl.Locale(closestSupportedLocale); + const closestSupportedLanguageInCurrentLocale = + new Intl.DisplayNames([devtoolsLocale.locale], {type: 'language'}).of(locale.language); + + const languageSetting = Common.Settings.Settings.instance().moduleSetting('language'); + return new Infobar( + InfobarType.Info, i18nString(UIStrings.devToolsLanguageMissmatch, {PH1: closestSupportedLanguageInCurrentLocale}), + [ + { + text: i18nString(UIStrings.setToBrowserLanguage), + highlight: true, + delegate: (): void => { + languageSetting.set('browserLanguage'); + getDisableLocaleInfoBarSetting().set(true); + reloadDevTools(); + }, + dismiss: true, + }, + { + text: i18nString(UIStrings.setToSpecificLanguage, {PH1: closestSupportedLanguageInCurrentLocale}), + highlight: true, + delegate: (): void => { + languageSetting.set(closestSupportedLocale); + getDisableLocaleInfoBarSetting().set(true); + reloadDevTools(); + }, + dismiss: true, + }, + ], + getDisableLocaleInfoBarSetting()); +} + +function reloadDevTools(): void { + if (DockController.instance().canDock() && DockController.instance().dockSide() === State.Undocked) { + Host.InspectorFrontendHost.InspectorFrontendHostInstance.setIsDocked(true, function() {}); + } + Host.InspectorFrontendHost.InspectorFrontendHostInstance.reattach(() => window.location.reload()); } let actionDelegateInstance: ActionDelegate; diff --git a/test/unittests/front_end/core/i18n/DevToolsLocale_test.ts b/test/unittests/front_end/core/i18n/DevToolsLocale_test.ts index 0d88f9e428..d84f0e91c2 100644 --- a/test/unittests/front_end/core/i18n/DevToolsLocale_test.ts +++ b/test/unittests/front_end/core/i18n/DevToolsLocale_test.ts @@ -79,4 +79,28 @@ describe('DevToolsLocale', () => { assert.strictEqual(devToolsLocale.locale, 'en-US'); }); }); + + describe('languageIsSupportedByDevTools', () => { + it('returns true if the locale is supported, false otherwise', () => { + const data: i18n.DevToolsLocale.DevToolsLocaleData = { + settingLanguage: 'zh-HK', + navigatorLanguage: '', + lookupClosestDevToolsLocale: () => 'zh', + }; + const devToolsLocale = i18n.DevToolsLocale.DevToolsLocale.instance({create: true, data}); + + assert.isTrue(devToolsLocale.languageIsSupportedByDevTools('zh-HK')); + assert.isFalse(devToolsLocale.languageIsSupportedByDevTools('de-DE')); + }); + }); +}); + +describe('localeLanguagesMatch', () => { + it('returns true if the language part of a locale matches, false otherwise', () => { + assert.isTrue(i18n.DevToolsLocale.localeLanguagesMatch('de-DE', 'de-AT')); + assert.isTrue(i18n.DevToolsLocale.localeLanguagesMatch('de-DE', 'de')); + + assert.isFalse(i18n.DevToolsLocale.localeLanguagesMatch('de', 'en')); + assert.isFalse(i18n.DevToolsLocale.localeLanguagesMatch('de-AT', 'en-US')); + }); }); diff --git a/test/unittests/front_end/core/i18n/i18n_test.ts b/test/unittests/front_end/core/i18n/i18n_test.ts index e4386a34a9..bf617f9557 100644 --- a/test/unittests/front_end/core/i18n/i18n_test.ts +++ b/test/unittests/front_end/core/i18n/i18n_test.ts @@ -55,7 +55,7 @@ describe('serialize/deserialize round-trip', () => { describe('getLocalizedLanguageRegion', () => { function createMockDevToolsLocale(locale: string): i18n.DevToolsLocale.DevToolsLocale { - return {locale, forceFallbackLocale: () => {}}; + return {locale, forceFallbackLocale: () => {}} as i18n.DevToolsLocale.DevToolsLocale; } it('build the correct language/region string', () => {