i18n: fix the LGTM review findings and tidy the branch texts

- i18n scanner: lastError sink, no false sinks on defaults and ternaries
- shared src/i18n/marker.js for modules that only mark strings
- wind field messages move to wind/fields.js; CITY_POIS imports from cityPois.js
- drop review IDs, change history and stale references from comments and docs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-16 21:00:28 +04:00
1 parent 0ed05568f7
commit 20f4cf3763
178 files changed
+918 -948

No files matched your search

@@ -103,6 +103,5 @@ for (const locale of ['en', 'es', 'ru']) {
card,
inLocale(locale, () => translateFeedError('ECCC HTTP 503')),
);
assert.equal(card, locale === 'en' ? 'ECCC HTTP 503' : 'ECCC: HTTP 503');
});
}
+2 -3
View File
@@ -217,9 +217,8 @@ export function createAirQualityLayer({ source, overlayHost } = {}) {
0,
);
// Readings and errors stay English data (analyst records, stats); the
// card shows them in the UI language, errors as the Layers panel row
// does. A provider's own error text has no catalog entry and shows as
// it came.
// card translates them as the Layers panel row does. A provider's own
// error text has no catalog entry and shows as it came.
return {
readout: true,
summary: {
+2 -2
View File
@@ -15,8 +15,8 @@
* Only ECCC's AQHI is implemented here; `AIR_QUALITY_PROVIDERS` is the seam a
* later provider registers against.
*/
// A portable export (package.json): no i18n here. The labels stay English data
// and the Layers panel shows them translated.
// A portable export (package.json): no i18n here. The labels stay English data;
// the Weather card (index.js) shows them translated.
const msg = (text) => text;
/** Band identifiers shared across providers, in ascending severity. */
+1 -2
View File
@@ -1,8 +1,7 @@
import { AWARENESS_RADIUS_M } from '../../data/militaryAwarenessEngine.js';
// The reason stays English for the voice analyst; the Contacts panel shows it
// in the UI language from `reasonTemplate`. Kept local so this module stays
// free of the i18n import.
// in the UI language from `reasonTemplate`.
const msg = (text) => text;
const fillTemplate = (template, values = {}) =>
template.replace(/\{(\w+)\}/g, (_, name) => String(values[name]));
+3 -4
View File
@@ -11,10 +11,9 @@ import {
AWARENESS_PAGE_SIZE,
} from './policy.js';
// installations/controls.js getStats composes coverageLabel inside the layer
// model (machine-composed English, pinned by militaryInstallations.test.mjs).
// This is the presentation boundary: map the closed vocabulary to the
// catalog, passing anything unknown through verbatim (docs/TRANSLATORS.md).
// coverageLabel is machine-composed English from installations/controls.js
// (pinned by src/data/militaryInstallations.test.mjs); translate the known
// forms here, pass anything else through.
const WITHIN_KM_RE = /^WITHIN (\d+) KM$/;
function localizeCoverageLabel(label) {
-2
View File
@@ -45,8 +45,6 @@ export function isHeadingEstimated(camera) {
*/
export function headingHudToken(camera) {
const hdg = Math.round(Number(camera?.headingDeg) || 0);
// The token copy lives in the layers catalog (en is byte-identical to the
// literals these replace); presentation stays here, translation at read.
return isHeadingEstimated(camera)
? t('HDG {value}° (ESTIMATED)', { value: hdg })
: t('HDG {value}°', { value: hdg });
+14 -14
View File
@@ -66,7 +66,7 @@ const RECORDS = [
type: 'bullet',
url: 'https://www.cctv-database.com/camera/hikvision-ds-2cd2087g3-li2uy/',
fov_deg: '108.8 horizontal (2.8mm) / 93.3 horizontal (4mm)',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 8, w: 3840, h: 2160, label: '8MP (4K)' },
night_vision: { type: 'color', range_m: 40 },
environment: ['outdoor'],
@@ -79,7 +79,7 @@ const RECORDS = [
type: 'dome',
url: 'https://www.cctv-database.com/camera/axis-f4105-lre/',
fov_deg: '110 horizontal, 60 vertical',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 2, w: 1920, h: 1080, label: '1080p HD' },
night_vision: { type: 'ir', range_m: 10 },
environment: ['outdoor'],
@@ -97,7 +97,7 @@ const RECORDS = [
mp: 8,
w: 1920,
h: 1080,
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
label: '4x 2MP (8MP total; 4x 1080p)',
},
lens_count: 4,
@@ -120,7 +120,7 @@ const RECORDS = [
mp: 8,
w: 1920,
h: 1080,
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
label: '4x 2MP (8MP total; 4x 1080p)',
},
lens_count: 4,
@@ -134,7 +134,7 @@ const RECORDS = [
url: 'https://www.cctv-database.com/camera/axis-q6054-e/',
aliases: ['AXIS Q6054-E', 'Q6054-E 50 Hz', 'Q6054-E 60 Hz'],
fov_deg: '62.9-2.2 horizontal',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 1, w: 1280, h: 720, label: '720p HD' },
ptz: true,
autotracking: true,
@@ -149,7 +149,7 @@ const RECORDS = [
url: 'https://www.cctv-database.com/camera/axis-q6155-e/',
aliases: ['AXIS Q6155-E', 'Q6155-E 50 Hz', 'Q6155-E 60 Hz'],
fov_deg: '66.7-2.36 horizontal',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 2, w: 1920, h: 1080, label: '1080p HD' },
ptz: true,
autotracking: true,
@@ -171,7 +171,7 @@ const RECORDS = [
'VG4-500',
],
fov_deg: '57.8-1.7 horizontal',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 0.4, label: '540 TVL (NTSC/PAL)' },
ptz: true,
autotracking: true,
@@ -186,7 +186,7 @@ const RECORDS = [
url: 'https://www.cctv-database.com/camera/bosch-autodome-ip-dynamic-7000-hd/',
aliases: ['AUTODOME IP dynamic 7000 HD', 'AUTODOME 7000 dynamic HD'],
fov_deg: '65-2.3 horizontal',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 2, w: 1920, h: 1080, label: '1080p HD (2MP)' },
ptz: true,
autotracking: true,
@@ -206,7 +206,7 @@ const RECORDS = [
'VG5-7130',
],
fov_deg: '59-2.1 horizontal',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 1, w: 1280, h: 720, label: '720p HD (1.3MP sensor)' },
ptz: true,
autotracking: true,
@@ -226,7 +226,7 @@ const RECORDS = [
'NDP-5502-30',
],
fov_deg: '60.9-2.4 horizontal',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 2, w: 1920, h: 1080, label: '1080p HD (2MP)' },
ptz: true,
varifocal: true,
@@ -245,7 +245,7 @@ const RECORDS = [
'NDP-7512-Z30K',
],
fov_deg: '64.7-2.3 horizontal',
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 2, w: 1920, h: 1080, label: '1080p HD (2MP)' },
ptz: true,
autotracking: true,
@@ -279,7 +279,7 @@ const RECORDS = [
'iDome 3925',
'Cohu iDome 3920 Series',
],
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 0.4, label: 'NTSC / 470 TVL' },
ptz: true,
varifocal: true,
@@ -292,7 +292,7 @@ const RECORDS = [
type: 'ptz',
url: 'https://www.cctv-database.com/camera/cohu-3950-iview/',
aliases: ['3950', '3950 Series', 'Cohu 3950 Series iVIEW'],
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 0.4, label: 'NTSC / 470 TVL' },
ptz: true,
varifocal: true,
@@ -305,7 +305,7 @@ const RECORDS = [
type: 'ptz',
url: 'https://www.cctv-database.com/camera/cohu-3960/',
aliases: ['3960 Series', '3964', '3965', 'Cohu 3960 iView'],
// i18n-ignore: datasheet resolution label (camera data)
// i18n-ignore: datasheet label
resolution: { mp: 0.4, label: 'NTSC/PAL / 470 TVL' },
ptz: true,
varifocal: true,
+5 -5
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { createCyclonesLayer } from './index.js';
import { setLocale, t } from '../../i18n/index.js';
// The exact string the server sends (server/providers/cyclones.js:8).
// The exact string the server sends (COVERAGE in server/providers/cyclones.js).
const SERVER_COVERAGE =
'Atlantic and eastern/central North Pacific; not worldwide cyclone coverage.';
const time = '2026-09-16T03:00:00.000Z';
@@ -59,7 +59,7 @@ function harness(feed) {
const translated = () => t(SERVER_COVERAGE);
test('K-35a: es, server snapshot without storms -> coverage line in Spanish', async () => {
test('es: server snapshot without storms -> coverage line in Spanish', async () => {
setLocale('es');
try {
assert.notEqual(translated(), SERVER_COVERAGE, 'es.json has a translation');
@@ -77,7 +77,7 @@ test('K-35a: es, server snapshot without storms -> coverage line in Spanish', as
}
});
test('K-35b: es, server snapshot with a storm -> coverage line in Spanish', async () => {
test('es: server snapshot with a storm -> coverage line in Spanish', async () => {
setLocale('es');
try {
const layer = harness({ getSnapshot: async () => snapshot([storm]) });
@@ -94,7 +94,7 @@ test('K-35b: es, server snapshot with a storm -> coverage line in Spanish', asyn
}
});
test('K-35c: es, unavailable without reason -> fallback status not the English literal', async () => {
test('es: unavailable without reason -> fallback status not the English literal', async () => {
setLocale('es');
try {
const layer = harness({
@@ -115,7 +115,7 @@ test('K-35c: es, unavailable without reason -> fallback status not the English l
}
});
test('K-35: en coverage and fallback status read as before', async () => {
test('en: coverage and fallback status read as before', async () => {
const layer = harness({ getSnapshot: async () => snapshot([]) });
layer.enable();
await layer.update();
+3 -5
View File
@@ -99,9 +99,8 @@ export const STEP_ANCHOR_FAST_ATTEMPTS = 5;
export const FLIGHT_PROGRESS_MS = 250;
// ---------------------------------------------------------------------------
// Display text: distances, durations and maneuvers in the page's language.
// The proxy's English `instruction` stays the machine value; the row and the
// cards phrase each step from its OSRM type/modifier/road instead.
// Display text in the page's language. The proxy's English `instruction` stays
// the machine value; steps are phrased from their OSRM type/modifier/road.
// ---------------------------------------------------------------------------
/**
@@ -639,8 +638,7 @@ export function directionsStats(state) {
armed,
pointerBlocked,
} = state;
// English in layer state; the layer row translates it.
const source = 'OSM routing'; // i18n-ignore: source name, translated by layerPanel
const source = 'OSM routing'; // English in layer state; the layer row translates it
if (pointerBlocked) {
return {
count: route?.steps.length || 0,
+5 -6
View File
@@ -1,13 +1,12 @@
// K-34: AIS degradation chip text and the traffic "SIMULATED — <reason>" text
// must be in the UI language (they stayed English in es before the fix);
// English keeps its wording.
// The AIS degradation chip and the traffic "SIMULATED — <reason>" text read in
// the UI language; English keeps its wording.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { setLocale, translateKnown } from '../i18n/index.js';
import { deriveAisFeedError } from '../data/aisLiveVessels.js';
import { createModel } from './traffic/model.js';
test('K-34: AIS degraded-feed reasons are localized in es', () => {
test('AIS degraded-feed reasons are localized in es', () => {
setLocale('es');
try {
const cases = [
@@ -34,7 +33,7 @@ test('K-34: AIS degraded-feed reasons are localized in es', () => {
}
});
test('K-34: traffic "SIMULATED — <reason>" is localized in es', () => {
test('traffic "SIMULATED — <reason>" is localized in es', () => {
setLocale('es');
try {
const { trafficFeedPresentation } = createModel({
@@ -55,7 +54,7 @@ test('K-34: traffic "SIMULATED — <reason>" is localized in es', () => {
}
});
test('K-34: English degraded-feed wording is unchanged', () => {
test('English degraded-feed wording is unchanged', () => {
setLocale('en');
assert.equal(
deriveAisFeedError({ status: 'down', reconnectAttempt: 3 }, 0),
+1
View File
@@ -45,6 +45,7 @@ export function createIngestion({
const sourceStale = snapshot.stale || snapshot.freshness === 'unknown';
feed._backoff = sourceStale;
feed._retryAt = 0;
// i18n-ignore: English data; the panel shows it through FEED_ERROR_PATTERNS (ui/statusText.js)
feed._lastError = sourceStale
? sourceAgeMs == null
? msg('Source snapshot time unavailable')
+1 -2
View File
@@ -41,8 +41,7 @@ export function createAdsbGnssSource({
);
throw Object.assign(
new Error(
// The status code form is matched by FEED_ERROR_PATTERNS
// (ui/statusText.js) and gnss/index.js.
// HTTP errors are matched by FEED_ERROR_PATTERNS in ui/statusText.js.
limited
? msg('GNSS feed rate limited (HTTP 429)')
: `GNSS feed unavailable (HTTP ${response.status})`,
@@ -1,4 +1,4 @@
// K-50: every class the bundled OSM names pack can put on a record has a
// Every class the bundled OSM names pack can put on a record has a
// catalog entry, so the selected card shows it in the UI language.
import { test } from 'node:test';
import assert from 'node:assert/strict';
@@ -22,7 +22,7 @@ const englishLabel = Object.fromEntries(
);
for (const locale of ['es', 'ru']) {
test(`every names-pack class is in the ${locale} catalog (K-50)`, () => {
test(`every names-pack class is in the ${locale} catalog`, () => {
const catalog = getCatalog(locale);
const missing = pack.classes.filter(
(c) => catalog[`installation class|${englishLabel[c]}`] === undefined,
+2 -4
View File
@@ -7,10 +7,8 @@ import {
} from './policy.js';
import { formatNumber, t, tn } from '../../i18n/index.js';
// installationFeedback.js composes its guidance inside the portable
// node/browser graph (the HUD summary in loadingFeedback.js reads the same
// module), so it hands back English marked with msg() plus the values;
// getStats is the browser presentation boundary that translates them.
// installationFeedback.js is shared with the HUD (src/loadingFeedback.js) and
// hands back msg()-marked English plus values; getStats translates them here.
function localizeInstallationFeedback(feedback) {
if (feedback.text !== undefined) {
const values = {};
@@ -6,7 +6,7 @@ import { setLocale } from '../../i18n/index.js';
import { installationTitle } from './classLabel.js';
import { formatAwarenessLabel } from '../../data/militaryAwarenessEngine.js';
// K-33: unnamed installations get an English label in data ("Firing range",
// Unnamed installations get an English label in data ("Firing range",
// "Military land", "Military airfield"; OpenFreeMap polygons: "Military area").
// The data stays English; the map card title reads in the UI language.
const { records } = normalizeMilitaryInstallations({
@@ -24,7 +24,7 @@ const { records } = normalizeMilitaryInstallations({
});
const names = [...records.map((r) => r.name), UNNAMED_MILITARY_AREA];
test('K-33: unnamed installations keep English names in data', () => {
test('unnamed installations keep English names in data', () => {
assert.deepEqual(names, [
'Firing range',
'Military land',
@@ -34,7 +34,7 @@ test('K-33: unnamed installations keep English names in data', () => {
]);
});
test('K-33: unnamed installation card title reads in the UI language', () => {
test('unnamed installation card title reads in the UI language', () => {
try {
for (const locale of ['es', 'ru']) {
setLocale(locale);
@@ -55,13 +55,13 @@ test('K-33: unnamed installation card title reads in the UI language', () => {
}
});
test('K-33: English card titles are unchanged', () => {
test('English card titles are unchanged', () => {
setLocale('en');
for (const name of names) assert.equal(installationTitle(name), name);
assert.equal(installationTitle(''), '');
});
test('K-33: the Contacts line names an unnamed installation in the UI language', () => {
test('the Contacts line names an unnamed installation in the UI language', () => {
try {
setLocale('en');
for (const name of names)
@@ -0,0 +1,85 @@
// A launch without a Launch Library status gets our 'Unknown'. The record keeps
// it English; the mission panel translates it and shows provider statuses as sent.
import assert from 'node:assert/strict';
import test from 'node:test';
import { setLocale } from '../../i18n/index.js';
import {
launchStatusAllowsOrbit,
normalizeRocketLaunches,
} from '../../data/rocketLaunches.js';
import { UNKNOWN_LAUNCH_STATUS } from './model.js';
import { createPanel } from './panel.js';
const NOW = new Date('2026-07-27T00:00:00Z');
function shownStatus(status) {
const outputs = new Map();
const state = {
_missionPanel: {
querySelector(selector) {
if (!outputs.has(selector))
outputs.set(selector, {
textContent: '',
closest: () => null,
removeAttribute() {},
});
return outputs.get(selector);
},
closest: () => null,
},
_selectedLaunchId: 'mission-1',
_launches: [
{
id: 'mission-1',
name: 'Example',
status,
payloads: [],
recoveryStages: [],
},
],
_replayTracks: new Map(),
};
createPanel({
state,
parts: {
overlays: { shortMissionLabel: (name) => name },
policyHelpers: { missionPathPresentation: () => ({}) },
replay: { syncReplayButton() {} },
},
}).renderMissionPanel();
return outputs.get('[data-mission-status]').textContent;
}
test('a launch without a status keeps the English fallback in its record', () => {
const [launch] = normalizeRocketLaunches(
{
results: [
{
id: 'no-status',
name: 'Test Flight',
net: '2026-07-20T10:00:00Z',
pad: {
name: 'Pad A',
location: { name: 'Test Range', coordinates: '12.5,45.5' },
},
},
],
},
NOW,
);
assert.equal(UNKNOWN_LAUNCH_STATUS, 'Unknown');
assert.equal(launch.status, 'Unknown');
assert.equal(launchStatusAllowsOrbit(launch.status), true);
});
test('the mission panel translates only the fallback status', (t) => {
t.after(() => setLocale('en'));
setLocale('en');
assert.equal(shownStatus(UNKNOWN_LAUNCH_STATUS), 'Unknown');
setLocale('ru');
assert.equal(shownStatus(UNKNOWN_LAUNCH_STATUS), 'Неизвестно');
assert.equal(shownStatus('Go for Launch'), 'Go for Launch');
setLocale('es');
assert.equal(shownStatus(UNKNOWN_LAUNCH_STATUS), 'Desconocido');
assert.equal(shownStatus('Launch Successful'), 'Launch Successful');
});
+8 -2
View File
@@ -1,6 +1,13 @@
import * as Cesium from 'cesium';
import { msg } from '../../i18n/index.js';
import { WINDOW_DAYS } from './policy.js';
/**
* Fallback when Launch Library sends no status. English in the record
* (launchStatusAllowsOrbit, analyst records); the mission panel translates it.
*/
export const UNKNOWN_LAUNCH_STATUS = msg('Unknown');
export function createModel({ state: layerState, services, parts, source }) {
function finiteCoordinate(value) {
const number = Number(value);
@@ -192,8 +199,7 @@ export function createModel({ state: layerState, services, parts, source }) {
launch.id || launch.slug || launch.name || `launch-${date}`,
),
name: launch.name || 'Unnamed launch',
// i18n-ignore: Launch Library status names are provider data, shown as sent
status: launch.status?.name || 'Unknown',
status: launch.status?.name || UNKNOWN_LAUNCH_STATUS,
launchTime: Number.isFinite(date)
? new Date(date).toISOString()
: null,
+9 -1
View File
@@ -4,6 +4,7 @@ import {
MISSION_CLOSE_VIEW_RANGE_M,
MISSION_GLOBE_VIEW_RANGE_M,
} from './policy.js';
import { UNKNOWN_LAUNCH_STATUS } from './model.js';
export function createPanel({ state: layerState, services, parts, source }) {
/**
@@ -191,7 +192,14 @@ export function createPanel({ state: layerState, services, parts, source }) {
layerState._missionPanel.querySelector('[data-mission-title]').textContent =
parts.overlays.shortMissionLabel(launch.name, 32).toUpperCase();
setMissionPanelField('[data-mission-provider]', launch.provider);
setMissionPanelField('[data-mission-status]', launch.status);
// Launch Library status names are provider data, shown as sent; only the
// fallback for a launch without one is ours to translate.
setMissionPanelField(
'[data-mission-status]',
launch.status === UNKNOWN_LAUNCH_STATUS
? t(UNKNOWN_LAUNCH_STATUS)
: launch.status,
);
setMissionPanelField(
'[data-mission-site]',
launch.launchSite && launch.launchSite !== 'Unknown launch site'
+2 -2
View File
@@ -1,4 +1,4 @@
// K-37: the Local ADS-B click card follows the UI language; English keeps its
// The Local ADS-B click card follows the UI language; English keeps its
// wording (index.test.mjs pins the full English card).
import test from 'node:test';
import assert from 'node:assert/strict';
@@ -86,7 +86,7 @@ test('the class line names the aircraft class in the UI language', async () => {
}
});
// C8: the receiver bands follow the UI language too ("1090 МГц").
// The receiver bands follow the UI language too ("1090 МГц").
test('ru: the receiver line names the bands in Russian', async () => {
const { localAdsbReceiverLine } = await import('./card.js');
setLocale('ru');
-4
View File
@@ -58,10 +58,6 @@ export function localAdsbReceiverLine(record) {
.join(' · ');
}
// policy.js keeps these labels in English (HEARD_BY_RECEIVER, BAND_LABELS,
// SOURCE_LABELS); the card writes them out here so that it can show them
// translated.
/** A receiver band (policy.js BAND_LABELS) in the UI language: "1090 МГц". */
function bandLabel(band) {
switch (String(band)) {
@@ -1,41 +0,0 @@
// A Local ADS-B feed problem can be the whole error (nothing heard, WebUSB
// not producing); the row shows it in the UI language like any other piece.
import assert from 'node:assert/strict';
import test from 'node:test';
import { setLocale } from '../../i18n/index.js';
import { LayerPanel } from '../../ui/layerPanel.js';
import { localAdsbStatus } from './status.js';
const receiver = {
webUsbSupported: true,
connected: false,
mode: 'adsb',
status: 'idle',
};
function rowFor(status) {
return LayerPanel.prototype._buildMetaText({
id: 'local-adsb',
enabled: true,
stats: { count: 0, lastUpdate: Date.now(), ...status },
});
}
for (const locale of ['es', 'ru']) {
for (const status of ['unreachable', 'invalid']) {
test(`${locale}: a lone ${status} decoder feed with nothing heard is translated`, () => {
setLocale(locale);
try {
const st = localAdsbStatus({
receiver,
feedState: {
configured: true,
feeds: [{ id: 'a', label: '1090', band: '1090', status }],
},
heard: 0,
});
const row = rowFor(st);
assert.doesNotMatch(row, /\b(unreachable|invalid)\b/, row);
} finally {
setLocale('en');
}
});
}
}
+42 -12
View File
@@ -1,13 +1,12 @@
// The Local ADS-B row's working texts ("3 heard · 5.8 msg/s",
// "2 feeds live · 14 heard", "listening", "USB error", feed problems) read in
// the UI language.
// The Local ADS-B row's working texts ("3 heard · 5.8 msg/s", "listening",
// "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 { LayerPanel } from '../../ui/layerPanel.js';
import { localAdsbStatus } from './status.js';
const receiver = {
const streaming = {
webUsbSupported: true,
connected: true,
mode: 'adsb',
@@ -15,24 +14,55 @@ const receiver = {
messagesPerSecond: 5.8,
};
function rowFor(status) {
const idle = {
webUsbSupported: true,
connected: false,
mode: 'adsb',
status: 'idle',
};
function rowFor(status, count) {
return LayerPanel.prototype._buildMetaText({
id: 'local-adsb',
enabled: true,
stats: { count: 3, lastUpdate: Date.now(), ...status },
stats: { count, lastUpdate: Date.now(), ...status },
});
}
function inLocale(locale, run) {
setLocale(locale);
try {
run();
} finally {
setLocale('en');
}
}
for (const locale of ['es', 'ru']) {
test(`${locale}: the streaming Local ADS-B row is translated`, () => {
setLocale(locale);
try {
inLocale(locale, () => {
const row = rowFor(
localAdsbStatus({ receiver, feedState: null, heard: 3 }),
localAdsbStatus({ receiver: streaming, feedState: null, heard: 3 }),
3,
);
assert.doesNotMatch(row, /\bheard\b/, row);
} finally {
setLocale('en');
}
});
});
for (const status of ['unreachable', 'invalid']) {
test(`${locale}: a lone ${status} decoder feed with nothing heard is translated`, () => {
inLocale(locale, () => {
const st = localAdsbStatus({
receiver: idle,
feedState: {
configured: true,
feeds: [{ id: 'a', label: '1090', band: '1090', status }],
},
heard: 0,
});
const row = rowFor(st, 0);
assert.doesNotMatch(row, /\b(unreachable|invalid)\b/, row);
});
});
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
// K-37 / K-29: the selected-fire card of Fire Perimeters and its accessible
// The selected-fire card of Fire Perimeters and its accessible
// label follow the UI language, like the legend of the same layer
// (perimeters/index.js). cards.test.mjs pins the English card.
import test from 'node:test';
@@ -82,7 +82,7 @@ for (const locale of ['es', 'ru']) {
});
}
// C5 / C11 / E7: known WFIGS values, the cost and the unnamed title follow the
// Known WFIGS values, the cost and the unnamed title follow the
// UI language; the row keeps the feed's English for the AI tools.
const expected = {
es: {
+2 -4
View File
@@ -146,10 +146,8 @@ function formatAge(deltaMs) {
* @returns {Object} World-overlay entry without `position`.
*/
export function buildIncidentCard(row, nowMs, { link = null } = {}) {
// Feed values stay as WFIGS sends them in the row (and in what the AI tools
// read); the card translates the known cause, behavior and complexity values
// and the wording around them. State, category, county and complex names
// are shown as sent.
// The row keeps WFIGS values as sent (AI tools read them); the card translates
// the known cause, behavior and complexity values and the wording around them.
const facts = [];
if (Number.isFinite(row.acres)) {
const acres = Math.round(row.acres);
+2 -2
View File
@@ -62,8 +62,8 @@ export const DEFAULT_RADIO_FILTER = 'all';
export const GLOBAL_RADIO_ALTITUDE_M = 2_000_000;
// Genre labels are marked for i18n extraction; categories.js translates them.
// Kept local so this module stays free of the i18n import.
// Genre labels stay English data (the radio source, shared with the AI tools,
// imports this module); categories.js translates them.
const msg = (text) => text;
export const MUSIC_GENRES = Object.freeze([
+1 -1
View File
@@ -47,8 +47,8 @@ export const SPLIT_PUBLISH_MS = 400;
/** The three modes, in share-link order (`m` = index). */
export const MODES = Object.freeze(['image', 'basemap', 'ab']);
const COMPARISON_IN_USE = msg('Comparison in use by another scene');
// Kept English in the snapshot (AI tools read it); the card translates them.
const COMPARISON_IN_USE = msg('Comparison in use by another scene');
const COMPARISON_UNAVAILABLE = msg('Esri map unavailable · no swipe');
const COMPARISON_NEEDS_GLOBE = msg('Swipe needs a globe map');
const ESRI_KEPT = msg('Imagery stays on Esri · CLEAR to use Google 3D');
+2 -2
View File
@@ -71,8 +71,8 @@ export function createIngestion({
// At least one group loaded — proceed with a fresh rebuild, but keep the
// partial outage visible at the layer control instead of presenting the
// reduced catalog as a fully healthy refresh. English data; the panel
// shows it through FEED_ERROR_PATTERNS (ui/statusText.js).
// reduced catalog as a fully healthy refresh.
// i18n-ignore: English data; the panel shows it through FEED_ERROR_PATTERNS (ui/statusText.js)
layerState._lastError = failed.length
? `${failed.length} CelesTrak group${failed.length === 1 ? '' : 's'} unavailable`
: null;
+1 -1
View File
@@ -704,7 +704,7 @@ export function createLineCoverage({
state.coverage.pending.get(key) !== controller
)
return;
state.coverage.lastError = error?.message || 'Coverage tile failed';
state.coverage.lastError = error?.message || msg('Coverage tile failed');
if (error?.keyRequired) state.keyRequired = true;
if (error?.keyRejected) {
// Every other tile would be refused too: stop asking.
+1 -1
View File
@@ -1,4 +1,4 @@
// K-37: the traffic road-status notes follow the UI language; English keeps
// The traffic road-status notes follow the UI language; English keeps
// its wording.
import test from 'node:test';
import assert from 'node:assert/strict';
+1 -2
View File
@@ -476,8 +476,7 @@ export function createIngestion({ state, services, parts, source }) {
`[Data:Transit] ${feed.id} poll failed:`,
error?.message || error,
);
// English data; the panel shows it through FEED_ERROR_PATTERNS
// i18n-ignore: translated on display (ui/statusText.js)
// i18n-ignore: English data, translated on display (ui/statusText.js)
status.error = `${feed.name} feed unavailable`;
status.retryInSec = Number.isFinite(error?.retryInSec)
? error.retryInSec
+2 -4
View File
@@ -321,10 +321,8 @@ export function aggregateTransitFeedHealth(statuses, nowMs) {
};
}
// AI tools (src/tools/queries/mobility.js) import this module, so it stays
// free of i18n: the card copy below calls the translate function `t` that the
// caller hands in. The browser selection passes the real one from src/i18n;
// without it the copy is English.
// AI tools (src/tools/queries/mobility.js) import this module, so no i18n here:
// the card copy takes `t` from the caller (selection.js); without it, English.
const msg = (text) => text;
/** English {placeholder} interpolation, the default `t` of the card copy. */
+1 -3
View File
@@ -278,10 +278,8 @@ export function createQueries({ state, parts }) {
: {}),
degraded: count > 0 && health.degraded,
loading: health.loading && count === 0,
// English data; the panel shows it through STATUS_PIECE_PATTERNS
// (ui/statusText.js).
loadingLabel:
// i18n-ignore: translated on display
// i18n-ignore: English data, translated on display (ui/statusText.js)
health.loading && count === 0
? `Loading ${active.map((feed) => feed.name).join(', ')}`
: undefined,
@@ -1,4 +1,4 @@
// K-37: the Transit selection card follows the UI language. The copy builder
// The Transit selection card follows the UI language. The copy builder
// stays free of i18n (AI tools import policy.js); the browser hands it t().
import test from 'node:test';
import assert from 'node:assert/strict';
+2 -4
View File
@@ -1,10 +1,8 @@
import * as Cesium from 'cesium';
import { t, msg } from '../../i18n/index.js';
// The vessels ingestion owner is a portable module graph (it also runs in the
// server-side source builds), so it writes stable English feed labels into
// state. Localize those known labels here, at the presentation read; anything
// else (a bespoke manager label) passes through verbatim.
// ingestion.js is a portable export (package.json) and writes English labels
// into state; translate the known ones here, pass anything else through.
const FEED_STATE_LABEL_KEYS = Object.freeze({
'refreshing...': msg('refreshing...'),
'loading...': msg('loading...'),
@@ -3,9 +3,8 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { setLocale, t } from '../../i18n/index.js';
import { WIND_FIELDS } from './fields.js';
import { SCALAR_UNAVAILABLE, WIND_FIELDS } from './fields.js';
import { fieldUnavailableText } from './rendering.js';
import { SCALAR_UNAVAILABLE } from './source.js';
for (const locale of ['es', 'ru']) {
test(`${locale}: every field's unavailable status has a translation`, () => {
+11
View File
@@ -1,5 +1,16 @@
import { msg } from '../../i18n/marker.js';
import { normalizeLongitude, sampleWind } from './model.js';
// Imported by src/tools: the catalog-free marker, not src/i18n/index.js. The
// texts stay English data and the wind layer and layer panel show them
// translated.
/** Status of a scalar overlay the provider could not supply, by overlay. */
export const SCALAR_UNAVAILABLE = Object.freeze({
temperature: msg('Temperature field unavailable'),
pressure: msg('Mean sea level pressure field unavailable'),
});
/** Fixed physical scales shared by imagery and the field legend. */
export const WIND_FIELDS = Object.freeze({
speed: Object.freeze({
+3 -4
View File
@@ -7,17 +7,16 @@ import { orderWeatherImagery } from '../weather/imageryOrder.js';
import { NO_IMAGERY_HOST } from '../weather/imageryHost.js';
import { msg } from '../../i18n/index.js';
import { createWindGpuRendering } from './gpuRendering.js';
import { SCALAR_UNAVAILABLE } from './source.js';
import { advectParticle, sampleWind } from './model.js';
import {
createFieldRaster,
SCALAR_UNAVAILABLE,
trailEraseAlpha,
windTrailColor,
} from './fields.js';
// A scalar overlay that cannot be drawn, in English for diagnostics; the
// panel status shows it translated (index.js). The scalar texts are the ones
// the source reports. setOptions only accepts these overlays.
// English for diagnostics; the row status shows it translated (index.js). The
// scalar texts are the source's own; setOptions accepts only these overlays.
const FIELD_UNAVAILABLE = Object.freeze({
speed: msg('Wind speed field unavailable'),
...SCALAR_UNAVAILABLE,
+1 -6
View File
@@ -1,15 +1,10 @@
import { readWindBody } from '../../sources/windBody.js';
import { SCALAR_UNAVAILABLE } from './fields.js';
// Imported by src/tools: no i18n here. The texts stay English data and the
// wind layer and layer panel show them translated.
const msg = (text) => text;
/** Status of a scalar overlay the provider could not supply, by overlay. */
export const SCALAR_UNAVAILABLE = Object.freeze({
temperature: msg('Temperature field unavailable'),
pressure: msg('Mean sea level pressure field unavailable'),
});
/** Create a bounded, cancellable source for the same-origin wind provider. */
export function createWindSource({
fetchImpl = (...args) => globalThis.fetch(...args),
+1 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { setLocale } from '../../i18n/index.js';
import { createWindLayer } from './index.js';
test('stale wind forecast is marked in the UI language (K-40)', async () => {
test('stale wind forecast is marked in the UI language', async () => {
const rendering = Object.fromEntries(
['attach', 'start', 'stop', 'clear', 'destroy', 'setField'].map((n) => [
n,
+1 -1
View File
@@ -1,4 +1,4 @@
// K-38: wind source failures are English data (src/layers/wind/source.js) and
// Wind source failures are English data (src/layers/wind/source.js) and
// are shown in the UI language; the HTTP line keeps its status number.
import test from 'node:test';
import assert from 'node:assert/strict';
+1 -1
View File
@@ -39,7 +39,7 @@ async function serverManifestOnUpstreamFailure() {
return JSON.parse(body);
}
test('en: the upstream failure reason reads as before (K-17)', async () => {
test('en: the upstream failure reason reads as before', async () => {
const manifest = await serverManifestOnUpstreamFailure();
const layer = harness({ getSnapshot: async () => manifest });
await layer.update();