Skip to content

Commit 72e0399

Browse files
Ensure language packs are installed on the server side
We need the language pack to be on the server side so that extensions running over there are translated correctly. This ensures the correct language pack is installed before the ExtensionScanner (which will translate the manifest of extensions) kicks in. Fixes #166836
1 parent c6a1522 commit 72e0399

File tree

10 files changed

+126
-27
lines changed

10 files changed

+126
-27
lines changed

src/vs/platform/languagePacks/browser/languagePacks.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { CancellationTokenSource } from 'vs/base/common/cancellation';
7-
import { Language } from 'vs/base/common/platform';
7+
import { withNullAsUndefined } from 'vs/base/common/types';
88
import { URI } from 'vs/base/common/uri';
99
import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
1010
import { IExtensionResourceLoaderService } from 'vs/platform/extensionResourceLoader/common/extensionResourceLoader';
@@ -20,7 +20,7 @@ export class WebLanguagePacksService extends LanguagePackBaseService {
2020
super(extensionGalleryService);
2121
}
2222

23-
async getBuiltInExtensionTranslationsUri(id: string): Promise<URI | undefined> {
23+
async getBuiltInExtensionTranslationsUri(id: string, language: string): Promise<URI | undefined> {
2424

2525
const queryTimeout = new CancellationTokenSource();
2626
setTimeout(() => queryTimeout.cancel(), 1000);
@@ -29,7 +29,7 @@ export class WebLanguagePacksService extends LanguagePackBaseService {
2929
let result;
3030
try {
3131
result = await this.extensionGalleryService.query({
32-
text: `tag:"lp-${Language.value()}"`,
32+
text: `tag:"lp-${language}"`,
3333
pageSize: 5
3434
}, queryTimeout.token);
3535
} catch (err) {
@@ -39,7 +39,7 @@ export class WebLanguagePacksService extends LanguagePackBaseService {
3939

4040
const languagePackExtensions = result.firstPage.find(e => e.properties.localizedLanguages?.length);
4141
if (!languagePackExtensions) {
42-
this.logService.trace(`No language pack found for language ${Language.value()}`);
42+
this.logService.trace(`No language pack found for language ${language}`);
4343
return undefined;
4444
}
4545

@@ -49,7 +49,7 @@ export class WebLanguagePacksService extends LanguagePackBaseService {
4949
const manifest = await this.extensionGalleryService.getManifest(languagePackExtensions, manifestTimeout.token);
5050

5151
// Find the translation from the language pack
52-
const localization = manifest?.contributes?.localizations?.find(l => l.languageId === Language.value());
52+
const localization = manifest?.contributes?.localizations?.find(l => l.languageId === language);
5353
const translation = localization?.translations.find(t => t.id === id);
5454
if (!translation) {
5555
this.logService.trace(`No translation found for id '${id}, in ${manifest?.name}`);
@@ -75,4 +75,14 @@ export class WebLanguagePacksService extends LanguagePackBaseService {
7575
getInstalledLanguages(): Promise<ILanguagePackItem[]> {
7676
return Promise.resolve([]);
7777
}
78+
79+
/**
80+
*
81+
* @returns The current language pack extension
82+
*/
83+
getCurrentLanguagePackExtensionId(): Promise<string | undefined> {
84+
// HACK: Since the locale service in workbench is responsible for setting the language pack extension id
85+
// we just read it from the local storage. Ideally we get it from a wellknown service instead.
86+
return Promise.resolve(withNullAsUndefined(window.localStorage.getItem('vscode.nls.languagePackExtensionId')));
87+
}
7888
}

src/vs/platform/languagePacks/common/languagePacks.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export interface ILanguagePackService {
2727
readonly _serviceBrand: undefined;
2828
getAvailableLanguages(): Promise<Array<ILanguagePackItem>>;
2929
getInstalledLanguages(): Promise<Array<ILanguagePackItem>>;
30-
getBuiltInExtensionTranslationsUri(id: string): Promise<URI | undefined>;
30+
getCurrentLanguagePackExtensionId(): Promise<string | undefined>;
31+
getBuiltInExtensionTranslationsUri(id: string, language: string): Promise<URI | undefined>;
3132
}
3233

3334
export abstract class LanguagePackBaseService extends Disposable implements ILanguagePackService {
@@ -37,9 +38,10 @@ export abstract class LanguagePackBaseService extends Disposable implements ILan
3738
super();
3839
}
3940

40-
abstract getBuiltInExtensionTranslationsUri(id: string): Promise<URI | undefined>;
41+
abstract getBuiltInExtensionTranslationsUri(id: string, language: string): Promise<URI | undefined>;
4142

4243
abstract getInstalledLanguages(): Promise<Array<ILanguagePackItem>>;
44+
abstract getCurrentLanguagePackExtensionId(): Promise<string | undefined>;
4345

4446
async getAvailableLanguages(): Promise<ILanguagePackItem[]> {
4547
const timeout = new CancellationTokenSource();

src/vs/platform/languagePacks/node/languagePacks.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens
1616
import { ILogService } from 'vs/platform/log/common/log';
1717
import { ILocalizationContribution } from 'vs/platform/extensions/common/extensions';
1818
import { ILanguagePackItem, LanguagePackBaseService } from 'vs/platform/languagePacks/common/languagePacks';
19-
import { Language } from 'vs/base/common/platform';
19+
import { Language, LANGUAGE_DEFAULT } from 'vs/base/common/platform';
2020
import { URI } from 'vs/base/common/uri';
2121

2222
interface ILanguagePack {
@@ -50,11 +50,11 @@ export class NativeLanguagePackService extends LanguagePackBaseService {
5050
});
5151
}
5252

53-
async getBuiltInExtensionTranslationsUri(id: string): Promise<URI | undefined> {
53+
async getBuiltInExtensionTranslationsUri(id: string, language: string): Promise<URI | undefined> {
5454
const packs = await this.cache.getLanguagePacks();
55-
const pack = packs[Language.value()];
55+
const pack = packs[language];
5656
if (!pack) {
57-
this.logService.warn(`No language pack found for ${Language.value()}`);
57+
this.logService.warn(`No language pack found for ${language}`);
5858
return undefined;
5959
}
6060

@@ -77,6 +77,25 @@ export class NativeLanguagePackService extends LanguagePackBaseService {
7777
return languages;
7878
}
7979

80+
/**
81+
* Gets the current language pack being used by the client or undefined if we are using the default language.
82+
* Note, since we use {@link Language.value()} here, this API can't be used in remote because a remote manages language per-connection.
83+
* @returns The current language pack or undefined if we are using the default language.
84+
*/
85+
async getCurrentLanguagePackExtensionId(): Promise<string | undefined> {
86+
const currentLangage = Language.value();
87+
if (currentLangage === LANGUAGE_DEFAULT) {
88+
return;
89+
}
90+
const languagePacks = await this.cache.getLanguagePacks();
91+
const currentLanguagePack = languagePacks[currentLangage];
92+
if (currentLanguagePack) {
93+
return currentLanguagePack.extensions[0].extensionIdentifier.id;
94+
}
95+
// No language pack found for the current language. Should be impossible.
96+
return undefined;
97+
}
98+
8099
private async postInstallExtension(extension: ILocalExtension): Promise<void> {
81100
if (extension && extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length) {
82101
this.logService.info('Adding language packs from the extension', extension.identifier.id);

src/vs/server/node/remoteExtensionsScanner.ts

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Event } from 'vs/base/common/event';
1212
import { IURITransformer, transformOutgoingURIs } from 'vs/base/common/uriIpc';
1313
import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
1414
import { ContextKeyDefinedExpr, ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyExpression, ContextKeyGreaterEqualsExpr, ContextKeyGreaterExpr, ContextKeyInExpr, ContextKeyNotEqualsExpr, ContextKeyNotExpr, ContextKeyNotInExpr, ContextKeyRegexExpr, ContextKeySmallerEqualsExpr, ContextKeySmallerExpr, IContextKeyExprMapper } from 'vs/platform/contextkey/common/contextkey';
15-
import { InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
15+
import { IExtensionGalleryService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
1616
import { ExtensionManagementCLI } from 'vs/platform/extensionManagement/common/extensionManagementCLI';
1717
import { IExtensionsScannerService, toExtensionDescription } from 'vs/platform/extensionManagement/common/extensionsScannerService';
1818
import { ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
@@ -22,6 +22,7 @@ import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentServi
2222
import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil';
2323
import { Schemas } from 'vs/base/common/network';
2424
import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner';
25+
import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks';
2526

2627
export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService {
2728

@@ -30,16 +31,18 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS
3031
private readonly _whenExtensionsReady: Promise<void>;
3132

3233
constructor(
33-
extensionManagementCLI: ExtensionManagementCLI,
34+
private readonly _extensionManagementCLI: ExtensionManagementCLI,
3435
environmentService: IServerEnvironmentService,
3536
private readonly _userDataProfilesService: IUserDataProfilesService,
3637
private readonly _extensionsScannerService: IExtensionsScannerService,
3738
private readonly _logService: ILogService,
39+
private readonly _extensionGalleryService: IExtensionGalleryService,
40+
private readonly _languagePackService: ILanguagePackService
3841
) {
3942
if (environmentService.args['install-builtin-extension']) {
4043
const installOptions: InstallOptions = { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] };
4144
performance.mark('code/server/willInstallBuiltinExtensions');
42-
this._whenExtensionsReady = extensionManagementCLI.installExtensions([], environmentService.args['install-builtin-extension'], installOptions, !!environmentService.args['force'])
45+
this._whenExtensionsReady = _extensionManagementCLI.installExtensions([], environmentService.args['install-builtin-extension'], installOptions, !!environmentService.args['force'])
4346
.then(() => performance.mark('code/server/didInstallBuiltinExtensions'), error => {
4447
_logService.error(error);
4548
});
@@ -51,7 +54,7 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS
5154
if (extensionsToInstall) {
5255
const idsOrVSIX = extensionsToInstall.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input);
5356
this._whenExtensionsReady
54-
.then(() => extensionManagementCLI.installExtensions(idsOrVSIX, [], { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] }, !!environmentService.args['force']))
57+
.then(() => _extensionManagementCLI.installExtensions(idsOrVSIX, [], { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] }, !!environmentService.args['force']))
5558
.then(null, error => {
5659
_logService.error(error);
5760
});
@@ -62,7 +65,7 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS
6265
return this._whenExtensionsReady;
6366
}
6467

65-
async scanExtensions(language?: string, profileLocation?: URI, extensionDevelopmentLocations?: URI[]): Promise<IExtensionDescription[]> {
68+
async scanExtensions(language?: string, profileLocation?: URI, extensionDevelopmentLocations?: URI[], languagePackId?: string): Promise<IExtensionDescription[]> {
6669
await this.whenExtensionsReady();
6770

6871
performance.mark('code/server/willScanExtensions');
@@ -72,7 +75,7 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS
7275
const extensionDevelopmentPaths = extensionDevelopmentLocations ? extensionDevelopmentLocations.filter(url => url.scheme === Schemas.file).map(url => url.fsPath) : undefined;
7376
profileLocation = profileLocation ?? this._userDataProfilesService.defaultProfile.extensionsResource;
7477

75-
const extensions = await this._scanExtensions(profileLocation, language ?? platform.language, extensionDevelopmentPaths);
78+
const extensions = await this._scanExtensions(profileLocation, language ?? platform.language, extensionDevelopmentPaths, languagePackId);
7679

7780
this._logService.trace('Scanned Extensions', extensions);
7881
this._massageWhenConditions(extensions);
@@ -101,8 +104,8 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS
101104
return extension;
102105
}
103106

104-
private async _scanExtensions(profileLocation: URI, language: string, extensionDevelopmentPath?: string[]): Promise<IExtensionDescription[]> {
105-
// Ensure that the language packs are available
107+
private async _scanExtensions(profileLocation: URI, language: string, extensionDevelopmentPath: string[] | undefined, languagePackId: string | undefined): Promise<IExtensionDescription[]> {
108+
await this._ensureLanguagePackIsInstalled(language, languagePackId);
106109

107110
const [builtinExtensions, installedExtensions, developedExtensions] = await Promise.all([
108111
this._scanBuiltinExtensions(language),
@@ -139,6 +142,44 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS
139142
return scannedExtension ? toExtensionDescription(scannedExtension, false) : null;
140143
}
141144

145+
private async _ensureLanguagePackIsInstalled(language: string, languagePackId: string | undefined): Promise<void> {
146+
if (
147+
// No need to install language packs for the default language
148+
language === platform.LANGUAGE_DEFAULT ||
149+
// The extension gallery service needs to be available
150+
!this._extensionGalleryService.isEnabled()
151+
) {
152+
return;
153+
}
154+
155+
try {
156+
const installed = await this._languagePackService.getInstalledLanguages();
157+
if (installed.find(p => p.id === language)) {
158+
this._logService.trace(`Language Pack ${language} is already installed. Skipping language pack installation.`);
159+
return;
160+
}
161+
} catch (err) {
162+
// We tried to see what is installed but failed. We can try installing anyway.
163+
this._logService.error(err);
164+
}
165+
166+
if (!languagePackId) {
167+
this._logService.trace(`No language pack id provided for language ${language}. Skipping language pack installation.`);
168+
return;
169+
}
170+
171+
this._logService.trace(`Language Pack ${languagePackId} for language ${language} is not installed. It will be installed now.`);
172+
try {
173+
await this._extensionManagementCLI.installExtensions([languagePackId], [], { isMachineScoped: true }, true, {
174+
log: (s) => this._logService.info(s),
175+
error: (s) => this._logService.error(s)
176+
});
177+
} catch (err) {
178+
// We tried to install the language pack but failed. We can continue without it thus using the default language.
179+
this._logService.error(err);
180+
}
181+
}
182+
142183
private _massageWhenConditions(extensions: IExtensionDescription[]): void {
143184
// Massage "when" conditions which mention `resourceScheme`
144185

@@ -269,7 +310,8 @@ export class RemoteExtensionsScannerChannel implements IServerChannel {
269310
const language = args[0];
270311
const profileLocation = args[1] ? URI.revive(uriTransformer.transformIncoming(args[1])) : undefined;
271312
const extensionDevelopmentPath = Array.isArray(args[2]) ? args[2].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined;
272-
const extensions = await this.service.scanExtensions(language, profileLocation, extensionDevelopmentPath);
313+
const languagePackId: string | undefined = args[3];
314+
const extensions = await this.service.scanExtensions(language, profileLocation, extensionDevelopmentPath, languagePackId);
273315
return extensions.map(extension => transformOutgoingURIs(extension, uriTransformer));
274316
}
275317
case 'scanSingleExtension': {

src/vs/server/node/serverServices.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ export async function setupServerServices(connectionToken: ServerConnectionToken
209209
instantiationService.invokeFunction(accessor => {
210210
const extensionManagementService = accessor.get(INativeServerExtensionManagementService);
211211
const extensionsScannerService = accessor.get(IExtensionsScannerService);
212+
const extensionGalleryService = accessor.get(IExtensionGalleryService);
213+
const languagePackService = accessor.get(ILanguagePackService);
212214
const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, userDataProfilesService, extensionHostStatusService);
213215
socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel);
214216

@@ -217,7 +219,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken
217219

218220
socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(environmentService, logService, ptyService, productService, extensionManagementService, configurationService));
219221

220-
const remoteExtensionsScanner = new RemoteExtensionsScannerService(instantiationService.createInstance(ExtensionManagementCLI), environmentService, userDataProfilesService, extensionsScannerService, logService);
222+
const remoteExtensionsScanner = new RemoteExtensionsScannerService(instantiationService.createInstance(ExtensionManagementCLI), environmentService, userDataProfilesService, extensionsScannerService, logService, extensionGalleryService, languagePackService);
221223
socketServer.registerChannel(RemoteExtensionsScannerChannelName, new RemoteExtensionsScannerChannel(remoteExtensionsScanner, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority)));
222224

223225
const remoteFileSystemChannel = new RemoteAgentFileSystemProviderChannel(logService, environmentService);

src/vs/workbench/api/browser/mainThreadLocalization.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ export class MainThreadLocalization extends Disposable implements MainThreadLoca
2121
super();
2222
}
2323

24-
async $fetchBuiltInBundleUri(id: string): Promise<URI | undefined> {
24+
async $fetchBuiltInBundleUri(id: string, language: string): Promise<URI | undefined> {
2525
try {
26-
const uri = await this.languagePackService.getBuiltInExtensionTranslationsUri(id);
26+
const uri = await this.languagePackService.getBuiltInExtensionTranslationsUri(id, language);
2727
return uri;
2828
} catch (e) {
2929
return undefined;

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2194,7 +2194,7 @@ export interface MainThreadThemingShape extends IDisposable {
21942194
}
21952195

21962196
export interface MainThreadLocalizationShape extends IDisposable {
2197-
$fetchBuiltInBundleUri(id: string): Promise<UriComponents | undefined>;
2197+
$fetchBuiltInBundleUri(id: string, language: string): Promise<UriComponents | undefined>;
21982198
$fetchBundleContents(uriComponents: UriComponents): Promise<string>;
21992199
}
22002200

src/vs/workbench/api/common/extHostLocalizationService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export class ExtHostLocalizationService implements ExtHostLocalizationShape {
9595

9696
private async getBundleLocation(extension: IExtensionDescription): Promise<URI | undefined> {
9797
if (extension.isBuiltin) {
98-
const uri = await this._proxy.$fetchBuiltInBundleUri(extension.identifier.value);
98+
const uri = await this._proxy.$fetchBuiltInBundleUri(extension.identifier.value, this.currentLanguage);
9999
return URI.revive(uri);
100100
}
101101

src/vs/workbench/contrib/localization/browser/localeService.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { localize } from 'vs/nls';
7-
import { Language } from 'vs/base/common/platform';
7+
import { Language, LANGUAGE_DEFAULT } from 'vs/base/common/platform';
88
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
99
import { ILanguagePackItem } from 'vs/platform/languagePacks/common/languagePacks';
1010
import { ILocaleService } from 'vs/workbench/contrib/localization/common/locale';
@@ -27,8 +27,12 @@ export class WebLocaleService implements ILocaleService {
2727
}
2828
if (locale) {
2929
window.localStorage.setItem('vscode.nls.locale', locale);
30+
if (languagePackItem.extensionId) {
31+
window.localStorage.setItem('vscode.nls.languagePackExtensionId', languagePackItem.extensionId);
32+
}
3033
} else {
3134
window.localStorage.removeItem('vscode.nls.locale');
35+
window.localStorage.removeItem('vscode.nls.languagePackExtensionId');
3236
}
3337

3438
const restartDialog = await this.dialogService.confirm({
@@ -45,6 +49,7 @@ export class WebLocaleService implements ILocaleService {
4549

4650
async clearLocalePreference(): Promise<void> {
4751
window.localStorage.removeItem('vscode.nls.locale');
52+
window.localStorage.removeItem('vscode.nls.languagePackExtensionId');
4853

4954
if (Language.value() === navigator.language) {
5055
return;
@@ -61,4 +66,20 @@ export class WebLocaleService implements ILocaleService {
6166
this.hostService.restart();
6267
}
6368
}
69+
70+
private async _ensureExtensionIdIsSet(): Promise<void> {
71+
const language = Language.value();
72+
const extensionId = window.localStorage.getItem('vscode.nls.languagePackExtensionId');
73+
if (language === LANGUAGE_DEFAULT) {
74+
if (extensionId) {
75+
window.localStorage.removeItem('vscode.nls.languagePackExtensionId');
76+
}
77+
return;
78+
}
79+
if (extensionId) {
80+
return;
81+
}
82+
83+
84+
}
6485
}

0 commit comments

Comments
 (0)