i18n: fix the LGTM review findings and tidy the branch texts

- i18n scanner: lastError sink, no false sinks on defaults and ternaries
- shared src/i18n/marker.js for modules that only mark strings
- wind field messages move to wind/fields.js; CITY_POIS imports from cityPois.js
- drop review IDs, change history and stale references from comments and docs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-16 21:00:28 +04:00
1 parent 0ed05568f7
commit 20f4cf3763
178 files changed
+918 -948

No files matched your search

+5 -5
View File
@@ -1,5 +1,5 @@
// K-8: the analyst card shows its headline, scope, chips and caveats in the
// interface language; the spoken headline stays English for the model.
// The analyst card shows its headline, scope, chips and caveats in the UI
// language; the spoken headline stays English for the model.
import assert from 'node:assert/strict';
import test from 'node:test';
import { setLocale } from '../i18n/index.js';
@@ -56,7 +56,7 @@ const radius = {
},
};
test('K-8: analyst card in es has no English text', () => {
test('analyst card in es has no English text', () => {
setLocale('es');
try {
const { view } = cardFor('analyst_query', analyst);
@@ -69,7 +69,7 @@ test('K-8: analyst card in es has no English text', () => {
}
});
test('K-8: analyst scope, window and caveats in ru', () => {
test('analyst scope, window and caveats in ru', () => {
setLocale('ru');
try {
const { shaped, view } = cardFor('analyst_query', radius);
@@ -84,7 +84,7 @@ test('K-8: analyst scope, window and caveats in ru', () => {
}
});
test('K-8: the English card reads as before, layers by name', () => {
test('en: the card keeps its English text and names layers', () => {
const { shaped, view } = cardFor('analyst_query', analyst);
assert.equal(view.result.title, '3 aircraft anywhere in the loaded data');
assert.deepEqual(view.result.chips, ['degraded', 'partial']);
+45 -11
View File
@@ -1,6 +1,7 @@
// The analyst card's empty notes can be a layer's status line: Local ADS-B
// with every decoder feed stale, Transit with a quiet feed ("(95s old)"). The
// card shows them in the UI language; the model gets the English.
// with every decoder feed stale, Transit with a quiet feed ("(95s old)") or
// its getStats().coverage text. The card shows them in the UI language; the
// model gets the English.
import assert from 'node:assert/strict';
import test from 'node:test';
import { setLocale } from '../i18n/index.js';
@@ -11,7 +12,7 @@ import {
voiceCardView,
} from './voiceCardPresentation.js';
function notesFor(layerKey, caveat) {
function viewFor(layer, caveat) {
const shaped = attachVoiceResult(
'analyst_query',
{
@@ -21,7 +22,7 @@ function notesFor(layerKey, caveat) {
scopeLabel: 'in view',
feedState: 'nominal',
unanswered: [],
coverage: { layersQueried: [{ layerKey, status: 'empty' }] },
coverage: { layersQueried: [{ ...layer, status: 'empty' }] },
display: { scope: 'in view', caveat },
items: [],
},
@@ -42,10 +43,23 @@ function notesFor(layerKey, caveat) {
result: shaped,
},
].reduce(reduceVoiceCard, initialVoiceCardState()),
).result.notes;
);
}
// Caveat = `${layerKey}: ${getEmptyNote(layerKey)}` (analystEngine.js:1026,
function notesFor(layerKey, caveat) {
return viewFor({ layerKey }, caveat).result.notes;
}
function inLocale(locale, run) {
setLocale(locale);
try {
run();
} finally {
setLocale('en');
}
}
// Caveat = `${layerKey}: ${getEmptyNote(layerKey)}` (data/analystEngine.js;
// gevActions.js getEmptyNote → stats.coverage || stats.statusMessage).
const cases = [
['local-adsb', 'local-adsb: feeds 1090, 978 stale · 0 heard'],
@@ -54,19 +68,39 @@ const cases = [
['transit', 'transit: TTC 0 · GO Transit 0 (95s old)'],
];
// Transit's coverage text when nothing is in view (transit/queries.js).
const transitCoverage = [
'transit: Fly below 3,000 km to a covered region',
'transit: No feed here yet · 12 regions available',
];
for (const locale of ['es', 'ru']) {
for (const [layerKey, caveat] of cases) {
test(`${locale}: analyst note "${caveat}" is translated`, () => {
setLocale(locale);
try {
inLocale(locale, () => {
const notes = notesFor(layerKey, caveat);
assert.ok(notes.length > 0, 'the note is shown');
assert.doesNotMatch(notes.join(' | '), /\b(stale|heard|old)\b/);
} finally {
setLocale('en');
}
});
});
}
test(`${locale}: Transit's empty note is translated on the analyst card`, () => {
inLocale(locale, () => {
for (const caveat of transitCoverage) {
const view = viewFor(
{ layerKey: 'transit', source: 'GTFS-RT' },
caveat,
);
const text = JSON.stringify(view.result);
assert.doesNotMatch(
text,
/Fly below|No feed here yet|regions available/,
text,
);
}
});
});
}
for (const [layerKey, caveat] of cases) {
-71
View File
@@ -1,71 +0,0 @@
// The analyst card shows a layer's "empty" note (Transit's English
// getStats().coverage text) in the UI language.
import assert from 'node:assert/strict';
import test from 'node:test';
import { setLocale } from '../i18n/index.js';
import { attachVoiceResult } from './speech.js';
import {
initialVoiceCardState,
reduceVoiceCard,
voiceCardView,
} from './voiceCardPresentation.js';
function viewFor(result) {
const shaped = attachVoiceResult('analyst_query', result, {});
return voiceCardView(
[
{
type: 'action-call',
callId: 'c1',
name: 'analyst_query',
arguments: {},
},
{
type: 'action-result',
callId: 'c1',
name: 'analyst_query',
result: shaped,
},
].reduce(reduceVoiceCard, initialVoiceCardState()),
);
}
// The caveat analystEngine.js:1026 builds from getEmptyNote (gevActions.js:4740)
// for Transit (transit/queries.js:249–251).
const notes = [
'transit: Fly below 3,000 km to a covered region',
'transit: No feed here yet · 12 regions available',
];
for (const locale of ['es', 'ru']) {
test(`${locale}: Transit's empty note is translated on the analyst card`, () => {
setLocale(locale);
try {
for (const caveat of notes) {
const view = viewFor({
ok: true,
count: 0,
complete: true,
scopeLabel: 'in view',
feedState: 'nominal',
unanswered: [],
coverage: {
layersQueried: [
{ layerKey: 'transit', source: 'GTFS-RT', status: 'empty' },
],
},
display: { scope: 'in view', caveat },
items: [],
});
const text = JSON.stringify(view.result);
assert.doesNotMatch(
text,
/Fly below|No feed here yet|regions available/,
text,
);
}
} finally {
setLocale('en');
}
});
}
+3 -5
View File
@@ -1,14 +1,11 @@
import { t } from '../i18n/index.js';
/** Build the voice control independently of its connection backend. */
// The assistant's name on its card. i18n-ignore: product name
const ASSISTANT_NAME = 'GEV';
/**
* The short state label in the voice heading. The heading is a few characters
* wide next to the cost readout, so these carry their own `voice` context and
* languages can pick shorter words than the shared CONNECTING / OFF labels.
* Unknown states show as they are, upper-cased.
* Short state label for the narrow voice heading. The `voice` context lets
* languages use shorter words; unknown states show upper-cased.
* @param {string} status machine state (dataset.status)
* @returns {string}
*/
@@ -29,6 +26,7 @@ export function voiceStatusLabel(status) {
}
}
/** Build the voice control independently of its connection backend. */
export function createVoiceControl({ reset = false } = {}) {
let root = document.getElementById('gev-voice-control');
if (root && reset) {
+1 -2
View File
@@ -10,8 +10,8 @@ export { readLayerLifecycleSummary } from './layerSummary.js';
import { defaultGeospatial } from '../search/defaults.js';
import { msg } from '../i18n/index.js';
import * as Cesium from 'cesium';
import { CITY_POIS } from '../cityPois.js';
import {
CITY_POIS,
findPoiByName,
flyToGlobeView,
flyToLandmark,
@@ -1643,7 +1643,6 @@ export async function controlCctv(dataManager, args = {}, styleManager = null) {
throw new Error(`Unknown CCTV action: ${args.action || 'missing'}`);
}
// Labels are place names returned to the voice model as tool data.
const RADIO_COUNTRY_CENTERS = new Map([
// i18n-ignore: place name in tool data
['us', { lat: 39.8, lon: -98.6, country: 'US', label: 'United States' }],
+1 -2
View File
@@ -58,8 +58,7 @@ import { createRealtimeBackend } from './realtimeBackend.js';
import { t } from '../i18n/index.js';
import { voiceStatusLabel } from './control.js';
// Status enum VALUES are machine identifiers (dataset.status, CSS hooks);
// only the LABEL rendered from them localizes, resolved at write time via t().
// Machine states (dataset.status, CSS); voiceStatusLabel() translates labels.
const STATUS_STATES = new Set([
'idle',
'connecting',
@@ -1,4 +1,4 @@
// K-10: the API-key cost tooltip states an incomplete estimate in the
// The API-key cost tooltip states an incomplete estimate in the
// interface language; the tracker keeps its English note for diagnostics.
import test from 'node:test';
import assert from 'node:assert/strict';
+1 -1
View File
@@ -1,4 +1,4 @@
// K-11: the status after a tier switch names the tier in the interface
// The status after a tier switch names the tier in the interface
// language, as the tier button does; the stored tier value stays English.
import test from 'node:test';
import assert from 'node:assert/strict';
+3 -4
View File
@@ -263,10 +263,9 @@ const UTC_DATE = Object.freeze({
});
/**
* The snapshot time the note carries (ISO, as the model got it) as the card
* shows it: "14:32:05 UTC", with the date first ("15.10.2026, 14:32:05 UTC")
* when the snapshot is not from today (UTC). English shows the ISO time as
* before; a value that is not a time shows as written.
* The note's ISO snapshot time as the card shows it: "14:32:05 UTC", dated
* ("15.10.2026, 14:32:05 UTC") when not from today (UTC). English and
* non-time values show as given.
*/
export function shownSnapshotTime(iso, now = Date.now()) {
const time = Date.parse(iso);
+1 -1
View File
@@ -301,7 +301,7 @@ test('common button and annotation bindings work with an alternate adapter and c
assert.equal(ui.helpDetail.textContent, 'Activate to toggle voice');
assert.equal(ui.status.textContent, 'LISTENING');
assert.equal(ui.detail.textContent, 'Ready');
// K-27: switching language (a reload) asks first during a session.
// Switching language (a reload) asks first during a session.
assert.deepEqual(describeUnsavedWork(), ['the active voice session']);
subscribed({ status: 'resolved' });
assert.deepEqual(events, [
+3 -5
View File
@@ -63,12 +63,10 @@ export function createVoiceCommands({
const updateStatus = session.subscribe((event) => {
if (event.type !== 'state') return;
ui.root.dataset.status = event.state;
// Session states are machine values (dataset.status); only the label
// shown for them is translated. Unknown states show as they are.
// States are machine values; only the label is translated.
ui.status.textContent = voiceStatusLabel(event.state);
// The session reports 'Voice off' in English: it is portable and has no
// locale, so that detail shows as the idle label. Other details are
// provider messages and show as they arrive.
// The portable session reports 'Voice off' in English: show the idle
// label instead. Other details are provider messages, shown as they come.
const idle = event.state === 'idle';
const detail = event.detail === 'Voice off' ? '' : event.detail;
ui.detail.textContent =
+9 -13
View File
@@ -17,11 +17,9 @@
* Builders are small pure functions keyed by tool name. Tools without one are
* returned unchanged.
*
* Only `display` and the plan/progress labels are interface text, translated
* with t(). `say`, `identityLine` and the progress lines are not shown: they go
* to the voice model to be spoken, so they stay as written. The tool result
* itself (layer names, sources, `error`) stays English as the model got it;
* the card shows those through the shown-name helpers and `shownError`.
* Only `display` and plan/progress labels are UI text (t()). `say`,
* `identityLine`, spoken progress lines and the tool result stay English for
* the model; the card shows them through the shown* helpers.
*/
import { analystHeadline } from './resultDisplay.js';
@@ -118,10 +116,8 @@ function shownSources(source) {
}
/*
* Tool errors stay English in the result: the model reads them. The card shows
* the ones it knows in the interface language. Each English text here is
* exactly what gevActions returns, so English shows as before; an error not
* listed shows as given.
* Tool errors stay English in the result (the model reads them). The card
* translates the known ones; the English must match gevActions exactly.
*/
const KNOWN_ERRORS = new Map(
[
@@ -818,7 +814,7 @@ function annotationTargets(args) {
.filter(Boolean);
}
// frame_overhead's targets as the schema names them; English is unchanged.
// frame_overhead targets as the schema names them.
function frameStepLabel(target) {
switch (target) {
case 'flights':
@@ -834,8 +830,8 @@ function frameStepLabel(target) {
}
}
// The other tools' plan steps, one label per tool in actionSchemas. The tool
// name itself is English model vocabulary and never shows.
// Plan-step labels for the other tools in actionSchemas. A tool missing here
// shows its raw name in English only.
const STEP_TITLES = Object.freeze({
zoom_to_globe: msg('Zoom out to globe'),
show_data_layers_menu: msg('Open data layers'),
@@ -906,7 +902,7 @@ export function planStepLabel(name, args = {}) {
case 'frame_overhead':
return frameStepLabel(spokenLabel(args.target || 'flights', 24));
default:
// A tool the card has no label for: English shows its name as before.
// A tool with no label: its name in English, "Action" elsewhere.
return getLocale() === 'en'
? spokenLabel(String(name || 'Action').replace(/_/g, ' '), 40)
: t('Action');
+5 -5
View File
@@ -1,5 +1,5 @@
// K-24: on a failed tool step the voice card shows the English `result.error`
// from gevActions under a translated title and step status.
// A failed tool step: the card shows the tool error in the UI language; the
// result keeps gevActions' English for the model.
import assert from 'node:assert/strict';
import test from 'node:test';
import { setLocale } from '../i18n/index.js';
@@ -10,7 +10,7 @@ import {
voiceCardView,
} from './voiceCardPresentation.js';
// Error texts as gevActions.js builds them (lines 2442, 646–647, 2476).
// Error texts as gevActions.js builds them (frameOverhead, select_nearest_aircraft).
const CASES = [
{
name: 'frame_overhead',
@@ -76,13 +76,13 @@ for (const locale of ['es', 'ru']) {
try {
assert.ok(view.result.visible, 'card shows a result');
assert.notEqual(view.result.title, '', 'card has a title');
// Title and step status are translated (this part passes today).
// Title and step status are translated.
assert.notEqual(
view.steps[0].statusText,
'failed',
'step status translated',
);
// The failure line is the raw English tool error.
// The error line is translated, not the raw English.
for (const line of view.result.lines) {
assert.ok(
!/\b(is not enabled|no airborne aircraft|of the current view)\b/.test(
+2 -2
View File
@@ -1,4 +1,4 @@
// K-7: the voice card names layers, sources and distances in the interface
// The voice card names layers, sources and distances in the interface
// language; what the model receives stays English.
import test from 'node:test';
import assert from 'node:assert/strict';
@@ -48,7 +48,7 @@ for (const locale of ['ru', 'es']) {
});
}
test('en: layer switch, distance and plan labels read as before', () => {
test('en: layer switch, distance and plan labels in English', () => {
const out = attachVoiceResult(
'set_layer_visibility',
{
+16 -19
View File
@@ -1,7 +1,6 @@
// PDF review C3, B4, E5, E6 and proposal 1: the voice card's plan steps name
// every tool in the interface language, the "Layer" step agrees in gender,
// error lines and long headlines show whole, and the Contacts snapshot time
// reads as a clock, not ISO. What the model receives stays English.
// Voice card in the UI language: every tool's plan step, the bare "Layer"
// step, whole error lines and long headlines, the Contacts snapshot clock.
// What the model receives stays English.
import assert from 'node:assert/strict';
import test from 'node:test';
import { setLocale } from '../i18n/index.js';
@@ -26,7 +25,7 @@ function inLocale(locale, run) {
}
}
test('C3: every tool has a plan label, never its raw name', () => {
test('every tool has a plan label, never its raw name', () => {
for (const name of TOOLS) {
const label = planStepLabel(name, {});
assert.notEqual(label, name.replace(/_/g, ' '), name);
@@ -46,13 +45,13 @@ test('C3: every tool has a plan label, never its raw name', () => {
});
for (const locale of ['ru', 'es']) {
test(`C3: ${locale} plan labels are all translated`, () => {
test(`${locale} plan labels are all translated`, () => {
const english = Object.fromEntries(
TOOLS.map((name) => [name, planStepLabel(name, { direction: 'out' })]),
);
inLocale(locale, () => {
for (const name of TOOLS) {
// A layer switch without a layer is checked below (B4).
// A layer switch without a layer is checked below.
const label = planStepLabel(name, { direction: 'out' });
assert.notEqual(label, english[name], `${name}: "${label}"`);
}
@@ -60,7 +59,7 @@ for (const locale of ['ru', 'es']) {
});
}
test('C3/E10: analyst query and an unknown tool in ru and es', () => {
test('analyst query and an unknown tool in ru and es', () => {
inLocale('ru', () => {
assert.equal(planStepLabel('analyst_query', {}), 'Аналитический запрос');
assert.equal(planStepLabel('brand_new_tool', {}), 'Действие');
@@ -71,7 +70,7 @@ test('C3/E10: analyst query and an unknown tool in ru and es', () => {
});
});
test('B4: a layer step with no layer agrees with "Capa"', () => {
test('a layer step with no layer agrees with "Capa"', () => {
const step = (enabled) => planStepLabel('set_layer_visibility', { enabled });
assert.equal(step(true), 'Layer on');
assert.equal(step(false), 'Layer off');
@@ -118,7 +117,7 @@ const NO_PLACE = {
feed: {},
};
test('proposal 1: an error line shows whole, in every language', () => {
test('an error line shows whole, in every language', () => {
for (const [locale, expected] of [
['en', NO_PLACE.error],
[
@@ -137,7 +136,7 @@ test('proposal 1: an error line shows whole, in every language', () => {
}
});
test('proposal 1: a successful line keeps its 80-character cap', () => {
test('a successful line keeps its 80-character cap', () => {
const long = 'x'.repeat(120);
const view = voiceCardView({
...initialVoiceCardState(),
@@ -146,7 +145,7 @@ test('proposal 1: a successful line keeps its 80-character cap', () => {
assert.equal(view.result.lines[0].length, 80);
});
test('E5: a translated analyst headline fits the card title whole', () => {
test('a translated analyst headline fits the card title whole', () => {
const title =
'Al menos 12 aeronaves militares en un radio de 40 km de Los Angeles';
const card = () =>
@@ -159,11 +158,11 @@ test('E5: a translated analyst headline fits the card title whole', () => {
assert.equal(card().length, 64);
});
test('E6: the snapshot time reads as a UTC clock, dated when not today', () => {
test('the snapshot time reads as a UTC clock, dated when not today', () => {
const iso = '2026-10-15T14:32:05.000Z';
const sameDay = Date.parse('2026-10-15T23:00:00Z');
const nextDay = Date.parse('2026-10-16T00:10:00Z');
assert.equal(shownSnapshotTime(iso, sameDay), iso, 'English as before');
assert.equal(shownSnapshotTime(iso, sameDay), iso, 'English keeps ISO');
inLocale('ru', () => {
assert.equal(shownSnapshotTime(iso, sameDay), '14:32:05 UTC');
assert.equal(shownSnapshotTime(iso, nextDay), '15.10.2026, 14:32:05 UTC');
@@ -175,7 +174,7 @@ test('E6: the snapshot time reads as a UTC clock, dated when not today', () => {
});
});
test('E6: the Contacts note shows the clock; the model note stays ISO', () => {
test('the Contacts note shows the clock; the model note stays ISO', () => {
const evaluatedAt = new Date();
evaluatedAt.setUTCHours(14, 32, 5, 0);
const iso = evaluatedAt.toISOString();
@@ -197,9 +196,7 @@ test('E6: the Contacts note shows the clock; the model note stays ISO', () => {
assert.equal(result.coverage.note, note, 'the result is not changed');
});
// C12: preset places show in the interface language on the card; the model's
// error and scope stay English.
test('C12: plan steps and errors name a preset place in the UI language', () => {
test('plan steps and errors name a preset place in the UI language', () => {
const failed = {
ok: false,
action: 'select_nearest_aircraft',
@@ -228,7 +225,7 @@ test('C12: plan steps and errors name a preset place in the UI language', () =>
assert.match(failed.error, /Tokyo view/, 'the result stays English');
});
test('C12: the analyst scope names Los Angeles in the UI language', () => {
test('the analyst scope names Los Angeles in the UI language', () => {
const result = {
ok: true,
count: 12,
+2 -4
View File
@@ -17,10 +17,8 @@ const MAX_STEPS = 6;
const MAX_LINES = 6;
const MAX_NOTES = 6;
const MAX_CAPTION = 220;
// Titles and lines wrap in the card; the caps only stop a runaway value. A
// translated headline runs longer than the English one ("Al menos 12 aeronaves
// militares en un radio de 40 km de Los Ángeles"), so other languages cut it at
// 120; English keeps 64. A failed result's lines (its error) are never cut.
// Titles and lines wrap; the caps only stop runaway values. Translations run
// longer (resultDisplay.js), so they cap at 120. Error lines are never cut.
const MAX_TITLE = 64;
const MAX_TRANSLATED_TITLE = 120;
const MAX_LINE = 80;
+2 -2
View File
@@ -50,7 +50,7 @@ export const VOICE_MODELS = Object.freeze({
standard: Object.freeze({
tier: 'standard',
id: 'gpt-realtime-2',
label: 'STANDARD', // i18n-ignore: tier name for reports and scripts; the app's tier button sets its own text
label: 'STANDARD', // i18n-ignore: model data; the tier button has its own text
/** USD per 1M tokens — gpt-realtime-2. */
rates: Object.freeze({
textInput: 4,
@@ -66,7 +66,7 @@ export const VOICE_MODELS = Object.freeze({
mini: Object.freeze({
tier: 'mini',
id: 'gpt-realtime-2.1-mini',
label: 'MINI', // i18n-ignore: tier name for reports and scripts; the app's tier button sets its own text
label: 'MINI', // i18n-ignore: model data; the tier button has its own text
/** USD per 1M tokens — gpt-realtime-2.1-mini (~3.2× cheaper on audio). */
rates: Object.freeze({
textInput: 0.6,