i18n: fix review findings and polish ru/es strings

Fixes the 51 P0–P2 findings from the full review of the i18n branch and
the issues found while checking every new string on screen in ru and es.

- Translate errors and statuses that still reached ru/es in English:
  feed and tile errors, provider statuses, the ALPR chip, the traffic
  mode chip, voice plan step labels, fire feed values, receiver bands,
  scene save reasons and the Cesium attribution link.
- Show preset city names in the UI language (src/ui/placeNames.js);
  data and everything sent to the model stay English.
- Share layer names between the layer panel, HUD and voice card
  (src/ui/layerNames.js).
- Keep long ru/es strings whole: voice card error lines, analyst
  titles, the voice tier status, the Nepal evidence button, the ALPR
  retry chip. English layout is unchanged.
- Ask before a language switch drops unsaved work.
- Catalog fixes: glossary terms (пуск, пресет), gender agreement and
  consistent "peticiones" in es, fewer calques in both languages.
- Fix English text that was itself wrong (e.g. "Exit cockpit to fly to
  a weather", raw tool ids in voice plan steps).

Catalogs: es/ru 2535 messages each, complete.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-15 23:49:52 +04:00
1 parent 8029747b7e
commit 00704ddc89
212 files changed
+9871 -909

No files matched your search

+134
View File
@@ -0,0 +1,134 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createCyclonesLayer } from './index.js';
import { setLocale, t } from '../../i18n/index.js';
// The exact string the server sends (server/providers/cyclones.js:8).
const SERVER_COVERAGE =
'Atlantic and eastern/central North Pacific; not worldwide cyclone coverage.';
const time = '2026-09-16T03:00:00.000Z';
const storm = {
id: 'ep152026',
name: 'Fifteen-E',
classification: 'PTC',
basin: 'EP',
position: { longitude: -125.8, latitude: 15.5 },
positionAt: time,
issuedAt: time,
advisoryNumber: '10',
windKt: 25,
pressureHpa: 1006,
geometryStatus: 'pending',
geometryAdvisoryNumber: null,
forecastPoints: [],
track: null,
cone: null,
advisoryUrl: 'https://www.nhc.noaa.gov/text/MIATCMEP5.shtml',
};
const snapshot = (storms, extra = {}) => ({
schemaVersion: 1,
storms,
unavailable: false,
stale: false,
fetchedAt: Date.parse(time),
coverage: SERVER_COVERAGE,
reason: null,
...extra,
});
function harness(feed) {
const rendering = {
async setSnapshot() {
return true;
},
setSelection() {},
getFocusSphere: () => ({ radius: 500000 }),
clear() {},
destroy() {},
getDiagnostics: () => ({ entities: 0, timerActive: false }),
};
const layer = createCyclonesLayer({
feed,
createRendering: () => rendering,
matchMedia: () => ({ matches: false }),
openLink: () => {},
});
layer.init({ camera: { flyToBoundingSphere() {} } });
layer.attachShellServices({ runNavigation: (fn) => fn() });
return layer;
}
const translated = () => t(SERVER_COVERAGE);
test('K-35a: es, server snapshot without storms -> coverage line in Spanish', async () => {
setLocale('es');
try {
assert.notEqual(translated(), SERVER_COVERAGE, 'es.json has a translation');
const layer = harness({ getSnapshot: async () => snapshot([]) });
layer.enable();
await layer.update();
const info = layer.getRowControls().info;
assert.ok(
info.endsWith(translated()),
`expected Spanish coverage, got:\n${info}`,
);
layer.destroy();
} finally {
setLocale('en');
}
});
test('K-35b: es, server snapshot with a storm -> coverage line in Spanish', async () => {
setLocale('es');
try {
const layer = harness({ getSnapshot: async () => snapshot([storm]) });
layer.enable();
await layer.update();
const info = layer.getRowControls().info;
assert.ok(
info.endsWith(translated()),
`expected Spanish coverage, got:\n${info}`,
);
layer.destroy();
} finally {
setLocale('en');
}
});
test('K-35c: es, unavailable without reason -> fallback status not the English literal', async () => {
setLocale('es');
try {
const layer = harness({
getSnapshot: async () =>
snapshot([], { unavailable: true, reason: null }),
});
layer.enable();
await layer.update();
const status = layer.getRowControls().summary.status;
assert.notEqual(
status,
'Cyclone advisories unavailable',
`fallback shown in English: ${status}`,
);
layer.destroy();
} finally {
setLocale('en');
}
});
test('K-35: en coverage and fallback status read as before', async () => {
const layer = harness({ getSnapshot: async () => snapshot([]) });
layer.enable();
await layer.update();
assert.ok(layer.getRowControls().info.endsWith(SERVER_COVERAGE));
layer.destroy();
const down = harness({
getSnapshot: async () => snapshot([], { unavailable: true, reason: null }),
});
down.enable();
await down.update();
assert.equal(
down.getRowControls().summary.status,
'Cyclone advisories unavailable',
);
down.destroy();
});
+21 -6
View File
@@ -14,7 +14,13 @@ import {
CYCLONE_OVERLAY_SOURCE_ID,
cycloneStormIdFromEntryId,
} from './labels.js';
import { formatUtcDayMonth, t, tn, msg } from '../../i18n/index.js';
import {
formatUtcDayMonth,
t,
tn,
msg,
translateKnown,
} from '../../i18n/index.js';
import { formatQuantity } from '../../ui/unitLabels.js';
// "10-12 15:00 UTC" never breaks across lines: no-break spaces, and a word
@@ -26,6 +32,15 @@ const utc = (value) =>
const COVERAGE = msg(
'Atlantic and eastern/central North Pacific; not worldwide cyclone coverage.',
);
// The server sends the same English coverage (server/providers/cyclones.js);
// data stays English and is translated here, where it is shown.
const coverageText = (coverage) =>
!coverage || coverage === COVERAGE ? t(COVERAGE) : translateKnown(coverage);
const ADVISORIES_UNAVAILABLE = msg('Cyclone advisories unavailable');
// Reason the server gives with an unavailable snapshot, translated when shown.
const SERVER_REASONS = new Set([msg('Cyclone data unavailable')]);
const errorText = (error) =>
error && (SERVER_REASONS.has(error) ? t(error) : translateKnown(error));
const CLASSIFICATION_NAMES = Object.freeze({
PTC: msg('Potential tropical cyclone'),
HU: msg('Hurricane'),
@@ -238,7 +253,7 @@ export function createCyclonesLayer({
snapshot = next;
selectedId = null;
++navigationGeneration;
error = next.reason || 'Cyclone advisories unavailable';
error = next.reason || ADVISORIES_UNAVAILABLE;
return true;
}
const applied = await rendering.setSnapshot(next, {
@@ -263,7 +278,7 @@ export function createCyclonesLayer({
return true;
} catch (cause) {
if (controller.signal.aborted || request !== controller) return false;
error = cause?.message || 'Cyclone advisories unavailable';
error = cause?.message || ADVISORIES_UNAVAILABLE;
// Failed acquisition has no bounded last-good age guarantee at this layer.
rendering?.clear();
snapshot = null;
@@ -332,7 +347,7 @@ export function createCyclonesLayer({
})
: t('Track/cone unavailable'));
const status =
error ||
errorText(error) ||
(snapshot?.stale
? t('Cached advisory · stale source')
: loading
@@ -435,8 +450,8 @@ export function createCyclonesLayer({
]
: [],
info: storm
? `${detail}\n${position}\n${intensity}\n${geometry}${status && status !== geometry ? '\n' + status : ''}\n${snapshot.coverage}`
: `${detail}${status ? '\n' + status : ''}\n${snapshot?.coverage || t(COVERAGE)}`,
? `${detail}\n${position}\n${intensity}\n${geometry}${status && status !== geometry ? '\n' + status : ''}\n${coverageText(snapshot.coverage)}`
: `${detail}${status ? '\n' + status : ''}\n${coverageText(snapshot?.coverage)}`,
infoTitle: t(
'Select a storm on the map, or choose a storm in the list to select it and move the camera. Click empty map space to clear the selection. NOAA NHC/CPHC advisory context. The cone describes forecast center-track uncertainty, not storm size or the full hazard area. Forecast point labels are source lead hours, not times computed from advisory issuance. Geometry follows the surface; height is not weather altitude. Consult the official advisory.',
),