feat(i18n): presentation extraction — t() call sites, data-i18n templates, locale selector

Route every application-owned presentation string through the i18n
core: t() call sites across the app/layers/ui/data/voice surfaces,
data-i18n* attributes in the src/ui/templates/*.html markup (applied at
boot by applyDocumentTranslations from src/standalone/application.js),
and the dock locale selector (runtime-rendered from the offered pair —
a single EN button while only English ships).

Rebased onto current main, where the extraction follows the module
splits: the shell wiring now lands in the owner modules
(panelChrome, visualSettings, locationNavigation, shareRestoration,
shellFeedback, applicationShell) and the voice status corpus in the
realtime* modules; scene status lives in director.js +
playbackAdapter.js.

Modules shared with the node runtime (loadingFeedback,
installationFeedback, regionalModel, keySetupCore, vessels ingestion)
stay environment-neutral English and localize at their browser render
sites (chip maps in shellFeedback/installations controls, the weather
table in cockpitBriefing, keySetupRequirementEnvVars compositions), so
the package-boundary graph stays clean; the browser boundary groups
now own the i18n modules they reach (scripts/package-boundaries.json).
This commit is contained in:
Meir Michanie committed 2026-10-01 12:41:20 +02:00
1 parent bde1e430ef
commit 284c4d4601
98 files changed
+3474 -1625

No files matched your search

+57 -6
View File
@@ -9,6 +9,51 @@ import {
reduceTrafficSyncFeedback,
} from '../loadingFeedback.js';
import { setSplitFlapText, disposeSplitFlap } from '../splitFlap.js';
import { t } from '../i18n/index.js';
// loadingFeedback.js composes the shared status copy inside the portable
// node/browser graph (the server-side HUD summary reads the same modules), so
// its labels stay English there. This chip is the presentation boundary: map
// the closed label vocabulary to the catalog, and pass anything unrecognized
// (a live provider error) through verbatim (docs/TRANSLATORS.md).
const STATUS_LABEL_KEYS = Object.freeze({
'LOADING LIVE DATA': 'shell.status.loadingLiveData',
'LOAD COMPLETE': 'shell.status.loadComplete',
'LOAD CANCELLED': 'shell.status.loadCancelled',
'LOAD FAILED': 'shell.status.loadFailed',
'LIVE DATA OFF': 'shell.status.liveDataOff',
'MAPPED SITES LOADED': 'shell.status.mappedSitesLoaded',
'RETRYING MAPPED SITES': 'shell.status.retryingMappedSites',
'FETCHING MAPPED SITES': 'shell.status.fetchingMappedSites',
'TURNING OFF LIVE DATA': 'shell.status.turningOffLiveData',
'REFRESHING LIVE DATA': 'shell.status.refreshingLiveData',
'syncing road network': 'shell.status.trafficSyncing',
});
function localizeStatusLabel(label) {
if (typeof label !== 'string' || !label) return label;
const key = STATUS_LABEL_KEYS[label.trim()];
return key ? t(key) : label;
}
const RETRY_DETAIL_RE = /^(?:ALPR cameras · )?retrying in (\d+)s$/;
function localizeStatusDetail(detail) {
if (typeof detail !== 'string' || !detail) return detail;
const match = detail.match(RETRY_DETAIL_RE);
if (match) return t('shell.status.retryingIn', { seconds: match[1] });
if (detail.trim() === 'retry pending') return t('shell.status.retryPending');
return detail;
}
function localizeLoadingPresentation(presentation) {
if (!presentation) return presentation;
return {
...presentation,
label: localizeStatusLabel(presentation.label),
detail: localizeStatusDetail(presentation.detail),
};
}
export class ShellFeedback {
constructor({ readLayers }) {
this.readLayers = readLayers;
@@ -76,10 +121,15 @@ export class ShellFeedback {
// the flap keeps textContent equal to the settled label throughout, so
// this stays a no-op on the repeat ticks exactly as it did before.
if (presentation.label)
setSplitFlapText(this._trafficSyncLabel, presentation.label, {
// Completion is a timing signal, not another animated loading stage.
immediate: !presentation.busy,
});
if (presentation.label)
setSplitFlapText(
this._trafficSyncLabel,
localizeStatusLabel(presentation.label),
{
// Completion is a timing signal, not another animated loading stage.
immediate: !presentation.busy,
},
);
// Written on every change INCLUDING the empty settled value — the reducer
// clears the progress number once the sync lands, and a truthiness guard
// here would strand the last "..." beside the settled label.
@@ -120,6 +170,7 @@ export class ShellFeedback {
if (this._loadingFeedbackState?.phase !== 'idle' || noticeNeedsTicker) {
this._armLoadingFeedbackTicker();
}
const localized = localizeLoadingPresentation(presentation);
this._globalLoadingStatus.hidden = !presentation;
if (!presentation) {
delete this._globalLoadingStatus.dataset.state;
@@ -131,8 +182,8 @@ export class ShellFeedback {
// here: this runs on every 60 ms and 500 ms tick. The detail line is the
// live layer roster inside an ellipsised, width-capped span — flapping a
// list that churns as layers join would be noise, not delight.
setSplitFlapText(this._globalLoadingLabel, presentation.label);
this._globalLoadingDetail.textContent = presentation.detail;
setSplitFlapText(this._globalLoadingLabel, localized.label);
this._globalLoadingDetail.textContent = localized.detail;
}
_showGlobalStatusNotice(message, options = {}) {