mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
i18n: fix the final pre-merge review findings
Fixes the P1 and the confirmed P2 findings of the final review of the
i18n branch.
- CI: the air-quality model no longer imports i18n (portable module),
so check:boundaries passes again.
- Air quality layer: named in the layer panel, HUD and voice; its
Weather card, bands and errors translate. A test keeps the layer-name
table in step with the layer registry.
- Layer statuses that still reached ru/es in English: GNSS, CelesTrak,
Transit loading and feed errors, launches, perimeters, ALPR, ECCC,
wind "field unavailable", Local ADS-B streaming lines.
- Voice and HUD: preset ids show city names ("Fly to San Francisco"),
the HUD city translates, cockpit refusals cover frame, aircraft and
satellite; empty Transit notes translate on the analyst card.
- tn() keeps a caller's formatted count ("12,345 map objects").
- Cyber HUD: the language button no longer covers the bottom-right
card or the cockpit position readout.
- CCTV buttons wrap only in non-English locales; English layout matches
main again.
- Switching language asks first when Provider Settings holds unsaved
keys.
Catalogs: es/ru 2611 messages each, complete.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
fada8b161b
commit
3adce33cc6
43 files changed
+1350
-97
No files matched your search
@@ -0,0 +1,72 @@
|
||||
// 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
|
||||
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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);
|
||||
});
|
||||
@@ -135,6 +135,8 @@ function shownCount(count, noun) {
|
||||
return tn(count, '{n} datacenters', '{n} datacenters', values);
|
||||
case 'dams':
|
||||
return tn(count, '{n} dams', '{n} dams', values);
|
||||
case 'air quality':
|
||||
return tn(count, '{n} air quality', '{n} air quality', values);
|
||||
case 'results':
|
||||
return tn(count, '{n} results', '{n} results', values);
|
||||
default:
|
||||
@@ -328,6 +330,27 @@ const NOTE_PATTERNS = [
|
||||
'Contacts panel snapshot — the same retained cohort supplies the card, speech, and follow-up memory',
|
||||
)) + period,
|
||||
],
|
||||
// Transit's empty note when no feed covers the view (transit/queries.js);
|
||||
// the model gets the English.
|
||||
[
|
||||
/^Fly below ([\d,]+) km to a covered region$/,
|
||||
([, km]) =>
|
||||
t('Fly below {km} km to a covered region', {
|
||||
km: formatNumber(Number(km.replace(/,/g, ''))),
|
||||
}),
|
||||
],
|
||||
[
|
||||
/^No feed here yet · ([\d,]+) regions available$/,
|
||||
([, regions]) => {
|
||||
const count = Number(regions.replace(/,/g, ''));
|
||||
return tn(
|
||||
count,
|
||||
'No feed here yet · {n} region available',
|
||||
'No feed here yet · {n} regions available',
|
||||
{ n: formatNumber(count) },
|
||||
);
|
||||
},
|
||||
],
|
||||
[
|
||||
/^([a-z0-9-]+) (off|loading|error)$/,
|
||||
([, key, status]) =>
|
||||
|
||||
+38
-7
@@ -344,13 +344,38 @@ function shownPair(items) {
|
||||
: items[0] || '';
|
||||
}
|
||||
|
||||
/** Ids such as `austin` or `local-firms` read as names on screen. */
|
||||
// A place as the card shows it: a preset city in the interface language,
|
||||
// whether the call named it ("Tokyo") or gave its id ("tokyo").
|
||||
function shownPlace(value, max) {
|
||||
return localizePlaceName(displayName(value, max));
|
||||
/**
|
||||
* 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. */
|
||||
function presetPlaceName(value) {
|
||||
const id = typeof value === 'string' ? value.trim() : '';
|
||||
return Object.hasOwn(PRESET_PLACE_NAMES, id) ? PRESET_PLACE_NAMES[id] : value;
|
||||
}
|
||||
|
||||
// A place as the card shows it: a preset city in the interface language,
|
||||
// whether the call named it ("Tokyo") or gave its id ("tokyo", "nyc").
|
||||
function shownPlace(value, max) {
|
||||
return localizePlaceName(displayName(presetPlaceName(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)
|
||||
@@ -915,9 +940,15 @@ export function narrationLabel(name, args = {}) {
|
||||
case 'annotate_map':
|
||||
return annotationTargets(args)[0] || '';
|
||||
case 'fly_to_location':
|
||||
return displayName(args.query || args.locationId || '', 32);
|
||||
return displayName(
|
||||
presetPlaceName(args.query || args.locationId || ''),
|
||||
32,
|
||||
);
|
||||
case 'select_nearest_aircraft':
|
||||
return displayName(args.locationQuery || args.locationId || '', 32);
|
||||
return displayName(
|
||||
presetPlaceName(args.locationQuery || args.locationId || ''),
|
||||
32,
|
||||
);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user