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

+18 -4
View File
@@ -18,7 +18,7 @@ import { cameraHeightAboveGround, viewCentre, whenIdle } from './view.js';
import { createGroundClick, groundClickHeightOk } from './groundClick.js';
import { createGroundCaster, nextSurfaceMode } from './groundCast.js';
import { createMeshSampler } from './meshSampler.js';
import { t, translateKnown } from '../../i18n/index.js';
import { msg, t, translateKnown } from '../../i18n/index.js';
/** Above this camera height the ground under the camera is not worth fetching. */
const SURFACE_WARM_BELOW_M = 6000;
@@ -583,10 +583,13 @@ export function createStreetLevelLayer({
// The lifecycle polls this every second: summarise, don't snapshot.
const snapshots = providerSnapshots();
const coverage = summarizeCoverage(snapshots);
// English data: the layer panel translates these through translateKnown.
let loadingLabel = '';
const keyLabel = coverage.keyRejected ? 'KEY REJECTED' : 'KEY REQUIRED';
const keyLabel = coverage.keyRejected
? msg('KEY REJECTED')
: msg('KEY REQUIRED');
if (coverage.keyRequired) loadingLabel = keyLabel;
else if (coverage.loading) loadingLabel = 'loading coverage...';
else if (coverage.loading) loadingLabel = msg('loading coverage...');
else if (coverage.hint && state.enabled) loadingLabel = coverage.hint;
return {
source: layer.source,
@@ -737,10 +740,21 @@ export function createStreetLevelLayer({
const stillOn = entries.some(
(entry) => entry.on && providerAvailable(entry),
);
// A provider's own refusal is English data that the panel translates;
// the core's refusal is built here, for the panel only, in the language.
if (state.enabled && stillOn)
state.street.error =
lastError?.message ||
`No street-level imagery within ${NEAREST_RADIUS_M} m of ${point ? 'that point' : 'the view centre'}`;
(point
? t('No street-level imagery within {radius} m of that point', {
radius: NEAREST_RADIUS_M,
})
: t(
'No street-level imagery within {radius} m of the view centre',
{
radius: NEAREST_RADIUS_M,
},
));
state.street.loading = false;
notify();
return false;
+19
View File
@@ -6,6 +6,7 @@ import { MAPILLARY_CREDIT_HTML } from './providers/mapillary/policy.js';
import { createMapillaryProvider } from './providers/mapillary/index.js';
import { createKartaViewProvider } from './providers/kartaview/index.js';
import { fakeStreetLevelProvider as fakeProvider } from '../../testSupport/streetLevelFakes.mjs';
import { setLocale } from '../../i18n/index.js';
/**
* A stand-in Cesium viewer the layer can be enabled on: a canvas for the
@@ -690,6 +691,24 @@ test('a lookup for the click-to-open providers asks only them and reports "that
);
});
test('the nearest-image refusal is written in the language of the page', async (t) => {
const { layer } = await withStreetView(t, { nearest: () => null });
layer.setProviderEnabled('google', true);
await settle();
setLocale('es');
try {
await layer.openNearest(
{ lat: 38.58, lon: -121.49 },
{ providerIds: ['google'], frame: false },
);
const error = layer.getUIState().street.error;
assert.notEqual(error, 'No street-level imagery within 50 m of that point');
assert.match(error, /50/, 'keeps the radius');
} finally {
setLocale('en');
}
});
test('an image opened from a ground click leaves the camera where it is', async (t) => {
const { layer, viewer } = await withStreetView(t);
layer.setProviderEnabled('google', true);
+8 -2
View File
@@ -1,3 +1,7 @@
// Reached by the providers' source modules (no browser globals, see
// check:boundaries), so no i18n import: the text stays English data here.
const msg = (text) => text;
/** Identity and shared tuning for the provider-neutral Street Level layer. */
export const STREET_LEVEL_LAYER_ID = 'street-level';
@@ -41,5 +45,7 @@ export const FOLLOW_EYE_HEIGHT_M = 2.4;
/** Above this, a ground click is a misclick, not a street. */
export const GROUND_CLICK_MAX_HEIGHT_M = 3_000;
export const AIM_AT_GROUND_MESSAGE =
'Aim the middle of the view at a street to look for imagery there';
/** English data: the Street Level panel translates it (translateKnown). */
export const AIM_AT_GROUND_MESSAGE = msg(
'Aim the middle of the view at a street to look for imagery there',
);
@@ -3,6 +3,8 @@
* `gm_authFailure` (key refused) reaches every `onAuthFailure` listener.
*/
import { msg } from '../../../../i18n/index.js';
const SCRIPT_ORIGIN = 'https://maps.googleapis.com';
const READY_CALLBACK = '__gevGoogleMapsReady';
/** The page's one script load, shared by every loader on it. */
@@ -44,7 +46,7 @@ export function createMapsLoader({
function load() {
const key = getApiKey?.();
if (!key)
return Promise.reject(new Error('GOOGLE_MAPS_API_KEY is not set'));
return Promise.reject(new Error(msg('GOOGLE_MAPS_API_KEY is not set')));
installAuthHandler();
if (global[PAGE_LOAD]) return global[PAGE_LOAD];
if (global.google?.maps?.importLibrary)
@@ -69,7 +71,7 @@ export function createMapsLoader({
if (global[PAGE_LOAD] === loading) delete global[PAGE_LOAD];
script.remove();
delete global[READY_CALLBACK];
reject(new Error('The Google Maps JavaScript API could not load'));
reject(new Error(msg('The Google Maps JavaScript API could not load')));
};
document.head.append(script);
});
@@ -9,6 +9,7 @@ import {
imageDateMs,
streetViewUrl,
} from './policy.js';
import { msg } from '../../../../i18n/index.js';
const PANORAMA_OPTIONS = Object.freeze({
visible: false,
@@ -204,7 +205,7 @@ export function createGoogleViewer({ loader, getService, render } = {}) {
else if (status)
finish(
reject,
new Error('Street View has no imagery for this place'),
new Error(msg('Street View has no imagery for this place')),
);
};
const onPosition = () => {
@@ -9,6 +9,7 @@ import {
} from './decode.js';
import { createKartaViewSource } from './source.js';
import { createKartaViewViewer } from './viewer.js';
import { msg } from '../../../../i18n/index.js';
import {
ARCHIVED_MESSAGE,
COLORS,
@@ -195,7 +196,8 @@ export function createKartaViewProvider({
const cached = photos.get(photoId);
if (cached) return cached;
const photo = remember(await source.getPhoto(photoId, { signal }));
if (!photo) throw new Error('KartaView has no position for this photo');
if (!photo)
throw new Error(msg('KartaView has no position for this photo'));
return photo;
}
@@ -491,7 +493,8 @@ export function createKartaViewProvider({
.open(photoId)
.catch((error) =>
context.actions.reportError(
error?.message || 'The KartaView photo could not be loaded',
error?.message ||
msg('The KartaView photo could not be loaded'),
),
),
render: context.services?.render,
@@ -50,8 +50,9 @@ export const COVERAGE_TUNING = Object.freeze({
});
export const TOO_HIGH_HINT = msg('zoom in to see KartaView coverage');
export const NO_COVERAGE_HINT = msg('no KartaView photos in view');
export const RATE_LIMITED_MESSAGE =
'KartaView is rate-limiting requests — they resume on their own';
export const RATE_LIMITED_MESSAGE = msg(
'KartaView is rate-limiting requests — they resume on their own',
);
/**
* The photo API answers 429 to more than about three requests at once, so
@@ -82,8 +83,9 @@ export const IMAGE_TIMEOUT_MS = 15_000;
export const IMAGE_PROBE_TIMEOUT_MS = 8_000;
/** Nearest photos probed (at once) before giving up on archived ones; ‹ › skip this many. */
export const NEAREST_PROBES = 6;
export const ARCHIVED_MESSAGE =
'KartaView’s photos here are archived and cannot be shown';
export const ARCHIVED_MESSAGE = msg(
'KartaView’s photos here are archived and cannot be shown',
);
/** Photos asked for around a point; the API returns them nearest first. */
export const NEAREST_LIMIT = 10;
@@ -136,10 +138,12 @@ export const MASK_MAX_BYTES = 2 * 1024 * 1024;
export const MASK_KEEP_SHARE = 0.5;
/** Points along a segment are tested this many mask pixels apart. */
export const MASK_SAMPLE_PX = 2;
export const MASK_SLOW_MESSAGE =
'KartaView did not filter this area in time — move the map to ask again';
export const FILTERED_OUT_MESSAGE =
'None of KartaView’s photos here pass the imagery filter';
export const MASK_SLOW_MESSAGE = msg(
'KartaView did not filter this area in time — move the map to ask again',
);
export const FILTERED_OUT_MESSAGE = msg(
'None of KartaView’s photos here pass the imagery filter',
);
/** Under the SINCE slider while KartaView is lit. */
export const SINCE_NOTE = 'KartaView: by upload date';
@@ -21,6 +21,7 @@ import {
TILE_TIMEOUT_MS,
photoFilterParams,
} from './policy.js';
import { msg } from '../../../../i18n/index.js';
/**
* An HTTP or API failure; `retryAfterSec` is set while KartaView rate-limits,
@@ -40,9 +41,10 @@ export class KartaViewSourceError extends Error {
}
}
const NO_ANSWER = 'KartaView did not answer in time — try again';
const UNREACHABLE = 'KartaView could not be reached — check the network';
const UNREADABLE = 'KartaView sent an answer that could not be read';
// Failure texts are English data: the Street Level panel translates them.
const NO_ANSWER = msg('KartaView did not answer in time — try again');
const UNREACHABLE = msg('KartaView could not be reached — check the network');
const UNREADABLE = msg('KartaView sent an answer that could not be read');
/** Statuses that mean a photo file is gone, not that storage hiccupped. */
const GONE_STATUSES = Object.freeze([403, 404, 409, 410]);
@@ -169,10 +171,15 @@ export function createKartaViewSource({
try {
response = await fetchImpl(url, { signal: controller.signal });
if (response.status === 429)
throw new KartaViewSourceError('KartaView is rate-limiting requests', {
status: 429,
retryAfterSec: retryAfterSeconds(response) ?? 2,
});
throw new KartaViewSourceError(
msg('KartaView is rate-limiting requests'),
{
status: 429,
retryAfterSec: retryAfterSeconds(response) ?? 2,
},
);
// English data with a number: the Street Level panel and the layer
// panel show it through the 'KartaView answered HTTP {status}' entry.
if (!response.ok)
throw new KartaViewSourceError(
`KartaView answered HTTP ${response.status}`,
@@ -180,7 +187,7 @@ export function createKartaViewSource({
);
const length = Number(response.headers?.get?.('content-length'));
const tooMuch = () =>
new KartaViewSourceError('KartaView sent too much data', {
new KartaViewSourceError(msg('KartaView sent too much data'), {
status: response.status,
});
if (length > maxBytes) throw tooMuch();
@@ -275,6 +282,7 @@ export function createKartaViewSource({
);
const code = Number(payload?.status?.apiCode);
// 600 ok, 601 empty, 602 partial: anything else is a refusal.
// Shown through 'KartaView refused the request (code {code})'.
if (Number.isFinite(code) && (code < 600 || code > 603))
throw new KartaViewSourceError(
`KartaView refused the request (code ${code})`,
@@ -288,7 +296,9 @@ export function createKartaViewSource({
if (error?.status !== 429 || attempt >= PHOTO_API_RETRIES) {
if (error?.status === 429)
throw new KartaViewSourceError(
'KartaView is rate-limiting requests — try again in a moment',
msg(
'KartaView is rate-limiting requests — try again in a moment',
),
{ status: 429, retryAfterSec: error.retryAfterSec },
);
throw error;
@@ -419,7 +429,7 @@ export function createKartaViewSource({
{ signal },
);
if (!result.data || typeof result.data !== 'object')
throw new KartaViewSourceError('KartaView has no such photo');
throw new KartaViewSourceError(msg('KartaView has no such photo'));
return result.data;
},
@@ -3,7 +3,7 @@ import {
PANO_FOV_DEFAULT_DEG,
wrapDegrees,
} from './panorama.js';
import { t } from '../../../../i18n/index.js';
import { msg, t } from '../../../../i18n/index.js';
import {
FLAT_FULL_SIZE,
FLAT_VIEW_SIZE,
@@ -17,7 +17,8 @@ import {
resizedImageUrl,
} from './policy.js';
const LOAD_FAILED = 'The KartaView photo could not be loaded';
/** English data: the Street Level panel translates it (translateKnown). */
const LOAD_FAILED = msg('The KartaView photo could not be loaded');
/**
* Load an image within the timeout. A CORS-clean one (for WebGL) is decoded
@@ -1,5 +1,9 @@
import { COLORS as SHARED_COLORS, PROVIDER_COLORS } from '../../policy.js';
// Reached by source.js (no browser globals, see check:boundaries), so no
// i18n import: the messages stay English data, translated where shown.
const msg = (text) => text;
export { NEAREST_RADIUS_M } from '../../policy.js';
/** Identity and tuning for the Mapillary street-level provider. */
@@ -36,11 +40,13 @@ export const COVERAGE_MAX_TILES = 9;
/** Overview (z0–5) coverage points seen from orbit. */
export const COVERAGE_OVERVIEW_MAX_TILES = 16;
/** Shown when Mapillary refuses the server's token (401/403). */
export const KEY_REJECTED_MESSAGE =
'Mapillary rejected MAPILLARY_CLIENT_TOKEN — replace it in Provider Settings';
export const KEY_REJECTED_MESSAGE = msg(
'Mapillary rejected MAPILLARY_CLIENT_TOKEN — replace it in Provider Settings',
);
/** Shown while Mapillary rate-limits tile requests (429). */
export const RATE_LIMITED_MESSAGE =
'Coverage requests are being rate-limited — they resume on their own';
export const RATE_LIMITED_MESSAGE = msg(
'Coverage requests are being rate-limited — they resume on their own',
);
/**
* Street-zoom coverage reaches this far from the camera: camera height times
* the factor, never below the floor, even when a tilted view sees further.
+4 -2
View File
@@ -1,3 +1,4 @@
import { msg } from '../../i18n/index.js';
import { freshStreet } from './state.js';
/**
@@ -124,7 +125,8 @@ export function createViewerHost({ state, parts }) {
if (!state.street.host) {
// A nearest-image lookup may have set it; nothing is loading now.
state.street.loading = false;
state.street.error = 'Open the Street Level panel to view imagery';
// English data: the Street Level panel translates it (translateKnown).
state.street.error = msg('Open the Street Level panel to view imagery');
notify();
return false;
}
@@ -152,7 +154,7 @@ export function createViewerHost({ state, parts }) {
if (ticket && !state.street.follow) parts.follow.lookAtPosition(ticket);
} catch (error) {
if (current())
state.street.error = error?.message || 'Image could not be opened';
state.street.error = error?.message || msg('Image could not be opened');
} finally {
if (current()) state.street.loading = false;
notify();