mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
i18n: fix the repeat-review findings
- Status pieces translate in one place (src/ui/statusText.js): a Local
ADS-B error with a single piece now translates, and the analyst card
splits empty-layer notes into pieces (Transit, Local ADS-B).
- Translate the ALPR retry chip text, the ECCC error on the Air quality
card, "Upstream response too large" and the "entry {n}" feed label.
- The unmarked-text check also sees error/loadingLabel/statusMessage/
status assignments, `status:` properties and showToast?.( /
this._showToast( calls; the scene import preview marks its pack
statuses, data and model-facing text carry i18n-ignore with a reason.
- Tests check the real code: the HUD locality formatter moves to
src/hudLocalityFormat.js, the unsaved-keys test pins the teardown.
- One copy each: city names come from src/cityPois.js, the GNSS HTTP
template and wind "field unavailable" texts; dead fallbacks removed.
- Leftovers of the reverted vessel-label change and the reformatting of
detectionHost.test.mjs / keySetup.test.mjs are back to main.
Catalogs: es/ru 2617 messages each, complete.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a2463ca2d2
commit
3157888780
60 files changed
+1577
-1606
No files matched your search
@@ -0,0 +1,80 @@
|
||||
// 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.
|
||||
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 notesFor(layerKey, caveat) {
|
||||
const shaped = attachVoiceResult(
|
||||
'analyst_query',
|
||||
{
|
||||
ok: true,
|
||||
count: 0,
|
||||
complete: true,
|
||||
scopeLabel: 'in view',
|
||||
feedState: 'nominal',
|
||||
unanswered: [],
|
||||
coverage: { layersQueried: [{ layerKey, status: 'empty' }] },
|
||||
display: { scope: 'in view', caveat },
|
||||
items: [],
|
||||
},
|
||||
{},
|
||||
);
|
||||
return voiceCardView(
|
||||
[
|
||||
{
|
||||
type: 'action-call',
|
||||
callId: 'c1',
|
||||
name: 'analyst_query',
|
||||
arguments: {},
|
||||
},
|
||||
{
|
||||
type: 'action-result',
|
||||
callId: 'c1',
|
||||
name: 'analyst_query',
|
||||
result: shaped,
|
||||
},
|
||||
].reduce(reduceVoiceCard, initialVoiceCardState()),
|
||||
).result.notes;
|
||||
}
|
||||
|
||||
// Caveat = `${layerKey}: ${getEmptyNote(layerKey)}` (analystEngine.js:1026,
|
||||
// gevActions.js getEmptyNote → stats.coverage || stats.statusMessage).
|
||||
const cases = [
|
||||
['local-adsb', 'local-adsb: feeds 1090, 978 stale · 0 heard'],
|
||||
['local-adsb', 'local-adsb: feed 1090 stale · 0 heard'],
|
||||
['transit', 'transit: TTC 0 (95s old)'],
|
||||
['transit', 'transit: TTC 0 · GO Transit 0 (95s old)'],
|
||||
];
|
||||
|
||||
for (const locale of ['es', 'ru']) {
|
||||
for (const [layerKey, caveat] of cases) {
|
||||
test(`${locale}: analyst note "${caveat}" is translated`, () => {
|
||||
setLocale(locale);
|
||||
try {
|
||||
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const [layerKey, caveat] of cases) {
|
||||
test(`en: analyst note "${caveat}" keeps its status line`, () => {
|
||||
setLocale('en');
|
||||
const line = caveat.slice(caveat.indexOf(': ') + 2);
|
||||
assert.ok(
|
||||
notesFor(layerKey, caveat).some((note) => note.endsWith(`: ${line}`)),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// N-07: the analyst card shows a layer's "empty" note from getStats().coverage
|
||||
// as is; Transit's notes are English template strings with no catalog entry,
|
||||
// so the es/ru card note stays English. Place: src/voice/analystTransitNote.test.mjs
|
||||
// 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';
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
// N-05: plan steps for preset ids from the tool schema (sf, nyc, dc) read
|
||||
// "Sf" / "Nyc" / "Dc" in every language; 'tokyo' translates.
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { setLocale } from '../i18n/index.js';
|
||||
import { CITY_POIS } from '../locations.js';
|
||||
import { GEV_ACTION_SCHEMAS } from './actionSchemas.js';
|
||||
import { narrationLabel, planStepLabel } from './speech.js';
|
||||
|
||||
test.afterEach(() => setLocale('en'));
|
||||
|
||||
test('es: preset location ids show the city name', () => {
|
||||
setLocale('es');
|
||||
assert.equal(
|
||||
planStepLabel('fly_to_location', { locationId: 'tokyo' }),
|
||||
'Volar a Tokio',
|
||||
);
|
||||
for (const id of ['sf', 'nyc', 'dc']) {
|
||||
const label = planStepLabel('fly_to_location', { locationId: id });
|
||||
assert.doesNotMatch(label, /\b(Sf|Nyc|Dc)$/, label);
|
||||
}
|
||||
assert.equal(
|
||||
planStepLabel('fly_to_location', { locationId: 'nyc' }),
|
||||
'Volar a Nueva York',
|
||||
);
|
||||
});
|
||||
|
||||
test('ru: a preset id in the nearest-aircraft step names the city', () => {
|
||||
setLocale('ru');
|
||||
assert.match(
|
||||
planStepLabel('select_nearest_aircraft', { locationId: 'dc' }),
|
||||
/Вашингтон/,
|
||||
);
|
||||
});
|
||||
|
||||
test('en: every preset id reads as its CITY_POIS name', () => {
|
||||
setLocale('en');
|
||||
for (const [id, city] of Object.entries(CITY_POIS)) {
|
||||
assert.equal(
|
||||
planStepLabel('fly_to_location', { locationId: id }),
|
||||
`Fly to ${city.name}`,
|
||||
id,
|
||||
);
|
||||
assert.equal(
|
||||
narrationLabel('fly_to_location', { locationId: id }),
|
||||
city.name,
|
||||
);
|
||||
}
|
||||
// Other places are shown as given.
|
||||
assert.equal(
|
||||
planStepLabel('fly_to_location', { query: 'Ulaanbaatar' }),
|
||||
'Fly to Ulaanbaatar',
|
||||
);
|
||||
});
|
||||
|
||||
test('every locationId the voice tools accept is a preset city', () => {
|
||||
const ids = new Set();
|
||||
const visit = (node) => {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
if (Array.isArray(node.locationId?.enum))
|
||||
node.locationId.enum.forEach((id) => ids.add(id));
|
||||
Object.values(node).forEach(visit);
|
||||
};
|
||||
visit(GEV_ACTION_SCHEMAS);
|
||||
assert.ok(ids.size > 0, 'found the locationId enums');
|
||||
for (const id of ids) assert.ok(Object.hasOwn(CITY_POIS, id), id);
|
||||
});
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from '../i18n/index.js';
|
||||
import { layerNameForId, sourceLabel } from '../ui/layerNames.js';
|
||||
import { localizePlaceName } from '../ui/placeNames.js';
|
||||
import { translateStatusLine } from '../ui/statusText.js';
|
||||
import { formatQuantity } from '../ui/unitLabels.js';
|
||||
|
||||
const LAYER_NOUNS = Object.freeze({
|
||||
@@ -372,7 +373,10 @@ function shownNote(text) {
|
||||
const shown = match ? show(match) : null;
|
||||
if (shown) return shown;
|
||||
}
|
||||
return translateKnown(text);
|
||||
// A layer's empty note can be its status line, English pieces joined by
|
||||
// " · " ("feeds 1090, 978 stale · 0 heard", "TTC 0 (95s old)").
|
||||
const known = translateKnown(text);
|
||||
return known !== text ? known : translateStatusLine(text);
|
||||
}
|
||||
|
||||
/** Feed states as the card's chips show them; the values stay English. */
|
||||
|
||||
+3
-21
@@ -39,6 +39,7 @@ import {
|
||||
sourceLabel,
|
||||
} from '../ui/layerNames.js';
|
||||
import { localizePlaceName } from '../ui/placeNames.js';
|
||||
import { CITY_POIS } from '../cityPois.js';
|
||||
import { formatQuantity } from '../ui/unitLabels.js';
|
||||
|
||||
const LAYER_NOUNS = Object.freeze({
|
||||
@@ -344,28 +345,10 @@ function shownPair(items) {
|
||||
: items[0] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* The preset city ids the voice tools take (the `locationId` enum in
|
||||
* actionSchemas.js) and the CITY_POIS names they stand for, so `sf` reads
|
||||
* "San Francisco", not "Sf". A copy, not an import: src/locations.js pulls in
|
||||
* Cesium. planStepPresetIds.i18n.test.mjs keeps it in step with CITY_POIS.
|
||||
*/
|
||||
const PRESET_PLACE_NAMES = Object.freeze({
|
||||
austin: 'Austin',
|
||||
sf: 'San Francisco',
|
||||
nyc: 'New York',
|
||||
tokyo: 'Tokyo',
|
||||
london: 'London',
|
||||
paris: 'Paris',
|
||||
dubai: 'Dubai',
|
||||
dc: 'Washington DC',
|
||||
tallinn: 'Tallinn',
|
||||
});
|
||||
|
||||
/** A preset city id as its English name; any other value unchanged. */
|
||||
/** A preset city id (`sf`, `nyc`) as its English name; any other value unchanged. */
|
||||
function presetPlaceName(value) {
|
||||
const id = typeof value === 'string' ? value.trim() : '';
|
||||
return Object.hasOwn(PRESET_PLACE_NAMES, id) ? PRESET_PLACE_NAMES[id] : value;
|
||||
return Object.hasOwn(CITY_POIS, id) ? CITY_POIS[id].name : value;
|
||||
}
|
||||
|
||||
// A place as the card shows it: a preset city in the interface language,
|
||||
@@ -375,7 +358,6 @@ function shownPlace(value, max) {
|
||||
}
|
||||
|
||||
/** Ids such as `austin` or `local-firms` read as names on screen. */
|
||||
|
||||
function displayName(value, max) {
|
||||
const label = spokenLabel(value, max);
|
||||
return /^[a-z0-9-]+$/.test(label)
|
||||
|
||||
@@ -7,7 +7,8 @@ import test from 'node:test';
|
||||
import { setLocale } from '../i18n/index.js';
|
||||
import { GEV_ACTION_SCHEMAS } from './actionSchemas.js';
|
||||
import { presentResult, shownSnapshotTime } from './resultDisplay.js';
|
||||
import { attachVoiceResult, planStepLabel } from './speech.js';
|
||||
import { CITY_POIS } from '../cityPois.js';
|
||||
import { attachVoiceResult, narrationLabel, planStepLabel } from './speech.js';
|
||||
import {
|
||||
initialVoiceCardState,
|
||||
reduceVoiceCard,
|
||||
@@ -244,3 +245,41 @@ test('C12: the analyst scope names Los Angeles in the UI language', () => {
|
||||
assert.match(title, /Los Ángeles$/, 'the headline is not cut at 64');
|
||||
assert.equal(result.scopeLabel, 'within 40 km of Los Angeles');
|
||||
});
|
||||
|
||||
// A preset city id from the tool schema (sf, nyc, dc) reads as its city name.
|
||||
test('a preset city id names the city in plan steps and narration', () => {
|
||||
const step = (id, action = 'fly_to_location') =>
|
||||
planStepLabel(action, { locationId: id });
|
||||
assert.equal(step('sf'), 'Fly to San Francisco');
|
||||
assert.equal(
|
||||
narrationLabel('fly_to_location', { locationId: 'dc' }),
|
||||
'Washington DC',
|
||||
);
|
||||
assert.equal(
|
||||
planStepLabel('fly_to_location', { query: 'Ulaanbaatar' }),
|
||||
'Fly to Ulaanbaatar',
|
||||
'other places are shown as given',
|
||||
);
|
||||
inLocale('es', () => {
|
||||
assert.equal(step('tokyo'), 'Volar a Tokio');
|
||||
assert.equal(step('nyc'), 'Volar a Nueva York');
|
||||
for (const id of ['sf', 'nyc', 'dc'])
|
||||
assert.doesNotMatch(step(id), /\b(Sf|Nyc|Dc)$/, id);
|
||||
});
|
||||
inLocale('ru', () => {
|
||||
assert.match(step('dc', 'select_nearest_aircraft'), /Вашингтон/);
|
||||
});
|
||||
});
|
||||
|
||||
test('every locationId the voice tools accept is a preset city', () => {
|
||||
const ids = new Set();
|
||||
const visit = (node) => {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
if (Array.isArray(node.locationId?.enum))
|
||||
node.locationId.enum.forEach((id) => ids.add(id));
|
||||
Object.values(node).forEach(visit);
|
||||
};
|
||||
visit(GEV_ACTION_SCHEMAS);
|
||||
assert.ok(ids.size > 0, 'found the locationId enums');
|
||||
for (const id of ids) assert.ok(Object.hasOwn(CITY_POIS, id), id);
|
||||
});
|
||||
Reference in new issue
Block a user