mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
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:
1 parent
0ed05568f7
commit
20f4cf3763
178 files changed
+918
-948
No files matched your search
@@ -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']);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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' }],
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, [
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user