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

+63 -15
View File
@@ -26,6 +26,7 @@ import {
getSceneAppendRecipeById,
} from './recipes.js';
import { sceneLayerPlan, sceneRequiresContextModeExit } from './scenePolicy.js';
import { t } from '../i18n/index.js';
import { createSceneDataPacks } from './dataPacks/controller.js';
import { createSceneInteractions } from './interactions.js';
import { createSceneSharing } from './sharing.js';
@@ -61,6 +62,31 @@ import {
SceneDocumentError,
} from '../director/document.js';
// Built-in recipe DISPLAY names. Recipe ids, the URL 'scene' param, stored
// project titles, and voice-matching titles all stay English (keep-English
// boundary + persisted data); only the presentation of an UNRENAMED built-in
// localizes, so an operator's renamed scene is never masked by the catalog.
const RECIPE_TITLE_KEYS = Object.freeze({
'flights-radar': 'setup.scenes.recipe.flightsRadar',
'orbital-watch': 'setup.scenes.recipe.orbitalWatch',
'thermal-threats': 'setup.scenes.recipe.thermalThreats',
'city-overload': 'setup.scenes.recipe.cityOverload',
'omniscience-pullback': 'setup.scenes.recipe.omnisciencePullback',
});
/**
* Display title for a scene: the localized recipe name while the stored title
* still matches the built-in verbatim, the stored title otherwise.
* @param {{id?: string, title?: string}} scene
* @returns {string}
*/
function displaySceneTitle(scene) {
const key = RECIPE_TITLE_KEYS[scene?.id];
if (!key) return scene?.title || '';
const builtin = SCENE_RECIPES.find((recipe) => recipe.id === scene.id);
return scene.title === builtin?.title ? t(key) : scene.title;
}
/** @constant {string} localStorage key for the serialized project */
const STORAGE_KEY = 'godsEyeView.sceneProject.v2';
const STORAGE_CHECKPOINT_KEY = 'godsEyeView.sceneProject.checkpoint.v1';
@@ -172,7 +198,7 @@ export class SceneDirector {
this._presentation = {
status: this._storageReadError
? 'Saved project could not be read; storage preserved. Import a valid file to resume saving.'
: 'Ready',
: t('setup.scenes.statusReady'),
progress: 0,
runtime: '',
playbackActive: false,
@@ -370,7 +396,10 @@ export class SceneDirector {
_shotOutcome(type, scene, shot, index = scene.shots.indexOf(shot)) {
if (type === 'shot-loaded')
this._presentation.status = `Loaded: ${scene.title} / ${shot.title}`;
this._presentation.status = t('setup.scenes.status.loaded', {
scene: scene.title,
shot: shot.title,
});
this._publish({
type,
sceneId: scene.id,
@@ -389,7 +418,12 @@ export class SceneDirector {
this._controls = new SceneControls({
subscribe: (listener) => this.subscribe(listener),
read: () => ({
scenes: this._project.scenes,
// Scene titles surface in the panel through SceneControls; built-in
// recipe titles display their localized name until renamed by the user.
scenes: this._project.scenes.map((scene) => ({
...scene,
title: displaySceneTitle(scene),
})),
selectedSceneId: this._selectedSceneId,
selectedShotId: this._selectedShotId,
running: this._running,
@@ -920,14 +954,16 @@ export class SceneDirector {
const camera = this.styleManager.getCameraState();
if (!camera) {
this._updateStatus('Cannot capture shot: camera not ready');
this._updateStatus(t('setup.scenes.status.captureCameraNotReady'));
return;
}
const shot = normalizeShot(
{
id: uid('shot'),
title: `Shot ${scene.shots.length + 1}`,
title: t('setup.scenes.status.shotTitleDefault', {
n: scene.shots.length + 1,
}),
durationSec: DEFAULT_SHOT_DURATION_SEC,
holdSec: DEFAULT_HOLD_SEC,
camera,
@@ -941,7 +977,12 @@ export class SceneDirector {
this._selectedShotId = shot.id;
this._saveProject();
this._shotOutcome('shot-captured', scene, shot);
this._updateStatus(`Captured: ${scene.title} / ${shot.title}`);
this._updateStatus(
t('setup.scenes.status.captured', {
scene: scene.title,
shot: shot.title,
}),
);
}
/**
@@ -954,7 +995,7 @@ export class SceneDirector {
const shot = scene.shots.find((item) => item.id === this._selectedShotId);
if (!shot) {
this._updateStatus('Select a shot first');
this._updateStatus(t('setup.scenes.status.selectShotFirst'));
return;
}
@@ -969,7 +1010,12 @@ export class SceneDirector {
this._saveProject();
this._shotOutcome('shot-updated', scene, shot);
this._updateStatus(`Updated: ${scene.title} / ${shot.title}`);
this._updateStatus(
t('setup.scenes.status.updated', {
scene: scene.title,
shot: shot.title,
}),
);
}
/**
@@ -1564,7 +1610,7 @@ export class SceneDirector {
this._claimingCamera = false;
}
if (claimed === false) {
this._updateStatus('Camera unavailable — exit cockpit first');
this._updateStatus(t('setup.scenes.status.cameraUnavailable'));
return false;
}
return true;
@@ -1699,7 +1745,7 @@ export class SceneDirector {
if (!queue.length) return { started: false, reason: 'scene-complete' };
}
if (!queue.length) {
this._updateStatus('No shots to run');
this._updateStatus(t('setup.scenes.status.noShotsToRun'));
return { started: false, reason: 'no-shots' };
}
@@ -1782,7 +1828,11 @@ export class SceneDirector {
releaseOnFinish: preview,
});
} catch (error) {
this._updateStatus(`Error: ${error.message || 'run failed'}`);
this._updateStatus(
t('setup.scenes.status.runError', {
message: error.message || 'run failed',
}),
);
this._logEvent('scene_run_error', {
message: error.message || 'unknown error',
});
@@ -1956,7 +2006,7 @@ export class SceneDirector {
link.click();
link.remove();
URL.revokeObjectURL(url);
this._presentation.status = 'Project exported';
this._presentation.status = t('setup.scenes.status.projectExported');
this._publish({ type: 'project-exported', project: this._project });
}
@@ -2218,9 +2268,7 @@ export class SceneDirector {
`[Scenes] Could not exit ${mode}:`,
result.error || 'unknown reason',
);
this._updateStatus(
`Could not exit ${mode} — scene layers may be refused`,
);
this._updateStatus(t('setup.scenes.status.contextExitFailed', { mode }));
this._logEvent('context_mode_exit_failed', {
mode,
error: result.error || null,
+9 -2
View File
@@ -1,4 +1,6 @@
/** Connect the portable Director runner to the existing scene presentation. */
import { t } from '../i18n/index.js';
export function createScenePlaybackAdapter(director, defaultShotDurationSec) {
let timing;
let travel;
@@ -14,7 +16,12 @@ export function createScenePlaybackAdapter(director, defaultShotDurationSec) {
director._renderSceneSelect();
director._renderShotList();
director._updateStatus(
`Running ${index + 1}/${total}: ${scene.title} / ${shot.title}`,
t('setup.scenes.status.runningShot', {
index: index + 1,
total,
scene: scene.title,
shot: shot.title,
}),
);
director._logEvent('shot_start', {
sceneId: scene.id,
@@ -71,7 +78,7 @@ export function createScenePlaybackAdapter(director, defaultShotDurationSec) {
},
complete() {
director._setProgress(1);
director._updateStatus('Scene run complete');
director._updateStatus(t('setup.scenes.status.runComplete'));
director._logEvent('scene_run_complete', {});
},
};