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

+38 -19
View File
@@ -16,12 +16,20 @@ import { createSurfaceKeyboard } from './ui/surfaceKeyboard.js';
* the chip and the dialog are removed outright.
*/
/** Chip label — pure, exported for tests. */
import { t, hasMessage } from './i18n/index.js';
/**
* Chip label — pure, exported for tests. The exact English values are pinned
* by keySetup.test.mjs; t() resolves them from the catalog, so the default
* locale renders byte-identical text and 'es' localizes at the same site.
* @param {{total?: number, setCount?: number}|null} status
* @returns {string}
*/
export function keySetupChipLabel(status) {
const missing = Math.max(0, (status?.total || 0) - (status?.setCount || 0));
return missing > 0
? `POWER UP · ${missing} ${missing === 1 ? 'KEY' : 'KEYS'} WAITING`
: 'POWERED UP';
? t('setup.keySetup.chipWaiting', { count: missing })
: t('setup.keySetup.chipReady');
}
/**
@@ -113,7 +121,10 @@ function buildRow(documentRef, key) {
const unlocks = documentRef.createElement('p');
unlocks.className = 'key-setup-unlocks';
unlocks.textContent = key.unlocks;
// The registry `unlocks` text localizes per registry id; an id missing from
// the catalog falls back to the server-provided English copy.
const unlocksKey = `setup.keySetup.unlocks.${key.id}`;
unlocks.textContent = hasMessage(unlocksKey) ? t(unlocksKey) : key.unlocks;
row.append(head, unlocks);
if (!external) {
@@ -138,8 +149,8 @@ function buildRow(documentRef, key) {
remove.type = 'button';
remove.className = 'key-setup-remove';
remove.dataset.keySetupRemove = JSON.stringify(key.envVars);
remove.textContent = 'REMOVE';
remove.title = `Remove ${key.title} from this app's saved keys`;
remove.textContent = t('setup.keySetup.row.remove');
remove.title = t('setup.keySetup.row.removeTitle', { title: key.title });
fields.append(remove);
}
row.append(fields);
@@ -284,17 +295,19 @@ export async function initKeySetup({
const storeLabel = () =>
status?.store === 'pinokio-environment'
? 'your app configuration'
: 'your local .env';
? t('setup.keySetup.store.pinokio')
: t('setup.keySetup.store.env');
const submitUpdates = async (updates, doneVerb) => {
// doneMessageKey names the whole saved/removed sentence; the storage
// destination is interpolated so the {store} phrase stays one spelling.
const submitUpdates = async (updates, doneMessageKey) => {
if (disposed || busy) return;
const googleWasUnset = !status?.keys?.find(
(key) => key.id === 'google-maps',
)?.set;
busy = true;
applyButton?.setAttribute('aria-disabled', 'true');
say('Saving…');
say(t('setup.keySetup.status.saving'));
try {
const response = await doFetch('/api/setup/keys', {
method: 'POST',
@@ -305,7 +318,11 @@ export async function initKeySetup({
const payload = await response.json().catch(() => ({}));
if (disposed) return;
if (!response.ok || !payload.ok) {
say(payload.error || `Save failed (${response.status}).`);
// payload.error is a server-authored message (API value) — shown as-is.
say(
payload.error ||
t('setup.keySetup.status.saveFailed', { status: response.status }),
);
return;
}
for (const input of root.querySelectorAll('input[data-env-var]'))
@@ -331,11 +348,13 @@ export async function initKeySetup({
signal: lifetime.signal,
});
}
say(
`${doneVerb} ${storeLabel()}. Restarting — this page reloads itself.`,
);
say(t(doneMessageKey, { store: storeLabel() }));
} catch (error) {
say(`Save failed: ${error?.message || error}`);
say(
t('setup.keySetup.status.saveFailedDetail', {
detail: error?.message || error,
}),
);
} finally {
busy = false;
applyButton?.setAttribute('aria-disabled', 'false');
@@ -352,10 +371,10 @@ export async function initKeySetup({
})),
);
if (!Object.keys(updates).length) {
say('Paste at least one key first.');
say(t('setup.keySetup.status.pasteFirst'));
return;
}
await submitUpdates(updates, 'Saved to');
await submitUpdates(updates, 'setup.keySetup.status.saved');
};
chip.addEventListener('click', openDialog);
@@ -377,11 +396,11 @@ export async function initKeySetup({
// a deliberate two-step the lure cannot pre-satisfy.
const ok =
typeof globalThis.confirm !== 'function' ||
globalThis.confirm('Remove this key from your saved configuration?');
globalThis.confirm(t('setup.keySetup.confirm.remove'));
if (!ok) return;
void submitUpdates(
Object.fromEntries(envVars.map((name) => [name, null])),
'Removed from',
'setup.keySetup.status.removed',
);
});