feat(i18n): translate the whole interface

Mark every UI string the unmarked-text check reported (534) plus text it
cannot see (helpers, joined labels, runtime-built panels), and translate
the result to Spanish and Russian: 1,679 messages each.

- Kept on purpose, with the reason where the text lives: provider and
  product names, place and camera names, codes and units, dB values, the HUD
  terminal readouts, demo shot titles that saved projects look up by title,
  and text for AI tools and the voice model.
- Modules shared with the server or AI tools stay free of the i18n import
  (local msg); the wind reading and voice session stay English and are
  translated where the page shows them.
- npm test now runs the unmarked-text check.
- Fixes on the way: a duplicated radio privacy paragraph, the contact panel
  toggle saying Expand while open, the CSS filter mistaking & for CSS.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-12 19:24:42 +04:00
1 parent ee72133574
commit 3129e531e6
99 files changed
+4670 -1294

No files matched your search

+38 -23
View File
@@ -17,6 +17,7 @@ import {
selectAuthorizedSdrDevice,
} from './usbDevices.js';
import { summarizeLocalAdsb } from '../sources/adsbRecords.js';
import { t } from '../i18n/index.js';
export { createRememberingWebUsb } from './usbDevices.js';
@@ -204,23 +205,25 @@ function clampFmFrequency(value) {
function describeConnectionError(error) {
const message = String(error?.message || error || '').trim();
if (/No device was selected|NotFoundError/i.test(message))
return 'No RTL-SDR device selected';
return t('No RTL-SDR device selected');
if (
/does not support the WebUSB|usb/i.test(message) &&
/support/i.test(message)
) {
return 'WebUSB requires desktop Chrome or Edge on localhost/HTTPS';
return t('WebUSB requires desktop Chrome or Edge on localhost/HTTPS');
}
if (/access|permission|claim|busy/i.test(message)) {
return 'RTL-SDR access failed; close other SDR apps and check USB permissions';
return t(
'RTL-SDR access failed; close other SDR apps and check USB permissions',
);
}
return message || 'RTL-SDR connection failed';
return message || t('RTL-SDR connection failed');
}
function streamingMessage(mode) {
return mode === 'adsb'
? 'Receiving 1090 MHz ADS-B'
: 'Receiving broadcast FM';
? t('Receiving 1090 MHz ADS-B')
: t('Receiving broadcast FM');
}
/**
@@ -255,8 +258,8 @@ export class SdrController {
connected: false,
status: webUsbSupported ? 'idle' : 'unsupported',
message: webUsbSupported
? 'Connect an RTL-SDR to begin.'
: 'WebUSB is unavailable in this browser',
? t('Connect an RTL-SDR to begin.')
: t('WebUSB is unavailable in this browser'),
mode: 'fm',
frequencyHz: FM_DEFAULT_HZ,
sampleRate: FM_SAMPLE_RATE,
@@ -402,7 +405,10 @@ 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: 'SDR decoder worker failed' });
this._setState({
status: 'error',
message: t('SDR decoder worker failed'),
});
if (this._device) void this.stop({ preserveMessage: true });
};
return worker;
@@ -600,7 +606,7 @@ export class SdrController {
if (!this.state.webUsbSupported) {
this._setState({
status: 'unsupported',
message: 'WebUSB requires desktop Chrome or Edge',
message: t('WebUSB requires desktop Chrome or Edge'),
});
return false;
}
@@ -609,7 +615,7 @@ export class SdrController {
const superseded = () => this._destroyed || token !== this._sessionToken;
this._setState({
status: 'connecting',
message: 'Waiting for an RTL-SDR device…',
message: t('Waiting for an RTL-SDR device…'),
});
let acquisition = null;
try {
@@ -620,7 +626,7 @@ export class SdrController {
// consumes the click activation.
if (nextMode === 'fm' && !(await this._ensureAudio())) {
throw new Error(
'Browser audio is suspended; press Connect again to resume it',
t('Browser audio is suspended; press Connect again to resume it'),
);
}
if (superseded()) return false;
@@ -1002,7 +1008,7 @@ export class SdrController {
this._setState({
connected: false,
status: 'error',
message: 'RTL-SDR sample stream stopped',
message: t('RTL-SDR sample stream stopped'),
});
await this.stop({ preserveMessage: true });
break;
@@ -1040,12 +1046,14 @@ export class SdrController {
status: 'tuning',
mode: nextMode,
gain: this._gainByMode[nextMode],
message: `Switching to ${nextMode === 'adsb' ? 'ADS-B' : 'FM'}…`,
message: t('Switching to {mode}…', {
mode: nextMode === 'adsb' ? 'ADS-B' : 'FM',
}),
});
try {
if (nextMode === 'fm' && !(await this._ensureAudio())) {
throw new Error(
'Browser audio is suspended; select FM again to resume it',
t('Browser audio is suspended; select FM again to resume it'),
);
}
// A stall teardown, stop() or reconnect during the switch supersedes
@@ -1116,7 +1124,7 @@ export class SdrController {
console.warn('[SDR] Gain change failed:', error);
this._setState({
status: 'error',
message: 'RTL-SDR gain change failed',
message: t('RTL-SDR gain change failed'),
});
return null;
}
@@ -1161,7 +1169,9 @@ export class SdrController {
...(quiet
? {}
: {
message: `Tuned to ${(frequency / 1_000_000).toFixed(1)} MHz FM`,
message: t('Tuned to {mhz} MHz FM', {
mhz: (frequency / 1_000_000).toFixed(1),
}),
}),
});
return true;
@@ -1226,13 +1236,18 @@ export class SdrController {
this._setState({
seeking: false,
seekMessage: `Found ${(peak.frequency / 1_000_000).toFixed(1)} MHz · ${peak.snr.toFixed(1)} dB`,
message: `FM signal found at ${(peak.frequency / 1_000_000).toFixed(1)} MHz`,
message: t('FM signal found at {mhz} MHz', {
mhz: (peak.frequency / 1_000_000).toFixed(1),
}),
});
return peak;
}
if (token === this._seekToken) {
await this.tuneFm(origin, { quiet: true });
this._setState({ seeking: false, seekMessage: 'No FM signal found' });
this._setState({
seeking: false,
seekMessage: t('No FM signal found'),
});
}
return null;
} finally {
@@ -1248,7 +1263,7 @@ export class SdrController {
cancelSeek() {
this._seekToken += 1;
if (!this.state.seeking) return false;
this._setState({ seeking: false, seekMessage: 'Seek stopped' });
this._setState({ seeking: false, seekMessage: t('Seek stopped') });
this._audioNode?.port.postMessage({
type: 'volume',
value: this.state.volume,
@@ -1268,7 +1283,7 @@ export class SdrController {
if (typeof navigator === 'undefined' || !navigator.geolocation) {
this._setState({
locationStatus: 'unavailable',
message: 'Browser location is unavailable',
message: t('Browser location is unavailable'),
});
return false;
}
@@ -1295,7 +1310,7 @@ export class SdrController {
console.warn('[SDR] Receiver location failed:', error);
this._setState({
locationStatus: 'denied',
message: 'Receiver location was not granted',
message: t('Receiver location was not granted'),
});
return false;
}
@@ -1333,7 +1348,7 @@ export class SdrController {
status: preserveMessage ? this.state.status : idleStatus,
...(preserveMessage
? {}
: { message: message || 'RTL-SDR disconnected' }),
: { message: message || t('RTL-SDR disconnected') }),
snr: null,
samplesPerSecond: 0,
workerBlocks: 0,