initial cut of vscode.l10n (#158328)

This commit is contained in:
Tyler James Leonhardt 2022-09-14 16:48:25 -07:00 committed by GitHub
parent 07d06e89e5
commit ccddb94f98
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 227 additions and 50 deletions

View file

@ -44,7 +44,8 @@
"treeItemCheckbox",
"treeViewReveal",
"workspaceTrust",
"telemetry"
"telemetry",
"localization"
],
"private": true,
"activationEvents": [],

View file

@ -268,6 +268,7 @@ export interface IRelaxedExtensionManifest {
description?: string;
main?: string;
browser?: string;
l10nBundleLocation?: string;
icon?: string;
categories?: string[];
keywords?: string[];

View file

@ -16,6 +16,7 @@ import { TokenClassificationExtensionPoints } from 'vs/workbench/services/themes
import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint';
// --- mainThread participants
import './mainThreadLocalization';
import './mainThreadBulkEdits';
import './mainThreadCodeInsets';
import './mainThreadCLICommands';

View file

@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MainContext, MainThreadLocalizationShape } from 'vs/workbench/api/common/extHost.protocol';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { Disposable } from 'vs/base/common/lifecycle';
@extHostNamedCustomer(MainContext.MainThreadLocalization)
export class MainThreadLocalization extends Disposable implements MainThreadLocalizationShape {
constructor(
extHostContext: IExtHostContext,
@IFileService private readonly fileService: IFileService,
) {
super();
}
async $fetchBundleContents(uriComponents: UriComponents): Promise<string> {
const contents = await this.fileService.readFile(URI.revive(uriComponents));
return contents.value.toString();
}
}

View file

