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

+126 -2
View File
@@ -36,6 +36,9 @@ import { ShellFeedback } from './shellFeedback.js';
import { runCctvLayerEnableTransition } from '../cctvFocusPolicy.js';
import { getLocale, persistLocaleAndReload, t } from '../i18n/index.js';
import { availableLocales } from '../i18n/locale.js';
/**
* Central UI orchestrator for the God's Eye View application.
*
@@ -565,6 +568,8 @@ export class StyleManager extends ShellFacade {
this._initUI();
this._initMapStackControl();
this._initPanelChrome();
this._initLocaleSelector();
this._applyRuntimeStaticHeaderText();
this._initLeftPanelAdaptiveLayout();
this._initRightPanelAdaptiveLayout();
this._initRadioPanel();
@@ -697,6 +702,109 @@ export class StyleManager extends ShellFacade {
this._displayBindings._initUI();
}
/**
* Renders the command-dock locale switch (runtime-built since the
* configurable-pair stage): one button per locale availableLocales()
* yields — the configured GEV_DEFAULT_LOCALE / GEV_SECONDARY_LOCALE pair,
* already deduped against the always-shipped English fallback. Labels are
* uppercase locale codes (EN/ES/FR); aria-labels come from
* shell.locale.<code>.ariaLabel in the active catalog. Pressed state is
* synced from the resolved locale at boot; a click persists the choice and
* reloads the page, so no live re-apply of document translations is needed
* here (persistLocaleAndReload strips ?lang and keeps the hash).
* @returns {void}
*/
_initLocaleSelector() {
const container = document.querySelector(
'#control-panel .dock-locale-switch',
);
if (!container) return;
const active = getLocale();
for (const locale of availableLocales()) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'dock-locale-btn';
button.dataset.locale = locale;
button.textContent = locale.toUpperCase();
button.setAttribute('aria-label', t(`shell.locale.${locale}.ariaLabel`));
const isActive = locale === active;
button.classList.toggle('active', isActive);
button.setAttribute('aria-pressed', String(isActive));
button.addEventListener('click', () => {
persistLocaleAndReload(button.dataset.locale);
});
container.appendChild(button);
}
}
/**
* Rewrites a header's trailing text node in place, preserving the leading
* dock-label-icon span that a textContent write would destroy (the phase-2
* "key-only" headers: .panel-title / .pp-header-label share their element
* with the icon, so no data-i18n attribute could target them — see
* docs/TRANSLATORS.md, runtime-only static sites).
* @param {Element|null} element
* @param {string} text
* @returns {void}
*/
_setHeaderText(element, text) {
if (!element) return;
const textNode = Array.from(element.childNodes)
.reverse()
.find(
(node) => node.nodeType === Node.TEXT_NODE && node.nodeValue?.trim(),
);
if (textNode) textNode.nodeValue = text;
else element.textContent = text;
}
/**
* Localizes the key-only dock/panel headers at boot: VISUAL PRESETS,
* LOCATION, and the DISPLAY rail label. English output is byte-identical to
* the static markup these replace.
* @returns {void}
*/
_applyRuntimeStaticHeaderText() {
this._setHeaderText(
document.querySelector('#control-panel .panel-title'),
t('cockpit.presets.title'),
);
this._setHeaderText(
document.querySelector('#location-bar .location-toolbar-label'),
t('cockpit.location.toolbarLabel'),
);
this._setHeaderText(
document.querySelector('#pp-toggles .pp-header-label'),
t('cockpit.display.title'),
);
// The cockpit context kicker ('CONTACT') is visible on the map itself, and
// the briefing kicker shares its element with the live-dot <i>; both are
// phase-2 key-only sites (see docs/TRANSLATORS.md). The briefing
// text node keeps renderBriefPage's leading-space convention.
this._setHeaderText(
document.querySelector('.cockpit-context-kicker'),
t('cockpit.context.kicker'),
);
// The phase-2 key-only standby description: one span holds BOTH mode
// descriptions split by a literal <br>, so no attribute write was possible
// without destroying the markup (docs/TRANSLATORS.md). Split into
// two keys and write them at boot, preserving the <br>.
const standbyDescription = document.querySelector(
'#context-mode-standby span',
);
if (standbyDescription) {
standbyDescription.replaceChildren(
document.createTextNode(t('cockpit.context.standbyContactsDesc')),
document.createElement('br'),
document.createTextNode(t('cockpit.context.standbyMissionsDesc')),
);
}
const briefKicker = document.querySelector('#cockpit-brief-kicker');
if (briefKicker) {
this._setHeaderText(briefKicker, ` ${t('cockpit.brief.kicker')}`);
}
}
/**
* Renders the owner-approved map stack chip row from the matching controller
* entries. Cesium ion/Bing chips remain keyboard-focusable but unavailable,
@@ -705,6 +813,18 @@ export class StyleManager extends ShellFacade {
*/
_initMapStackControl() {
if (!this.mapStackController) return;
// Key-only i18n sites (mapStackChips.test.mjs pins the static markup
// verbatim, so no data-i18n* attribute can be added): localize at this
// runtime write instead. The locale resolves before UI init and never
// changes without a reload, so an init-time write is sufficient.
if (this._mapSourceLabel)
this._mapSourceLabel.textContent = t('cockpit.presets.mapSourceLabel');
this._mapStackChips.setAttribute(
'aria-label',
t('cockpit.presets.mapSourceChipsAriaLabel'),
);
this._mapSourceControls?.destroy();
this._mapSourceControls = createMapSourceControls({
container: this._mapStackChips,
@@ -986,7 +1106,7 @@ export class StyleManager extends ShellFacade {
const { cctvLayer } = this.services;
if (this._disposed) return false;
if (!this._dataManager || !this._dataManager.layers?.has('cctv')) {
this._showToast('CCTV layer unavailable');
this._showToast(t('cockpit.cctv.toastLayerUnavailable'));
return false;
}
const enabled = this._dataManager.isEnabled('cctv');
@@ -1433,7 +1553,11 @@ export class StyleManager extends ShellFacade {
this._lifetime.listen(this._shareBtn, 'click', async () => {
const success = await this.shareLinkManager.copyLink();
if (!this._disposed)
this._showToast(success ? 'Link copied!' : 'Copy failed');
this._showToast(
success
? t('cockpit.share.toastCopied')
: t('cockpit.share.toastCopyFailed'),
);
});
}