mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
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]>
227 lines
7.5 KiB
JavaScript
227 lines
7.5 KiB
JavaScript
import { localReceiverFeedName } from '../layers/localAdsb/feedNames.js';
|
|
import { formatNumber, msg, t, tn } from '../i18n/index.js';
|
|
import { formatQuantity } from './unitLabels.js';
|
|
|
|
/**
|
|
* Pure presentation of the Local RTL-SDR card in the Radio panel. Receives the
|
|
* receiver session snapshot and returns what each control should show.
|
|
*/
|
|
|
|
// Receiver status codes shown on the connection chip and the connect button.
|
|
const STATUS_LABELS = Object.freeze({
|
|
idle: msg('IDLE'),
|
|
connecting: msg('CONNECTING'),
|
|
tuning: msg('TUNING'),
|
|
streaming: msg('STREAMING'),
|
|
error: msg('ERROR'),
|
|
unsupported: msg('UNSUPPORTED'),
|
|
});
|
|
|
|
function statusLabel(status) {
|
|
const key = String(status || 'idle');
|
|
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 дБ").
|
|
*/
|
|
export function localSdrGainOptionLabel(value) {
|
|
const gain = Number(value);
|
|
if (!Number.isFinite(gain)) return String(value);
|
|
return formatQuantity(gain, 'dB', {
|
|
minimumFractionDigits: 1,
|
|
maximumFractionDigits: 1,
|
|
});
|
|
}
|
|
|
|
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)
|
|
? `${fixed(state.iqLevelDbfs, 1)} dBFS`
|
|
: '--';
|
|
}
|
|
|
|
function statusText(state) {
|
|
const fmActive = state.mode === 'fm';
|
|
const iqStatus =
|
|
state.samplesPerSecond > 0
|
|
? `${fixed(state.samplesPerSecond / 1_000_000, 2)} MS/s IQ`
|
|
: t('waiting for IQ');
|
|
const device = state.deviceLabel ? ` · ${state.deviceLabel}` : '';
|
|
if (state.connected && !fmActive) {
|
|
const decoded = tn(
|
|
state.decodedMessages,
|
|
'{count} message',
|
|
'{count} messages',
|
|
);
|
|
return `${state.message}${device} · ${iqStatus} · ${decoded}`;
|
|
}
|
|
const detail = state.seeking
|
|
? state.seekMessage
|
|
: state.seekMessage || state.message;
|
|
if (state.connected && fmActive) {
|
|
const workerStatus =
|
|
state.workerBlocks > 0
|
|
? tn(state.workerBlocks, 'DSP {count} block', 'DSP {count} blocks')
|
|
: t('DSP waiting');
|
|
const rfStatus = `RF ${formatIq(state)}`;
|
|
const audioSignal = Number.isFinite(state.audioLevelDbfs)
|
|
? t('{level} dBFS audio', { level: fixed(state.audioLevelDbfs, 1) })
|
|
: t('audio signal --');
|
|
const audio = t('audio {state}', {
|
|
state: audioStateLabel(state.audioState),
|
|
});
|
|
return `${detail}${device} · ${iqStatus} · ${workerStatus} · ${rfStatus} · ${audioSignal} · ${audio}`;
|
|
}
|
|
return detail;
|
|
}
|
|
|
|
/**
|
|
* Derive the card's control states from one receiver snapshot.
|
|
* @param {object} state SdrController.getState() snapshot.
|
|
* @returns {object} Control presentation.
|
|
*/
|
|
export function localSdrCardView(state) {
|
|
const transitional =
|
|
state.status === 'connecting' || state.status === 'tuning';
|
|
const fmActive = state.mode === 'fm';
|
|
const adsbActive = state.mode === 'adsb';
|
|
const fmInteractive =
|
|
state.connected && fmActive && state.status === 'streaming';
|
|
return {
|
|
connectionLabel: state.connected
|
|
? adsbActive
|
|
? tn(state.aircraftHeard || 0, '{count} HEARD', '{count} HEARD')
|
|
: t('STREAMING')
|
|
: statusLabel(state.status),
|
|
connectionActive: Boolean(state.connected),
|
|
connectLabel: transitional
|
|
? statusLabel(state.status)
|
|
: state.connected
|
|
? t('DISCONNECT')
|
|
: t('CONNECT'),
|
|
connectDisabled: transitional || !state.webUsbSupported,
|
|
connectPressed: Boolean(state.connected),
|
|
fmActive,
|
|
adsbActive,
|
|
modeDisabled: transitional,
|
|
locateLabel:
|
|
state.locationStatus === 'requesting'
|
|
? t('LOCATING…')
|
|
: state.locationStatus === 'ready'
|
|
? t('LOCATED')
|
|
: t('LOCATE'),
|
|
locateDisabled: state.locationStatus === 'requesting',
|
|
locateActive: state.locationStatus === 'ready',
|
|
changeDeviceDisabled: transitional || !state.webUsbSupported,
|
|
gainValue: formatGain(state.gain),
|
|
gainDisabled: transitional,
|
|
statsHidden: !(adsbActive && state.connected),
|
|
stats: {
|
|
rate: Number.isFinite(state.messagesPerSecond)
|
|
? fixed(state.messagesPerSecond, 1)
|
|
: '--',
|
|
heard: String(state.aircraftHeard || 0),
|
|
positioned: String(state.aircraftPositioned || 0),
|
|
iq: formatIq(state),
|
|
},
|
|
frequencyDisabled: !fmActive || transitional,
|
|
frequencyValue: fmActive
|
|
? (state.frequencyHz / 1_000_000).toFixed(1)
|
|
: null,
|
|
tuneDisabled: !state.connected || !fmActive || transitional,
|
|
seekDisabled: !fmInteractive || Boolean(state.seeking),
|
|
volumePercent: Math.round((Number(state.volume) || 0) * 100),
|
|
volumeDisabled: !fmActive,
|
|
statusText: statusText(state),
|
|
statusError: state.status === 'error' || state.status === 'unsupported',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Whether the receiver is producing FM audio (the state that must not overlap
|
|
* internet-radio playback).
|
|
* @param {object} state SdrController.getState() snapshot.
|
|
* @returns {boolean}
|
|
*/
|
|
export function localSdrFmAudioActive(state) {
|
|
return Boolean(
|
|
state?.connected && state.mode === 'fm' && state.status === 'streaming',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Whether internet radio is loading or playing a station.
|
|
* @param {object} state Radio UI state.
|
|
* @returns {boolean}
|
|
*/
|
|
export function internetRadioAudioActive(state) {
|
|
return ['loading', 'buffering', 'playing'].includes(state?.audioState);
|
|
}
|
|
|
|
/**
|
|
* One-line decoder-feed summary for the card, or null when the server has no
|
|
* feed configured. Statuses show while Local ADS-B polls the feeds; otherwise
|
|
* the line names the configured bands.
|
|
* @param {object|null} feedState Local receiver feed session snapshot.
|
|
* @returns {string|null}
|
|
*/
|
|
export function localSdrFeedLine(feedState) {
|
|
const feeds = feedState?.configured === true ? feedState.feeds : null;
|
|
if (!Array.isArray(feeds) || !feeds.length) return null;
|
|
const names = feeds.map((feed) => {
|
|
const name = localReceiverFeedName(feed, feeds);
|
|
return feedState.polling || feed?.status === 'invalid'
|
|
? `${name} ${feedStatusLabel(feed?.status)}`
|
|
: name;
|
|
});
|
|
const suffix = feedState.polling ? '' : t(' · read while Local ADS-B is on');
|
|
return `${t('Decoder feeds: {feeds}', { feeds: names.join(' · ') })}${suffix}`;
|
|
}
|