From 3d64a54a219942404123e6d16456187a5a9182fe Mon Sep 17 00:00:00 2001 From: daikaginza Date: Sat, 17 Oct 2026 07:32:21 +0700 Subject: [PATCH] fix(layers): keep the Local ADS-B receiver error in English The Local ADS-B layer copied the SDR controller's `message`, which is Radio card text in the UI language, into its `error` stat. The controller now publishes an English `errorText` next to the translated `message` for every receiver error, and the layer reports that. A receiver is any object with getState(); one that does not publish errorText still shows its error from `message`, as before. The Radio card text is unchanged, and the Layers panel still shows the error translated. --- src/layers/localAdsb/index.js | 8 +-- src/layers/localAdsb/status.js | 2 +- src/layers/localAdsb/statusLocale.test.mjs | 41 +++++++++++++- src/sdr/controller.js | 62 ++++++++++------------ 4 files changed, 75 insertions(+), 38 deletions(-) diff --git a/src/layers/localAdsb/index.js b/src/layers/localAdsb/index.js index fcf54b5d5..1028450b5 100644 --- a/src/layers/localAdsb/index.js +++ b/src/layers/localAdsb/index.js @@ -66,10 +66,12 @@ const DEFAULT_DISPLAY = Object.freeze({ * Local ADS-B layer: draws aircraft heard by the user's own receivers. * * Two inputs feed it. The receiver is any object with `getState()` returning - * `{ connected, status, message, mode, webUsbSupported, aircraft, messagesPerSecond }`, + * `{ connected, status, message, errorText, mode, webUsbSupported, aircraft, messagesPerSecond }`, * `subscribe(listener)` and `setMode(mode)`; `aircraft` holds records from - * `src/sources/adsbRecords.js`. The browser WebUSB session is one such - * receiver. The optional `feeds` session (`./feeds.js`) supplies records read + * `src/sources/adsbRecords.js`, and the optional `errorText` is the English + * text of an `error` status (`message`, the UI-language one, stands in for a + * receiver without it). The browser WebUSB session is one such receiver. + * The optional `feeds` session (`./feeds.js`) supplies records read * by the server from local 1090/978 MHz decoders; it polls only while this * layer is enabled. Records are merged by ICAO (`mergeLocalAdsbRecords`). * Aircraft heard only on 978 MHz UAT carry a thin ring around the marker. diff --git a/src/layers/localAdsb/status.js b/src/layers/localAdsb/status.js index d264c5e15..7e19997b6 100644 --- a/src/layers/localAdsb/status.js +++ b/src/layers/localAdsb/status.js @@ -51,7 +51,7 @@ export function feedsConfigured(feedState) { function webUsbStatus(receiver, heard, feedState) { if (receiver.status === 'error') - return { status: 'error', error: receiver.message }; + return { status: 'error', error: receiver.errorText ?? receiver.message }; if (receiver.status === 'connecting' || receiver.status === 'tuning') return { loading: true, loadingLabel: msg('opening receiver') }; if ( diff --git a/src/layers/localAdsb/statusLocale.test.mjs b/src/layers/localAdsb/statusLocale.test.mjs index 8fe8245db..d6ac821a4 100644 --- a/src/layers/localAdsb/statusLocale.test.mjs +++ b/src/layers/localAdsb/statusLocale.test.mjs @@ -2,7 +2,8 @@ // "USB error", feed problems, even as the whole error) read in the UI language. import assert from 'node:assert/strict'; import test from 'node:test'; -import { setLocale } from '../../i18n/index.js'; +import { setLocale, t } from '../../i18n/index.js'; +import { SdrController } from '../../sdr/controller.js'; import { LayerPanel } from '../../ui/layerPanel.js'; import { localAdsbStatus } from './status.js'; @@ -66,3 +67,41 @@ for (const locale of ['es', 'ru']) { }); } } + +// A receiver error is English data for the layer (stats, and what the voice +// tools and the analyst read); the Radio card and the row show it translated. +for (const locale of ['es', 'ru']) { + test(`${locale}: a WebUSB receiver error stays English in stats and reads translated`, async () => { + const receiver = new SdrController({ + storage: null, + webUsb: {}, + providerFactory: () => ({ + get: async () => { + throw new Error('No device was selected.'); + }, + }), + }); + setLocale(locale); + try { + assert.equal(await receiver.connect('adsb'), false); + const state = receiver.getState(); + assert.equal(state.status, 'error'); + assert.equal(state.errorText, 'No RTL-SDR device selected'); + assert.equal(state.message, t('No RTL-SDR device selected')); + assert.notEqual(state.message, state.errorText); + + const status = localAdsbStatus({ + receiver: state, + feedState: null, + heard: 0, + }); + assert.equal(status.error, 'No RTL-SDR device selected'); + const row = rowFor(status, 0); + assert.ok(row.includes(state.message), row); + assert.doesNotMatch(row, /No RTL-SDR device selected/, row); + } finally { + setLocale('en'); + await receiver.destroy(); + } + }); +} diff --git a/src/sdr/controller.js b/src/sdr/controller.js index 50fec4a38..9a8923339 100644 --- a/src/sdr/controller.js +++ b/src/sdr/controller.js @@ -283,22 +283,32 @@ function clampFmFrequency(value) { ); } +/** A failed connection in English; errorState() shows it translated. */ function describeConnectionError(error) { const message = String(error?.message || error || '').trim(); if (/No device was selected|NotFoundError/i.test(message)) - return t('No RTL-SDR device selected'); + return msg('No RTL-SDR device selected'); if ( /does not support the WebUSB|usb/i.test(message) && /support/i.test(message) ) { - return t('WebUSB requires desktop Chrome or Edge on localhost/HTTPS'); + return msg('WebUSB requires desktop Chrome or Edge on localhost/HTTPS'); } if (/access|permission|claim|busy/i.test(message)) { - return t( + return msg( 'RTL-SDR access failed; close other SDR apps and check USB permissions', ); } - return message ? translateKnown(message) : t('RTL-SDR connection failed'); + return message || msg('RTL-SDR connection failed'); +} + +/** + * State fields for a receiver error: `message` in the UI language for the + * Radio card, `errorText` as English data for the Local ADS-B layer's stats. + * @param {string} text English error text (ours or the browser's). + */ +function errorState(text) { + return { status: 'error', message: translateKnown(text), errorText: text }; } function streamingMessage(mode) { @@ -555,10 +565,7 @@ export class SdrController { // A failed worker (e.g. its module did not load) never recovers: drop // it so the next session builds and configures a fresh one. this._discardWorker(); - this._setState({ - status: 'error', - message: t('SDR decoder worker failed'), - }); + this._setState(errorState(msg('SDR decoder worker failed'))); if (this._device) void this.stop({ preserveMessage: true }); }; return worker; @@ -779,16 +786,16 @@ export class SdrController { // Published before stopping; stop() preserves it, and nothing is set // afterwards, so a reconnect started during teardown keeps its own state. this._setState({ - status: 'error', + ...errorState( + reason === 'startup' + ? msg('SDR decoder did not start; receiver stopped') + : reason === 'stalled' + ? msg('SDR decoder stopped responding; receiver stopped') + : msg('SDR decoder fell behind; receiver stopped'), + ), connected: false, decoderOverload: reason, decoderOverloadSnapshot: snapshot, - message: - reason === 'startup' - ? t('SDR decoder did not start; receiver stopped') - : reason === 'stalled' - ? t('SDR decoder stopped responding; receiver stopped') - : t('SDR decoder fell behind; receiver stopped'), }); void this.stop({ preserveMessage: true }); } @@ -1085,7 +1092,7 @@ export class SdrController { // consumes the click activation. if (nextMode === 'fm' && !(await this._ensureAudio())) { throw new Error( - t('Browser audio is suspended; press Connect again to resume it'), + msg('Browser audio is suspended; press Connect again to resume it'), ); } if (superseded()) return false; @@ -1153,8 +1160,7 @@ export class SdrController { if (superseded()) return false; this._setState({ connected: false, - status: 'error', - message: describeConnectionError(error), + ...errorState(describeConnectionError(error)), }); return false; } @@ -1507,8 +1513,7 @@ export class SdrController { console.warn('[SDR] Sample read failed:', error); this._setState({ connected: false, - status: 'error', - message: t('RTL-SDR sample stream stopped'), + ...errorState(msg('RTL-SDR sample stream stopped')), }); await this.stop({ preserveMessage: true }); break; @@ -1553,7 +1558,7 @@ export class SdrController { try { if (nextMode === 'fm' && !(await this._ensureAudio())) { throw new Error( - t('Browser audio is suspended; select FM again to resume it'), + msg('Browser audio is suspended; select FM again to resume it'), ); } // A stall teardown, stop() or reconnect during the switch supersedes @@ -1578,10 +1583,7 @@ export class SdrController { } catch (error) { if (!this._owns(token, device)) return false; console.warn('[SDR] Mode switch failed:', error); - this._setState({ - status: 'error', - message: describeConnectionError(error), - }); + this._setState(errorState(describeConnectionError(error))); return false; } } @@ -1622,10 +1624,7 @@ export class SdrController { if (targetMode === 'adsb') this._setState(this._resetAdsbCounters()); } catch (error) { console.warn('[SDR] Gain change failed:', error); - this._setState({ - status: 'error', - message: t('RTL-SDR gain change failed'), - }); + this._setState(errorState(msg('RTL-SDR gain change failed'))); return null; } return setting; @@ -1679,10 +1678,7 @@ export class SdrController { }); return true; } catch (error) { - this._setState({ - status: 'error', - message: describeConnectionError(error), - }); + this._setState(errorState(describeConnectionError(error))); return false; } }