Skip to content

Commit

Permalink
Ensure language packs are installed on the server side
Browse files Browse the repository at this point in the history
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
  • Loading branch information
TylerLeonhardt committed Feb 16, 2023
1 parent c6a1522 commit 72e0399
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 27 deletions.
20 changes: 15 additions & 5 deletions src/vs/platform/languagePacks/browser/languagePacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

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

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

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

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

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

// Find the translation from the language pack
const localization = manifest?.contributes?.localizations?.find(l => l.languageId === Language.value());
const localization = manifest?.contributes?.localizations?.find(l => l.languageId === language);
const translation = localization?.translations.find(t => t.id === id);
if (!translation) {
this.logService.trace(`No translation found for id '${id}, in ${manifest?.name}`);
Expand All @@ -75,4 +75,14 @@ export class WebLanguagePacksService extends LanguagePackBaseService {
getInstalledLanguages(): Promise<ILanguagePackItem[]> {
return Promise.resolve([]);
}

/**
*
* @returns The current language pack extension
*/
getCurrentLanguagePackExtensionId(): Promise<string | undefined> {
// HACK: Since the locale service in workbench is responsible for setting the language pack extension id
// we just read it from the local storage. Ideally we get it from a wellknown service instead.
return Promise.resolve(withNullAsUndefined(window.localStorage.getItem('vscode.nls.languagePackExtensionId')));
}
}
6 changes: 4 additions & 2 deletions src/vs/platform/languagePacks/common/languagePacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export interface ILanguagePackService {
readonly _serviceBrand: undefined;
getAvailableLanguages(): Promise<Array<ILanguagePackItem>>;
getInstalledLanguages(): Promise<Array<ILanguagePackItem>>;
getBuiltInExtensionTranslationsUri(id: string): Promise<URI | undefined>;
getCurrentLanguagePackExtensionId(): Promise<string | undefined>;
getBuiltInExtensionTranslationsUri(id: string, language: string): Promise<URI | undefined>;
}

export abstract class LanguagePackBaseService extends Disposable implements ILanguagePackService {
Expand All @@ -37,9 +38,10 @@ export abstract class LanguagePackBaseService extends Disposable implements ILan
super();
}

abstract getBuiltInExtensionTranslationsUri(id: string): Promise<URI | undefined>;
abstract getBuiltInExtensionTranslationsUri(id: string, language: string): Promise<URI | undefined>;

abstract getInstalledLanguages(): Promise<Array<ILanguagePackItem>>;
abstract getCurrentLanguagePackExtensionId(): Promise<string | undefined>;

async getAvailableLanguages(): Promise<ILanguagePackItem[]> {
const timeout = new CancellationTokenSource();
Expand Down
27 changes: 23 additions & 4 deletions src/vs/platform/languagePacks/node/languagePacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens
import { ILogService } from 'vs/platform/log/common/log';
import { ILocalizationContribution } from 'vs/platform/extensions/common/extensions';
import { ILanguagePackItem, LanguagePackBaseService } from 'vs/platform/languagePacks/common/languagePacks';
import { Language } from 'vs/base/common/platform';
import { Language, LANGUAGE_DEFAULT } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';

interface ILanguagePack {
Expand Down Expand Up @@ -50,11 +50,11 @@ export class NativeLanguagePackService extends LanguagePackBaseService {
});
}

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

Expand All @@ -77,6 +77,25 @@ export class NativeLanguagePackService extends LanguagePackBaseService {
return languages;
}

/**
* Gets the current language pack being used by the client or undefined if we are using the default language.
* Note, since we use {@link Language.value()} here, this API can't be used in remote because a remote manages language per-connection.
* @returns The current language pack or undefined if we are using the default language.
*/
async getCurrentLanguagePackExtensionId(): Promise<string | undefined> {
const currentLangage = Language.value();
if (currentLangage === LANGUAGE_DEFAULT) {
return;
}
const languagePacks = await this.cache.getLanguagePacks();
const currentLanguagePack = languagePacks[currentLangage];
if (currentLanguagePack) {
return currentLanguagePack.extensions[0].extensionIdentifier.id;
}
// No language pack found for the current language. Should be impossible.
return undefined;
}

private async postInstallExtension(extension: ILocalExtension): Promise<void> {
if (extension && extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length) {
this.logService.info('Adding language packs from the extension', extension.identifier.id);
Expand Down
60 changes: 51 additions & 9 deletions src/vs/server/node/remoteExtensionsScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Event } from 'vs/base/common/event';
import { IURITransformer, transformOutgoingURIs } from 'vs/base/common/uriIpc';
import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { ContextKeyDefinedExpr, ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyExpression, ContextKeyGreaterEqualsExpr, ContextKeyGreaterExpr, ContextKeyInExpr, ContextKeyNotEqualsExpr, ContextKeyNotExpr, ContextKeyNotInExpr, ContextKeyRegexExpr, ContextKeySmallerEqualsExpr, ContextKeySmallerExpr, IContextKeyExprMapper } from 'vs/platform/contextkey/common/contextkey';
import { InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionGalleryService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionManagementCLI } from 'vs/platform/extensionManagement/common/extensionManagementCLI';
import { IExtensionsScannerService, toExtensionDescription } from 'vs/platform/extensionManagement/common/extensionsScannerService';
import { ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
Expand All @@ -22,6 +22,7 @@ import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentServi
import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { Schemas } from 'vs/base/common/network';
import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner';
import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks';

export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService {

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

constructor(
extensionManagementCLI: ExtensionManagementCLI,
private readonly _extensionManagementCLI: ExtensionManagementCLI,
environmentService: IServerEnvironmentService,
private readonly _userDataProfilesService: IUserDataProfilesService,
private readonly _extensionsScannerService: IExtensionsScannerService,
private readonly _logService: ILogService,
private readonly _extensionGalleryService: IExtensionGalleryService,
private readonly _languagePackService: ILanguagePackService
) {
if (environmentService.args['install-builtin-extension']) {
const installOptions: InstallOptions = { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] };
performance.mark('code/server/willInstallBuiltinExtensions');
this._whenExtensionsReady = extensionManagementCLI.installExtensions([], environmentService.args['install-builtin-extension'], installOptions, !!environmentService.args['force'])
this._whenExtensionsReady = _extensionManagementCLI.installExtensions([], environmentService.args['install-builtin-extension'], installOptions, !!environmentService.args['force'])
.then(() => performance.mark('code/server/didInstallBuiltinExtensions'), error => {
_logService.error(error);
});
Expand All @@ -51,7 +54,7 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS
if (extensionsToInstall) {
const idsOrVSIX = extensionsToInstall.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input);
this._whenExtensionsReady
.then(() => extensionManagementCLI.installExtensions(idsOrVSIX, [], { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] }, !!environmentService.args['force']))
.then(() => _extensionManagementCLI.installExtensions(idsOrVSIX, [], { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] }, !!environmentService.args['force']))
.then(null, error => {
_logService.error(error);
});
Expand All @@ -62,7 +65,7 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS
return this._whenExtensionsReady;
}

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

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

const extensions = await this._scanExtensions(profileLocation, language ?? platform.language, extensionDevelopmentPaths);
const extensions = await this._scanExtensions(profileLocation, language ?? platform.language, extensionDevelopmentPaths, languagePackId);

this._logService.trace('Scanned Extensions', extensions);
this._massageWhenConditions(extensions);
Expand Down Expand Up @@ -101,8 +104,8 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS
return extension;
}

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

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

private async _ensureLanguagePackIsInstalled(language: string, languagePackId: string | undefined): Promise<void> {
if (
// No need to install language packs for the default language
language === platform.LANGUAGE_DEFAULT ||
// The extension gallery service needs to be available
!this._extensionGalleryService.isEnabled()
) {
return;
}

try {
const installed = await this._languagePackService.getInstalledLanguages();
if (installed.find(p => p.id === language)) {
this._logService.trace(`Language Pack ${language} is already installed. Skipping language pack installation.`);
return;
}
} catch (err) {
// We tried to see what is installed but failed. We can try installing anyway.
this._logService.error(err);
}

if (!languagePackId) {
this._logService.trace(`No language pack id provided for language ${language}. Skipping language pack installation.`);
return;
}

this._logService.trace(`Language Pack ${languagePackId} for language ${language} is not installed. It will be installed now.`);
try {
await this._extensionManagementCLI.installExtensions([languagePackId], [], { isMachineScoped: true }, true, {
log: (s) => this._logService.info(s),
error: (s) => this._logService.error(s)
});
} catch (err) {
// We tried to install the language pack but failed. We can continue without it thus using the default language.
this._logService.error(err);
}
}

private _massageWhenConditions(extensions: IExtensionDescription[]): void {
// Massage "when" conditions which mention `resourceScheme`

Expand Down Expand Up @@ -269,7 +310,8 @@ export class RemoteExtensionsScannerChannel implements IServerChannel {
const language = args[0];
const profileLocation = args[1] ? URI.revive(uriTransformer.transformIncoming(args[1])) : undefined;
const extensionDevelopmentPath = Array.isArray(args[2]) ? args[2].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined;
const extensions = await this.service.scanExtensions(language, profileLocation, extensionDevelopmentPath);
const languagePackId: string | undefined = args[3];
const extensions = await this.service.scanExtensions(language, profileLocation, extensionDevelopmentPath, languagePackId);
return extensions.map(extension => transformOutgoingURIs(extension, uriTransformer));
}
case 'scanSingleExtension': {
Expand Down
4 changes: 3 additions & 1 deletion src/vs/server/node/serverServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ export async function setupServerServices(connectionToken: ServerConnectionToken
instantiationService.invokeFunction(accessor => {
const extensionManagementService = accessor.get(INativeServerExtensionManagementService);
const extensionsScannerService = accessor.get(IExtensionsScannerService);
const extensionGalleryService = accessor.get(IExtensionGalleryService);
const languagePackService = accessor.get(ILanguagePackService);
const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, userDataProfilesService, extensionHostStatusService);
socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel);

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

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

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

const remoteFileSystemChannel = new RemoteAgentFileSystemProviderChannel(logService, environmentService);
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/api/browser/mainThreadLocalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export class MainThreadLocalization extends Disposable implements MainThreadLoca
super();
}

async $fetchBuiltInBundleUri(id: string): Promise<URI | undefined> {
async $fetchBuiltInBundleUri(id: string, language: string): Promise<URI | undefined> {
try {
const uri = await this.languagePackService.getBuiltInExtensionTranslationsUri(id);
const uri = await this.languagePackService.getBuiltInExtensionTranslationsUri(id, language);
return uri;
} catch (e) {
return undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2194,7 +2194,7 @@ export interface MainThreadThemingShape extends IDisposable {
}

export interface MainThreadLocalizationShape extends IDisposable {
$fetchBuiltInBundleUri(id: string): Promise<UriComponents | undefined>;
$fetchBuiltInBundleUri(id: string, language: string): Promise<UriComponents | undefined>;
$fetchBundleContents(uriComponents: UriComponents): Promise<string>;
}

Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/api/common/extHostLocalizationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export class ExtHostLocalizationService implements ExtHostLocalizationShape {

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

Expand Down
23 changes: 22 additions & 1 deletion src/vs/workbench/contrib/localization/browser/localeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { localize } from 'vs/nls';
import { Language } from 'vs/base/common/platform';
import { Language, LANGUAGE_DEFAULT } from 'vs/base/common/platform';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { ILanguagePackItem } from 'vs/platform/languagePacks/common/languagePacks';
import { ILocaleService } from 'vs/workbench/contrib/localization/common/locale';
Expand All @@ -27,8 +27,12 @@ export class WebLocaleService implements ILocaleService {
}
if (locale) {
window.localStorage.setItem('vscode.nls.locale', locale);
if (languagePackItem.extensionId) {
window.localStorage.setItem('vscode.nls.languagePackExtensionId', languagePackItem.extensionId);
}
} else {
window.localStorage.removeItem('vscode.nls.locale');
window.localStorage.removeItem('vscode.nls.languagePackExtensionId');
}

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

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

if (Language.value() === navigator.language) {
return;
Expand All @@ -61,4 +66,20 @@ export class WebLocaleService implements ILocaleService {
this.hostService.restart();
}
}

private async _ensureExtensionIdIsSet(): Promise<void> {
const language = Language.value();
const extensionId = window.localStorage.getItem('vscode.nls.languagePackExtensionId');
if (language === LANGUAGE_DEFAULT) {
if (extensionId) {
window.localStorage.removeItem('vscode.nls.languagePackExtensionId');
}
return;
}
if (extensionId) {
return;
}


}
}
Loading

0 comments on commit 72e0399

Please sign in to comment.