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';
@@ -272,6 +272,9 @@ export async function setupServerService
 
 		socketServer.registerChannel('mcpManagement', new McpManagementChannel(mcpManagementService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority)));
 
+		const languagePackChannel = ProxyChannel.fromService<RemoteAgentConnectionContext>(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
@@ -98,7 +98,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<string> => {
+	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<string> {
+	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
@@ -25,6 +25,7 @@ import { URI } from '../../base/common/u
 import { streamToBuffer } from '../../base/common/buffer.js';
 import { IProductConfiguration } from '../../base/common/product.js';
 import { isString, Mutable } 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 { ICSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js';
@@ -385,14 +386,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}/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
@@ -21,6 +21,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 ----- */
 
@@ -110,6 +111,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<ILanguagePackService>(remoteAgentService.getConnection()!.getChannel('languagePacks'))
 	}
 
 	async getBuiltInExtensionTranslationsUri(id: string, language: string): Promise<URI | undefined> {
@@ -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<ILanguagePackItem[]> {
-		return Promise.resolve([]);
+		return this.languagePackService.getInstalledLanguages()
 	}
 }
Index: code-server/lib/vscode/src/vs/workbench/services/localization/electron-browser/localeService.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/services/localization/electron-browser/localeService.ts
+++ code-server/lib/vscode/src/vs/workbench/services/localization/electron-browser/localeService.ts
@@ -51,7 +51,8 @@ class NativeLocaleService implements ILo
 		@IProductService private readonly productService: IProductService
 	) { }
 
-	private async validateLocaleFile(): Promise<boolean> {
+	// Make public just so we do not have to patch all the unused code out.
+	public async validateLocaleFile(): Promise<boolean> {
 		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<boolean> {
-		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
@@ -475,9 +475,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;
 		}
@@ -782,7 +779,7 @@ export abstract class InstallInOtherServ
 		}
 
 		if (isLanguagePackExtension(this.extension.local.manifest)) {
-			return true;
+			return false;
 		}
 
 		// Prefers to run on UI
@@ -2073,17 +2070,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<any> {
@@ -2100,7 +2086,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);
@@ -2110,17 +2095,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<any> {
Index: code-server/lib/vscode/src/vs/workbench/workbench.web.main.internal.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/workbench.web.main.internal.ts
+++ code-server/lib/vscode/src/vs/workbench/workbench.web.main.internal.ts
@@ -55,7 +55,7 @@ import './services/dialogs/browser/fileD
 import './services/host/browser/browserHostService.js';
 import './services/lifecycle/browser/lifecycleService.js';
 import './services/clipboard/browser/clipboardService.js';
-import './services/localization/browser/localeService.js';
+import './services/localization/electron-browser/localeService.js';
 import './services/path/browser/pathService.js';
 import './services/themes/browser/browserHostColorSchemeService.js';
 import './services/encryption/browser/encryptionService.js';
