mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
refactor(i18n): key messages by English text; add Russian
Replace the semantic keys with the English source text, so code reads as the
UI does and English needs no catalog: t('Save keys'), tn(count, one, other)
for plurals, msg() for text stored in data, and bare data-i18n* markup that
translates the element's own English. Other languages are one flat JSON each.
- Simplify src/i18n: drop namespaces, the configurable locale pair
(GEV_DEFAULT_LOCALE / GEV_SECONDARY_LOCALE) and its build defines. Locale
comes from ?lang=, the stored choice, then browser languages.
- Add scripts/i18n.mjs (npm run i18n): extracts every message from t/tn/msg
and markup, and fails on missing, unused, placeholder-mismatched or
wrongly-shaped plural entries. npm test runs it via catalog.test.mjs.
- Add Russian (src/i18n/locales/ru.json, 833 messages), started from the
community catalog in #312 and reviewed against the current English.
- Fix data-i18n placed on parents that would wipe their children (cockpit
LEVEL and local weather cells, first-run EXPLORE MANUALLY tile), and a
doubled degree sign in the TRACK ACQUIRED signal.
- Rewrite docs/TRANSLATORS.md for the new workflow; drop sections the earlier
merge resurrected in docs/CURRENT-STATE.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
dfbaeafbd8
commit
13efd411b6
112 files changed
+4419
-4856
No files matched your search
@@ -57,8 +57,8 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
function hidePanel() {
|
||||
if (layerState.panel) {
|
||||
const markup = `<div class="military-awareness-standby">
|
||||
<strong>${layerState.enabled ? t('layers.awareness.standbyReady') : t('layers.awareness.standbyOff')}</strong>
|
||||
<span>${layerState.enabled ? t('layers.awareness.standbySelect') : t('layers.awareness.standbyEnable')}</span>
|
||||
<strong>${layerState.enabled ? t('CONTEXT READY') : t('GLOBAL CONTEXT OFF')}</strong>
|
||||
<span>${layerState.enabled ? t('SELECT A FLIGHT, VESSEL, OR MAPPED INSTALLATION') : t('ENABLE TO LOAD OBSERVED / MAPPED PROXIMITY')}</span>
|
||||
</div>`;
|
||||
layerState.panel.hidden = false;
|
||||
if (layerState.panelMarkup !== markup) {
|
||||
@@ -79,11 +79,10 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
const label = formatAwarenessLabel(item);
|
||||
const targetId = item.icao24 || item.mmsi || item.id;
|
||||
if (!targetId) {
|
||||
return `<li><span class="military-awareness-target unavailable" aria-label="${t('layers.awareness.unavailableAria')}">${escapeHtml(label)} <span>${formatAwarenessDistance(item.distanceM)}</span></span></li>`;
|
||||
return `<li><span class="military-awareness-target unavailable" aria-label="${t('Unavailable')}">${escapeHtml(label)} <span>${formatAwarenessDistance(item.distanceM)}</span></span></li>`;
|
||||
}
|
||||
const accessibleLabel =
|
||||
label === '—' ? t('layers.awareness.unavailableAria') : label;
|
||||
return `<li><button type="button" class="military-awareness-target" data-awareness-layer="${escapeHtml(cohort.id)}" data-awareness-id="${escapeHtml(targetId)}" aria-label="${escapeHtml(t('layers.awareness.focusAria', { label: accessibleLabel }))}">${escapeHtml(label)} <span>${formatAwarenessDistance(item.distanceM)}</span></button>${namedAreasHtml(item.memberNames)}</li>`;
|
||||
const accessibleLabel = label === '—' ? t('Unavailable') : label;
|
||||
return `<li><button type="button" class="military-awareness-target" data-awareness-layer="${escapeHtml(cohort.id)}" data-awareness-id="${escapeHtml(targetId)}" aria-label="${escapeHtml(t('Focus {label}', { label: accessibleLabel }))}">${escapeHtml(label)} <span>${formatAwarenessDistance(item.distanceM)}</span></button>${namedAreasHtml(item.memberNames)}</li>`;
|
||||
})
|
||||
.join('');
|
||||
const pageCount = Math.max(
|
||||
@@ -104,10 +103,10 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
|
||||
function navigationControlsHtml() {
|
||||
const canPrevious = layerState.navigationIndex > 0;
|
||||
return `<div class="military-awareness-controls" role="group" aria-label="${t('layers.awareness.controlsAria')}">
|
||||
<button type="button" data-awareness-action="previous" title="${t('layers.awareness.previousTitle')}"${canPrevious ? '' : ' disabled'}>${t('layers.awareness.previous')}</button>
|
||||
<button type="button" data-awareness-action="focus">${t('layers.awareness.focus')}</button>
|
||||
<button type="button" data-awareness-action="next" title="${t('layers.awareness.nextTitle')}"${parts.navigation.canNavigateNext() ? '' : ' disabled'}>${t('layers.awareness.next')}</button>
|
||||
return `<div class="military-awareness-controls" role="group" aria-label="${t('Global Context navigation')}">
|
||||
<button type="button" data-awareness-action="previous" title="${t('Previous — prior visited contact in the 250 km window')}"${canPrevious ? '' : ' disabled'}>${t('PREVIOUS')}</button>
|
||||
<button type="button" data-awareness-action="focus">${t('FOCUS')}</button>
|
||||
<button type="button" data-awareness-action="next" title="${t('Next — nearest unvisited contact in the 250 km window')}"${parts.navigation.canNavigateNext() ? '' : ' disabled'}>${t('NEXT')}</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -189,7 +188,7 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
${namedAreasHtml(subject.memberNames)}
|
||||
${navigationControlsHtml()}
|
||||
${cohorts.map(rowHtml).join('')}
|
||||
<p class="military-awareness-note" tabindex="-1" data-awareness-focus-continuation>${t('layers.awareness.note')}</p>`;
|
||||
<p class="military-awareness-note" tabindex="-1" data-awareness-focus-continuation>${t('Open-source mapped/observed context. Missing broadcasts, unloaded map areas, or unmapped sites are not evidence of absence.')}</p>`;
|
||||
panel.hidden = false;
|
||||
if (layerState.panelMarkup !== markup) {
|
||||
const focusSnapshot = captureAwarenessPanelFocus(panel);
|
||||
|
||||
@@ -138,7 +138,7 @@ export function createRendering({
|
||||
'--compass-rotation',
|
||||
`${-headingDeg}deg`,
|
||||
);
|
||||
layerState.compassHeading.textContent = t('layers.awareness.hdg', {
|
||||
layerState.compassHeading.textContent = t('HDG {value}°', {
|
||||
value: String(headingDeg).padStart(3, '0'),
|
||||
});
|
||||
layerState.compassHeading.style.left = `${geometry.centerX}px`;
|
||||
@@ -224,12 +224,12 @@ export function createRendering({
|
||||
)
|
||||
: null;
|
||||
const bearingText = Number.isFinite(bearing)
|
||||
? t('layers.awareness.brg', {
|
||||
? t('BRG {value}°', {
|
||||
value: String(Math.round(bearing)).padStart(3, '0'),
|
||||
})
|
||||
: t('layers.awareness.brgUnknown');
|
||||
: t('BRG —');
|
||||
const courseText = Number.isFinite(item.track)
|
||||
? ` · ${t('layers.awareness.crs', {
|
||||
? ` · ${t('CRS {value}°', {
|
||||
value: String(Math.round(item.track)).padStart(3, '0'),
|
||||
})}`
|
||||
: '';
|
||||
|
||||
@@ -19,10 +19,9 @@ const WITHIN_KM_RE = /^WITHIN (\d+) KM$/;
|
||||
|
||||
function localizeCoverageLabel(label) {
|
||||
if (typeof label !== 'string' || !label) return label;
|
||||
if (label === 'CURRENT VIEWPORT ONLY')
|
||||
return t('layers.awareness.coverage.viewport');
|
||||
if (label === 'CURRENT VIEWPORT ONLY') return t('CURRENT VIEWPORT ONLY');
|
||||
const within = WITHIN_KM_RE.exec(label);
|
||||
if (within) return t('layers.awareness.coverage.within', { km: within[1] });
|
||||
if (within) return t('WITHIN {km} KM', { km: within[1] });
|
||||
return label;
|
||||
}
|
||||
|
||||
@@ -94,7 +93,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
cohorts: [
|
||||
{
|
||||
id: 'flights',
|
||||
label: t('layers.awareness.cohort.flights'),
|
||||
label: t('Flights'),
|
||||
source: flightsState.stats.source || SOURCE_LABEL.flights,
|
||||
provenance: retainedProvenance(
|
||||
'flights',
|
||||
@@ -113,7 +112,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
},
|
||||
{
|
||||
id: 'military',
|
||||
label: t('layers.awareness.cohort.military'),
|
||||
label: t('Military flights'),
|
||||
source: militaryState.stats.source || SOURCE_LABEL.military,
|
||||
provenance: retainedProvenance(
|
||||
'military',
|
||||
@@ -132,7 +131,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
},
|
||||
{
|
||||
id: 'ais-live-vessels',
|
||||
label: t('layers.awareness.cohort.vessels'),
|
||||
label: t('AIS vessels'),
|
||||
source: vesselsState.stats.source || SOURCE_LABEL['ais-live-vessels'],
|
||||
provenance: retainedProvenance(
|
||||
'ais-live-vessels',
|
||||
@@ -147,7 +146,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
},
|
||||
{
|
||||
id: 'military-installations',
|
||||
label: t('layers.awareness.cohort.installations'),
|
||||
label: t('Mapped installations'),
|
||||
source:
|
||||
installationsState.stats.source ||
|
||||
SOURCE_LABEL['military-installations'],
|
||||
@@ -159,7 +158,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
),
|
||||
coverage:
|
||||
localizeCoverageLabel(installationsState.stats.coverageLabel) ||
|
||||
t('layers.awareness.coverage.viewport'),
|
||||
t('CURRENT VIEWPORT ONLY'),
|
||||
summary: parts.queries.summarizeInstallationViewport(
|
||||
installations,
|
||||
installationsState,
|
||||
|
||||
Vendored
+2
-2
@@ -68,10 +68,10 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
if (layerState._loading) {
|
||||
stats.loadingLabel =
|
||||
layerState._activeCityIds.size > 0
|
||||
? t('layers.bike.loadingSyncing', {
|
||||
? t('syncing {count} city feeds...', {
|
||||
count: layerState._activeCityIds.size,
|
||||
})
|
||||
: t('layers.bike.loadingScanning');
|
||||
: t('scanning nearby systems...');
|
||||
}
|
||||
if (layerState._error) stats.error = layerState._error;
|
||||
return stats;
|
||||
|
||||
@@ -24,8 +24,8 @@ export function createSelection({
|
||||
const stationLabel =
|
||||
stationName ||
|
||||
(record?.stationId
|
||||
? t('layers.bike.stationWithId', { id: record.stationId })
|
||||
: t('layers.bike.stationFallback'));
|
||||
? t('Station {id}', { id: record.stationId })
|
||||
: t('Station'));
|
||||
const bikes = Number.isFinite(record?.bikesAvailable)
|
||||
? record.bikesAvailable
|
||||
: '?';
|
||||
@@ -36,17 +36,18 @@ export function createSelection({
|
||||
|
||||
const lines = [
|
||||
stationLabel,
|
||||
t('layers.bike.card.capacity', { bikes, docks, capacity }),
|
||||
t('🚲 {bikes} avail · {docks} docks · {capacity} cap', {
|
||||
bikes,
|
||||
docks,
|
||||
capacity,
|
||||
}),
|
||||
];
|
||||
|
||||
// Append warnings for stations that are offline or partially non-operational
|
||||
const abnormal = [];
|
||||
if (record?.isInstalled === false)
|
||||
abnormal.push(t('layers.bike.warning.notInstalled'));
|
||||
if (record?.isRenting === false)
|
||||
abnormal.push(t('layers.bike.warning.notRenting'));
|
||||
if (record?.isReturning === false)
|
||||
abnormal.push(t('layers.bike.warning.notReturning'));
|
||||
if (record?.isInstalled === false) abnormal.push(t('⚠️ Not installed'));
|
||||
if (record?.isRenting === false) abnormal.push(t('⚠️ Not renting'));
|
||||
if (record?.isReturning === false) abnormal.push(t('⚠️ Not returning'));
|
||||
if (abnormal.length > 0) {
|
||||
lines.push(abnormal.join(' · '));
|
||||
}
|
||||
|
||||
@@ -48,6 +48,6 @@ export function headingHudToken(camera) {
|
||||
// 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('layers.cctv.summary.hdgEstimated', { value: hdg })
|
||||
: t('layers.cctv.summary.hdg', { value: hdg });
|
||||
? t('HDG {value}° (ESTIMATED)', { value: hdg })
|
||||
: t('HDG {value}°', { value: hdg });
|
||||
}
|
||||
@@ -20,10 +20,13 @@ export function createPresentation({
|
||||
const active = parts.selection.getActiveRecord();
|
||||
if (!active) {
|
||||
return layerState._records.length
|
||||
? t('layers.cctv.summary.standingBy', {
|
||||
count: layerState._records.length,
|
||||
})
|
||||
: t('layers.cctv.summary.empty');
|
||||
? t(
|
||||
'{count} CAMERAS STANDING BY · NO CAMERA SELECTED · CLICK A CAMERA TO ACTIVATE',
|
||||
{
|
||||
count: layerState._records.length,
|
||||
},
|
||||
)
|
||||
: t('No cameras available in catalog.');
|
||||
}
|
||||
|
||||
const area = parts.model.sectorAreaKm2(
|
||||
@@ -37,37 +40,33 @@ export function createPresentation({
|
||||
const calBadge = parts.calibration.deriveCalBadge(active.camera);
|
||||
|
||||
return [
|
||||
t('layers.cctv.summary.city', {
|
||||
t('{city} CCTV', {
|
||||
city: active.camera.city.toUpperCase(),
|
||||
}),
|
||||
`${active.camera.name.toUpperCase()}`,
|
||||
// A synthetic bearing (headingConfidence 'low', no human calibration)
|
||||
// is tagged so a hashed guess never reads as a surveyed facing (#639).
|
||||
headingHudToken(active.camera),
|
||||
t('layers.cctv.summary.fov', {
|
||||
t('FOV {value}°', {
|
||||
value: Math.round(active.camera.fovDeg),
|
||||
}),
|
||||
t('layers.cctv.summary.coverage', { value: area.toFixed(2) }),
|
||||
t('COVERAGE {value}km²', { value: area.toFixed(2) }),
|
||||
overlapCount > 0
|
||||
? t('layers.cctv.summary.overlap', { count: overlapCount })
|
||||
: t('layers.cctv.summary.isolated'),
|
||||
layerState._showProjection
|
||||
? t('layers.cctv.summary.projMonitor')
|
||||
: t('layers.cctv.summary.projOff'),
|
||||
layerState._coverageMode === 'viewshed'
|
||||
? t('layers.cctv.summary.viewshed')
|
||||
: null,
|
||||
t('layers.cctv.summary.cal', {
|
||||
? t('OVERLAP {count} cams', { count: overlapCount })
|
||||
: t('ISOLATED VIEW'),
|
||||
layerState._showProjection ? t('PROJ MONITOR') : t('PROJ OFF'),
|
||||
layerState._coverageMode === 'viewshed' ? t('VIEWSHED') : null,
|
||||
t('CAL {value}', {
|
||||
value: calBadge.replace('-', ' ').toUpperCase(),
|
||||
}),
|
||||
health?.sourceKind
|
||||
? t('layers.cctv.summary.src', {
|
||||
? t('SRC {value}', {
|
||||
value: String(health.sourceKind).toUpperCase(),
|
||||
})
|
||||
: t('layers.cctv.summary.src', {
|
||||
: t('SRC {value}', {
|
||||
value: String(active.camera.feedType || 'image').toUpperCase(),
|
||||
}),
|
||||
t('layers.cctv.summary.context', { value: viewBand.toUpperCase() }),
|
||||
t('{value} CONTEXT', { value: viewBand.toUpperCase() }),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
|
||||
@@ -116,7 +116,7 @@ export function createCards({
|
||||
if (!card.interactive) return card;
|
||||
return {
|
||||
...card,
|
||||
accessibilityLabel: t('layers.firms.card.focusAria', {
|
||||
accessibilityLabel: t('Focus fire detection {title}, {details}', {
|
||||
title: card.title,
|
||||
details: card.details.join(', '),
|
||||
}),
|
||||
|
||||
+13
-21
@@ -331,19 +331,17 @@ export function createModel({
|
||||
|
||||
function buildSelectedFireCard(fire, nowMs) {
|
||||
const meta = [
|
||||
t('layers.firms.card.confSuffix', {
|
||||
t('{value} conf', {
|
||||
value: confidenceBucket(fire.confidence),
|
||||
}),
|
||||
];
|
||||
if (fire.acqMs > 0) {
|
||||
const age = formatAge(nowMs - fire.acqMs);
|
||||
if (age) meta.push(t('layers.firms.card.ageSuffix', { value: age }));
|
||||
if (age) meta.push(t('{value} ago', { value: age }));
|
||||
}
|
||||
const sat = satelliteShortName(fire.satellite);
|
||||
meta.push(
|
||||
sat
|
||||
? `${fire.sensor || 'VIIRS'} ${sat}`
|
||||
: fire.sensor || t('layers.firms.card.sensorUnavailable'),
|
||||
sat ? `${fire.sensor || 'VIIRS'} ${sat}` : fire.sensor || t('sensor n/a'),
|
||||
);
|
||||
return {
|
||||
id: `selected-fire:${fireDetectionKey(fire)}`,
|
||||
@@ -353,11 +351,11 @@ export function createModel({
|
||||
cullPosition: fireCullPosition(fire),
|
||||
gapPx: frpPixelSize(fire.frp),
|
||||
accent: accentForSeverity(detectionColorStop(fire).name),
|
||||
title: t('layers.firms.card.fire', { frp: formatFrp(fire.frp) }),
|
||||
title: t('FIRE · {frp} MW', { frp: formatFrp(fire.frp) }),
|
||||
details: [
|
||||
meta.join(' · '),
|
||||
formatLatLon(fire.lat, fire.lon) +
|
||||
(fire.night ? ` · ${t('layers.firms.card.night')}` : ''),
|
||||
(fire.night ? ` · ${t('NIGHT')}` : ''),
|
||||
],
|
||||
selected: true,
|
||||
priority: Number.MAX_SAFE_INTEGER,
|
||||
@@ -390,7 +388,7 @@ export function createModel({
|
||||
cullPosition: candidate.cullPosition || candidate.position,
|
||||
gapPx: frpPixelSize(fire.frp),
|
||||
accent: accentForSeverity(detectionColorStop(fire).name),
|
||||
title: t('layers.firms.card.ambient', { frp: formatFrp(fire.frp) }),
|
||||
title: t('▲ {frp} MW', { frp: formatFrp(fire.frp) }),
|
||||
details: [meta.join(' · ')],
|
||||
selected: false,
|
||||
priority: Number(fire.frp) || 0,
|
||||
@@ -408,16 +406,11 @@ export function createModel({
|
||||
|
||||
function buildCellCard(candidate, nowMs) {
|
||||
const cell = candidate.cell;
|
||||
const noun =
|
||||
cell.count === 1
|
||||
? t('layers.firms.card.fireNoun')
|
||||
: t('layers.firms.card.firesNoun');
|
||||
const parts = [
|
||||
t('layers.firms.card.maxFrp', { value: formatFrp(cell.maxFrp) }),
|
||||
];
|
||||
const noun = cell.count === 1 ? t('FIRE') : t('FIRES');
|
||||
const parts = [t('max {value} MW', { value: formatFrp(cell.maxFrp) })];
|
||||
if (cell.newestAcqMs > 0) {
|
||||
const age = formatAge(nowMs - cell.newestAcqMs);
|
||||
if (age) parts.push(t('layers.firms.card.newAge', { value: age }));
|
||||
if (age) parts.push(t('new {value}', { value: age }));
|
||||
}
|
||||
return {
|
||||
id: `cell:${cell.latCell ?? 'x'}:${cell.lonCell ?? 'x'}`,
|
||||
@@ -503,12 +496,11 @@ export function createModel({
|
||||
/** Millisecond delta → "<1m ago" / "Xm ago" / "Xh ago" (fresh-feed readout). */
|
||||
|
||||
function formatAgoMinutes(deltaMs) {
|
||||
if (!Number.isFinite(deltaMs) || deltaMs < 0)
|
||||
return t('layers.meta.justNow');
|
||||
if (!Number.isFinite(deltaMs) || deltaMs < 0) return t('just now');
|
||||
const minutes = Math.floor(deltaMs / 60000);
|
||||
if (minutes < 1) return t('layers.firms.underMinuteAgo');
|
||||
if (minutes < 90) return t('layers.meta.minutesAgo', { count: minutes });
|
||||
return t('layers.meta.hoursAgo', { count: Math.round(minutes / 60) });
|
||||
if (minutes < 1) return t('<1m ago');
|
||||
if (minutes < 90) return t('{count}m ago', { count: minutes });
|
||||
return t('{count}h ago', { count: Math.round(minutes / 60) });
|
||||
}
|
||||
|
||||
/** Normalized 0..1 confidence → low/nominal/high display bucket. */
|
||||
|
||||
@@ -60,24 +60,24 @@ export function createQueries({
|
||||
getStats() {
|
||||
const now = Date.now();
|
||||
const staleText = layerState._lastUpdate
|
||||
? t('layers.firms.staleCached', {
|
||||
? t('STALE · cached {age}', {
|
||||
age:
|
||||
components.model.formatAge(now - layerState._lastUpdate) || '<1h',
|
||||
})
|
||||
: t('layers.status.stale');
|
||||
: t('STALE');
|
||||
let loadingLabel = '';
|
||||
if (layerState._loading) {
|
||||
loadingLabel = layerState._fires.length
|
||||
? t('layers.meta.refreshing')
|
||||
: t('layers.meta.loading');
|
||||
? t('refreshing...')
|
||||
: t('loading...');
|
||||
} else if (layerState._keyRequired) {
|
||||
loadingLabel = t('layers.firms.keyRequired');
|
||||
loadingLabel = t('KEY REQUIRED');
|
||||
} else if (layerState._stale) {
|
||||
loadingLabel = staleText;
|
||||
} else if (layerState._error) {
|
||||
loadingLabel = layerState._error;
|
||||
} else if (layerState._lastUpdate) {
|
||||
loadingLabel = t('layers.firms.liveUpdated', {
|
||||
loadingLabel = t('LIVE · updated {age}', {
|
||||
age: components.model.formatAgoMinutes(now - layerState._lastUpdate),
|
||||
});
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export function createQueries({
|
||||
// the operator can take.
|
||||
keyRequired: layerState._keyRequired,
|
||||
error: layerState._keyRequired
|
||||
? t('layers.firms.keyRequired')
|
||||
? t('KEY REQUIRED')
|
||||
: layerState._stale
|
||||
? staleText
|
||||
: layerState._error,
|
||||
|
||||
@@ -756,7 +756,7 @@ export function createTracking({
|
||||
const spd = info.velocity ? `${Math.round(info.velocity * 1.944)} kts` : '';
|
||||
const stale =
|
||||
flightState.records.missingPolls.get(icao24) || flightState.feed._backoff
|
||||
? t('layers.status.stale')
|
||||
? t('STALE')
|
||||
: '';
|
||||
const lines = [[cs, fl, spd, stale].filter(Boolean).join(' · ')];
|
||||
// Converted contacts report their class as TR-3B and nothing else — the
|
||||
|
||||
Vendored
+18
-17
@@ -5,7 +5,7 @@ import {
|
||||
DISTANCE_PREFILTER_MARGIN_M,
|
||||
ANCHOR_REFRESH_M,
|
||||
} from './policy.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { t, msg } from '../../i18n/index.js';
|
||||
|
||||
// installationFeedback.js composes its guidance inside the portable
|
||||
// node/browser graph (the server-side HUD summary reads the same module), so
|
||||
@@ -13,19 +13,20 @@ import { t } from '../../i18n/index.js';
|
||||
// boundary: map the closed message vocabulary to the catalog, passing any
|
||||
// live provider error through verbatim (docs/TRANSLATORS.md).
|
||||
const INSTALLATION_FEEDBACK_KEYS = Object.freeze({
|
||||
'Retrying mapped sites…': 'layers.installations.feedback.retrying',
|
||||
'Fetching mapped sites…': 'layers.installations.feedback.fetching',
|
||||
'Zoom in to search mapped installations':
|
||||
'layers.installations.feedback.zoomIn',
|
||||
'Showing cached mapped sites': 'layers.installations.feedback.cached',
|
||||
'Mapped sites not loaded': 'layers.installations.feedback.notLoaded',
|
||||
'Mapped sites loaded': 'layers.installations.feedback.loaded',
|
||||
'Overpass rate-limited': 'layers.installations.feedback.reason.rateLimited',
|
||||
'Overpass timed out': 'layers.installations.feedback.reason.timeout',
|
||||
'Overpass could not complete the query':
|
||||
'layers.installations.feedback.reason.queryFailed',
|
||||
'Overpass temporarily unavailable':
|
||||
'layers.installations.feedback.reason.unavailable',
|
||||
'Retrying mapped sites…': msg('Retrying mapped sites…'),
|
||||
'Fetching mapped sites…': msg('Fetching mapped sites…'),
|
||||
'Zoom in to search mapped installations': msg(
|
||||
'Zoom in to search mapped installations',
|
||||
),
|
||||
'Showing cached mapped sites': msg('Showing cached mapped sites'),
|
||||
'Mapped sites not loaded': msg('Mapped sites not loaded'),
|
||||
'Mapped sites loaded': msg('Mapped sites loaded'),
|
||||
'Overpass rate-limited': msg('Overpass rate-limited'),
|
||||
'Overpass timed out': msg('Overpass timed out'),
|
||||
'Overpass could not complete the query': msg(
|
||||
'Overpass could not complete the query',
|
||||
),
|
||||
'Overpass temporarily unavailable': msg('Overpass temporarily unavailable'),
|
||||
});
|
||||
|
||||
const INSTALLATION_RETRY_IN_RE = /^(.*) — retrying in (\d+)s$/;
|
||||
@@ -38,7 +39,7 @@ function localizeInstallationFeedback(message) {
|
||||
const retryIn = message.match(INSTALLATION_RETRY_IN_RE);
|
||||
if (retryIn) {
|
||||
const reasonKey = INSTALLATION_FEEDBACK_KEYS[retryIn[1]];
|
||||
return t('layers.installations.feedback.retryIn', {
|
||||
return t('{reason} — retrying in {seconds}s', {
|
||||
reason: reasonKey ? t(reasonKey) : retryIn[1],
|
||||
seconds: retryIn[2],
|
||||
});
|
||||
@@ -46,7 +47,7 @@ function localizeInstallationFeedback(message) {
|
||||
const pending = message.match(INSTALLATION_RETRY_PENDING_RE);
|
||||
if (pending) {
|
||||
const reasonKey = INSTALLATION_FEEDBACK_KEYS[pending[1]];
|
||||
return t('layers.installations.feedback.retryPending', {
|
||||
return t('{reason} — retry pending', {
|
||||
reason: reasonKey ? t(reasonKey) : pending[1],
|
||||
});
|
||||
}
|
||||
@@ -280,7 +281,7 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
}),
|
||||
),
|
||||
loadingLabel: layerState.loading
|
||||
? t('layers.installations.loading')
|
||||
? t('loading mapped installation context')
|
||||
: '',
|
||||
coverage: layerState.coverage,
|
||||
coverageLabel:
|
||||
|
||||
@@ -271,7 +271,7 @@ export function createRendering({
|
||||
entity.gevTrackedId = `installations:${record.id}`;
|
||||
entity.gevDisplayPosition = () => displayPosition;
|
||||
entity.gevLabelModel = {
|
||||
title: record.name || t('layers.installations.fallbackTitle'),
|
||||
title: record.name || t('MAPPED INSTALLATION'),
|
||||
details: [
|
||||
String(record.class || 'installation')
|
||||
.replaceAll('_', ' ')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { t, msg } from '../../i18n/index.js';
|
||||
import {
|
||||
createCyberSonarSampler,
|
||||
isCyberContactSonarActive as isCyberSonarActive,
|
||||
@@ -120,10 +120,10 @@ export function createOverlays({ state: layerState, services, parts, source }) {
|
||||
const details = selected
|
||||
? [
|
||||
siteName
|
||||
? t('layers.missions.launchSiteValue', {
|
||||
? t('LAUNCH SITE · {site}', {
|
||||
site: shortMissionLabel(siteName, 20).toUpperCase(),
|
||||
})
|
||||
: t('layers.missions.launchSite'),
|
||||
: t('LAUNCH SITE'),
|
||||
]
|
||||
: [];
|
||||
return {
|
||||
@@ -589,38 +589,37 @@ export function createOverlays({ state: layerState, services, parts, source }) {
|
||||
const mission = shortMissionLabel(launch.name, 22).toUpperCase();
|
||||
const siteName = compactLaunchSiteName(launch.launchSite);
|
||||
const siteCallout = siteName
|
||||
? t('layers.missions.launchSiteValue', {
|
||||
? t('LAUNCH SITE · {site}', {
|
||||
site: shortMissionLabel(siteName, 20).toUpperCase(),
|
||||
})
|
||||
: t('layers.missions.launchSite');
|
||||
: t('LAUNCH SITE');
|
||||
let title = mission;
|
||||
let detail = siteCallout;
|
||||
if (mode === 'countdown') {
|
||||
title = t('layers.missions.replay.countdown', {
|
||||
title = t('T−{time} · {mission}', {
|
||||
time: String(state.countdownSeconds).padStart(2, '0'),
|
||||
mission,
|
||||
});
|
||||
detail = `${t('layers.missions.replay.standbyDetail')}\n${siteCallout}`;
|
||||
detail = `${t('LAUNCH STANDBY')}\n${siteCallout}`;
|
||||
} else if (mode === 'ascent') {
|
||||
title =
|
||||
state.elapsedSinceStart < 1
|
||||
? t('layers.missions.replay.liftoff', { mission })
|
||||
? t('LIFTOFF · {mission}', { mission })
|
||||
: t(
|
||||
launch.trajectory.length > 1
|
||||
? 'layers.missions.replay.ascentReplay'
|
||||
: 'layers.missions.replay.ascentEstimate',
|
||||
? msg('ASCENT REPLAY · {mission}')
|
||||
: msg('ASCENT ESTIMATE · {mission}'),
|
||||
{ mission },
|
||||
);
|
||||
detail = parts.policyHelpers.formatMissionEventTime(state.eventTime);
|
||||
} else if (mode === 'recovery') {
|
||||
title = t('layers.missions.replay.recovery', { mission });
|
||||
title = t('STAGE RE-ENTRY / RECOVERY · {mission}', { mission });
|
||||
detail = parts.policyHelpers.formatMissionEventTime(state.eventTime);
|
||||
} else if (mode === 'orbit') {
|
||||
title = t('layers.missions.replay.orbit', { mission });
|
||||
title = t('ORBIT REPLAY · {mission}', { mission });
|
||||
detail = parts.policyHelpers.formatMissionEventTime(state.eventTime);
|
||||
}
|
||||
if (layerState._replayPaused)
|
||||
title = t('layers.missions.replay.pausedPrefix', { title });
|
||||
if (layerState._replayPaused) title = t('PAUSED · {title}', { title });
|
||||
const nextText = `${title}\n${detail}`;
|
||||
if (nextText !== layerState._replayVehicleOverlayText) {
|
||||
layerState._replayVehicleOverlayText = nextText;
|
||||
|
||||
@@ -220,12 +220,12 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
return `<tr><td>${escapeMissionText(payload.name)}${payload.amount > 1 ? ` ×${payload.amount}` : ''}${detail ? `<small>${escapeMissionText(detail)}</small>` : ''}</td><td>${escapeMissionText(payload.type || t('layers.missions.rows.unspecified'))}</td><td>${escapeMissionText(payload.destination || launch.orbit?.name || t('layers.missions.rows.unavailable'))}</td></tr>`;
|
||||
return `<tr><td>${escapeMissionText(payload.name)}${payload.amount > 1 ? ` ×${payload.amount}` : ''}${detail ? `<small>${escapeMissionText(detail)}</small>` : ''}</td><td>${escapeMissionText(payload.type || t('UNSPECIFIED'))}</td><td>${escapeMissionText(payload.destination || launch.orbit?.name || t('UNAVAILABLE'))}</td></tr>`;
|
||||
})
|
||||
: [];
|
||||
if (launch.payloads.length > 5) {
|
||||
payloadRows.push(
|
||||
`<tr><td colspan="3" class="mission-table-empty">${t('layers.missions.rows.additionalPayloads', { count: launch.payloads.length - 5 })}</td></tr>`,
|
||||
`<tr><td colspan="3" class="mission-table-empty">${t('+{count} additional payload records', { count: launch.payloads.length - 5 })}</td></tr>`,
|
||||
);
|
||||
}
|
||||
layerState._missionPanel.querySelector(
|
||||
@@ -233,7 +233,7 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
).innerHTML = missionTableRows(
|
||||
payloadRows,
|
||||
3,
|
||||
t('layers.missions.rows.classified'),
|
||||
t('PAYLOAD DATA UNAVAILABLE'),
|
||||
);
|
||||
const stageRows = launch.recoveryStages.map((stage) => {
|
||||
const endpoint = stage.endpoint;
|
||||
@@ -241,21 +241,21 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
stage.destination ||
|
||||
(endpoint?.accuracy === 'PAD / RTLS'
|
||||
? launch.launchSite
|
||||
: t('layers.missions.rows.unavailable'));
|
||||
: t('UNAVAILABLE'));
|
||||
const position = endpoint
|
||||
? `${endpoint.lat.toFixed(2)}, ${endpoint.lon.toFixed(2)} · ${endpoint.accuracy}`
|
||||
: stage.downrangeKm > 0
|
||||
? t('layers.missions.rows.kmDownrange', {
|
||||
? t('{value} KM DOWNRANGE', {
|
||||
value: stage.downrangeKm.toLocaleString(),
|
||||
})
|
||||
: t('layers.missions.rows.positionUnavailable');
|
||||
: t('POSITION UNAVAILABLE');
|
||||
const stageDetail = [
|
||||
Number.isFinite(stage.flightNumber)
|
||||
? t('layers.missions.rows.flightNumber', {
|
||||
? t('FLIGHT {count}', {
|
||||
count: stage.flightNumber,
|
||||
})
|
||||
: null,
|
||||
stage.reused ? t('layers.missions.rows.reused') : null,
|
||||
stage.reused ? t('REUSED') : null,
|
||||
stage.recoveryType,
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -263,7 +263,7 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
return `<tr><td>${escapeMissionText(stage.name)}${stageDetail ? `<small>${escapeMissionText(stageDetail)}</small>` : ''}</td><td>${escapeMissionText(stage.status)}</td><td>${escapeMissionText(destination)}<small>${escapeMissionText(position)}</small></td></tr>`;
|
||||
});
|
||||
layerState._missionPanel.querySelector('[data-mission-stages]').innerHTML =
|
||||
missionTableRows(stageRows, 3, t('layers.missions.rows.noStageData'));
|
||||
missionTableRows(stageRows, 3, t('NO STAGE RE-ENTRY / RECOVERY DATA'));
|
||||
const stageSection = layerState._missionPanel.querySelector(
|
||||
'[data-mission-stages-section]',
|
||||
);
|
||||
@@ -291,7 +291,7 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
'[data-mission-roster-count]',
|
||||
);
|
||||
if (count)
|
||||
count.textContent = t('layers.missions.roster.count', {
|
||||
count.textContent = t('{count} / 30D', {
|
||||
count: layerState._launches.length,
|
||||
});
|
||||
if (!list) return;
|
||||
@@ -301,7 +301,7 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
layerState._launches,
|
||||
);
|
||||
if (!entries.length) {
|
||||
list.innerHTML = `<div class="space-mission-roster-empty">${t('layers.missions.roster.empty')}</div>`;
|
||||
list.innerHTML = `<div class="space-mission-roster-empty">${t('NO MISSIONS AVAILABLE IN THE CURRENT 30-DAY WINDOW')}</div>`;
|
||||
restoreMissionRosterFocus(
|
||||
list,
|
||||
focusSnapshot,
|
||||
@@ -314,15 +314,12 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
list.innerHTML = entries
|
||||
.map(({ launch, index }) => {
|
||||
const color = parts.model.missionMarkerColor(launch).toCssColorString();
|
||||
const date =
|
||||
launch.launchTime?.slice(0, 10) ||
|
||||
t('layers.missions.roster.dateUnavailable');
|
||||
const provider =
|
||||
launch.provider || t('layers.missions.roster.unspecifiedOperator');
|
||||
const date = launch.launchTime?.slice(0, 10) || t('DATE UNAVAILABLE');
|
||||
const provider = launch.provider || t('UNSPECIFIED OPERATOR');
|
||||
const label = parts.overlays
|
||||
.shortMissionLabel(launch.name, 27)
|
||||
.toUpperCase();
|
||||
return `<button type="button" class="space-mission-roster-item" data-mission-roster-index="${index}" data-mission-roster-id="${escapeMissionText(launch.id)}" aria-label="${escapeMissionText(t('layers.missions.roster.selectAria', { mission: label }))}"><span class="space-mission-roster-marker" style="--mission-roster-color:${color}" aria-hidden="true"></span><span class="space-mission-roster-copy"><strong>${escapeMissionText(label)}</strong><small>${escapeMissionText(provider)} · ${escapeMissionText(date)}</small></span><span class="space-mission-roster-chevron" aria-hidden="true">›</span></button>`;
|
||||
return `<button type="button" class="space-mission-roster-item" data-mission-roster-index="${index}" data-mission-roster-id="${escapeMissionText(launch.id)}" aria-label="${escapeMissionText(t('Select {mission}', { mission: label }))}"><span class="space-mission-roster-marker" style="--mission-roster-color:${color}" aria-hidden="true"></span><span class="space-mission-roster-copy"><strong>${escapeMissionText(label)}</strong><small>${escapeMissionText(provider)} · ${escapeMissionText(date)}</small></span><span class="space-mission-roster-chevron" aria-hidden="true">›</span></button>`;
|
||||
})
|
||||
.join('');
|
||||
list.querySelectorAll('[data-mission-roster-index]').forEach((button) => {
|
||||
@@ -379,7 +376,7 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
setMissionPanelField(
|
||||
'[data-mission-distance]',
|
||||
Number.isFinite(altitudeM)
|
||||
? t('layers.missions.telemetry.km', {
|
||||
? t('{value} KM', {
|
||||
value: Math.max(0, altitudeM / 1000).toLocaleString(undefined, {
|
||||
maximumFractionDigits: 0,
|
||||
}),
|
||||
@@ -392,7 +389,7 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
setMissionPanelField(
|
||||
'[data-mission-speed]',
|
||||
Number.isFinite(speedMps)
|
||||
? t('layers.missions.telemetry.kmPerSecond', {
|
||||
? t('{value} KM/S', {
|
||||
value: (speedMps / 1000).toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
@@ -473,16 +470,16 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
layerState._missionPanel.className = 'context-space-mission-detail';
|
||||
layerState._missionPanel.setAttribute(
|
||||
'aria-label',
|
||||
t('layers.missions.panel.selectedHeader'),
|
||||
t('SELECTED SPACE MISSION'),
|
||||
);
|
||||
layerState._missionPanel.innerHTML = `<div class="space-mission-view-header"><span>${t('layers.missions.panel.selectedHeader')}</span><button type="button" data-mission-close title="${t('layers.missions.panel.showAllTitle')}" aria-label="${t('layers.missions.panel.deselectAria')}">×</button></div><div class="space-mission-detail"><strong data-mission-title>${t('layers.missions.panel.mission')}</strong><span data-mission-field data-mission-provider></span><span data-mission-field>${t('layers.missions.panel.status')}<b data-mission-status></b></span><span data-mission-field>${t('layers.missions.panel.launchSite')}<b data-mission-site></b></span><span data-mission-field>${t('layers.missions.panel.launchTime')}<b data-mission-time></b></span><span data-mission-field>${t('layers.missions.panel.orbit')}<b data-mission-orbit></b></span><span>${t('layers.missions.panel.ascentPath')}<b data-mission-ascent-source></b></span><span data-mission-field>${t('layers.missions.panel.distance')}<b data-mission-distance></b></span><span data-mission-field>${t('layers.missions.panel.speed')}<b data-mission-speed></b></span></div><section class="mission-data-section"><h4>${t('layers.missions.panel.payload')}</h4><div class="mission-table-scroll"><table class="mission-data-table"><thead><tr><th>${t('layers.missions.panel.colName')}</th><th>${t('layers.missions.panel.colType')}</th><th>${t('layers.missions.panel.colDestination')}</th></tr></thead><tbody data-mission-payloads></tbody></table></div></section><section class="mission-data-section" data-mission-stages-section><h4>${t('layers.missions.panel.stagesSection')}</h4><div class="mission-table-scroll"><table class="mission-data-table"><thead><tr><th>${t('layers.missions.panel.colStage')}</th><th>${t('layers.missions.panel.colStatus')}</th><th>${t('layers.missions.panel.colFinalPosition')}</th></tr></thead><tbody data-mission-stages></tbody></table></div></section><div class="mission-replay-speed-control"><div class="mission-replay-speed-header"><label for="space-mission-replay-speed">${t('layers.missions.replay.speedLabel')}</label><output class="gev-slider-value" for="space-mission-replay-speed" data-mission-replay-speed-output>1×</output></div><input id="space-mission-replay-speed" class="gev-quantitative-slider" type="range" min="0.25" max="4" step="0.25" value="1" data-mission-replay-speed aria-label="${t('layers.missions.replay.speedAria')}"><div class="mission-replay-speed-scale" aria-hidden="true"><span>0.25×</span><span>1×</span><span>4×</span></div></div><div class="mission-action-row"><button type="button" class="mission-focus-button" data-mission-focus>${t('layers.missions.panel.focus')}</button><button type="button" class="mission-replay-button" data-mission-replay aria-pressed="false">${t('layers.missions.replay.start')}</button></div><div class="space-mission-nav"><button type="button" class="mission-nav-button" data-mission-prev title="${t('layers.missions.panel.prevTitle')}"><span aria-hidden="true">‹</span> ${t('layers.missions.panel.prev')}</button><span class="mission-nav-index" data-mission-index>—</span><button type="button" class="mission-nav-button" data-mission-next title="${t('layers.missions.panel.nextTitle')}">${t('layers.missions.panel.next')} <span aria-hidden="true">›</span></button></div><button type="button" class="panel-layer-toggle" data-mission-show-all>${t('layers.missions.panel.showAll')}</button>`;
|
||||
layerState._missionPanel.innerHTML = `<div class="space-mission-view-header"><span>${t('SELECTED SPACE MISSION')}</span><button type="button" data-mission-close title="${t('Show all missions')}" aria-label="${t('Deselect mission')}">×</button></div><div class="space-mission-detail"><strong data-mission-title>${t('MISSION')}</strong><span data-mission-field data-mission-provider></span><span data-mission-field>${t('STATUS · ')}<b data-mission-status></b></span><span data-mission-field>${t('LAUNCH SITE · ')}<b data-mission-site></b></span><span data-mission-field>${t('LAUNCH TIME · ')}<b data-mission-time></b></span><span data-mission-field>${t('ORBIT · ')}<b data-mission-orbit></b></span><span>${t('ASCENT PATH · ')}<b data-mission-ascent-source></b></span><span data-mission-field>${t('CURRENT DISTANCE FROM EARTH · ')}<b data-mission-distance></b></span><span data-mission-field>${t('SATELLITE SPEED · ')}<b data-mission-speed></b></span></div><section class="mission-data-section"><h4>${t('PAYLOAD')}</h4><div class="mission-table-scroll"><table class="mission-data-table"><thead><tr><th>${t('NAME')}</th><th>${t('TYPE')}</th><th>${t('DESTINATION')}</th></tr></thead><tbody data-mission-payloads></tbody></table></div></section><section class="mission-data-section" data-mission-stages-section><h4>${t('STAGE / RE-ENTRY / RECOVERY')}</h4><div class="mission-table-scroll"><table class="mission-data-table"><thead><tr><th>${t('STAGE')}</th><th>${t('STATUS')}</th><th>${t('FINAL POSITION')}</th></tr></thead><tbody data-mission-stages></tbody></table></div></section><div class="mission-replay-speed-control"><div class="mission-replay-speed-header"><label for="space-mission-replay-speed">${t('REPLAY SPEED')}</label><output class="gev-slider-value" for="space-mission-replay-speed" data-mission-replay-speed-output>1×</output></div><input id="space-mission-replay-speed" class="gev-quantitative-slider" type="range" min="0.25" max="4" step="0.25" value="1" data-mission-replay-speed aria-label="${t('Replay speed multiplier')}"><div class="mission-replay-speed-scale" aria-hidden="true"><span>0.25×</span><span>1×</span><span>4×</span></div></div><div class="mission-action-row"><button type="button" class="mission-focus-button" data-mission-focus>${t('FOCUS')}</button><button type="button" class="mission-replay-button" data-mission-replay aria-pressed="false">${t('REPLAY ASCENT')}</button></div><div class="space-mission-nav"><button type="button" class="mission-nav-button" data-mission-prev title="${t('Previous mission')}"><span aria-hidden="true">‹</span> ${t('PREV')}</button><span class="mission-nav-index" data-mission-index>—</span><button type="button" class="mission-nav-button" data-mission-next title="${t('Next mission')}">${t('NEXT')} <span aria-hidden="true">›</span></button></div><button type="button" class="panel-layer-toggle" data-mission-show-all>${t('SHOW ALL / DESELECT')}</button>`;
|
||||
layerState._missionPanel
|
||||
.querySelector('.mission-action-row')
|
||||
.insertAdjacentHTML(
|
||||
'beforeend',
|
||||
`<div class="mission-replay-transport" data-mission-replay-transport hidden>
|
||||
<button type="button" data-mission-replay-toggle title="${t('layers.missions.replay.pause')}" aria-label="${t('layers.missions.replay.pause')}">Ⅱ</button>
|
||||
<button type="button" class="cancel" data-mission-replay-cancel title="${t('layers.missions.replay.cancel')}" aria-label="${t('layers.missions.replay.cancel')}"><span aria-hidden="true">×</span></button>
|
||||
<button type="button" data-mission-replay-toggle title="${t('Pause replay')}" aria-label="${t('Pause replay')}">Ⅱ</button>
|
||||
<button type="button" class="cancel" data-mission-replay-cancel title="${t('Cancel replay')}" aria-label="${t('Cancel replay')}"><span aria-hidden="true">×</span></button>
|
||||
</div>`,
|
||||
);
|
||||
host.appendChild(layerState._missionPanel);
|
||||
|
||||
@@ -522,7 +522,7 @@ export function createPaths({ state: layerState, services, parts, source }) {
|
||||
return {
|
||||
lat: Cesium.Math.toDegrees(lat),
|
||||
lon: Cesium.Math.toDegrees(Cesium.Math.negativePiToPi(lon)),
|
||||
accuracy: t('layers.missions.estDownrange'),
|
||||
accuracy: t('EST. DOWNRANGE'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -77,14 +77,14 @@ export function createPolicyHelpers({
|
||||
: 0;
|
||||
return {
|
||||
orbit: orbitName
|
||||
? `${orbitAllowed ? '' : t('layers.missions.rows.plannedPrefix')}${orbitName}`
|
||||
? `${orbitAllowed ? '' : t('PLANNED · ')}${orbitName}`
|
||||
: null,
|
||||
ascent:
|
||||
suppliedTrajectoryPoints > 1
|
||||
? t('layers.missions.rows.suppliedTrajectory')
|
||||
? t('SUPPLIED TRAJECTORY POINTS')
|
||||
: replayAvailable
|
||||
? t('layers.missions.rows.reconstructedEstimate')
|
||||
: t('layers.missions.rows.unavailable'),
|
||||
? t('RECONSTRUCTED ESTIMATE')
|
||||
: t('UNAVAILABLE'),
|
||||
replayAvailable: Boolean(replayAvailable),
|
||||
};
|
||||
}
|
||||
@@ -198,7 +198,7 @@ export function createPolicyHelpers({
|
||||
function formatMissionEventTime(launchTime) {
|
||||
const date = new Date(launchTime);
|
||||
if (!launchTime || !Number.isFinite(date.getTime()))
|
||||
return t('layers.missions.rows.unavailable');
|
||||
return t('UNAVAILABLE');
|
||||
return `${date.toISOString().slice(0, 10)}\n${date.toISOString().slice(11, 19)} UTC`;
|
||||
}
|
||||
|
||||
|
||||
@@ -201,10 +201,10 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
if (speedControl) speedControl.hidden = !replayAvailable;
|
||||
button.hidden = active || !replayAvailable;
|
||||
button.disabled = !replayAvailable;
|
||||
button.textContent = t('layers.missions.replay.start');
|
||||
button.textContent = t('REPLAY ASCENT');
|
||||
button.classList.remove('active');
|
||||
button.setAttribute('aria-pressed', String(active));
|
||||
button.title = t('layers.missions.replay.startTitle');
|
||||
button.title = t('Replay the estimated ascent with a following camera');
|
||||
if (transport) {
|
||||
transport.hidden = !active;
|
||||
transport.classList.toggle(
|
||||
@@ -219,13 +219,11 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
toggleButton.textContent = layerState._replayPaused ? '▶' : 'Ⅱ';
|
||||
toggleButton.setAttribute(
|
||||
'aria-label',
|
||||
layerState._replayPaused
|
||||
? t('layers.missions.replay.resume')
|
||||
: t('layers.missions.replay.pause'),
|
||||
layerState._replayPaused ? t('Resume replay') : t('Pause replay'),
|
||||
);
|
||||
toggleButton.title = layerState._replayPaused
|
||||
? t('layers.missions.replay.resume')
|
||||
: t('layers.missions.replay.pause');
|
||||
? t('Resume replay')
|
||||
: t('Pause replay');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,19 +234,19 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
);
|
||||
if (!transport || !layerState._replayCameraLaunchId) return;
|
||||
const phase = state.countdownActive
|
||||
? t('layers.missions.replay.phaseCountdown', {
|
||||
? t('T minus {seconds}', {
|
||||
seconds: state.countdownSeconds,
|
||||
})
|
||||
: state.preCountdownActive
|
||||
? t('layers.missions.replay.phasePreparing')
|
||||
? t('Preparing launch site')
|
||||
: state.elapsedSinceStart < 1
|
||||
? t('layers.missions.replay.phaseLiftoff')
|
||||
? t('Liftoff')
|
||||
: state.ascending
|
||||
? t('layers.missions.replay.phaseAscent')
|
||||
: t('layers.missions.replay.phaseOrbit');
|
||||
? t('Ascent replay')
|
||||
: t('Orbit replay');
|
||||
transport.setAttribute(
|
||||
'aria-label',
|
||||
`${phase}${layerState._replayPaused ? t('layers.missions.replay.pausedSuffix') : ''}`,
|
||||
`${phase}${layerState._replayPaused ? t(', paused') : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export function createQueries({
|
||||
*/
|
||||
|
||||
function _formatAltitude(altitudeFt) {
|
||||
if (!Number.isFinite(altitudeFt)) return t('layers.readout.altUnknown');
|
||||
if (!Number.isFinite(altitudeFt)) return t('Alt unknown');
|
||||
return `${Math.round(altitudeFt)} ft`;
|
||||
}
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ export function createTracking({
|
||||
function _buildTrackedLabel(info, icao24) {
|
||||
const stale =
|
||||
flightState.records.missingPolls.get(icao24) || flightState.feed._backoff
|
||||
? ` · ${t('layers.status.stale')}`
|
||||
? ` · ${t('STALE')}`
|
||||
: '';
|
||||
const callsign =
|
||||
(parts.queries._toCleanText(info?.callsign) ||
|
||||
@@ -230,14 +230,12 @@ export function createTracking({
|
||||
// what the Easter egg replaces the real type with.
|
||||
const type = tr3bTypeLabel(
|
||||
icao24,
|
||||
parts.queries._toCleanText(info?.type) || t('layers.readout.typeUnknown'),
|
||||
parts.queries._toCleanText(info?.type) || t('Type unknown'),
|
||||
);
|
||||
const registration =
|
||||
parts.queries._toCleanText(info?.registration) ||
|
||||
t('layers.readout.regUnknown');
|
||||
parts.queries._toCleanText(info?.registration) || t('Reg unknown');
|
||||
const operator =
|
||||
parts.queries._toCleanText(info?.operator) ||
|
||||
t('layers.readout.operatorUnknown');
|
||||
parts.queries._toCleanText(info?.operator) || t('Operator unknown');
|
||||
const altitude = parts.queries._formatAltitude(info?.altitudeFt);
|
||||
const speedKt = info?.speedMps ? Math.round(info.speedMps * 1.944) : null;
|
||||
const tail = speedKt ? `${altitude} · ${speedKt} kt` : altitude;
|
||||
|
||||
@@ -70,20 +70,20 @@ export function createCategories({
|
||||
function buildRadioCategories(stations) {
|
||||
const rows = Array.isArray(stations) ? stations : [];
|
||||
const categories = [
|
||||
{ id: 'all', label: t('layers.radio.filterAll') },
|
||||
{ id: 'news', label: t('layers.radio.category.news') },
|
||||
{ id: 'talk', label: t('layers.radio.category.talk') },
|
||||
{ id: 'weather', label: t('layers.radio.category.weather') },
|
||||
{ id: 'public-safety', label: t('layers.radio.category.publicSafety') },
|
||||
{ id: 'all', label: t('All') },
|
||||
{ id: 'news', label: t('News') },
|
||||
{ id: 'talk', label: t('Talk') },
|
||||
{ id: 'weather', label: t('Weather / Emergency') },
|
||||
{ id: 'public-safety', label: t('Public Safety') },
|
||||
{
|
||||
id: 'aviation-marine',
|
||||
label: t('layers.radio.category.aviationMarine'),
|
||||
label: t('Aviation / Marine'),
|
||||
},
|
||||
{
|
||||
id: 'traffic-transit',
|
||||
label: t('layers.radio.category.trafficTransit'),
|
||||
label: t('Traffic / Transit'),
|
||||
},
|
||||
{ id: 'music', label: t('layers.radio.category.music') },
|
||||
{ id: 'music', label: t('Music') },
|
||||
];
|
||||
|
||||
for (const [genre, label] of MUSIC_GENRES) {
|
||||
@@ -92,7 +92,7 @@ export function createCategories({
|
||||
categories.push({ id, label });
|
||||
}
|
||||
}
|
||||
categories.push({ id: 'other', label: t('layers.radio.category.other') });
|
||||
categories.push({ id: 'other', label: t('Other') });
|
||||
return categories.map((category) => ({
|
||||
...category,
|
||||
color: parts.model.radioCategoryColor(category.id),
|
||||
|
||||
Vendored
+13
-8
@@ -494,22 +494,27 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
const active =
|
||||
layerState._params.catalog === 'dense' &&
|
||||
layerState._denseStatus === 'ready';
|
||||
let title = t('layers.satellites.denseTitleAdd');
|
||||
if (loading) title = t('layers.satellites.denseTitleLoading');
|
||||
let title = t(
|
||||
'Add the full Starlink broadband shell (thousands of extra points)',
|
||||
);
|
||||
if (loading) title = t('Loading the Starlink shell…');
|
||||
else if (failed)
|
||||
title = t('layers.satellites.denseTitleFailed', {
|
||||
detail: layerState._denseError || t('layers.satellites.loadFailed'),
|
||||
title = t('Starlink {detail} — click to retry', {
|
||||
detail: layerState._denseError || t('load failed'),
|
||||
});
|
||||
else if (active) title = t('layers.satellites.denseTitleActive');
|
||||
else if (active)
|
||||
title = t(
|
||||
'Showing the full Starlink shell — click for the core catalog only',
|
||||
);
|
||||
return {
|
||||
chips: [
|
||||
{
|
||||
id: 'catalog',
|
||||
label: loading
|
||||
? t('layers.satellites.denseChipLoading')
|
||||
? t('DENSE ···')
|
||||
: failed
|
||||
? t('layers.satellites.denseChipFailed')
|
||||
: t('layers.satellites.denseChip'),
|
||||
? t('DENSE ✕')
|
||||
: t('DENSE'),
|
||||
active,
|
||||
busy: loading,
|
||||
disabled: loading,
|
||||
|
||||
@@ -12,7 +12,7 @@ export function createIngestion({ state, parts, source }) {
|
||||
|
||||
state._loading = true;
|
||||
state._error = null;
|
||||
state._loadingLabel = t('layers.meta.loading');
|
||||
state._loadingLabel = t('loading...');
|
||||
// Ownership token (the militaryAwareness activationId pattern): a
|
||||
// disable/abort followed by a fresh enable starts a NEWER load while this
|
||||
// one is still settling. The stale load must bail after every await and
|
||||
|
||||
+24
-16
@@ -261,12 +261,12 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
mode,
|
||||
error: null,
|
||||
loadingLabel: fetching
|
||||
? t('layers.traffic.syncingRoads', { source: roadSource })
|
||||
? t('Syncing flow · Roads: {source}', { source: roadSource })
|
||||
: coveragePct > 0
|
||||
? t('layers.traffic.liveRoadsFlowHidden', {
|
||||
? t('LIVE · Roads: {source} · Roads without flow hidden', {
|
||||
source: roadSource,
|
||||
})
|
||||
: t('layers.traffic.liveRoadsNoFlowInView', {
|
||||
: t('LIVE · Roads: {source} · No flow roads in view', {
|
||||
source: roadSource,
|
||||
}),
|
||||
};
|
||||
@@ -276,13 +276,13 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
mode,
|
||||
error: null,
|
||||
loadingLabel: fetching
|
||||
? t('layers.traffic.syncingRoads', { source: roadSource })
|
||||
? t('Syncing flow · Roads: {source}', { source: roadSource })
|
||||
: coveragePct > 0
|
||||
? t('layers.traffic.roadsFlowLive', {
|
||||
? t('LIVE · Roads: {source} · Flow {percent}%', {
|
||||
source: roadSource,
|
||||
percent: coveragePct,
|
||||
})
|
||||
: t('layers.traffic.roadsFlowSimulated', {
|
||||
: t('SIMULATED · Roads: {source} · Flow {percent}%', {
|
||||
source: roadSource,
|
||||
percent: coveragePct,
|
||||
}),
|
||||
@@ -293,16 +293,24 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
mode,
|
||||
error: null,
|
||||
loadingLabel: fetching
|
||||
? t('layers.traffic.loadingSyncing')
|
||||
? t(
|
||||
'Syncing flow · Roads: OpenStreetMap · Flow: TomTom · Unmatched: simulated',
|
||||
)
|
||||
: coveragePct > 0
|
||||
? coveragePct < 100
|
||||
? t('layers.traffic.liveCoverageUnmatched', {
|
||||
percent: coveragePct,
|
||||
})
|
||||
: t('layers.traffic.liveCoverage', {
|
||||
percent: coveragePct,
|
||||
})
|
||||
: t('layers.traffic.simulatedNoMatches'),
|
||||
? t(
|
||||
'LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov · Unmatched: simulated',
|
||||
{
|
||||
percent: coveragePct,
|
||||
},
|
||||
)
|
||||
: t(
|
||||
'LIVE · Roads: OpenStreetMap · Flow: TomTom · {percent}% cov',
|
||||
{
|
||||
percent: coveragePct,
|
||||
},
|
||||
)
|
||||
: t('SIMULATED · Roads: OpenStreetMap · Flow: TomTom (no matches)'),
|
||||
};
|
||||
}
|
||||
// Keyless simulation — one terse line that names the mode and the remedy
|
||||
@@ -312,8 +320,8 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
mode,
|
||||
error: null,
|
||||
loadingLabel: statusUnavailable
|
||||
? t('layers.traffic.simUnavailable')
|
||||
: t('layers.traffic.simKeyless'),
|
||||
? t('SIMULATED — traffic service unreachable')
|
||||
: t('SIMULATED — add TomTom key for live'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+11
-11
@@ -21,13 +21,13 @@ export function createCards({
|
||||
const stale = (record.missedRefreshes || 0) > 0;
|
||||
el.classList.add('active');
|
||||
el.textContent = [
|
||||
t('layers.vessel.hudName', { name: trimHudValue(record.name, 32) }),
|
||||
t('layers.vessel.hudTypeLine', {
|
||||
type: trimHudValue(record.type || t('layers.vessel.fallback'), 24),
|
||||
t('AIS: {name}', { name: trimHudValue(record.name, 32) }),
|
||||
t('{type} SPD: {speed} HDG: {heading}', {
|
||||
type: trimHudValue(record.type || t('VESSEL'), 24),
|
||||
speed: formatSpeed(record.speed),
|
||||
heading: formatHeading(record.heading ?? record.course),
|
||||
}),
|
||||
`MMSI: ${record.mmsi || '--'} ${formatPositionTime(record)}${stale ? ` · ${t('layers.status.stale')}` : ''}`,
|
||||
`MMSI: ${record.mmsi || '--'} ${formatPositionTime(record)}${stale ? ` · ${t('STALE')}` : ''}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function createCards({
|
||||
const el = document.getElementById('hud-ais-vessel');
|
||||
if (!el) return;
|
||||
el.classList.remove('active');
|
||||
el.textContent = t('layers.vessel.hudIdle');
|
||||
el.textContent = t('AIS: --');
|
||||
}
|
||||
|
||||
function trimHudValue(value, maxLength) {
|
||||
@@ -90,7 +90,7 @@ export function createCards({
|
||||
const direction = record.heading ?? record.course;
|
||||
const details = [
|
||||
[
|
||||
vesselTypeShort(record) || t('layers.vessel.fallback'),
|
||||
vesselTypeShort(record) || t('VESSEL'),
|
||||
formatSpeed(record.speed),
|
||||
Number.isFinite(direction) ? `${Math.round(direction)}°` : '--°',
|
||||
].join(' · '),
|
||||
@@ -99,7 +99,7 @@ export function createCards({
|
||||
if (destination) details.push(`→ ${trimHudValue(destination, 24)}`);
|
||||
const stale = (record.missedRefreshes || 0) > 0;
|
||||
details.push(
|
||||
`MMSI ${record.mmsi || '--'} · ${formatPositionTime(record)}${stale ? ` · ${t('layers.status.stale')}` : ''}`,
|
||||
`MMSI ${record.mmsi || '--'} · ${formatPositionTime(record)}${stale ? ` · ${t('STALE')}` : ''}`,
|
||||
);
|
||||
return {
|
||||
id: vesselOverlayEntryId(record),
|
||||
@@ -156,7 +156,7 @@ export function createCards({
|
||||
function displayVesselName(record) {
|
||||
const name = String(record.name || '').trim();
|
||||
if (name && name !== 'VESSEL' && name !== record.mmsi) return name;
|
||||
return record.mmsi ? `MMSI ${record.mmsi}` : t('layers.vessel.fallback');
|
||||
return record.mmsi ? `MMSI ${record.mmsi}` : t('VESSEL');
|
||||
}
|
||||
|
||||
function formatSpeed(speed) {
|
||||
@@ -168,10 +168,10 @@ export function createCards({
|
||||
}
|
||||
|
||||
function formatPositionTime(record) {
|
||||
if (!record.lastPositionUtc) return t('layers.vessel.posLive');
|
||||
if (!record.lastPositionUtc) return t('POS: LIVE');
|
||||
const date = new Date(record.lastPositionUtc);
|
||||
if (Number.isNaN(date.getTime())) return t('layers.vessel.posLive');
|
||||
return t('layers.vessel.posAt', { time: date.toISOString().slice(11, 19) });
|
||||
if (Number.isNaN(date.getTime())) return t('POS: LIVE');
|
||||
return t('POS: {time}Z', { time: date.toISOString().slice(11, 19) });
|
||||
}
|
||||
return {
|
||||
updateSelectedVesselHud,
|
||||
|
||||
@@ -38,7 +38,7 @@ export function createLifecycle({
|
||||
state.feed.firstConnectStartedAt = startedAt;
|
||||
state.feed.firstConnectDeadline = startedAt + AIS_FIRST_CONNECT_GRACE_MS;
|
||||
state.feed.error = null;
|
||||
state.feed.loadingLabel = t('layers.vessel.awaitingFirstPosition');
|
||||
state.feed.loadingLabel = t('awaiting first AIS position…');
|
||||
scheduleFirstConnectExpiry(sessionId, AIS_FIRST_CONNECT_GRACE_MS);
|
||||
}
|
||||
|
||||
@@ -60,8 +60,8 @@ export function createLifecycle({
|
||||
state.feed.firstConnectPhase = 'unavailable';
|
||||
state.feed.loadingLabel = '';
|
||||
state.feed.error = state.feed.lastMessageAt
|
||||
? t('layers.vessel.awaitingPositions')
|
||||
: t('layers.vessel.awaitingFirstMessage');
|
||||
? t('awaiting usable AIS positions…')
|
||||
: t('awaiting first AIS message…');
|
||||
state.feed.stale = state.feed.count > 0;
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { t } from '../../i18n/index.js';
|
||||
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.
|
||||
const FEED_STATE_LABEL_KEYS = Object.freeze({
|
||||
'refreshing...': 'layers.meta.refreshing',
|
||||
'loading...': 'layers.meta.loading',
|
||||
'awaiting first AIS position…': 'layers.vessel.awaitingFirstPosition',
|
||||
'refreshing...': msg('refreshing...'),
|
||||
'loading...': msg('loading...'),
|
||||
'awaiting first AIS position…': msg('awaiting first AIS position…'),
|
||||
});
|
||||
|
||||
function localizeFeedLabel(raw) {
|
||||
@@ -98,8 +98,8 @@ export function createQueries({
|
||||
if (acceptedRowCount > 0) return null; // accepted rows may be stale while reconnecting, but remain usable
|
||||
if (AIS_HEALTHY_STATUSES.has(status)) {
|
||||
return payload?.lastMessageAt
|
||||
? t('layers.vessel.awaitingPositions')
|
||||
: t('layers.vessel.awaitingFirstMessage');
|
||||
? t('awaiting usable AIS positions…')
|
||||
: t('awaiting first AIS message…');
|
||||
}
|
||||
if (!status) return null;
|
||||
const detail =
|
||||
@@ -158,9 +158,7 @@ export function createQueries({
|
||||
error: emptyCoverage
|
||||
? null
|
||||
: deriveAisFeedError(payload, acceptedRowCount) ||
|
||||
(acceptedRowCount === 0
|
||||
? t('layers.vessel.awaitingPositions')
|
||||
: null),
|
||||
(acceptedRowCount === 0 ? t('awaiting usable AIS positions…') : null),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -532,7 +530,7 @@ export function createQueries({
|
||||
lastUpdate: state.feed.lastUpdate,
|
||||
loading: state.feed.loading || waitingForFirstPosition,
|
||||
loadingLabel: waitingForFirstPosition
|
||||
? t('layers.vessel.awaitingFirstPosition')
|
||||
? t('awaiting first AIS position…')
|
||||
: localizeFeedLabel(state.feed.loadingLabel),
|
||||
error: state.feed.error,
|
||||
stale: state.feed.stale,
|
||||
|
||||
Reference in new issue
Block a user