mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
i18n: fix the repeat-review findings
- Status pieces translate in one place (src/ui/statusText.js): a Local
ADS-B error with a single piece now translates, and the analyst card
splits empty-layer notes into pieces (Transit, Local ADS-B).
- Translate the ALPR retry chip text, the ECCC error on the Air quality
card, "Upstream response too large" and the "entry {n}" feed label.
- The unmarked-text check also sees error/loadingLabel/statusMessage/
status assignments, `status:` properties and showToast?.( /
this._showToast( calls; the scene import preview marks its pack
statuses, data and model-facing text carry i18n-ignore with a reason.
- Tests check the real code: the HUD locality formatter moves to
src/hudLocalityFormat.js, the unsaved-keys test pins the teardown.
- One copy each: city names come from src/cityPois.js, the GNSS HTTP
template and wind "field unavailable" texts; dead fallbacks removed.
- Leftovers of the reverted vessel-label change and the reformatting of
detectionHost.test.mjs / keySetup.test.mjs are back to main.
Catalogs: es/ru 2617 messages each, complete.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a2463ca2d2
commit
3157888780
60 files changed
+1577
-1606
No files matched your search
@@ -1,11 +1,11 @@
|
||||
// N-02: the Air quality layer (merged from main, AQHI) is fully English in
|
||||
// es/ru: layer name, Weather card (title, coverage, status, legend, risk) and
|
||||
// its own failure text. Place: src/layers/airQuality/airQualityLocale.test.mjs
|
||||
// The Air quality layer (AQHI) reads in the UI language: layer name, Weather
|
||||
// card (title, coverage, status, legend, risk) and its own failure text.
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { setLocale, t } from '../../i18n/index.js';
|
||||
import { layerNameForId, localizeLayerName } from '../../ui/layerNames.js';
|
||||
import { LayerPanel } from '../../ui/layerPanel.js';
|
||||
import { translateFeedError } from '../../ui/statusText.js';
|
||||
import { createAirQualityLayer } from './index.js';
|
||||
|
||||
function inLocale(locale, run) {
|
||||
@@ -89,3 +89,20 @@ for (const locale of ['es', 'ru']) {
|
||||
assert.ok(!row.includes('Air quality request failed'), row);
|
||||
});
|
||||
}
|
||||
|
||||
for (const locale of ['en', 'es', 'ru']) {
|
||||
test(`${locale}: the Weather card shows an HTTP failure as the row does`, async () => {
|
||||
const layer = harness({
|
||||
getSnapshot: async () => {
|
||||
throw new Error('ECCC HTTP 503');
|
||||
},
|
||||
});
|
||||
await layer.update();
|
||||
const card = inLocale(locale, () => layer.getRowControls().summary.status);
|
||||
assert.equal(
|
||||
card,
|
||||
inLocale(locale, () => translateFeedError('ECCC HTTP 503')),
|
||||
);
|
||||
assert.equal(card, locale === 'en' ? 'ECCC HTTP 503' : 'ECCC: HTTP 503');
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { msg, t, tn, translateKnown } from '../../i18n/index.js';
|
||||
import { msg, t, tn } from '../../i18n/index.js';
|
||||
import { translateFeedError } from '../../ui/statusText.js';
|
||||
import {
|
||||
AIR_QUALITY_BANDS,
|
||||
AIR_QUALITY_OVERLAY_SOURCE_ID,
|
||||
@@ -216,8 +217,9 @@ export function createAirQualityLayer({ source, overlayHost } = {}) {
|
||||
0,
|
||||
);
|
||||
// Readings and errors stay English data (analyst records, stats); the
|
||||
// card shows them in the UI language. A provider's own error text has
|
||||
// no catalog entry and shows as it came.
|
||||
// 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.
|
||||
return {
|
||||
readout: true,
|
||||
summary: {
|
||||
@@ -226,7 +228,7 @@ export function createAirQualityLayer({ source, overlayHost } = {}) {
|
||||
validTime: newest ? new Date(newest).toISOString() : undefined,
|
||||
detail: `${provider.network} · ${provider.scale}`,
|
||||
status: _lastError
|
||||
? translateKnown(_lastError)
|
||||
? translateFeedError(_lastError)
|
||||
: _count === 0
|
||||
? _lastUpdate === null
|
||||
? t('No readings available')
|
||||
@@ -249,7 +251,7 @@ export function createAirQualityLayer({ source, overlayHost } = {}) {
|
||||
id: reading.id,
|
||||
ordinal: index + 1,
|
||||
lead: `${reading.scale} ${reading.label}`,
|
||||
text: `${reading.name} · ${reading.risk ? t(reading.risk) : reading.risk}`,
|
||||
text: `${reading.name} · ${t(reading.risk)}`,
|
||||
accent: reading.color,
|
||||
})),
|
||||
},
|
||||
|
||||
@@ -132,6 +132,7 @@ export function createTracking({
|
||||
icao24,
|
||||
// Honesty cue: the contact is coasting on dead reckoning, so the
|
||||
// narrated position/velocity are last-known rather than live.
|
||||
// i18n-ignore: voice-context data, English for the model
|
||||
status: described.stale ? 'stale (missed polls)' : 'live',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { formatNumber, msg, t, translateKnown } from '../../i18n/index.js';
|
||||
import { formatNumber, msg, t } from '../../i18n/index.js';
|
||||
import { translateFeedError } from '../../ui/statusText.js';
|
||||
import {
|
||||
GNSS_LEVEL_COLORS,
|
||||
GNSS_MIN_AIRCRAFT,
|
||||
@@ -31,14 +32,6 @@ const LEVEL_LABELS = Object.freeze({
|
||||
high: msg('Low-accuracy score > 10%'),
|
||||
});
|
||||
|
||||
/** A source failure (English data, source.js) in the UI language. */
|
||||
function shownSourceError(text) {
|
||||
const http = /^GNSS feed unavailable \(HTTP (\d{3})\)$/.exec(text);
|
||||
return http
|
||||
? t('GNSS feed unavailable (HTTP {status})', { status: http[1] })
|
||||
: translateKnown(text);
|
||||
}
|
||||
|
||||
/** Degrees at the centre of the current view, or null before the camera settles. */
|
||||
export function defaultGnssViewAnchor(viewer) {
|
||||
const canvas = viewer?.scene?.canvas;
|
||||
@@ -385,7 +378,7 @@ export function createGnssIntegrityLayer({
|
||||
'Regional sample · 250 nm radius · cells from visited views over 30 min',
|
||||
) +
|
||||
(_lastError && _retryAt
|
||||
? ` · ${shownSourceError(_lastError)} · ${
|
||||
? ` · ${translateFeedError(_lastError)} · ${
|
||||
_retryAt <= 8640000000000000
|
||||
? t('Retrying automatically at {time} UTC', {
|
||||
time: new Date(_retryAt).toISOString().slice(11, 19),
|
||||
|
||||
@@ -42,7 +42,7 @@ export function createAdsbGnssSource({
|
||||
throw Object.assign(
|
||||
new Error(
|
||||
// The status code form is matched by FEED_ERROR_PATTERNS
|
||||
// (ui/layerPanel.js) and gnss/index.js.
|
||||
// (ui/statusText.js) and gnss/index.js.
|
||||
limited
|
||||
? msg('GNSS feed rate limited (HTTP 429)')
|
||||
: `GNSS feed unavailable (HTTP ${response.status})`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// N-03: the GNSS source's own failures are English data (source.js); the
|
||||
// The GNSS source's own failures are English data (source.js); the
|
||||
// card's info line shows them in the UI language, the HTTP form included.
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
@@ -192,6 +192,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',
|
||||
launchTime: Number.isFinite(date)
|
||||
? new Date(date).toISOString()
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { LAYER_SOURCE } from './policy.js';
|
||||
|
||||
// Marks English that the Layers panel translates when it shows the status.
|
||||
// Texts with a number or a feed name stay English data too; the panel shows
|
||||
// them through STATUS_PIECE_PATTERNS (ui/layerPanel.js), piece by piece.
|
||||
// them through STATUS_PIECE_PATTERNS (ui/statusText.js), piece by piece.
|
||||
const msg = (text) => text;
|
||||
|
||||
/**
|
||||
@@ -75,6 +75,7 @@ function webUsbStatus(receiver, heard, feedState) {
|
||||
const rate = Number.isFinite(receiver.messagesPerSecond)
|
||||
? `${receiver.messagesPerSecond} msg/s`
|
||||
: msg('listening');
|
||||
// i18n-ignore: English data, shown through STATUS_PIECE_PATTERNS
|
||||
return { status: 'streaming', loadingLabel: `${heard} heard · ${rate}` };
|
||||
}
|
||||
|
||||
@@ -117,6 +118,7 @@ export function localAdsbStatus({ receiver, feedState, heard }) {
|
||||
return {
|
||||
source,
|
||||
status: 'streaming',
|
||||
// i18n-ignore: English data, shown through STATUS_PIECE_PATTERNS
|
||||
loadingLabel: `${live.length} ${plural(live.length, 'feed', 'feeds')} live · ${heardText}${rate}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// N-10: the Local ADS-B row's working texts ("3 heard · 5.8 msg/s",
|
||||
// "2 feeds live · 14 heard", "listening", "USB error", feed problems) have no
|
||||
// catalog entries and stay English in es/ru. Place: src/layers/localAdsb/statusLocale.test.mjs
|
||||
// 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.
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { setLocale } from '../../i18n/index.js';
|
||||
|
||||
@@ -125,6 +125,7 @@ export function createTracking({
|
||||
icao24,
|
||||
// Honesty cue: the contact is coasting on dead reckoning, so the
|
||||
// narrated position/velocity are last-known rather than live.
|
||||
// i18n-ignore: voice-context data, English for the model
|
||||
status: described.stale ? 'stale (missed polls)' : 'live',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@ 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/layerPanel.js).
|
||||
// shows it through FEED_ERROR_PATTERNS (ui/statusText.js).
|
||||
layerState._lastError = failed.length
|
||||
? `${failed.length} CelesTrak group${failed.length === 1 ? '' : 's'} unavailable`
|
||||
: null;
|
||||
|
||||
@@ -477,7 +477,7 @@ export function createIngestion({ state, services, parts, source }) {
|
||||
error?.message || error,
|
||||
);
|
||||
// English data; the panel shows it through FEED_ERROR_PATTERNS
|
||||
// (ui/layerPanel.js).
|
||||
// i18n-ignore: translated on display (ui/statusText.js)
|
||||
status.error = `${feed.name} feed unavailable`;
|
||||
status.retryInSec = Number.isFinite(error?.retryInSec)
|
||||
? error.retryInSec
|
||||
|
||||
@@ -278,9 +278,10 @@ export function createQueries({ state, parts }) {
|
||||
: {}),
|
||||
degraded: count > 0 && health.degraded,
|
||||
loading: health.loading && count === 0,
|
||||
// English data; the panel shows it through LOADING_LABEL_PATTERNS
|
||||
// (ui/layerPanel.js).
|
||||
// English data; the panel shows it through STATUS_PIECE_PATTERNS
|
||||
// (ui/statusText.js).
|
||||
loadingLabel:
|
||||
// i18n-ignore: translated on display
|
||||
health.loading && count === 0
|
||||
? `Loading ${active.map((feed) => feed.name).join(', ')}`
|
||||
: undefined,
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// The speed unit of a vessel's detection callout follows the UI language.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import aisLiveVesselsLayer, {
|
||||
_setVesselOverlayHostForTest,
|
||||
_setVesselStateForTest,
|
||||
_updateVesselCardsForTest,
|
||||
} from '../../data/aisLiveVessels.js';
|
||||
import { setLocale } from '../../i18n/index.js';
|
||||
|
||||
const record = (mmsi, name, screen, z = 0) => ({
|
||||
name,
|
||||
mmsi,
|
||||
type: 'Cargo',
|
||||
destination: '',
|
||||
speed: 9.5,
|
||||
course: 80,
|
||||
heading: 80,
|
||||
lastPositionUtc: '',
|
||||
missedRefreshes: 0,
|
||||
position: { x: 0, y: 0, z, screen },
|
||||
billboard: null,
|
||||
});
|
||||
|
||||
function detectable(cameraZ) {
|
||||
const carded = record('200', 'GOLDEN BEAR', { x: 40, y: 30 });
|
||||
const plain = record('300', 'BAY PILOT', { x: 41, y: 31 });
|
||||
const originalProjection = Cesium.SceneTransforms.worldToWindowCoordinates;
|
||||
Cesium.SceneTransforms.worldToWindowCoordinates = (_scene, position) =>
|
||||
position.screen;
|
||||
_setVesselOverlayHostForTest({
|
||||
setEntries() {},
|
||||
setVisible() {},
|
||||
clearSource() {},
|
||||
});
|
||||
_setVesselStateForTest({
|
||||
viewer: {
|
||||
scene: {
|
||||
canvas: { clientWidth: 1600, clientHeight: 900 },
|
||||
camera: { positionWC: new Cesium.Cartesian3(0, 0, cameraZ) },
|
||||
},
|
||||
},
|
||||
records: [carded, plain],
|
||||
billboardCollection: { show: true, remove() {} },
|
||||
});
|
||||
try {
|
||||
// Both share one 118 px grid cell, so only one of them gets a card.
|
||||
_updateVesselCardsForTest([carded, plain]);
|
||||
return Object.fromEntries(
|
||||
aisLiveVesselsLayer
|
||||
.getDetectableObjects()
|
||||
.map((object) => [object.id, object]),
|
||||
);
|
||||
} finally {
|
||||
Cesium.SceneTransforms.worldToWindowCoordinates = originalProjection;
|
||||
_setVesselStateForTest({ enabled: false });
|
||||
_setVesselOverlayHostForTest();
|
||||
}
|
||||
}
|
||||
|
||||
test('ru: the callout speed unit is translated', () => {
|
||||
setLocale('ru');
|
||||
try {
|
||||
const objects = detectable(4_000_000);
|
||||
assert.equal(objects['GOLDEN BEAR'].metric, '10 уз');
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
assert.equal(detectable(4_000_000)['GOLDEN BEAR'].metric, '10 kn');
|
||||
});
|
||||
@@ -69,7 +69,6 @@ export function createVesselState({ source, services }) {
|
||||
/** Sprites whose animated emphasis remains outside the 1.0 deadband. */
|
||||
activeFocusCount: 0,
|
||||
activeLabelCount: 0,
|
||||
/** @type {Set<Object>} Records that have an ambient card this pass. */
|
||||
selectedRecord: null,
|
||||
/** @type {{setPositions: Function, clear: Function, destroy: Function}|null} Selected-vessel fading trail */
|
||||
trail: null,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// N-09: a scalar overlay that cannot be drawn reports a marked English status
|
||||
// A scalar overlay that cannot be drawn reports a marked English status
|
||||
// (rendering.js), which the Wind row shows through t() in the UI language.
|
||||
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 { 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`, () => {
|
||||
@@ -19,3 +20,10 @@ for (const locale of ['es', 'ru']) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('only a known overlay has an unavailable status', () => {
|
||||
assert.equal(fieldUnavailableText('constructor'), null);
|
||||
assert.equal(fieldUnavailableText('none'), null);
|
||||
for (const [overlay, text] of Object.entries(SCALAR_UNAVAILABLE))
|
||||
assert.equal(fieldUnavailableText(overlay), text);
|
||||
});
|
||||
@@ -205,6 +205,7 @@ export function createWindGpuRendering({ cesium: C, getViewer }) {
|
||||
} catch (error) {
|
||||
clear();
|
||||
diagnostics.error =
|
||||
// i18n-ignore: GPU diagnostics only; the panel never shows this field
|
||||
error instanceof Error ? error.message : 'Wind geometry unavailable';
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ export function createWindLayer({
|
||||
model: model === 'ifs' ? 'ECMWF' : 'GFS',
|
||||
validTime:
|
||||
formatWindValidTime(manifest?.cycle?.validIso) || 'Unavailable',
|
||||
// i18n-ignore: English reading for AI tools; the card status uses t()
|
||||
status: loading
|
||||
? 'Loading forecast'
|
||||
: error ||
|
||||
|
||||
@@ -7,6 +7,7 @@ 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,
|
||||
@@ -16,16 +17,17 @@ import {
|
||||
|
||||
// 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 (source.js).
|
||||
// the source reports. setOptions only accepts these overlays.
|
||||
const FIELD_UNAVAILABLE = Object.freeze({
|
||||
speed: msg('Wind speed field unavailable'),
|
||||
temperature: msg('Temperature field unavailable'),
|
||||
pressure: msg('Mean sea level pressure field unavailable'),
|
||||
...SCALAR_UNAVAILABLE,
|
||||
});
|
||||
|
||||
/** English status for a scalar overlay with no drawable field. */
|
||||
export function fieldUnavailableText(overlay) {
|
||||
return FIELD_UNAVAILABLE[overlay] || `${overlay} field unavailable`;
|
||||
return Object.hasOwn(FIELD_UNAVAILABLE, overlay)
|
||||
? FIELD_UNAVAILABLE[overlay]
|
||||
: null;
|
||||
}
|
||||
const WIND_FIELD_FADE_HIGH_METERS = 1_200_000;
|
||||
const WIND_FIELD_FADE_LOW_METERS = 200_000;
|
||||
|
||||
@@ -4,6 +4,12 @@ import { readWindBody } from '../../sources/windBody.js';
|
||||
// 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),
|
||||
@@ -44,10 +50,7 @@ export function createWindSource({
|
||||
const optionalUnavailable =
|
||||
overlay !== 'none' &&
|
||||
scalar === undefined &&
|
||||
scalarError ===
|
||||
(overlay === 'temperature'
|
||||
? msg('Temperature field unavailable')
|
||||
: msg('Mean sea level pressure field unavailable'));
|
||||
scalarError === SCALAR_UNAVAILABLE[overlay];
|
||||
const scalarValid =
|
||||
overlay === 'none'
|
||||
? scalar === undefined && scalarError === undefined
|
||||
|
||||
Reference in new issue
Block a user