Path: blob/main/src/vs/workbench/contrib/localization/electron-browser/localization.contribution.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { localize } from '../../../../nls.js';6import { Registry } from '../../../../platform/registry/common/platform.js';7import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from '../../../common/contributions.js';8import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js';9import * as platform from '../../../../base/common/platform.js';10import { IExtensionManagementService, IExtensionGalleryService, InstallOperation, ILocalExtension, InstallExtensionResult, DidUninstallExtensionEvent } from '../../../../platform/extensionManagement/common/extensionManagement.js';11import { INotificationService, NeverShowAgainScope, NotificationPriority } from '../../../../platform/notification/common/notification.js';12import Severity from '../../../../base/common/severity.js';13import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';14import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';15import { minimumTranslatedStrings } from './minimalTranslations.js';16import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';17import { CancellationToken } from '../../../../base/common/cancellation.js';18import { ILocaleService } from '../../../services/localization/common/locale.js';19import { IProductService } from '../../../../platform/product/common/productService.js';20import { BaseLocalizationWorkbenchContribution } from '../common/localization.contribution.js';2122class NativeLocalizationWorkbenchContribution extends BaseLocalizationWorkbenchContribution {23private static LANGUAGEPACK_SUGGESTION_IGNORE_STORAGE_KEY = 'extensionsAssistant/languagePackSuggestionIgnore';2425constructor(26@INotificationService private readonly notificationService: INotificationService,27@ILocaleService private readonly localeService: ILocaleService,28@IProductService private readonly productService: IProductService,29@IStorageService private readonly storageService: IStorageService,30@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,31@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,32@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,33@ITelemetryService private readonly telemetryService: ITelemetryService,34) {35super();3637this.checkAndInstall();38this._register(this.extensionManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e)));39this._register(this.extensionManagementService.onDidUninstallExtension(e => this.onDidUninstallExtension(e)));40}4142private async onDidInstallExtensions(results: readonly InstallExtensionResult[]): Promise<void> {43for (const result of results) {44if (result.operation === InstallOperation.Install && result.local) {45await this.onDidInstallExtension(result.local, !!result.context?.extensionsSync);46}47}4849}5051private async onDidInstallExtension(localExtension: ILocalExtension, fromSettingsSync: boolean): Promise<void> {52const localization = localExtension.manifest.contributes?.localizations?.[0];53if (!localization || platform.language === localization.languageId) {54return;55}56const { languageId, languageName } = localization;5758this.notificationService.prompt(59Severity.Info,60localize('updateLocale', "Would you like to change {0}'s display language to {1} and restart?", this.productService.nameLong, languageName || languageId),61[{62label: localize('changeAndRestart', "Change Language and Restart"),63run: async () => {64await this.localeService.setLocale({65id: languageId,66label: languageName ?? languageId,67extensionId: localExtension.identifier.id,68// If settings sync installs the language pack, then we would have just shown the notification so no69// need to show the dialog.70}, true);71}72}],73{74sticky: true,75priority: NotificationPriority.URGENT,76neverShowAgain: { id: 'langugage.update.donotask', isSecondary: true, scope: NeverShowAgainScope.APPLICATION }77}78);79}8081private async onDidUninstallExtension(_event: DidUninstallExtensionEvent): Promise<void> {82if (!await this.isLocaleInstalled(platform.language)) {83this.localeService.setLocale({84id: 'en',85label: 'English'86});87}88}8990private async checkAndInstall(): Promise<void> {91const language = platform.language;92let locale = platform.locale ?? '';93const languagePackSuggestionIgnoreList: string[] = JSON.parse(94this.storageService.get(95NativeLocalizationWorkbenchContribution.LANGUAGEPACK_SUGGESTION_IGNORE_STORAGE_KEY,96StorageScope.APPLICATION,97'[]'98)99);100101if (!this.galleryService.isEnabled()) {102return;103}104if (!language || !locale || platform.Language.isDefaultVariant()) {105return;106}107if (locale.startsWith(language) || languagePackSuggestionIgnoreList.includes(locale)) {108return;109}110111const installed = await this.isLocaleInstalled(locale);112if (installed) {113return;114}115116const fullLocale = locale;117let tagResult = await this.galleryService.query({ text: `tag:lp-${locale}` }, CancellationToken.None);118if (tagResult.total === 0) {119// Trim the locale and try again.120locale = locale.split('-')[0];121tagResult = await this.galleryService.query({ text: `tag:lp-${locale}` }, CancellationToken.None);122if (tagResult.total === 0) {123return;124}125}126127const extensionToInstall = tagResult.total === 1 ? tagResult.firstPage[0] : tagResult.firstPage.find(e => e.publisher === 'MS-CEINTL' && e.name.startsWith('vscode-language-pack'));128const extensionToFetchTranslationsFrom = extensionToInstall ?? tagResult.firstPage[0];129130if (!extensionToFetchTranslationsFrom.assets.manifest) {131return;132}133134const [manifest, translation] = await Promise.all([135this.galleryService.getManifest(extensionToFetchTranslationsFrom, CancellationToken.None),136this.galleryService.getCoreTranslation(extensionToFetchTranslationsFrom, locale)137]);138const loc = manifest?.contributes?.localizations?.find(x => locale.startsWith(x.languageId.toLowerCase()));139const languageName = loc ? (loc.languageName || locale) : locale;140const languageDisplayName = loc ? (loc.localizedLanguageName || loc.languageName || locale) : locale;141const translationsFromPack: { [key: string]: string } = translation?.contents?.['vs/workbench/contrib/localization/electron-browser/minimalTranslations'] ?? {};142const promptMessageKey = extensionToInstall ? 'installAndRestartMessage' : 'showLanguagePackExtensions';143const useEnglish = !translationsFromPack[promptMessageKey];144145const translations: { [key: string]: string } = {};146Object.keys(minimumTranslatedStrings).forEach(key => {147if (!translationsFromPack[key] || useEnglish) {148translations[key] = minimumTranslatedStrings[key].replace('{0}', () => languageName);149} else {150translations[key] = `${translationsFromPack[key].replace('{0}', () => languageDisplayName)} (${minimumTranslatedStrings[key].replace('{0}', () => languageName)})`;151}152});153154const logUserReaction = (userReaction: string) => {155/* __GDPR__156"languagePackSuggestion:popup" : {157"owner": "TylerLeonhardt",158"userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },159"language": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }160}161*/162this.telemetryService.publicLog('languagePackSuggestion:popup', { userReaction, language: locale });163};164165const searchAction = {166label: translations['searchMarketplace'],167run: async () => {168logUserReaction('search');169await this.extensionsWorkbenchService.openSearch(`tag:lp-${locale}`);170}171};172173const installAndRestartAction = {174label: translations['installAndRestart'],175run: async () => {176logUserReaction('installAndRestart');177await this.localeService.setLocale({178id: locale,179label: languageName,180extensionId: extensionToInstall?.identifier.id,181galleryExtension: extensionToInstall182// The user will be prompted if they want to install the language pack before this.183}, true);184}185};186187const promptMessage = translations[promptMessageKey];188189this.notificationService.prompt(190Severity.Info,191promptMessage,192[extensionToInstall ? installAndRestartAction : searchAction,193{194label: localize('neverAgain', "Don't Show Again"),195isSecondary: true,196run: () => {197languagePackSuggestionIgnoreList.push(fullLocale);198this.storageService.store(199NativeLocalizationWorkbenchContribution.LANGUAGEPACK_SUGGESTION_IGNORE_STORAGE_KEY,200JSON.stringify(languagePackSuggestionIgnoreList),201StorageScope.APPLICATION,202StorageTarget.USER203);204logUserReaction('neverShowAgain');205}206}],207{208priority: NotificationPriority.OPTIONAL,209onCancel: () => {210logUserReaction('cancelled');211}212}213);214}215216private async isLocaleInstalled(locale: string): Promise<boolean> {217const installed = await this.extensionManagementService.getInstalled();218return installed.some(i => !!i.manifest.contributes?.localizations?.length219&& i.manifest.contributes.localizations.some(l => locale.startsWith(l.languageId.toLowerCase())));220}221}222223const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);224workbenchRegistry.registerWorkbenchContribution(NativeLocalizationWorkbenchContribution, LifecyclePhase.Eventually);225226227