From df639a5a06bcda7c0ac99b6abdbe2eab31b772e6 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Fri, 16 Oct 2026 00:02:31 +0400 Subject: [PATCH] i18n: wrap the ru/es push-to-talk hint; show city names in voice results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The help tray over the mic clipped the longer ru/es hint ("Hold Space to speak · tap Space to activate focused controls"); let it wrap in non-English locales. English layout is unchanged. - The fly-to headline and the nearest-aircraft distance line now show a preset city in the interface language (Tokyo → Токио / Tokio); speech sent to the model stays English. Co-Authored-By: Claude Opus 5.5 --- src/ui/styles/command-dock-sliding.css | 5 +++ src/voice/speech.js | 8 +++- src/voice/voiceCardPlaceNames.i18n.test.mjs | 48 +++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src/voice/voiceCardPlaceNames.i18n.test.mjs diff --git a/src/ui/styles/command-dock-sliding.css b/src/ui/styles/command-dock-sliding.css index 556603d1c..993fe7df5 100644 --- a/src/ui/styles/command-dock-sliding.css +++ b/src/ui/styles/command-dock-sliding.css @@ -647,6 +647,11 @@ white-space: nowrap; } +/* The ru/es push-to-talk hint is longer than the tray: wrap it instead of clipping. */ +html:not([lang='en']) #gev-voice-control .gev-voice-help-detail { + white-space: normal; +} + #gev-voice-control:not([data-status='error']):hover .gev-voice-help-tray, #gev-voice-control:not([data-status='error']) #gev-voice-button:focus-visible diff --git a/src/voice/speech.js b/src/voice/speech.js index c37989acb..6c5bd5e66 100644 --- a/src/voice/speech.js +++ b/src/voice/speech.js @@ -401,7 +401,9 @@ function setLayerVisibility(result, args = {}) { function flyToLocation(result) { const label = spokenLabel(result?.label || result?.query || 'there'); - const title = spokenLabel(result?.label || result?.query) || t('there'); + const title = + spokenLabel(localizePlaceName(result?.label || result?.query)) || + t('there'); if (result?.cancelled) return null; if (!result?.ok) { return { @@ -481,7 +483,9 @@ function selectNearestAircraft(result) { ); const noun = LAYER_NOUNS[result?.layerId] || 'aircraft'; const shownLocation = - spokenLabel(result?.location?.label || result?.location) || t('there'); + spokenLabel( + localizePlaceName(result?.location?.label || result?.location), + ) || t('there'); const feed = result?.feed || {}; const sources = shownSources(feed.source); if (result?.cancelled) return null; diff --git a/src/voice/voiceCardPlaceNames.i18n.test.mjs b/src/voice/voiceCardPlaceNames.i18n.test.mjs new file mode 100644 index 000000000..9f5bdcd39 --- /dev/null +++ b/src/voice/voiceCardPlaceNames.i18n.test.mjs @@ -0,0 +1,48 @@ +// The voice card names a preset city in the interface language in the +// fly-to headline and the nearest-aircraft distance line; what the model +// says stays English. +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { setLocale } from '../i18n/index.js'; +import { SPEECH_BUILDERS } from './speech.js'; + +function inLocale(locale, run) { + setLocale(locale); + try { + return run(); + } finally { + setLocale('en'); + } +} + +const FLY = { ok: true, arrived: true, label: 'Tokyo' }; +const NEAREST = { + ok: true, + layerId: 'flights', + location: { label: 'Tokyo' }, + aircraft: { callsign: 'JAL123', distanceKm: 12, altitudeM: 3000 }, + feed: {}, +}; + +test('the fly-to headline shows the city in the interface language', () => { + assert.equal(SPEECH_BUILDERS.fly_to_location(FLY).display.title, 'Tokyo'); + const ru = inLocale('ru', () => SPEECH_BUILDERS.fly_to_location(FLY)); + assert.equal(ru.display.title, 'Токио'); + assert.match(ru.say, /Tokyo/); + const es = inLocale('es', () => SPEECH_BUILDERS.fly_to_location(FLY)); + assert.equal(es.display.title, 'Tokio'); +}); + +test('the nearest-aircraft distance line shows the city in the interface language', () => { + const en = SPEECH_BUILDERS.select_nearest_aircraft(NEAREST); + assert.match(en.display.lines[0], /from Tokyo$/); + const ru = inLocale('ru', () => + SPEECH_BUILDERS.select_nearest_aircraft(NEAREST), + ); + assert.match(ru.display.lines[0], /Токио/); + assert.match(ru.say, /Tokyo/); + const es = inLocale('es', () => + SPEECH_BUILDERS.select_nearest_aircraft(NEAREST), + ); + assert.match(es.display.lines[0], /Tokio/); +});