Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/languagePacks/node/languagePacks.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as fs from 'fs';
7
import { createHash } from 'crypto';
8
import { equals } from '../../../base/common/arrays.js';
9
import { Queue } from '../../../base/common/async.js';
10
import { Disposable } from '../../../base/common/lifecycle.js';
11
import { Schemas } from '../../../base/common/network.js';
12
import { join } from '../../../base/common/path.js';
13
import { Promises } from '../../../base/node/pfs.js';
14
import { INativeEnvironmentService } from '../../environment/common/environment.js';
15
import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, ILocalExtension } from '../../extensionManagement/common/extensionManagement.js';
16
import { areSameExtensions } from '../../extensionManagement/common/extensionManagementUtil.js';
17
import { ILogService } from '../../log/common/log.js';
18
import { ILocalizationContribution } from '../../extensions/common/extensions.js';
19
import { ILanguagePackItem, LanguagePackBaseService } from '../common/languagePacks.js';
20
import { URI } from '../../../base/common/uri.js';
21
22
interface ILanguagePack {
23
hash: string;
24
label: string | undefined;
25
extensions: {
26
extensionIdentifier: IExtensionIdentifier;
27
version: string;
28
}[];
29
translations: { [id: string]: string };
30
}
31
32
export class NativeLanguagePackService extends LanguagePackBaseService {
33
private readonly cache: LanguagePacksCache;
34
35
constructor(
36
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
37
@INativeEnvironmentService environmentService: INativeEnvironmentService,
38
@IExtensionGalleryService extensionGalleryService: IExtensionGalleryService,
39
@ILogService private readonly logService: ILogService
40
) {
41
super(extensionGalleryService);
42
this.cache = this._register(new LanguagePacksCache(environmentService, logService));
43
this.extensionManagementService.registerParticipant({
44
postInstall: async (extension: ILocalExtension): Promise<void> => {
45
return this.postInstallExtension(extension);
46
},
47
postUninstall: async (extension: ILocalExtension): Promise<void> => {
48
return this.postUninstallExtension(extension);
49
}
50
});
51
}
52
53
async getBuiltInExtensionTranslationsUri(id: string, language: string): Promise<URI | undefined> {
54
const packs = await this.cache.getLanguagePacks();
55
const pack = packs[language];
56
if (!pack) {
57
this.logService.warn(`No language pack found for ${language}`);
58
return undefined;
59
}
60
61
const translation = pack.translations[id];
62
return translation ? URI.file(translation) : undefined;
63
}
64
65
async getInstalledLanguages(): Promise<Array<ILanguagePackItem>> {
66
const languagePacks = await this.cache.getLanguagePacks();
67
const languages: ILanguagePackItem[] = Object.keys(languagePacks).map(locale => {
68
const languagePack = languagePacks[locale];
69
const baseQuickPick = this.createQuickPickItem(locale, languagePack.label);
70
return {
71
...baseQuickPick,
72
extensionId: languagePack.extensions[0].extensionIdentifier.id,
73
};
74
});
75
languages.push(this.createQuickPickItem('en', 'English'));
76
languages.sort((a, b) => a.label.localeCompare(b.label));
77
return languages;
78
}
79
80
private async postInstallExtension(extension: ILocalExtension): Promise<void> {
81
if (extension && extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length) {
82
this.logService.info('Adding language packs from the extension', extension.identifier.id);
83
await this.update();
84
}
85
}
86
87
private async postUninstallExtension(extension: ILocalExtension): Promise<void> {
88
const languagePacks = await this.cache.getLanguagePacks();
89
if (Object.keys(languagePacks).some(language => languagePacks[language] && languagePacks[language].extensions.some(e => areSameExtensions(e.extensionIdentifier, extension.identifier)))) {
90
this.logService.info('Removing language packs from the extension', extension.identifier.id);
91
await this.update();
92
}
93
}
94
95
async update(): Promise<boolean> {
96
const [current, installed] = await Promise.all([this.cache.getLanguagePacks(), this.extensionManagementService.getInstalled()]);
97
const updated = await this.cache.update(installed);
98
return !equals(Object.keys(current), Object.keys(updated));
99
}
100
}
101
102
class LanguagePacksCache extends Disposable {
103
104
private languagePacks: { [language: string]: ILanguagePack } = {};
105
private languagePacksFilePath: string;
106
private languagePacksFileLimiter: Queue<any>;
107
private initializedCache: boolean | undefined;
108
109
constructor(
110
@INativeEnvironmentService environmentService: INativeEnvironmentService,
111
@ILogService private readonly logService: ILogService
112
) {
113
super();
114
this.languagePacksFilePath = join(environmentService.userDataPath, 'languagepacks.json');
115
this.languagePacksFileLimiter = new Queue();
116
}
117
118
getLanguagePacks(): Promise<{ [language: string]: ILanguagePack }> {
119
// if queue is not empty, fetch from disk
120
if (this.languagePacksFileLimiter.size || !this.initializedCache) {
121
return this.withLanguagePacks()
122
.then(() => this.languagePacks);
123
}
124
return Promise.resolve(this.languagePacks);
125
}
126
127
update(extensions: ILocalExtension[]): Promise<{ [language: string]: ILanguagePack }> {
128
return this.withLanguagePacks(languagePacks => {
129
Object.keys(languagePacks).forEach(language => delete languagePacks[language]);
130
this.createLanguagePacksFromExtensions(languagePacks, ...extensions);
131
}).then(() => this.languagePacks);
132
}
133
134
private createLanguagePacksFromExtensions(languagePacks: { [language: string]: ILanguagePack }, ...extensions: ILocalExtension[]): void {
135
for (const extension of extensions) {
136
if (extension && extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length) {
137
this.createLanguagePacksFromExtension(languagePacks, extension);
138
}
139
}
140
Object.keys(languagePacks).forEach(languageId => this.updateHash(languagePacks[languageId]));
141
}
142
143
private createLanguagePacksFromExtension(languagePacks: { [language: string]: ILanguagePack }, extension: ILocalExtension): void {
144
const extensionIdentifier = extension.identifier;
145
const localizations = extension.manifest.contributes && extension.manifest.contributes.localizations ? extension.manifest.contributes.localizations : [];
146
for (const localizationContribution of localizations) {
147
if (extension.location.scheme === Schemas.file && isValidLocalization(localizationContribution)) {
148
let languagePack = languagePacks[localizationContribution.languageId];
149
if (!languagePack) {
150
languagePack = {
151
hash: '',
152
extensions: [],
153
translations: {},
154
label: localizationContribution.localizedLanguageName ?? localizationContribution.languageName
155
};
156
languagePacks[localizationContribution.languageId] = languagePack;
157
}
158
const extensionInLanguagePack = languagePack.extensions.filter(e => areSameExtensions(e.extensionIdentifier, extensionIdentifier))[0];
159
if (extensionInLanguagePack) {
160
extensionInLanguagePack.version = extension.manifest.version;
161
} else {
162
languagePack.extensions.push({ extensionIdentifier, version: extension.manifest.version });
163
}
164
for (const translation of localizationContribution.translations) {
165
languagePack.translations[translation.id] = join(extension.location.fsPath, translation.path);
166
}
167
}
168
}
169
}
170
171
private updateHash(languagePack: ILanguagePack): void {
172
if (languagePack) {
173
const md5 = createHash('md5'); // CodeQL [SM04514] Used to create an hash for language pack extension version, which is not a security issue
174
for (const extension of languagePack.extensions) {
175
md5.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 user
176
}
177
languagePack.hash = md5.digest('hex');
178
}
179
}
180
181
private withLanguagePacks<T>(fn: (languagePacks: { [language: string]: ILanguagePack }) => T | null = () => null): Promise<T> {
182
return this.languagePacksFileLimiter.queue(() => {
183
let result: T | null = null;
184
return fs.promises.readFile(this.languagePacksFilePath, 'utf8')
185
.then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err))
186
.then<{ [language: string]: ILanguagePack }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } })
187
.then(languagePacks => { result = fn(languagePacks); return languagePacks; })
188
.then(languagePacks => {
189
for (const language of Object.keys(languagePacks)) {
190
if (!languagePacks[language]) {
191
delete languagePacks[language];
192
}
193
}
194
this.languagePacks = languagePacks;
195
this.initializedCache = true;
196
const raw = JSON.stringify(this.languagePacks);
197
this.logService.debug('Writing language packs', raw);
198
return Promises.writeFile(this.languagePacksFilePath, raw);
199
})
200
.then(() => result, error => this.logService.error(error));
201
});
202
}
203
}
204
205
function isValidLocalization(localization: ILocalizationContribution): boolean {
206
if (typeof localization.languageId !== 'string') {
207
return false;
208
}
209
if (!Array.isArray(localization.translations) || localization.translations.length === 0) {
210
return false;
211
}
212
for (const translation of localization.translations) {
213
if (typeof translation.id !== 'string') {
214
return false;
215
}
216
if (typeof translation.path !== 'string') {
217
return false;
218
}
219
}
220
if (localization.languageName && typeof localization.languageName !== 'string') {
221
return false;
222
}
223
if (localization.localizedLanguageName && typeof localization.localizedLanguageName !== 'string') {
224
return false;
225
}
226
return true;
227
}
228
229