@ -93,6 +93,7 @@ import { combinedDisposable } from 'vs/base/common/lifecycle';
import { checkProposedApiEnabled, ExtensionIdentifierSet, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/contrib/debug/common/debug';
import { IExtHostTelemetryLogService } from 'vs/workbench/api/common/extHostTelemetryLogService';
import { IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService';
export interface IExtensionRegistries {
mine: ExtensionDescriptionRegistry;
@ -151,6 +152,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostSearch = rpcProtocol.set(ExtHostContext.ExtHostSearch, accessor.get(IExtHostSearch));
const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, accessor.get(IExtHostTask));
const extHostOutputService = rpcProtocol.set(ExtHostContext.ExtHostOutputService, accessor.get(IExtHostOutputService));
const extHostLocalization = rpcProtocol.set(ExtHostContext.ExtHostLocalization, accessor.get(IExtHostLocalizationService));
// manually create and register addressable instances
const extHostUrls = rpcProtocol.set(ExtHostContext.ExtHostUrls, new ExtHostUrls(rpcProtocol));
@ -1167,6 +1169,28 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
}
};
// namespace: l10n
const l10n: typeof vscode.l10n = {
t(...params: [message: string, ...args: string[]] | [{ message: string; args: string[]; comment: string[] }]): string {
checkProposedApiEnabled(extension, 'localization');
if (typeof params[0] === 'string') {
const key = params.shift() as string;
return extHostLocalization.getMessage(extension.identifier.value, { message: key, args: params as string[] });
}
return extHostLocalization.getMessage(extension.identifier.value, params[0]);
},
get bundle() {
checkProposedApiEnabled(extension, 'localization');
return extHostLocalization.getBundle(extension.identifier.value);
},
get uri() {
checkProposedApiEnabled(extension, 'localization');
return extHostLocalization.getBundleUri(extension.identifier.value);
}
};
return <typeof vscode>{
version: initData.version,
// namespaces
@ -1176,6 +1200,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
debug,
env,
extensions,
l10n,
languages,
notebooks,
scm,

View file

@ -28,7 +28,9 @@ import { ILoggerService, ILogService } from 'vs/platform/log/common/log';
import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService';
import { ExtHostVariableResolverProviderService, IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService';
import { ExtHostTelemetryLogService, IExtHostTelemetryLogService } from 'vs/workbench/api/common/extHostTelemetryLogService';
import { ExtHostLocalizationService, IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService';
registerSingleton(IExtHostLocalizationService, ExtHostLocalizationService, true);
registerSingleton(ILoggerService, ExtHostLoggerService, true);
registerSingleton(ILogService, ExtHostLogService, true);
registerSingleton(IExtHostApiDeprecationService, ExtHostApiDeprecationService, false);

View file

@ -2144,6 +2144,10 @@ export interface ExtHostThemingShape {
export interface MainThreadThemingShape extends IDisposable {
}
export interface MainThreadLocalizationShape extends IDisposable {
$fetchBundleContents(uriComponents: UriComponents): Promise<string>;
}
export interface TunnelDto {
remoteAddress: { port: number; host: string };
localAddress: { port: number; host: string } | string;
@ -2193,6 +2197,19 @@ export interface ExtHostTestingShape {
$refreshTests(controllerId: string, token: CancellationToken): Promise<void>;
}
export interface ExtHostLocalizationShape {
getMessage(extensionId: string, details: IStringDetails): string;
getBundle(extensionId: string): { [key: string]: string };
getBundleUri(extensionId: string): URI | undefined;
initializeLocalizedMessages(extension: IExtensionDescription): Promise<void>;
}
export interface IStringDetails {
message: string;
args?: string[];
comment?: string[];
}
export interface ITestControllerPatch {
label?: string;
canRefresh?: boolean;
@ -2310,6 +2327,7 @@ export const MainContext = {
MainThreadTunnelService: createProxyIdentifier<MainThreadTunnelServiceShape>('MainThreadTunnelService'),
MainThreadTimeline: createProxyIdentifier<MainThreadTimelineShape>('MainThreadTimeline'),
MainThreadTesting: createProxyIdentifier<MainThreadTestingShape>('MainThreadTesting'),
MainThreadLocalization: createProxyIdentifier<MainThreadLocalizationShape>('MainThreadLocalizationShape')
};
export const ExtHostContext = {
@ -2364,4 +2382,5 @@ export const ExtHostContext = {
ExtHostTimeline: createProxyIdentifier<ExtHostTimelineShape>('ExtHostTimeline'),
ExtHostTesting: createProxyIdentifier<ExtHostTestingShape>('ExtHostTesting'),
ExtHostTelemetry: createProxyIdentifier<ExtHostTelemetryShape>('ExtHostTelemetry'),
ExtHostLocalization: createProxyIdentifier<ExtHostLocalizationShape>('ExtHostLocalization'),
};

View file

@ -40,6 +40,7 @@ import { ExtHostSecretState, IExtHostSecretState } from 'vs/workbench/api/common
import { ExtensionSecrets } from 'vs/workbench/api/common/extHostSecrets';
import { Schemas } from 'vs/base/common/network';
import { IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy';
import { IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService';
interface ITestRunner {
/** Old test runner API, as exported from `vscode/lib/testrunner` */
@ -90,6 +91,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
protected readonly _logService: ILogService;
protected readonly _extHostTunnelService: IExtHostTunnelService;
protected readonly _extHostTerminalService: IExtHostTerminalService;
protected readonly _extHostLocalizationService: IExtHostLocalizationService;
protected readonly _mainThreadWorkspaceProxy: MainThreadWorkspaceShape;
protected readonly _mainThreadTelemetryProxy: MainThreadTelemetryShape;
@ -125,6 +127,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
@IExtensionStoragePaths storagePath: IExtensionStoragePaths,
@IExtHostTunnelService extHostTunnelService: IExtHostTunnelService,
@IExtHostTerminalService extHostTerminalService: IExtHostTerminalService,
@IExtHostLocalizationService extHostLocalizationService: IExtHostLocalizationService
) {
super();
this._hostUtils = hostUtils;
@ -136,6 +139,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
this._logService = logService;
this._extHostTunnelService = extHostTunnelService;
this._extHostTerminalService = extHostTerminalService;
this._extHostLocalizationService = extHostLocalizationService;
this._mainThreadWorkspaceProxy = this._extHostContext.getProxy(MainContext.MainThreadWorkspace);
this._mainThreadTelemetryProxy = this._extHostContext.getProxy(MainContext.MainThreadTelemetry);

View file

@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Language } from 'vs/base/common/platform';
import { format } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { ExtHostLocalizationShape, IStringDetails, MainContext, MainThreadLocalizationShape } from 'vs/workbench/api/common/extHost.protocol';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
export class ExtHostLocalizationService implements ExtHostLocalizationShape {
readonly _serviceBrand: undefined;
private readonly _proxy: MainThreadLocalizationShape;
private readonly bundleCache: Map<string, { contents: { [key: string]: string }; uri: URI }> = new Map();
constructor(
@IExtHostRpcService rpc: IExtHostRpcService,
@ILogService private readonly logService: ILogService
) {
this._proxy = rpc.getProxy(MainContext.MainThreadLocalization);
}
getMessage(extensionId: string, details: IStringDetails): string {
const { message, args, comment } = details;
if (Language.isDefault()) {
return format(message, args);
}
let key = message;
if (comment && comment.length > 0) {
key += `/${comment.join()}`;
}
const str = this.bundleCache.get(extensionId)?.contents[key];
if (!str) {
this.logService.warn(`Using default string since no string found in i18n bundle that has the key: ${key}`);
}
return format(str ?? key, args);
}
getBundle(extensionId: string): { [key: string]: string } {
return this.bundleCache.get(extensionId)?.contents ?? {};
}
getBundleUri(extensionId: string): URI | undefined {
return this.bundleCache.get(extensionId)?.uri;
}
async initializeLocalizedMessages(extension: IExtensionDescription): Promise<void> {
if (Language.isDefault()
// TODO: support builtin extensions
|| !extension.l10nBundleLocation
) {
return;
}
if (this.bundleCache.has(extension.identifier.value)) {
return;
}
let contents: { [key: string]: string } | undefined;
const bundleLocation = this.getBundleLocation(extension);
if (!bundleLocation) {
this.logService.error(`No bundle location found for extension ${extension.identifier.value}`);
return;
}
const bundleUri = URI.joinPath(bundleLocation, `bundle.l10n.${Language.value()}.json`);
try {
const response = await this._proxy.$fetchBundleContents(bundleUri);
contents = JSON.parse(response);
} catch (e) {
this.logService.error(`Failed to load translations for ${extension.identifier.value} from ${bundleUri}: ${e.message}`);
return;
}
if (contents) {
this.bundleCache.set(extension.identifier.value, {
contents,
uri: bundleUri
});
}
}
private getBundleLocation(extension: IExtensionDescription): URI | undefined {
// TODO: support builtin extensions using IExtHostInitDataService
// if (extension.isBuiltin && this.initData.nlsBaseUrl) {
// return URI.joinPath(this.initData.nlsBaseUrl, extension.identifier.value, 'main');
// }
if (extension.l10nBundleLocation) {
return URI.joinPath(extension.extensionLocation, extension.l10nBundleLocation);
}
return undefined;
}
}
export const IExtHostLocalizationService = createDecorator<IExtHostLocalizationService>('IExtHostLocalizationService');
export interface IExtHostLocalizationService extends ExtHostLocalizationService { }

View file

@ -136,6 +136,7 @@ export class ExtensionHostMain {
initData.environment.extensionTestsLocationURI = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.extensionTestsLocationURI));
initData.environment.globalStorageHome = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.globalStorageHome));
initData.environment.workspaceStorageHome = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.workspaceStorageHome));
initData.nlsBaseUrl = URI.revive(rpcProtocol.transformIncomingURIs(initData.nlsBaseUrl));
initData.logsLocation = URI.revive(rpcProtocol.transformIncomingURIs(initData.logsLocation));
initData.logFile = URI.revive(rpcProtocol.transformIncomingURIs(initData.logFile));
initData.workspace = rpcProtocol.transformIncomingURIs(initData.workspace);

View file

@ -89,7 +89,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
return extensionDescription.main;
}
protected _loadCommonJSModule<T>(extension: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
protected async _loadCommonJSModule<T>(extension: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
if (module.scheme !== Schemas.file) {
throw new Error(`Cannot load URI: '${module}', must be of file-scheme`);
}
@ -98,20 +98,21 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
this._logService.trace(`ExtensionService#loadCommonJSModule ${module.toString(true)}`);
this._logService.flush();
const extensionId = extension?.identifier.value;
if (extension) {
await this._extHostLocalizationService.initializeLocalizedMessages(extension);
}
try {
if (extensionId) {
performance.mark(`code/extHost/willLoadExtensionCode/${extensionId}`);
}
r = require.__$__nodeRequire<T>(module.fsPath);
} catch (e) {
return Promise.reject(e);
} finally {
if (extensionId) {
performance.mark(`code/extHost/didLoadExtensionCode/${extensionId}`);
}
activationTimesBuilder.codeLoadingStop();
}
return Promise.resolve(r);
return r;
}
public async $setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void> {

View file

@ -12,7 +12,6 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'
import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes';
import { timeout } from 'vs/base/common/async';
import { ExtHostConsoleForwarder } from 'vs/workbench/api/worker/extHostConsoleForwarder';
import { Language } from 'vs/base/common/platform';
class WorkerRequireInterceptor extends RequireInterceptor {
@ -96,17 +95,14 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
throw err;
}
const strings: { [key: string]: string[] } = await this.fetchTranslatedStrings(extension);
if (extension) {
await this._extHostLocalizationService.initializeLocalizedMessages(extension);
}
// define commonjs globals: `module`, `exports`, and `require`
const _exports = {};
const _module = { exports: _exports };
const _require = (request: string) => {
// In order to keep vscode-nls synchronous, we prefetched the translations above
// and then return them here when the extension is loaded.
if (request === 'vscode-nls-web-data') {
return strings;
}
const result = this._fakeModules!.getModule(request, module);
if (result === undefined) {
throw new Error(`Cannot load module '${request}'`);
@ -144,44 +140,6 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
await timeout(10);
}
}
private async fetchTranslatedStrings(extension: IExtensionDescription | null): Promise<{ [key: string]: string[] }> {
let strings: { [key: string]: string[] } = {};
if (!extension) {
return {};
}
const translationsUri = Language.isDefaultVariant()
// If we are in the default variant, load the translations for en only.
? extension.browserNlsBundleUris?.en
// Otherwise load the translations for the current locale with English as a fallback.
: extension.browserNlsBundleUris?.[Language.value()] ?? extension.browserNlsBundleUris?.en;
if (extension && translationsUri) {
try {
const response = await fetch(translationsUri.toString(true));
if (!response.ok) {
throw new Error(await response.text());
}
strings = await response.json();
} catch (e) {
try {
console.error(`Failed to load translations for ${extension.identifier.value} from ${translationsUri}: ${e.message}`);
const englishStrings = extension.browserNlsBundleUris?.en;
if (englishStrings) {
const response = await fetch(englishStrings.toString(true));
if (!response.ok) {
throw new Error(await response.text());
}
strings = await response.json();
}
throw new Error('No English strings found');
} catch (e) {
// TODO what should this do? We really shouldn't ever be here...
console.error(e);
}
}
}
return strings;
}
}
function ensureSuffix(path: string, suffix: string): string {

View file

@ -277,6 +277,12 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost
const [telemetryInfo, initData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]);
const workspace = this._contextService.getWorkspace();
const deltaExtensions = this.extensions.set(initData.allExtensions, initData.myExtensions);
const nlsBaseUrl = this._productService.extensionsGallery?.nlsBaseUrl;
let nlsUrlWithDetails: URI | undefined = undefined;
// Only use the nlsBaseUrl if we are using a language other than the default, English.
if (nlsBaseUrl && this._productService.commit && !platform.Language.isDefaultVariant()) {
nlsUrlWithDetails = URI.joinPath(URI.parse(nlsBaseUrl), this._productService.commit, this._productService.version, platform.Language.value());
}
return {
commit: this._productService.commit,
version: this._productService.version,
@ -304,6 +310,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost
},
allExtensions: deltaExtensions.toAdd,
myExtensions: deltaExtensions.myToAdd,
nlsBaseUrl: nlsUrlWithDetails,
telemetryInfo,
logLevel: this._logService.getLevel(),
logsLocation: this._extensionHostLogsLocation,

View file

@ -28,6 +28,7 @@ export interface IExtensionHostInitData {
workspace?: IStaticWorkspaceData | null;
allExtensions: IExtensionDescription[];
myExtensions: ExtensionIdentifier[];
nlsBaseUrl?: URI;
telemetryInfo: ITelemetryInfo;
logLevel: LogLevel;
logsLocation: URI;

View file

@ -37,6 +37,7 @@ export const allApiProposals = Object.freeze({
inlineCompletionsNew: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsNew.d.ts',
interactiveWindow: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveWindow.d.ts',
ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts',
localization: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.localization.d.ts',
notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts',
notebookContentProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookContentProvider.d.ts',
notebookControllerKind: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerKind.d.ts',

View file

@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'vscode' {
export namespace l10n {
/**
* A string that can be pulled out of a localization bundle if it exists.
*/
export function t(message: string, ...args: string[]): string;
/**
* A string that can be pulled out of a localization bundle if it exists.
*/
export function t(options: { message: string; args: string[]; comment: string[] }): string;
/**
* The bundle of localized strings that have been loaded for the extension.
*/
export const bundle: { [key: string]: string };
/**
* The URI of the localization bundle that has been loaded for the extension.
*/
export const uri: Uri | undefined;
}
}