Path: blob/main/src/vs/platform/languagePacks/node/languagePacks.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 * as fs from 'fs';6import { createHash } from 'crypto';7import { equals } from '../../../base/common/arrays.js';8import { Queue } from '../../../base/common/async.js';9import { Disposable } from '../../../base/common/lifecycle.js';10import { Schemas } from '../../../base/common/network.js';11import { join } from '../../../base/common/path.js';12import { Promises } from '../../../base/node/pfs.js';13import { INativeEnvironmentService } from '../../environment/common/environment.js';14import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, ILocalExtension } from '../../extensionManagement/common/extensionManagement.js';15import { areSameExtensions } from '../../extensionManagement/common/extensionManagementUtil.js';16import { ILogService } from '../../log/common/log.js';17import { ILocalizationContribution } from '../../extensions/common/extensions.js';18import { ILanguagePackItem, LanguagePackBaseService } from '../common/languagePacks.js';19import { URI } from '../../../base/common/uri.js';2021interface ILanguagePack {22hash: string;23label: string | undefined;24extensions: {25extensionIdentifier: IExtensionIdentifier;26version: string;27}[];28translations: { [id: string]: string };29}3031export class NativeLanguagePackService extends LanguagePackBaseService {32private readonly cache: LanguagePacksCache;3334constructor(35@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,36@INativeEnvironmentService environmentService: INativeEnvironmentService,37@IExtensionGalleryService extensionGalleryService: IExtensionGalleryService,38@ILogService private readonly logService: ILogService39) {40super(extensionGalleryService);41this.cache = this._register(new LanguagePacksCache(environmentService, logService));42this.extensionManagementService.registerParticipant({43postInstall: async (extension: ILocalExtension): Promise<void> => {44return this.postInstallExtension(extension);45},46postUninstall: async (extension: ILocalExtension): Promise<void> => {47return this.postUninstallExtension(extension);48}49});50}5152async getBuiltInExtensionTranslationsUri(id: string, language: string): Promise<URI | undefined> {53const packs = await this.cache.getLanguagePacks();54const pack = packs[language];55if (!pack) {56this.logService.warn(`No language pack found for ${language}`);57return undefined;58}5960const translation = pack.translations[id];61return translation ? URI.file(translation) : undefined;62}6364async getInstalledLanguages(): Promise<Array<ILanguagePackItem>> {65const languagePacks = await this.cache.getLanguagePacks();66const languages: ILanguagePackItem[] = Object.keys(languagePacks).map(locale => {67const languagePack = languagePacks[locale];68const baseQuickPick = this.createQuickPickItem(locale, languagePack.label);69return {70...baseQuickPick,71extensionId: languagePack.extensions[0].extensionIdentifier.id,72};73});74languages.push(this.createQuickPickItem('en', 'English'));75languages.sort((a, b) => a.label.localeCompare(b.label));76return languages;77}7879private async postInstallExtension(extension: ILocalExtension): Promise<void> {80if (extension && extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length) {81this.logService.info('Adding language packs from the extension', extension.identifier.id);82await this.update();83}84}8586private async postUninstallExtension(extension: ILocalExtension): Promise<void> {87const languagePacks = await this.cache.getLanguagePacks();88if (Object.keys(languagePacks).some(language => languagePacks[language] && languagePacks[language].extensions.some(e => areSameExtensions(e.extensionIdentifier, extension.identifier)))) {89this.logService.info('Removing language packs from the extension', extension.identifier.id);90await this.update();91}92}9394async update(): Promise<boolean> {95const [current, installed] = await Promise.all([this.cache.getLanguagePacks(), this.extensionManagementService.getInstalled()]);96const updated = await this.cache.update(installed);97return !equals(Object.keys(current), Object.keys(updated));98}99}100101class LanguagePacksCache extends Disposable {102103private languagePacks: { [language: string]: ILanguagePack } = {};104private languagePacksFilePath: string;105private languagePacksFileLimiter: Queue<any>;106private initializedCache: boolean | undefined;107108constructor(109@INativeEnvironmentService environmentService: INativeEnvironmentService,110@ILogService private readonly logService: ILogService111) {112super();113this.languagePacksFilePath = join(environmentService.userDataPath, 'languagepacks.json');114this.languagePacksFileLimiter = new Queue();115}116117getLanguagePacks(): Promise<{ [language: string]: ILanguagePack }> {118// if queue is not empty, fetch from disk119if (this.languagePacksFileLimiter.size || !this.initializedCache) {120return this.withLanguagePacks()121.then(() => this.languagePacks);122}123return Promise.resolve(this.languagePacks);124}125126update(extensions: ILocalExtension[]): Promise<{ [language: string]: ILanguagePack }> {127return this.withLanguagePacks(languagePacks => {128Object.keys(languagePacks).forEach(language => delete languagePacks[language]);129this.createLanguagePacksFromExtensions(languagePacks, ...extensions);130}).then(() => this.languagePacks);131}132133private createLanguagePacksFromExtensions(languagePacks: { [language: string]: ILanguagePack }, ...extensions: ILocalExtension[]): void {134for (const extension of extensions) {135if (extension && extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length) {136this.createLanguagePacksFromExtension(languagePacks, extension);137}138}139Object.keys(languagePacks).forEach(languageId => this.updateHash(languagePacks[languageId]));140}141142private createLanguagePacksFromExtension(languagePacks: { [language: string]: ILanguagePack }, extension: ILocalExtension): void {143const extensionIdentifier = extension.identifier;144const localizations = extension.manifest.contributes && extension.manifest.contributes.localizations ? extension.manifest.contributes.localizations : [];145for (const localizationContribution of localizations) {146if (extension.location.scheme === Schemas.file && isValidLocalization(localizationContribution)) {147let languagePack = languagePacks[localizationContribution.languageId];148if (!languagePack) {149languagePack = {150hash: '',151extensions: [],152translations: {},153label: localizationContribution.localizedLanguageName ?? localizationContribution.languageName154};155languagePacks[localizationContribution.languageId] = languagePack;156}157const extensionInLanguagePack = languagePack.extensions.filter(e => areSameExtensions(e.extensionIdentifier, extensionIdentifier))[0];158if (extensionInLanguagePack) {159extensionInLanguagePack.version = extension.manifest.version;160} else {161languagePack.extensions.push({ extensionIdentifier, version: extension.manifest.version });162}163for (const translation of localizationContribution.translations) {164languagePack.translations[translation.id] = join(extension.location.fsPath, translation.path);165}166}167}168}169170private updateHash(languagePack: ILanguagePack): void {171if (languagePack) {172const md5 = createHash('md5'); // CodeQL [SM04514] Used to create an hash for language pack extension version, which is not a security issue173for (const extension of languagePack.extensions) {174md5.update(extension.extensionIdentifier.uuid || extension.extensionIdentifier.id).update(extension.version); // CodeQL [SM01510] The extension UUID is not sensitive info and is not manually created by a user175}176languagePack.hash = md5.digest('hex');177}178}179180private withLanguagePacks<T>(fn: (languagePacks: { [language: string]: ILanguagePack }) => T | null = () => null): Promise<T> {181return this.languagePacksFileLimiter.queue(() => {182let result: T | null = null;183return fs.promises.readFile(this.languagePacksFilePath, 'utf8')184.then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err))185.then<{ [language: string]: ILanguagePack }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } })186.then(languagePacks => { result = fn(languagePacks); return languagePacks; })187.then(languagePacks => {188for (const language of Object.keys(languagePacks)) {189if (!languagePacks[language]) {190delete languagePacks[language];191}192}193this.languagePacks = languagePacks;194this.initializedCache = true;195const raw = JSON.stringify(this.languagePacks);196this.logService.debug('Writing language packs', raw);197return Promises.writeFile(this.languagePacksFilePath, raw);198})199.then(() => result, error => this.logService.error(error));200});201}202}203204function isValidLocalization(localization: ILocalizationContribution): boolean {205if (typeof localization.languageId !== 'string') {206return false;207}208if (!Array.isArray(localization.translations) || localization.translations.length === 0) {209return false;210}211for (const translation of localization.translations) {212if (typeof translation.id !== 'string') {213return false;214}215if (typeof translation.path !== 'string') {216return false;217}218}219if (localization.languageName && typeof localization.languageName !== 'string') {220return false;221}222if (localization.localizedLanguageName && typeof localization.localizedLanguageName !== 'string') {223return false;224}225return true;226}227228229