From dd2411ed762a9c65147821c6402f4adcfd2ea26e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 4 Aug 2026 14:11:01 -0400 Subject: [PATCH 1/3] Use VS Code language for automatic dictation Fixes #328854 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b11adb7d-72c1-4e64-ba1e-f798dd9bf490 --- .../browser/speechToText/chatSpeechToTextService.ts | 3 ++- .../chat/browser/speechToText/dictationLanguage.ts | 8 ++++---- .../chat/test/browser/chatSpeechToTextService.test.ts | 10 +++++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts b/src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts index 7f4967a970d33a..0be0cd8fa7046b 100644 --- a/src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts +++ b/src/vs/workbench/contrib/chat/browser/speechToText/chatSpeechToTextService.ts @@ -9,6 +9,7 @@ import { VSBuffer, encodeBase64 } from '../../../../../base/common/buffer.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { computeLevenshteinDistance } from '../../../../../base/common/diff/diff.js'; import { joinPath } from '../../../../../base/common/resources.js'; +import { Language } from '../../../../../base/common/platform.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IAction, toAction } from '../../../../../base/common/actions.js'; @@ -925,7 +926,7 @@ export class ChatSpeechToTextService extends Disposable implements IChatSpeechTo const model = this._getModelId(); const language = resolveDictationLanguage( this._configurationService.getValue('agents.voice.language'), - window.navigator.language, + Language.value(), ); await local.start({ cacheDir, model, language }); diff --git a/src/vs/workbench/contrib/chat/browser/speechToText/dictationLanguage.ts b/src/vs/workbench/contrib/chat/browser/speechToText/dictationLanguage.ts index 3b1180256ffd11..c89298b5474f49 100644 --- a/src/vs/workbench/contrib/chat/browser/speechToText/dictationLanguage.ts +++ b/src/vs/workbench/contrib/chat/browser/speechToText/dictationLanguage.ts @@ -52,12 +52,12 @@ const NEMOTRON_DEFAULT_LOCALE_BY_LANGUAGE: Readonly> = { /** * Resolve the on-device dictation language using the same setting semantics as - * Voice Mode. Automatic follows the browser locale when Nemotron supports it, - * then falls back to the model's language detection. + * Voice Mode. Automatic follows the VS Code display language when Nemotron + * supports it, then falls back to the model's language detection. */ -export function resolveDictationLanguage(configuredLanguage: unknown, browserLanguage: string | undefined): string { +export function resolveDictationLanguage(configuredLanguage: unknown, displayLanguage: string | undefined): string { const configured = typeof configuredLanguage === 'string' ? configuredLanguage.trim() : ''; - const candidate = configured && configured.toLowerCase() !== 'auto' ? configured : browserLanguage; + const candidate = configured && configured.toLowerCase() !== 'auto' ? configured : displayLanguage; if (!candidate || typeof Intl.getCanonicalLocales !== 'function') { return 'auto'; } diff --git a/src/vs/workbench/contrib/chat/test/browser/chatSpeechToTextService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatSpeechToTextService.test.ts index 876a3a3014013d..8cc9bf532f1783 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatSpeechToTextService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatSpeechToTextService.test.ts @@ -12,10 +12,10 @@ suite('ChatSpeechToTextService', () => { ensureNoDisposablesAreLeakedInTestSuite(); - test('resolves the dictation language from Voice Mode configuration and browser locale', () => { + test('resolves the dictation language from Voice Mode configuration and VS Code display language', () => { assert.deepStrictEqual({ - explicit: resolveDictationLanguage('fr-FR', 'de-DE'), - automatic: resolveDictationLanguage('auto', 'uk-UA'), + explicitOverridesDisplayLanguage: resolveDictationLanguage('fr-FR', 'de-DE'), + automaticUsesDisplayLanguage: resolveDictationLanguage('auto', 'de-DE'), regionalAutomatic: resolveDictationLanguage('auto', 'pt-BR'), additionalSupportedAutomatic: resolveDictationLanguage('auto', 'he-IL'), unsupportedRegion: resolveDictationLanguage('auto', 'en-AU'), @@ -25,8 +25,8 @@ suite('ChatSpeechToTextService', () => { invalidExplicit: resolveDictationLanguage('not a locale', 'de-DE'), missing: resolveDictationLanguage(undefined, undefined), }, { - explicit: 'fr-FR', - automatic: 'uk-UA', + explicitOverridesDisplayLanguage: 'fr-FR', + automaticUsesDisplayLanguage: 'de-DE', regionalAutomatic: 'pt-BR', additionalSupportedAutomatic: 'he-IL', unsupportedRegion: 'en-US', From 65f7e93784422fb8ed3202bbe634fe73411fc6f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:16:03 +0000 Subject: [PATCH 2/3] Initial plan From 0eb0e9937a481d103c683e635d5ad09637f2c96f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:23:28 +0000 Subject: [PATCH 3/3] Respect display language for voice auto locale Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com> --- .../browser/agentsVoice.contribution.ts | 2 +- .../chat/browser/chat.shared.contribution.ts | 2 +- .../browser/speechToText/dictationLanguage.ts | 29 +++++++++---- .../browser/voiceClient/voiceClientService.ts | 41 +++++++++++-------- .../browser/chatSpeechToTextService.test.ts | 8 +++- .../voiceClient/voiceClientService.test.ts | 16 +++++++- 6 files changed, 70 insertions(+), 28 deletions(-) diff --git a/src/vs/workbench/contrib/agentsVoice/browser/agentsVoice.contribution.ts b/src/vs/workbench/contrib/agentsVoice/browser/agentsVoice.contribution.ts index 7644f12f89e88b..28bfc695efdf44 100644 --- a/src/vs/workbench/contrib/agentsVoice/browser/agentsVoice.contribution.ts +++ b/src/vs/workbench/contrib/agentsVoice/browser/agentsVoice.contribution.ts @@ -677,7 +677,7 @@ configurationRegistry.registerConfiguration({ nls.localize('agents.voice.language.ko', "Korean"), nls.localize('agents.voice.language.zh', "Chinese"), ], - markdownDescription: nls.localize('agents.voice.language', "The language used for speech recognition, dictation, and spoken responses. The selectable languages support native voice output. Automatic follows the system or browser locale for speech recognition and dictation, and uses English voice output when the detected language does not support native voice output. Changing this while voice mode is connected takes effect immediately."), + markdownDescription: nls.localize('agents.voice.language', "The language used for speech recognition, dictation, and spoken responses. The selectable languages support native voice output. Automatic uses the configured display language for speech recognition and dictation when available; otherwise, it follows the system or browser locale. English voice output is used when the detected language does not support native voice output. Changing this while voice mode is connected takes effect immediately."), default: 'auto', scope: ConfigurationScope.APPLICATION, }, diff --git a/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts index 277a275d055b6b..3183b3120fff60 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts @@ -291,7 +291,7 @@ configurationRegistry.registerConfiguration({ nls.localize('dictation.model.mai.label', "MAI — Cloud"), ], markdownEnumDescriptions: [ - nls.localize('dictation.model.nemotronMultilingual', "NVIDIA Nemotron 3.5 multilingual streaming RNN-T, run on-device through Microsoft Foundry Local. Works offline; no audio leaves the device. Automatic language selection follows the Voice Mode language setting and system or browser locale, with model detection as a fallback. Downloaded on first use and cached on disk."), + nls.localize('dictation.model.nemotronMultilingual', "NVIDIA Nemotron 3.5 multilingual streaming RNN-T, run on-device through Microsoft Foundry Local. Works offline; no audio leaves the device. Automatic language selection follows the Voice Mode language setting; when that setting is Automatic, dictation uses the configured display language when available, then the system or browser locale, with model detection as a fallback. Downloaded on first use and cached on disk."), nls.localize('dictation.model.mai', "Cloud transcription through the same Microsoft AI voice service used by Voice Mode. Requires a network connection and GitHub sign-in; audio is streamed to the service."), ], markdownDescription: nls.localize('dictation.model', "The model used for dictation. On-device models download on first use and run locally through Microsoft Foundry Local; the cloud option streams audio to the Microsoft AI voice service."), diff --git a/src/vs/workbench/contrib/chat/browser/speechToText/dictationLanguage.ts b/src/vs/workbench/contrib/chat/browser/speechToText/dictationLanguage.ts index 3b1180256ffd11..d1e8cb5cd2d753 100644 --- a/src/vs/workbench/contrib/chat/browser/speechToText/dictationLanguage.ts +++ b/src/vs/workbench/contrib/chat/browser/speechToText/dictationLanguage.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Language } from '../../../../../base/common/platform.js'; + const NEMOTRON_LOCALES = new Set([ 'ar-AR', 'bg-BG', 'cs-CZ', 'da-DK', 'de-DE', 'en-GB', 'en-US', 'es-ES', 'es-US', 'et-EE', 'fi-FI', 'fr-CA', 'fr-FR', 'el-GR', 'he-IL', 'hi-IN', @@ -50,16 +52,29 @@ const NEMOTRON_DEFAULT_LOCALE_BY_LANGUAGE: Readonly> = { zh: 'zh-CN', }; +function getConfiguredDisplayLanguage(): string | undefined { + return Language.isDefaultVariant() ? undefined : Language.value(); +} + /** * Resolve the on-device dictation language using the same setting semantics as - * Voice Mode. Automatic follows the browser locale when Nemotron supports it, - * then falls back to the model's language detection. + * Voice Mode. Automatic follows the configured display language when available, + * then the system or browser locale, then the model's language detection. */ -export function resolveDictationLanguage(configuredLanguage: unknown, browserLanguage: string | undefined): string { +export function resolveDictationLanguage(configuredLanguage: unknown, browserLanguage: string | undefined, displayLanguage = getConfiguredDisplayLanguage()): string { const configured = typeof configuredLanguage === 'string' ? configuredLanguage.trim() : ''; - const candidate = configured && configured.toLowerCase() !== 'auto' ? configured : browserLanguage; + if (configured && configured.toLowerCase() !== 'auto') { + return resolveSupportedDictationLanguage(configured) ?? 'auto'; + } + + return resolveSupportedDictationLanguage(displayLanguage) + ?? resolveSupportedDictationLanguage(browserLanguage) + ?? 'auto'; +} + +function resolveSupportedDictationLanguage(candidate: string | undefined): string | undefined { if (!candidate || typeof Intl.getCanonicalLocales !== 'function') { - return 'auto'; + return undefined; } try { @@ -67,8 +82,8 @@ export function resolveDictationLanguage(configuredLanguage: unknown, browserLan if (NEMOTRON_LOCALES.has(canonical)) { return canonical; } - return NEMOTRON_DEFAULT_LOCALE_BY_LANGUAGE[canonical.split('-')[0]] ?? 'auto'; + return NEMOTRON_DEFAULT_LOCALE_BY_LANGUAGE[canonical.split('-')[0]]; } catch { - return 'auto'; + return undefined; } } diff --git a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts index f27c290d007d02..dfeab9f43af1f3 100644 --- a/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts +++ b/src/vs/workbench/contrib/chat/browser/voiceClient/voiceClientService.ts @@ -7,6 +7,7 @@ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { mainWindow } from '../../../../../base/browser/window.js'; +import { Language } from '../../../../../base/common/platform.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; @@ -59,6 +60,26 @@ function asOptionalNonEmptyString(value: unknown): string | undefined { return result && result.length > 0 ? result : undefined; } +function canonicalizeSupportedLanguage(value: string | undefined, supportedBases: ReadonlySet): string | undefined { + const candidate = value?.trim(); + if (!candidate || typeof Intl.getCanonicalLocales !== 'function') { + return undefined; + } + + try { + const canonical = Intl.getCanonicalLocales(candidate)[0]; + return supportedBases.has(canonical.split('-')[0]) ? canonical : undefined; + } catch { + return undefined; + } +} + +export function resolveAutomaticVoiceLanguage(browserLanguage: string | undefined, displayLanguage: string | undefined): string { + return canonicalizeSupportedLanguage(displayLanguage, ASR_SUPPORTED_LANGUAGE_BASES) + ?? canonicalizeSupportedLanguage(browserLanguage, ASR_SUPPORTED_LANGUAGE_BASES) + ?? DEFAULT_LANGUAGE; +} + function asTranscriptionStatus(value: unknown): IVoiceTranscription['status'] | undefined { return value === 'partial' || value === 'final' ? value : undefined; } @@ -201,7 +222,7 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic private _getLanguage(): string { const configured = this._configurationService.getValue('agents.voice.language'); if (typeof configured === 'string' && configured.trim().toLowerCase() !== 'auto') { - const language = this._canonicalizeSupportedLanguage(configured, TTS_SUPPORTED_LANGUAGE_BASES); + const language = canonicalizeSupportedLanguage(configured, TTS_SUPPORTED_LANGUAGE_BASES); if (language) { return language; } @@ -209,22 +230,8 @@ export class VoiceClientService extends Disposable implements IVoiceClientServic return DEFAULT_LANGUAGE; } - return this._canonicalizeSupportedLanguage(this._window?.navigator.language, ASR_SUPPORTED_LANGUAGE_BASES) - ?? DEFAULT_LANGUAGE; - } - - private _canonicalizeSupportedLanguage(value: string | undefined, supportedBases: ReadonlySet): string | undefined { - const candidate = value?.trim(); - if (!candidate || typeof Intl.getCanonicalLocales !== 'function') { - return undefined; - } - - try { - const canonical = Intl.getCanonicalLocales(candidate)[0]; - return supportedBases.has(canonical.split('-')[0]) ? canonical : undefined; - } catch { - return undefined; - } + const displayLanguage = Language.isDefaultVariant() ? undefined : Language.value(); + return resolveAutomaticVoiceLanguage(this._window?.navigator.language, displayLanguage); } private _sendSetLanguage(): void { diff --git a/src/vs/workbench/contrib/chat/test/browser/chatSpeechToTextService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatSpeechToTextService.test.ts index 876a3a3014013d..dddaaafd45025e 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatSpeechToTextService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatSpeechToTextService.test.ts @@ -12,9 +12,12 @@ suite('ChatSpeechToTextService', () => { ensureNoDisposablesAreLeakedInTestSuite(); - test('resolves the dictation language from Voice Mode configuration and browser locale', () => { + test('resolves the dictation language from Voice Mode configuration, display language, and browser locale', () => { assert.deepStrictEqual({ explicit: resolveDictationLanguage('fr-FR', 'de-DE'), + explicitWithDisplayLanguage: resolveDictationLanguage('fr-FR', 'de-DE', 'ja'), + displayLanguage: resolveDictationLanguage('auto', 'en-US', 'de'), + unsupportedDisplayLanguage: resolveDictationLanguage('auto', 'pt-BR', 'id-ID'), automatic: resolveDictationLanguage('auto', 'uk-UA'), regionalAutomatic: resolveDictationLanguage('auto', 'pt-BR'), additionalSupportedAutomatic: resolveDictationLanguage('auto', 'he-IL'), @@ -26,6 +29,9 @@ suite('ChatSpeechToTextService', () => { missing: resolveDictationLanguage(undefined, undefined), }, { explicit: 'fr-FR', + explicitWithDisplayLanguage: 'fr-FR', + displayLanguage: 'de-DE', + unsupportedDisplayLanguage: 'pt-BR', automatic: 'uk-UA', regionalAutomatic: 'pt-BR', additionalSupportedAutomatic: 'he-IL', diff --git a/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceClientService.test.ts b/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceClientService.test.ts index f3765510417a39..866290e68185ca 100644 --- a/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceClientService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/voiceClient/voiceClientService.test.ts @@ -11,7 +11,7 @@ import { TestConfigurationService } from '../../../../../../platform/configurati import { NullLogService } from '../../../../../../platform/log/common/log.js'; import product from '../../../../../../platform/product/common/product.js'; import { IProductService } from '../../../../../../platform/product/common/productService.js'; -import { VoiceClientService } from '../../../browser/voiceClient/voiceClientService.js'; +import { resolveAutomaticVoiceLanguage, VoiceClientService } from '../../../browser/voiceClient/voiceClientService.js'; import { IVoiceAudioResponse, IVoiceBargeIn, IVoiceNarrationAck, IVoiceNarrationSignal, IVoiceSpeechStarted, IVoiceTranscription } from '../../../common/voiceClient/voiceClientService.js'; class TestWebSocket { @@ -577,6 +577,20 @@ suite('VoiceClientService', () => { }); }); + test('resolves automatic language from display language before browser locale', () => { + assert.deepStrictEqual({ + displayLanguage: resolveAutomaticVoiceLanguage('en-US', 'de'), + browserLocale: resolveAutomaticVoiceLanguage('pt-BR', undefined), + unsupportedDisplayLanguage: resolveAutomaticVoiceLanguage('pt-BR', 'he-IL'), + missing: resolveAutomaticVoiceLanguage(undefined, undefined), + }, { + displayLanguage: 'de', + browserLocale: 'pt-BR', + unsupportedDisplayLanguage: 'pt-BR', + missing: 'en-US', + }); + }); + test('falls back for an unsupported configured BCP-47 locale', async () => { const { service } = createService({ 'agents.voice.language': 'uk-UA' });