Add display language support VS Code web appears to implement language support by setting a cookie and downloading language packs from a URL configured in the product.json. This patch supports language pack extensions and uses files on the remote to set the language instead, so it works like the desktop version. Index: code-server/lib/vscode/src/vs/server/node/serverServices.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/server/node/serverServices.ts +++ code-server/lib/vscode/src/vs/server/node/serverServices.ts @@ -12,7 +12,7 @@ import * as path from '../../base/common import { IURITransformer } from '../../base/common/uriIpc.js'; import { getMachineId, getSqmMachineId, getdevDeviceId } from '../../base/node/id.js'; import { Promises } from '../../base/node/pfs.js'; -import { ClientConnectionEvent, IMessagePassingProtocol, IPCServer, StaticRouter } from '../../base/parts/ipc/common/ipc.js'; +import { ClientConnectionEvent, IMessagePassingProtocol, IPCServer, ProxyChannel, StaticRouter } from '../../base/parts/ipc/common/ipc.js'; import { ProtocolConstants } from '../../base/parts/ipc/common/ipc.net.js'; import { IConfigurationService } from '../../platform/configuration/common/configuration.js'; import { ConfigurationService } from '../../platform/configuration/common/configurationService.js'; @@ -243,6 +243,9 @@ export async function setupServerService const channel = new ExtensionManagementChannel(extensionManagementService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority)); socketServer.registerChannel('extensions', channel); + const languagePackChannel = ProxyChannel.fromService(accessor.get(ILanguagePackService), disposables); + socketServer.registerChannel('languagePacks', languagePackChannel); + // clean up extensions folder remoteExtensionsScanner.whenExtensionsReady().then(() => extensionManagementService.cleanUp()); Index: code-server/lib/vscode/src/vs/platform/environment/common/environmentService.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/platform/environment/common/environmentService.ts +++ code-server/lib/vscode/src/vs/platform/environment/common/environmentService.ts @@ -101,7 +101,7 @@ export abstract class AbstractNativeEnvi return URI.file(join(vscodePortable, 'argv.json')); } - return joinPath(this.userHome, this.productService.dataFolderName, 'argv.json'); + return joinPath(this.appSettingsHome, 'argv.json'); } @memoize Index: code-server/lib/vscode/src/vs/server/node/remoteLanguagePacks.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/server/node/remoteLanguagePacks.ts +++ code-server/lib/vscode/src/vs/server/node/remoteLanguagePacks.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { promises as fs } from 'fs'; +import * as path from 'path'; import { FileAccess } from '../../base/common/network.js'; import { join } from '../../base/common/path.js'; import type { INLSConfiguration } from '../../nls.js'; @@ -33,7 +35,94 @@ export async function getNLSConfiguratio if (!result) { result = resolveNLSConfiguration({ userLocale: language, osLocale: language, commit: product.commit, userDataPath, nlsMetadataPath }); nlsConfigurationCache.set(cacheKey, result); + // If the language pack does not yet exist, it defaults to English, which is + // then cached and you have to restart even if you then install the pack. + result.then((r) => { + if (!language.startsWith('en') && r.resolvedLanguage.startsWith('en')) { + nlsConfigurationCache.delete(cacheKey); + } + }) } return result; } + +/** + * Copied from from src/main.js. + */ +export const getLocaleFromConfig = async (argvResource: string): Promise => { + try { + const content = stripComments(await fs.readFile(argvResource, 'utf8')); + return JSON.parse(content).locale; + } catch (error) { + if (error.code !== "ENOENT") { + console.warn(error) + } + return 'en'; + } +}; + +/** + * Copied from from src/main.js. + */ +const stripComments = (content: string): string => { + const regexp = /('(?:[^\\']*(?:\\.)?)*')|('(?:[^\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g; + + return content.replace(regexp, (match, _m1, _m2, m3, m4) => { + // Only one of m1, m2, m3, m4 matches + if (m3) { + // A block comment. Replace with nothing + return ''; + } else if (m4) { + // A line comment. If it ends in \r?\n then keep it. + const length_1 = m4.length; + if (length_1 > 2 && m4[length_1 - 1] === '\n') { + return m4[length_1 - 2] === '\r' ? '\r\n' : '\n'; + } + else { + return ''; + } + } else { + // We match a string + return match; + } + }); +}; + +/** + * Generate translations then return a path to a JavaScript file that sets the + * translations into global variables. This file is loaded by the browser to + * set global variables that the loader uses when looking for translations. + * + * Normally, VS Code pulls these files from a CDN but we want them to be local. + */ +export async function getBrowserNLSConfiguration(locale: string, userDataPath: string): Promise { + if (locale.startsWith('en')) { + return ''; // Use fallback translations. + } + + const nlsConfig = await getNLSConfiguration(locale, userDataPath); + const messagesFile = nlsConfig?.languagePack?.messagesFile; + const resolvedLanguage = nlsConfig?.resolvedLanguage; + if (!messagesFile || !resolvedLanguage) { + return ''; // Use fallback translations. + } + + const nlsFile = path.join(path.dirname(messagesFile), "nls.messages.js"); + try { + await fs.stat(nlsFile); + return nlsFile; // We already generated the file. + } catch (error) { + // ENOENT is fine, that just means we need to generate the file. + if (error.code !== 'ENOENT') { + throw error; + } + } + + const messages = (await fs.readFile(messagesFile)).toString(); + const content = `globalThis._VSCODE_NLS_MESSAGES=${messages}; +globalThis._VSCODE_NLS_LANGUAGE=${JSON.stringify(resolvedLanguage)};` + await fs.writeFile(nlsFile, content, "utf-8"); + + return nlsFile; +} Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts +++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts @@ -26,6 +26,7 @@ import { URI } from '../../base/common/u import { streamToBuffer } from '../../base/common/buffer.js'; import { IProductConfiguration } from '../../base/common/product.js'; import { isString } from '../../base/common/types.js'; +import { getLocaleFromConfig, getBrowserNLSConfiguration } from './remoteLanguagePacks.js'; import { CharCode } from '../../base/common/charCode.js'; import { IExtensionManifest } from '../../platform/extensions/common/extensions.js'; import { isESM } from '../../base/common/amd.js'; @@ -99,6 +100,7 @@ export class WebClientServer { private readonly _webExtensionResourceUrlTemplate: URI | undefined; private readonly _staticRoute: string; + private readonly _serverRoot: string; private readonly _callbackRoute: string; private readonly _webExtensionRoute: string; @@ -114,6 +116,7 @@ export class WebClientServer { ) { this._webExtensionResourceUrlTemplate = this._productService.extensionsGallery?.resourceUrlTemplate ? URI.parse(this._productService.extensionsGallery.resourceUrlTemplate) : undefined; + this._serverRoot = serverRootPath; this._staticRoute = `${serverRootPath}/static`; this._callbackRoute = `${serverRootPath}/callback`; this._webExtensionRoute = `/web-extension-resource`; @@ -352,14 +355,22 @@ export class WebClientServer { }; const cookies = cookie.parse(req.headers.cookie || ''); - const locale = cookies['vscode.nls.locale'] || req.headers['accept-language']?.split(',')[0]?.toLowerCase() || 'en'; + const locale = this._environmentService.args.locale || await getLocaleFromConfig(this._environmentService.argvResource.fsPath) || cookies['vscode.nls.locale'] || req.headers['accept-language']?.split(',')[0]?.toLowerCase() || 'en'; let WORKBENCH_NLS_BASE_URL: string | undefined; let WORKBENCH_NLS_URL: string; if (!locale.startsWith('en') && this._productService.nlsCoreBaseUrl) { WORKBENCH_NLS_BASE_URL = this._productService.nlsCoreBaseUrl; WORKBENCH_NLS_URL = `${WORKBENCH_NLS_BASE_URL}${this._productService.commit}/${this._productService.version}/${locale}/nls.messages.js`; } else { - WORKBENCH_NLS_URL = ''; // fallback will apply + try { + const nlsFile = await getBrowserNLSConfiguration(locale, this._environmentService.userDataPath); + WORKBENCH_NLS_URL = nlsFile + ? `${vscodeBase}${this._serverRoot}/vscode-remote-resource?path=${encodeURIComponent(nlsFile)}` + : ''; + } catch (error) { + console.error("Failed to generate translations", error); + WORKBENCH_NLS_URL = ''; + } } const values: { [key: string]: string } = { Index: code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/server/node/serverEnvironmentService.ts +++ code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts @@ -19,6 +19,7 @@ export const serverOptions: OptionDescri 'disable-file-downloads': { type: 'boolean' }, 'disable-file-uploads': { type: 'boolean' }, 'disable-getting-started-override': { type: 'boolean' }, + 'locale': { type: 'string' }, /* ----- server setup ----- */ @@ -105,6 +106,7 @@ export interface ServerParsedArgs { 'disable-file-downloads'?: boolean; 'disable-file-uploads'?: boolean; 'disable-getting-started-override'?: boolean, + 'locale'?: string /* ----- server setup ----- */ Index: code-server/lib/vscode/src/vs/platform/languagePacks/browser/languagePacks.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/platform/languagePacks/browser/languagePacks.ts +++ code-server/lib/vscode/src/vs/platform/languagePacks/browser/languagePacks.ts @@ -5,18 +5,24 @@ import { CancellationTokenSource } from '../../../base/common/cancellation.js'; import { URI } from '../../../base/common/uri.js'; +import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; import { IExtensionGalleryService } from '../../extensionManagement/common/extensionManagement.js'; import { IExtensionResourceLoaderService } from '../../extensionResourceLoader/common/extensionResourceLoader.js'; -import { ILanguagePackItem, LanguagePackBaseService } from '../common/languagePacks.js'; +import { ILanguagePackItem, ILanguagePackService, LanguagePackBaseService } from '../common/languagePacks.js'; import { ILogService } from '../../log/common/log.js'; +import { IRemoteAgentService } from '../../../workbench/services/remote/common/remoteAgentService.js'; export class WebLanguagePacksService extends LanguagePackBaseService { + private readonly languagePackService: ILanguagePackService; + constructor( + @IRemoteAgentService remoteAgentService: IRemoteAgentService, @IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @ILogService private readonly logService: ILogService ) { super(extensionGalleryService); + this.languagePackService = ProxyChannel.toService(remoteAgentService.getConnection()!.getChannel('languagePacks')) } async getBuiltInExtensionTranslationsUri(id: string, language: string): Promise { @@ -72,6 +78,6 @@ export class WebLanguagePacksService ext // Web doesn't have a concept of language packs, so we just return an empty array getInstalledLanguages(): Promise { - return Promise.resolve([]); + return this.languagePackService.getInstalledLanguages() } } Index: code-server/lib/vscode/src/vs/workbench/services/localization/electron-sandbox/localeService.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/workbench/services/localization/electron-sandbox/localeService.ts +++ code-server/lib/vscode/src/vs/workbench/services/localization/electron-sandbox/localeService.ts @@ -51,7 +51,8 @@ class NativeLocaleService implements ILo @IProductService private readonly productService: IProductService ) { } - private async validateLocaleFile(): Promise { + // Make public just so we do not have to patch all the unused code out. + public async validateLocaleFile(): Promise { try { const content = await this.textFileService.read(this.environmentService.argvResource, { encoding: 'utf8' }); @@ -78,9 +79,6 @@ class NativeLocaleService implements ILo } private async writeLocaleValue(locale: string | undefined): Promise { - if (!(await this.validateLocaleFile())) { - return false; - } await this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['locale'], value: locale }], true); return true; } Index: code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -433,9 +433,6 @@ export class InstallAction extends Exten if (this.extension.isBuiltin) { return; } - if (this.extensionsWorkbenchService.canSetLanguage(this.extension)) { - return; - } if (this.extension.state !== ExtensionState.Uninstalled) { return; } @@ -740,7 +737,7 @@ export abstract class InstallInOtherServ } if (isLanguagePackExtension(this.extension.local.manifest)) { - return true; + return false; } // Prefers to run on UI @@ -2001,17 +1998,6 @@ export class SetLanguageAction extends E update(): void { this.enabled = false; this.class = SetLanguageAction.DisabledClass; - if (!this.extension) { - return; - } - if (!this.extensionsWorkbenchService.canSetLanguage(this.extension)) { - return; - } - if (this.extension.gallery && language === getLocale(this.extension.gallery)) { - return; - } - this.enabled = true; - this.class = SetLanguageAction.EnabledClass; } override async run(): Promise { @@ -2028,7 +2014,6 @@ export class ClearLanguageAction extends private static readonly DisabledClass = `${this.EnabledClass} disabled`; constructor( - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @ILocaleService private readonly localeService: ILocaleService, ) { super(ClearLanguageAction.ID, ClearLanguageAction.TITLE.value, ClearLanguageAction.DisabledClass, false); @@ -2038,17 +2023,6 @@ export class ClearLanguageAction extends update(): void { this.enabled = false; this.class = ClearLanguageAction.DisabledClass; - if (!this.extension) { - return; - } - if (!this.extensionsWorkbenchService.canSetLanguage(this.extension)) { - return; - } - if (this.extension.gallery && language !== getLocale(this.extension.gallery)) { - return; - } - this.enabled = true; - this.class = ClearLanguageAction.EnabledClass; } override async run(): Promise { Index: code-server/lib/vscode/build/gulpfile.reh.js =================================================================== --- code-server.orig/lib/vscode/build/gulpfile.reh.js +++ code-server/lib/vscode/build/gulpfile.reh.js @@ -59,6 +59,7 @@ const serverResourceIncludes = [ // NLS 'out-build/nls.messages.json', + 'out-build/nls.keys.json', // Required to generate translations. // Process monitor 'out-build/vs/base/node/cpuUsage.sh',