mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
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:
1 parent
bde1e430ef
commit
284c4d4601
98 files changed
+3474
-1625
No files matched your search
+13
-11
@@ -1,3 +1,5 @@
|
||||
import { t } from '../i18n/index.js';
|
||||
|
||||
/** Build the voice control independently of its connection backend. */
|
||||
export function createVoiceControl({ reset = false } = {}) {
|
||||
let root = document.getElementById('gev-voice-control');
|
||||
@@ -12,14 +14,14 @@ export function createVoiceControl({ reset = false } = {}) {
|
||||
root.dataset.speaker = 'idle';
|
||||
root.innerHTML = `
|
||||
<div class="gev-voice-heading">
|
||||
<div class="gev-voice-kicker">AI AGENT</div>
|
||||
<div id="gev-voice-status">OFF</div>
|
||||
<div class="gev-voice-kicker">${t('setup.voice.kicker.agent')}</div>
|
||||
<div id="gev-voice-status">${t('setup.voice.status.idle')}</div>
|
||||
<div class="gev-voice-cost">
|
||||
<button id="gev-voice-tier" class="gev-voice-tier-btn" type="button" aria-pressed="false" title="Voice model tier — applies next session">STD</button>
|
||||
<span id="gev-voice-cost-value" class="gev-voice-cost-value" data-level="ok" title="Estimated session cost">~$0.00</span>
|
||||
<button id="gev-voice-tier" class="gev-voice-tier-btn" type="button" aria-pressed="false" title="${t('setup.voice.tier.buttonTitle')}">STD</button>
|
||||
<span id="gev-voice-cost-value" class="gev-voice-cost-value" data-level="ok" title="${t('setup.voice.cost.buttonTitle')}">~$0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
<button id="gev-voice-button" type="button" aria-label="Voice control — activate to toggle voice; hold Space to speak" aria-describedby="gev-voice-help">
|
||||
<button id="gev-voice-button" type="button" aria-label="${t('setup.voice.button.ariaLabel')}" aria-describedby="gev-voice-help">
|
||||
<span class="gev-mic-orbit"><img src="/mic.svg" alt="" /></span>
|
||||
<span class="gev-mic-label">ON/OFF</span>
|
||||
</button>
|
||||
@@ -27,19 +29,19 @@ export function createVoiceControl({ reset = false } = {}) {
|
||||
${Array.from({ length: 15 }, (_, index) => `<span style="--bar:${index}"></span>`).join('')}
|
||||
</div>
|
||||
<div class="gev-voice-readout">
|
||||
<div id="gev-voice-detail">VOICE STANDBY</div>
|
||||
<div id="gev-voice-detail">${t('setup.voice.detail.standby')}</div>
|
||||
</div>
|
||||
<div id="gev-voice-help" class="gev-voice-help-tray" role="tooltip">
|
||||
<span class="gev-voice-help-kicker">VOICE CONTROL</span>
|
||||
<span class="gev-voice-help-detail">Hold Space to speak · tap Space to activate focused controls</span>
|
||||
<span class="gev-voice-help-kicker">${t('setup.voice.kicker.control')}</span>
|
||||
<span class="gev-voice-help-detail">${t('setup.voice.hint.default')}</span>
|
||||
</div>
|
||||
<div class="gev-voice-error-tray" role="alert" aria-live="assertive">
|
||||
<div class="gev-voice-error-header">
|
||||
<span>VOICE SYSTEM ERROR</span>
|
||||
<button class="gev-voice-error-dismiss" type="button">DISMISS</button>
|
||||
<span>${t('setup.voice.error.trayTitle')}</span>
|
||||
<button class="gev-voice-error-dismiss" type="button">${t('setup.voice.error.dismiss')}</button>
|
||||
</div>
|
||||
<div id="gev-voice-error-detail"></div>
|
||||
<div class="gev-voice-error-hint">Check microphone permission and network access, then try again.</div>
|
||||
<div class="gev-voice-error-hint">${t('setup.voice.error.hint')}</div>
|
||||
</div>
|
||||
`;
|
||||
const commandDock = document.getElementById('command-dock');
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '../i18n/index.js';
|
||||
|
||||
const DISCONNECT_GRACE_MS = 6000;
|
||||
|
||||
function releaseStartResources({ localStream = null, localPc = null } = {}) {
|
||||
@@ -59,7 +61,7 @@ export class RealtimeConnection {
|
||||
this.input.pushToTalkKeyHeld = pushToTalkKeyHeld;
|
||||
this.input.spaceKeyHeld = spaceKeyHeld;
|
||||
if (!window.RTCPeerConnection || !navigator.mediaDevices?.getUserMedia) {
|
||||
this.setStatus('error', 'WebRTC microphone support unavailable');
|
||||
this.setStatus('error', t('setup.voice.detail.microphoneUnavailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -78,7 +80,7 @@ export class RealtimeConnection {
|
||||
// — this is what "applies next session" means.
|
||||
this.cost.prepareSession();
|
||||
this.syncCostUi();
|
||||
this.setStatus('connecting', 'Requesting microphone');
|
||||
this.setStatus('connecting', t('setup.voice.detail.requestingMicrophone'));
|
||||
this.debugLog('session.starting', {
|
||||
epoch,
|
||||
tier: this.cost.voiceTier,
|
||||
@@ -176,9 +178,9 @@ export class RealtimeConnection {
|
||||
if (!ownsChannel()) return;
|
||||
const detail = this.input.pushToTalkMode
|
||||
? this.input.pushToTalkKeyHeld
|
||||
? 'Release Space to send'
|
||||
: 'Hold Space to talk'
|
||||
: 'Ask or command';
|
||||
? t('setup.voice.detail.releaseSpaceSend')
|
||||
: t('setup.voice.detail.holdSpaceTalk')
|
||||
: t('setup.voice.detail.askOrCommand');
|
||||
this.setStatus('listening', detail);
|
||||
this.debugLog('data_channel.open', {
|
||||
connection: this.connectionDiagnostics(dataChannel),
|
||||
|
||||
@@ -44,13 +44,17 @@ export {
|
||||
|
||||
import { createRealtimeBackend } from './realtimeBackend.js';
|
||||
|
||||
const STATUS = {
|
||||
idle: 'OFF',
|
||||
connecting: 'CONNECTING',
|
||||
listening: 'LISTENING',
|
||||
executing: 'EXECUTING',
|
||||
error: 'ERROR',
|
||||
};
|
||||
import { t } from '../i18n/index.js';
|
||||
|
||||
// Status enum VALUES are machine identifiers (dataset.status, CSS hooks);
|
||||
// only the LABEL rendered from them localizes, resolved at write time via t().
|
||||
const STATUS_LABEL_KEYS = Object.freeze({
|
||||
idle: 'setup.voice.status.idle',
|
||||
connecting: 'setup.voice.status.connecting',
|
||||
listening: 'setup.voice.status.listening',
|
||||
executing: 'setup.voice.status.executing',
|
||||
error: 'setup.voice.status.error',
|
||||
});
|
||||
|
||||
/** Compose voice state owners and coordinate ordered session startup/teardown. */
|
||||
export class GevRealtimeController extends RealtimeFacade {
|
||||
@@ -267,7 +271,7 @@ export class GevRealtimeController extends RealtimeFacade {
|
||||
this.ui.root.remove();
|
||||
}
|
||||
if (!preserveStatus && !removeUi) {
|
||||
this.setStatus('idle', 'Voice off');
|
||||
this.setStatus('idle', t('setup.voice.detail.voiceOff'));
|
||||
}
|
||||
this.setRadioVoiceDucking(false);
|
||||
if (removeUi) this.emitSessionEvent({ type: 'disposed' });
|
||||
@@ -287,24 +291,28 @@ export class GevRealtimeController extends RealtimeFacade {
|
||||
this.ui.root.dataset.status = status;
|
||||
if (status === 'error') this.ui.root.classList.remove('error-dismissed');
|
||||
this.updateVoiceButtonLabel();
|
||||
this.ui.status.textContent = STATUS[status] || STATUS.idle;
|
||||
this.ui.status.textContent = t(
|
||||
STATUS_LABEL_KEYS[status] || STATUS_LABEL_KEYS.idle,
|
||||
);
|
||||
const resolvedDetail =
|
||||
status === 'listening' && this.pushToTalkMode
|
||||
? this.pushToTalkKeyHeld
|
||||
? 'Release Space to send'
|
||||
: 'Hold Space to talk'
|
||||
? t('setup.voice.detail.releaseSpaceSend')
|
||||
: t('setup.voice.detail.holdSpaceTalk')
|
||||
: detail;
|
||||
const primaryDetail =
|
||||
status === 'error'
|
||||
? 'VOICE UNAVAILABLE'
|
||||
? t('setup.voice.detail.unavailable')
|
||||
: resolvedDetail ||
|
||||
(status === 'idle' ? 'VOICE STANDBY' : 'VOICE ACTIVE');
|
||||
(status === 'idle'
|
||||
? t('setup.voice.detail.standby')
|
||||
: t('setup.voice.detail.active'));
|
||||
this.ui.detail.textContent = primaryDetail;
|
||||
this.ui.detail.title = primaryDetail;
|
||||
if (this.ui.errorDetail) {
|
||||
this.ui.errorDetail.textContent =
|
||||
status === 'error'
|
||||
? resolvedDetail || 'Voice session could not be started.'
|
||||
? resolvedDetail || t('setup.voice.error.sessionStart')
|
||||
: '';
|
||||
}
|
||||
if (status === 'idle' || status === 'connecting' || status === 'error') {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from '../i18n/index.js';
|
||||
import {
|
||||
readStoredVoiceTier,
|
||||
readStoredVoiceLimits,
|
||||
@@ -50,18 +51,25 @@ export class RealtimeCost {
|
||||
const pendingId = resolveVoiceModel(pendingTier).id;
|
||||
this.ui.tierButton.title =
|
||||
this.isActive() && state.modelId !== pendingId
|
||||
? `Next session: ${pendingId} — this session stays on ${state.modelId}`
|
||||
: `Voice model: ${pendingId} — click to switch to ${
|
||||
isMini ? 'standard' : 'mini'
|
||||
}; applies next session`;
|
||||
? t('setup.voice.tierNextSession', {
|
||||
pendingId,
|
||||
modelId: state.modelId,
|
||||
})
|
||||
: t('setup.voice.tierSwitchHint', {
|
||||
pendingId,
|
||||
target: isMini ? 'standard' : 'mini',
|
||||
});
|
||||
}
|
||||
if (this.ui?.costValue) {
|
||||
this.ui.costValue.textContent = state.display;
|
||||
this.ui.costValue.dataset.level = state.level;
|
||||
this.ui.costValue.title =
|
||||
`Estimated session cost on ${state.modelId} — ${state.responses} response(s). ` +
|
||||
`Warns at ${formatCostUsd(state.warnUsd)}, ends the session at ${formatCostUsd(state.capUsd)}.` +
|
||||
(state.note ? ` ${state.note}` : '');
|
||||
t('setup.voice.costTooltip', {
|
||||
modelId: state.modelId,
|
||||
responses: state.responses,
|
||||
warn: formatCostUsd(state.warnUsd),
|
||||
cap: formatCostUsd(state.capUsd),
|
||||
}) + (state.note ? ` ${state.note}` : '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +112,9 @@ export class RealtimeCost {
|
||||
if (this.isActive() && this.ui?.detail) {
|
||||
this.setStatus(
|
||||
this.status,
|
||||
`${this.voiceTier.toUpperCase()} applies next session`,
|
||||
t('setup.voice.tier.appliesNextSession', {
|
||||
tier: this.voiceTier.toUpperCase(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return this.voiceTier;
|
||||
@@ -182,7 +192,10 @@ export class RealtimeCost {
|
||||
try {
|
||||
this.stop({ preserveStatus: true });
|
||||
} finally {
|
||||
this.setStatus('idle', `Session ended — cost cap ${state.display}`);
|
||||
this.setStatus(
|
||||
'idle',
|
||||
t('setup.voice.status.sessionCostCap', { cost: state.display }),
|
||||
);
|
||||
this.syncCostUi();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from '../i18n/index.js';
|
||||
import {
|
||||
PUSH_TO_TALK_HOLD_DELAY_MS,
|
||||
MICROPHONE_VISUALIZER_GATE,
|
||||
@@ -108,7 +109,10 @@ export class RealtimeInput {
|
||||
this.ui.root.dataset.pushToTalk = 'held';
|
||||
this.setMicrophoneEnabled(true);
|
||||
if (this.status === 'listening')
|
||||
this.setStatus('listening', 'Release Space to send');
|
||||
this.setStatus(
|
||||
'listening',
|
||||
t('setup.voice.detail.releaseSpaceSend'),
|
||||
);
|
||||
} else {
|
||||
this.start({ pushToTalk: true });
|
||||
// start() performs a controlled stop() before connecting. Restore the
|
||||
@@ -181,7 +185,7 @@ export class RealtimeInput {
|
||||
if (!this.pushToTalkMode) return;
|
||||
this.setMicrophoneEnabled(false);
|
||||
if (this.status === 'listening')
|
||||
this.setStatus('listening', 'Hold Space to talk');
|
||||
this.setStatus('listening', t('setup.voice.detail.holdSpaceTalk'));
|
||||
else this.updateVoiceButtonLabel();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '../i18n/index.js';
|
||||
|
||||
export const PUSH_TO_TALK_HOLD_DELAY_MS = 500;
|
||||
|
||||
// The input meter is intentionally stricter than the assistant-output meter:
|
||||
@@ -150,8 +152,8 @@ export function resolveVoiceVisualizerSpeaker(
|
||||
*/
|
||||
export function resolveVoiceControlHint(pushToTalkMode, pushToTalkKeyHeld) {
|
||||
return pushToTalkMode && pushToTalkKeyHeld
|
||||
? 'Release Space to send'
|
||||
: 'Hold Space to speak · tap Space to activate focused controls';
|
||||
? t('setup.voice.detail.releaseSpaceSend')
|
||||
: t('setup.voice.hint.default');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from '../i18n/index.js';
|
||||
import {
|
||||
silenceRadioForVoice,
|
||||
startPreparedRadioAfterPlaybackReady,
|
||||
@@ -245,7 +246,7 @@ export class RealtimeRadio {
|
||||
if (radioHandoff.result?.ok || radioHandoff.cancelled || !stillCurrent)
|
||||
return;
|
||||
if (this.dc?.readyState === 'open' && !this.userTurnPending) {
|
||||
this.setStatus('listening', 'Radio did not start');
|
||||
this.setStatus('listening', t('setup.voice.detail.radioDidNotStart'));
|
||||
this.queueResponseCreate(
|
||||
'Say exactly one short correction: “The Radio station could not start. Voice is still on.”',
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { realtimeSessionEvent } from './realtimeEvents.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { readLayerLifecycleSummary } from './layerSummary.js';
|
||||
import {
|
||||
CALL_DEDUPE_MS,
|
||||
@@ -207,7 +208,7 @@ export class RealtimeTurns {
|
||||
eventId: payload.event_id,
|
||||
activeResponseMessage: payload.error?.message || null,
|
||||
});
|
||||
this.setStatus('listening', 'Ask or command');
|
||||
this.setStatus('listening', t('setup.voice.detail.askOrCommand'));
|
||||
return;
|
||||
}
|
||||
// A conversation.item.delete for a stale viewport screenshot can land
|
||||
@@ -327,7 +328,7 @@ export class RealtimeTurns {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setStatus('executing', 'Running command');
|
||||
this.setStatus('executing', t('setup.voice.detail.runningCommand'));
|
||||
this.pruneProcessedCalls();
|
||||
let sentOutput = false;
|
||||
let lastResult = null;
|
||||
@@ -577,7 +578,7 @@ export class RealtimeTurns {
|
||||
),
|
||||
);
|
||||
}
|
||||
this.setStatus('listening', 'Ask or command');
|
||||
this.setStatus('listening', t('setup.voice.detail.askOrCommand'));
|
||||
}
|
||||
|
||||
sendToolOutput(callId, result) {
|
||||
@@ -642,7 +643,7 @@ export class RealtimeTurns {
|
||||
// connection is still live. Recover to listening so the user can retry
|
||||
// (mirrors the transient-blip philosophy, H8).
|
||||
if (this.dc?.readyState === 'open') {
|
||||
this.setStatus('listening', 'Ask or command');
|
||||
this.setStatus('listening', t('setup.voice.detail.askOrCommand'));
|
||||
}
|
||||
}
|
||||
if (!this.radio.pendingRadioPlaybackResult) {
|
||||
|
||||
Reference in new issue
Block a user