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

+53 -9
View File
@@ -1,5 +1,5 @@
import { localReceiverFeedName } from '../layers/localAdsb/feedNames.js';
import { msg, t, tn } from '../i18n/index.js';
import { formatNumber, msg, t, tn } from '../i18n/index.js';
import { formatQuantity } from './unitLabels.js';
/**
@@ -22,6 +22,39 @@ function statusLabel(status) {
return STATUS_LABELS[key] ? t(STATUS_LABELS[key]) : key.toUpperCase();
}
// AudioContext.state values (plus the controller's "idle") as shown in the
// FM status line; the value itself stays machine.
const AUDIO_STATE_LABELS = Object.freeze({
idle: msg('idle'),
running: msg('running'),
suspended: msg('suspended'),
closed: msg('closed'),
});
function audioStateLabel(audioState) {
const key = String(audioState || 'idle');
return AUDIO_STATE_LABELS[key] ? t(AUDIO_STATE_LABELS[key]) : key;
}
// Decoder feed statuses from the server route, as shown on the card. They
// carry their own context: the word agrees with "feed" (es "fuente no válida"),
// unlike the shared "invalid" / "stale" elsewhere.
function feedStatusLabel(status) {
const key = String(status || 'unreachable');
switch (key) {
case 'live':
return t('live', { context: 'feed status' });
case 'stale':
return t('stale', { context: 'feed status' });
case 'unreachable':
return t('unreachable', { context: 'feed status' });
case 'invalid':
return t('invalid', { context: 'feed status' });
default:
return key;
}
}
/**
* Text for a numeric gain option: the option value stays the machine value
* ("28.0"); only the label follows the locale ("28,0 дБ").
@@ -39,9 +72,18 @@ function formatGain(gain) {
return gain === 'auto' || !Number.isFinite(gain) ? 'auto' : gain.toFixed(1);
}
/** A reading with a fixed number of decimals, in the UI language ("2,40"). */
function fixed(value, digits) {
return formatNumber(value, {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
useGrouping: false,
});
}
function formatIq(state) {
return Number.isFinite(state.iqLevelDbfs)
? `${state.iqLevelDbfs.toFixed(1)} dBFS`
? `${fixed(state.iqLevelDbfs, 1)} dBFS`
: '--';
}
@@ -49,7 +91,7 @@ function statusText(state) {
const fmActive = state.mode === 'fm';
const iqStatus =
state.samplesPerSecond > 0
? `${(state.samplesPerSecond / 1_000_000).toFixed(2)} MS/s IQ`
? `${fixed(state.samplesPerSecond / 1_000_000, 2)} MS/s IQ`
: t('waiting for IQ');
const device = state.deviceLabel ? ` · ${state.deviceLabel}` : '';
if (state.connected && !fmActive) {
@@ -70,9 +112,11 @@ function statusText(state) {
: t('DSP waiting');
const rfStatus = `RF ${formatIq(state)}`;
const audioSignal = Number.isFinite(state.audioLevelDbfs)
? t('{level} dBFS audio', { level: state.audioLevelDbfs.toFixed(1) })
? t('{level} dBFS audio', { level: fixed(state.audioLevelDbfs, 1) })
: t('audio signal --');
const audio = t('audio {state}', { state: state.audioState });
const audio = t('audio {state}', {
state: audioStateLabel(state.audioState),
});
return `${detail}${device} · ${iqStatus} · ${workerStatus} · ${rfStatus} · ${audioSignal} · ${audio}`;
}
return detail;
@@ -121,7 +165,7 @@ export function localSdrCardView(state) {
statsHidden: !(adsbActive && state.connected),
stats: {
rate: Number.isFinite(state.messagesPerSecond)
? state.messagesPerSecond.toFixed(1)
? fixed(state.messagesPerSecond, 1)
: '--',
heard: String(state.aircraftHeard || 0),
positioned: String(state.aircraftPositioned || 0),
@@ -174,9 +218,9 @@ export function localSdrFeedLine(feedState) {
const names = feeds.map((feed) => {
const name = localReceiverFeedName(feed, feeds);
return feedState.polling || feed?.status === 'invalid'
? `${name} ${feed?.status || 'unreachable'}`
? `${name} ${feedStatusLabel(feed?.status)}`
: name;
});
const suffix = feedState.polling ? '' : ' · read while Local ADS-B is on';
return `Decoder feeds: ${names.join(' · ')}${suffix}`;
const suffix = feedState.polling ? '' : t(' · read while Local ADS-B is on');
return `${t('Decoder feeds: {feeds}', { feeds: names.join(' · ') })}${suffix}`;
}