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

+105
View File
@@ -0,0 +1,105 @@
// K-38: refusals and failures that source modules produce in English reach
// the page translated — through a placeholder at the point of display, or
// through a catalog entry when the text is stored as data.
import assert from 'node:assert/strict';
import test from 'node:test';
import { setLocale, t, translateKnown } from '../i18n/index.js';
import { presentStreetLevelPanel } from './streetLevelPresentation.js';
import { providerSnapshot } from '../testSupport/streetLevelFakes.mjs';
import { createWeatherSource } from '../layers/weather/source.js';
import {
presentRoutingFailure,
presentRoutingRefusal,
} from '../layers/directions/index.js';
import { AIM_AT_GROUND_MESSAGE } from '../layers/streetLevel/policy.js';
const NO_IMAGERY = 'No street-level imagery within 50 m of that point';
function snapshot(error) {
return {
enabled: true,
keyRequired: false,
filter: { pano: 'all', sinceDays: 0 },
providers: [providerSnapshot()],
coverage: { loading: false, count: 0, hint: '', error: null },
legend: [],
sequence: { providerId: null, selectedId: null, images: 0, loading: false },
street: { open: false, loading: false, error, providerId: null },
groundClickReady: true,
};
}
for (const locale of ['es', 'ru']) {
test(`K-38 (${locale}): the nearest-image refusal is built in the language and kept by the panel`, () => {
setLocale(locale);
try {
// What streetLevel/index.js openNearest writes into street.error.
const refusal = t(
'No street-level imagery within {radius} m of that point',
{ radius: 50 },
);
assert.notEqual(refusal, NO_IMAGERY, 'should be translated');
assert.match(refusal, /50/, 'keeps the radius');
assert.equal(presentStreetLevelPanel(snapshot(refusal)).error, refusal);
// The screen-centre refusal is stored as English data and translated
// by the panel.
const aim = presentStreetLevelPanel(snapshot(AIM_AT_GROUND_MESSAGE));
assert.notEqual(aim.error, AIM_AT_GROUND_MESSAGE, 'should be translated');
} finally {
setLocale('en');
}
});
test(`K-38 (${locale}): the route proxy's refusals are shown translated`, () => {
setLocale(locale);
try {
const shown = t('Routing failed: {error}', {
error: presentRoutingRefusal('route leg too long'),
});
assert.doesNotMatch(shown, /route leg too long/);
const limited = presentRoutingFailure(
Object.assign(new Error('rate limited — try again in a moment'), {
status: 429,
refusal: 'rate limited',
}),
);
assert.doesNotMatch(limited, /rate limited|try again in a moment/);
const http = presentRoutingFailure(
Object.assign(new Error('Routing unavailable (HTTP 503)'), {
status: 503,
}),
);
assert.notEqual(http, 'Routing unavailable (HTTP 503)');
assert.match(http, /503/);
// A text the proxy never sends is shown as given.
assert.equal(presentRoutingRefusal('something else'), 'something else');
} finally {
setLocale('en');
}
});
test(`K-38 (${locale}): a weather manifest timeout is shown translated`, async () => {
const source = createWeatherSource({
fetchImpl: (_url, { signal }) =>
new Promise((_resolve, reject) =>
signal.addEventListener('abort', () => reject(signal.reason)),
),
timeoutMs: 10,
});
let message = null;
try {
await source.getSnapshot({ product: 'radar' });
} catch (error) {
message = error.message;
}
assert.equal(message, 'Weather request timed out', 'stored in English');
setLocale(locale);
try {
// The layer shows `t(problem)` (weather/index.js presentWeatherProblem).
assert.notEqual(t(message), message, 'should be translated');
assert.notEqual(translateKnown(message), message);
} finally {
setLocale('en');
}
});
}