diff --git a/tests/console_cockpit_decision_validation.mjs b/tests/console_cockpit_decision_validation.mjs
index a76a4763..b2188743 100644
--- a/tests/console_cockpit_decision_validation.mjs
+++ b/tests/console_cockpit_decision_validation.mjs
@@ -32,9 +32,19 @@ try {
effective_tier: 'BALANCED_COMPOUNDING', effective_policy_id: 'demo-balanced-v3',
effective_policy_revision: 'r7', effective_observed_at: '2026-11-01T09:10:00Z',
temporary_restriction: { active: true, reason: 'Reduction only', until: null },
- values: [{ key: 'max_gross_exposure', label_key: null, value: 45, unit: 'percent', status: 'known', source: 'server' }],
+ values: [
+ { key: 'max_gross_exposure', label_key: null, value: 45, unit: 'percent', status: 'known', source: 'server' },
+ { key: 'cash_floor', label_key: null, value: 100, unit: 'USD', status: 'known', source: 'mystery' },
+ { key: 'mystery_limit', label_key: 'some.label.key', value: 2, unit: null, status: 'known', source: 'server' },
+ ],
};
export const policy = (language) => wrap(language, h(EffectivePolicyPanel, { binding }));
+ export const policyKnownCode = (language) => wrap(language, h(EffectivePolicyPanel, {
+ binding: { ...binding, temporary_restriction: { active: true, reason: 'reduction_only', until: null } },
+ }));
+ export const policyNoReason = (language) => wrap(language, h(EffectivePolicyPanel, {
+ binding: { ...binding, temporary_restriction: { active: true, reason: null, until: null } },
+ }));
const base = {
operation_id: 'op-1', request_id: 'req-1', decision_id: 'decision-1', decision_revision: 3,
action: 'approve', title_key: null, account_alias: null, platform: null, release_id: null,
@@ -80,6 +90,18 @@ try {
export const pauseBlocked = (language) => wrap(language, h(PauseControl, {
operatingState: 'running', canPause: false, canResume: true, pending: false, reason: 'Platform does not allow pausing here',
}));
+ export const pauseKnownCode = (language) => wrap(language, h(PauseControl, {
+ operatingState: 'running', canPause: false, canResume: true, pending: false, reason: 'runtime_control_pending',
+ }));
+ export const failedKnownReason = (language) => wrap(language, h(ApplicationStatus, {
+ operation: { ...base, state: 'failed', failure_reason: 'runtime_control_pending' }, status: 'ready',
+ }));
+ export const failedFreeText = (language) => wrap(language, h(ApplicationStatus, {
+ operation: { ...base, state: 'failed', failure_reason: 'platform exploded at 03:00' }, status: 'ready',
+ }));
+ export const unknownFreeText = (language) => wrap(language, h(ApplicationStatus, {
+ operation: { ...base, state: 'unknown', applied_at: null, unknown_reason: 'weird thing happened' }, status: 'ready',
+ }));
export const confirmed = (language) => wrap(language, h(ApplicationStatus, {
operation: { ...base, runtime_confirmed_at: '2026-11-01T10:05:00Z', runtime_state: 'running' }, status: 'ready',
}));
@@ -323,7 +345,30 @@ try {
const policyEn = ui.policy("en");
assert.match(policyEn, /Growth compounding/, "the selected preference is shown");
assert.match(policyEn, /Balanced compounding/, "the observed policy is not overwritten by the selection");
- assert.match(policyEn, /Reduction only/, "a temporary restriction is shown separately");
+ assert.match(policyEn, /Risk reduction only/, "the legacy restriction string shows its bilingual product label");
+ assert.doesNotMatch(policyEn.replace(/ data-[a-z-]+="[^"]*"/g, ''), /Reduction only/, "the raw restriction string is suppressed");
+ assert.match(ui.policyKnownCode("zh"), /仅允许降低风险/);
+ assert.match(ui.policyKnownCode("en"), /Risk reduction only/);
+ assert.match(ui.policyNoReason("zh"), /已启用临时限制,原因未知/);
+ assert.match(ui.policyNoReason("en"), /Temporary restriction active; reason unknown/);
+ // P03: limit keys, sources and units are copy-driven; unknowns never render raw.
+ const policyVisible = policyEn.replace(/ data-[a-z-]+="[^"]*"/g, '');
+ assert.match(policyEn, /Maximum gross exposure/);
+ assert.match(policyEn, /45%/, "percent renders as the symbol in both languages");
+ assert.match(policyEn, /Cash floor/);
+ assert.match(policyEn, /Other limit/);
+ assert.match(policyEn, /Source: Server/);
+ assert.match(policyEn, /Unknown source/);
+ assert.doesNotMatch(policyVisible, /max_gross_exposure|cash_floor|mystery_limit|some\.label\.key|45 percent|percent|USD/,
+ "raw limit keys, dotted label keys and unknown units never reach visible text");
+ assert.doesNotMatch(policyVisible, /Source: mystery|Source: server|Source: null/);
+ const policyZh = ui.policy("zh");
+ assert.match(policyZh, /最大总敞口/);
+ assert.match(policyZh, /现金下限/);
+ assert.match(policyZh, /其他限制/);
+ assert.match(policyZh, /来源:服务端/);
+ assert.match(policyZh, /来源未知/);
+ assert.match(ui.policy("zh"), /临时限制/);
assert.match(policyEn, /Policy mapping unavailable/, "a missing approved mapping stays unavailable");
// Applied is not runtime-confirmed, and the caveat follows the server capability flag.
@@ -363,8 +408,19 @@ try {
assert.match(ui.pause("zh"), /暂停不会平仓/);
assert.doesNotMatch(ui.pause("en"), /aria-describedby/, "an enabled control needs no reason link");
const pauseBlocked = ui.pauseBlocked("en");
- assert.match(pauseBlocked, /Platform does not allow pausing here/);
+ assert.match(pauseBlocked, /This action is not supported right now\./, "free-form pause reasons fall back to the safe copy");
+ assert.doesNotMatch(pauseBlocked, /Platform does not allow pausing here/);
assert.match(pauseBlocked, /aria-describedby=/);
+ assert.match(ui.pauseBlocked("zh"), /当前不支持该操作/);
+ assert.match(ui.pauseKnownCode("en"), /The previous action is awaiting confirmation/, "known pause reason codes keep their copy");
+ assert.match(ui.pauseKnownCode("zh"), /上次操作仍待确认/);
+ // P06: application reasons render through copy; free-form failure text is suppressed.
+ assert.match(ui.failedKnownReason("en"), /The previous action is awaiting confirmation/);
+ const failedTextEn = ui.failedFreeText("en");
+ assert.match(failedTextEn, /Operation outcome pending confirmation/);
+ assert.doesNotMatch(failedTextEn, /platform exploded/);
+ assert.match(ui.failedFreeText("zh"), /操作结果待确认/);
+ assert.doesNotMatch(ui.unknownFreeText("en"), /weird thing happened/);
// Accepted delivery is not delivered, and coverage stays visible.
const digest = ui.digestAccepted("en");
diff --git a/tests/console_frontend_i18n_validation.mjs b/tests/console_frontend_i18n_validation.mjs
new file mode 100644
index 00000000..d888fdc4
--- /dev/null
+++ b/tests/console_frontend_i18n_validation.mjs
@@ -0,0 +1,128 @@
+// Frontend text-leak regression: M01/M02/M05/M06 translate fallbacks and P01/P02/P04 rendering.
+import assert from "node:assert/strict";
+import { mkdtemp, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { createRequire } from "node:module";
+import { fileURLToPath } from "node:url";
+import { build } from "../web/strategy-switch-console/frontend/node_modules/esbuild/lib/main.js";
+
+const root = fileURLToPath(new URL("../web/strategy-switch-console/frontend/", import.meta.url));
+const directory = await mkdtemp(join(tmpdir(), "frontend-i18n-"));
+const visible = (html) => html.replace(/ data-[a-z-]+="[^"]*"/g, "");
+try {
+ const outfile = join(directory, "i18n.cjs");
+ await build({
+ stdin: {
+ resolveDir: root,
+ contents: `
+ import { createElement as h } from 'react';
+ import { renderToStaticMarkup } from 'react-dom/server';
+ import { I18nProvider, translate, useT } from './src/i18n';
+ import { AuditTimeline, regimeLabel } from './src/components/cockpit';
+ import { loadAuditEvents } from './src/api/cockpit';
+ export { translate, loadAuditEvents };
+ export const tr = (language, key, fallback) => translate(language, key, {}, fallback);
+ const RegimeText = ({ value }) => { const t = useT(); return h('p', { className: 'regime-text' }, regimeLabel(value, t)); };
+ export const regime = (language, value) => renderToStaticMarkup(h(I18nProvider, { language }, h(RegimeText, { value })));
+ const events = [
+ { event_id: 'e1', occurred_at: '2026-11-01T10:00:00Z', actor: 'owner', action: 'record_owner_decision',
+ subject: { kind: 'owner_decision', id: 'demo' }, revision: 7, decision_id: 'd-1', operation_id: null, outcome: 'decided' },
+ { event_id: 'e2', occurred_at: '2026-11-01T10:00:01Z', actor: null, action: 'some_new_action',
+ subject: { kind: 'mystery_kind', id: 'x-1' }, revision: null, decision_id: null, operation_id: null, outcome: 'weird_outcome' },
+ ];
+ export const audit = (language) => renderToStaticMarkup(h(I18nProvider, { language },
+ h(AuditTimeline, { events, status: 'ready' })));
+ `,
+ },
+ bundle: true,
+ platform: "node",
+ format: "cjs",
+ outfile,
+ jsx: "automatic",
+ loader: { ".css": "empty" },
+ define: { "import.meta.env": "{}" },
+ logLevel: "silent",
+ });
+ const ui = createRequire(import.meta.url)(outfile);
+
+ // M05/M01/M02/M04/M06/M07: dynamic keys fall back to localized copy, never the raw key.
+ assert.equal(ui.tr("zh", "decisions.action.some_action"), "未知操作");
+ assert.equal(ui.tr("en", "decisions.action.some_action"), "Unknown action");
+ assert.equal(ui.tr("zh", "some.label.key"), "未知");
+ assert.equal(ui.tr("en", "some.label.key"), "Unknown");
+ assert.equal(ui.tr("zh", "constructor"), "未知", "prototype keys never resolve as functions");
+ assert.equal(ui.tr("zh", "reason.mystery_code"), "原因未知");
+ assert.equal(ui.tr("en", "reason.mystery_code"), "Unknown reason");
+ assert.equal(ui.tr("zh", "state.mystery_status"), "未知状态");
+ assert.equal(ui.tr("zh", "research.state.mystery"), "未知状态");
+ assert.equal(ui.tr("zh", "research.source.synthetic-source"), "其他研究来源");
+ assert.equal(ui.tr("en", "research.source.synthetic-source"), "Other research source");
+ assert.equal(ui.tr("en", "research.source.research-mirror"), "Research mirror");
+ assert.equal(ui.tr("zh", "research.source.quant-advisor-research"), "量化研究信号");
+ // M02/M06: known machine values keep their vetted copy; unknown modes never render the raw enum.
+ assert.equal(ui.tr("zh", "strategies.execution.paper"), "模拟");
+ assert.equal(ui.tr("zh", "strategies.execution.live"), "实盘");
+ assert.equal(ui.tr("zh", "strategies.execution.legacy_live"), "实盘");
+ assert.equal(ui.tr("en", "strategies.execution.dry_run"), "Dry run");
+ assert.equal(ui.tr("zh", "strategies.execution.mystery_mode"), "执行模式未知");
+ assert.equal(ui.tr("en", "strategies.execution.mystery_mode"), "Execution mode unknown");
+ assert.equal(ui.tr("en", "reason.runtime_control_pending"), "The previous action is awaiting confirmation");
+ assert.equal(ui.tr("zh", "accounts.field.mystery_field"), "其他参数");
+ assert.equal(ui.tr("en", "accounts.field.mystery_field"), "Other parameter");
+ assert.equal(ui.tr("zh", "initial_enrollment.fact.some_field"), "其他字段");
+ assert.equal(ui.tr("zh", "initial_enrollment.scope_kind.mystery"), "未知");
+
+ // P04: regime effects render through copy only.
+ const regimeZhKnown = visible(ui.regime("zh", "risk_reduction_only"));
+ assert.match(regimeZhKnown, /仅允许降低风险/);
+ assert.match(visible(ui.regime("en", "risk_reduction_only")), /Risk reduction only/);
+ const regimeUnknown = visible(ui.regime("zh", "mystery_effect"));
+ assert.match(regimeUnknown, /市场状态影响未知/);
+ assert.match(visible(ui.regime("en", "mystery_effect")), /Regime effect unknown/);
+ assert.doesNotMatch(regimeUnknown, /mystery effect|mystery_effect/, "unknown regimes are never humanized or raw");
+
+ // P01/P02: audit actions, outcomes and structured subjects are labeled, not machine text.
+ const auditZh = visible(ui.audit("zh"));
+ assert.match(auditZh, /记录负责人决定/);
+ assert.match(auditZh, /对象:负责人决定 · demo/);
+ assert.match(auditZh, /结果 已决定/);
+ assert.match(auditZh, /记录修订 7/);
+ assert.match(auditZh, /其他操作/);
+ assert.match(auditZh, /其他对象/);
+ assert.match(auditZh, /结果 未知/, "unknown outcomes render the exact localized value");
+ assert.doesNotMatch(auditZh, /结果未知|weird_outcome/);
+ const auditEn = visible(ui.audit("en"));
+ assert.match(auditEn, /Owner decision recorded/);
+ assert.match(auditEn, /Subject: Owner decision · demo/);
+ assert.match(auditEn, /Outcome Decided/);
+ assert.match(auditEn, /Record revision 7/);
+ assert.match(auditEn, /Other action/);
+ assert.match(auditEn, /Other subject/);
+ assert.match(auditEn, /Outcome Unknown/, "unknown outcomes render the exact localized value");
+ assert.doesNotMatch(auditEn, /Outcome unknown/);
+ assert.doesNotMatch(auditEn, /record owner decision|owner_decision:|some new action|mystery kind|weird outcome/,
+ "raw action, kind and outcome codes never reach visible text");
+
+ // P02 adapter: subjects stay structured; a legacy "kind: id" string splits instead of concatenating.
+ const realFetch = globalThis.fetch;
+ try {
+ const jsonResponse = (payload) => ({ status: 200, ok: true, json: async () => payload });
+ globalThis.fetch = async () => jsonResponse({ events: [
+ { event_id: "e1", occurred_at: "2026-11-01T10:00:00Z", action: "approval_recorded", subject: { kind: "strategy_promotion", id: "p-1" }, outcome: "applied", revision: 2 },
+ { event_id: "e2", occurred_at: "2026-11-01T10:00:01Z", action: "approval_recorded", subject: "owner_decision: d-9", outcome: null, revision: null },
+ { event_id: "e3", occurred_at: "2026-11-01T10:00:02Z", action: "approval_recorded", subject: "plain-id", outcome: null, revision: null },
+ ] });
+ const read = await ui.loadAuditEvents();
+ assert.equal(read.status, "ready");
+ assert.deepEqual(read.data[0].subject, { kind: "strategy_promotion", id: "p-1" });
+ assert.deepEqual(read.data[1].subject, { kind: "owner_decision", id: "d-9" });
+ assert.deepEqual(read.data[2].subject, { kind: "unknown", id: "plain-id" });
+ assert.equal(read.data[0].action, "approval_recorded");
+ } finally {
+ globalThis.fetch = realFetch;
+ }
+} finally {
+ await rm(directory, { recursive: true, force: true });
+}
+console.log("console_frontend_i18n_validation: ok");
diff --git a/tests/initial_enrollment_ui_validation.mjs b/tests/initial_enrollment_ui_validation.mjs
index ebe98200..c5fea184 100644
--- a/tests/initial_enrollment_ui_validation.mjs
+++ b/tests/initial_enrollment_ui_validation.mjs
@@ -110,5 +110,39 @@ try {
assert.ok(html.includes(`class="decision-ref">${language==='zh'?'编号':'Ref'} IN-TEST`),`${language}: that one occurrence is the header ref`);
assert.doesNotMatch(html,/class="reconciliation-ref"/,`${language}: the material row reuses the header ref instead of repeating the number`);
}
+ // Leak regressions M02-M04/M07: enrollment enums, unknown fact fields, scope enums and
+ // diff field names render only vetted bilingual copy; raw machine values never reach visible text or title.
+ // (M02 execution-mode copy and its unknown fallback are exercised via translate in console_frontend_i18n_validation.mjs.)
+ const strip=(h)=>h.replace(/ data-[a-z-]+="[^"]*"/g,'');
+ const positionsItem={symbol:'AAPL',action:'BUY',quantity:'10',status:'WeirdState',order_type:'LMT',sec_type:'STK',right:'CALL',currency:'USD'};
+ const openOrderItem={status:'Submitted',action:'SELL',side:'SELL',order_type:'MKT',price:'3.5'};
+ const filledItem={status:'Filled',legs:[{right:'P',sec_type:'OPT'}]};
+ const facts2={observed_at:'2026-10-29T00:00:00Z',surfaces:Object.fromEntries(names.map((n,i)=>[n,{status:'complete',
+ items:n==='positions'?[positionsItem,openOrderItem,filledItem]:[],
+ scope:n==='positions'?{kind:'mystery_kind',currency:'USD',window_start:'2026-10-01T00:00:00Z',window_end:null,limit_per_namespace:'50',gateway_availability:'weird',truncated:'maybe'}:{kind:kinds[i],currency:null,window_start:null,window_end:null,limit_per_namespace:null,gateway_availability:null,truncated:null}}]))};
+ const leakItem={...item,diff:[
+ {field:'some_field',label_key:'unknown.label.key',before:'1',after:'2',affects_live:false},
+ {field:'dca_mode',label_key:'accounts.field.dca_mode',before:'fixed',after:'smart',affects_live:false},
+ ],initial_enrollment:{...item.initial_enrollment,facts:facts2}};
+ for (const language of ['zh','en']) {
+ const html=strip(ui.renderLang(leakItem,language));
+ const known=language==='zh'
+ ? ['买入','卖出','已提交','已成交','限价','市价','股票','期权','看涨','看跌']
+ : ['Buy','Sell','Submitted','Filled','Limit','Market','Stock','Option','Call','Put'];
+ for (const label of known) assert.ok(html.includes(`>${label}<`),`${language}: enum value renders as ${label}`);
+ assert.ok(html.includes(`>${language==='zh'?'未知':'Unknown'}<`),`${language}: unknown enum falls back to safe copy`);
+ assert.ok(html.includes(language==='zh'?'其他参数':'Other parameter'),`${language}: unknown diff field is labeled`);
+ assert.ok(html.includes(language==='zh'?'定投模式':'DCA mode'),`${language}: known diff field keeps its label`);
+ const forbidden=language==='zh'
+ ? ['>BUY<','>SELL<','>Submitted<','>Filled<','>LMT<','>MKT<','>STK<','>OPT<','>CALL<','>P<','>WeirdState<','mystery_kind','>weird<','>maybe<','some_field','unknown.label.key','>current_broker_day<']
+ : ['>BUY<','>SELL<','>LMT<','>MKT<','>STK<','>OPT<','>CALL<','>P<','>WeirdState<','mystery_kind','>weird<','>maybe<','some_field','unknown.label.key'];
+ for (const raw of forbidden) assert.ok(!html.includes(raw),`${language}: raw value ${raw} never appears in visible markup`);
+ assert.doesNotMatch(html,/title="[^"]*(some_field|unknown\.label\.key|BUY|SELL|LMT|STK|CALL)/,`${language}: no raw value hides in a title attribute`);
+ // Allowed business values survive verbatim.
+ assert.ok(html.includes('>AAPL<'),`${language}: symbol stays verbatim`);
+ assert.ok(html.includes('>USD<'),`${language}: currency stays verbatim`);
+ assert.ok(html.includes('>2026-10-01T00:00:00Z<'),`${language}: window timestamp stays verbatim`);
+ assert.ok(html.includes('>50<'),`${language}: numeric limit stays verbatim`);
+ }
console.log('initial_enrollment_ui_validation: PASS');
} finally {await rm(dir,{recursive:true,force:true});}
diff --git a/web/strategy-switch-console/frontend/package.json b/web/strategy-switch-console/frontend/package.json
index 8fe82872..d49db6bd 100644
--- a/web/strategy-switch-console/frontend/package.json
+++ b/web/strategy-switch-console/frontend/package.json
@@ -6,7 +6,7 @@
"scripts": {
"build": "tsc --noEmit && vite build && node scripts/check-budget.mjs",
"sync:v2-assets": "node scripts/sync-v2-asset-map.mjs",
- "test:operations": "node --experimental-strip-types ../../../tests/console_v2_operations_validation.mjs && node --experimental-strip-types ../../../tests/console_v2_regressions_validation.mjs && node --experimental-strip-types ../../../tests/console_v2_preferences_accounts_validation.mjs && node ../../../tests/console_runtime_control_result_ui_validation.mjs && node ../../../tests/console_page_load_boundary_validation.mjs && node --test ../../../tests/fe8b_fix03_ui_validation.mjs ../../../tests/console_notification_receipts_ui_validation.mjs && npm run test:performance && node --test ../../../tests/console_execution_evidence_validation.mjs && node ../../../tests/console_strategy_execution_validation.mjs && node --experimental-strip-types ../../../tests/console_binance_wallet_cards_validation.mjs && node ../../../tests/console_tabs_scope_sources_ui_validation.mjs && node ../../../tests/console_shell_unavailable_source_ui_validation.mjs && node ../../../tests/console_cockpit_decision_validation.mjs",
+ "test:operations": "node --experimental-strip-types ../../../tests/console_v2_operations_validation.mjs && node --experimental-strip-types ../../../tests/console_v2_regressions_validation.mjs && node --experimental-strip-types ../../../tests/console_v2_preferences_accounts_validation.mjs && node ../../../tests/console_runtime_control_result_ui_validation.mjs && node ../../../tests/console_page_load_boundary_validation.mjs && node --test ../../../tests/fe8b_fix03_ui_validation.mjs ../../../tests/console_notification_receipts_ui_validation.mjs && npm run test:performance && node --test ../../../tests/console_execution_evidence_validation.mjs && node ../../../tests/console_strategy_execution_validation.mjs && node --experimental-strip-types ../../../tests/console_binance_wallet_cards_validation.mjs && node ../../../tests/console_tabs_scope_sources_ui_validation.mjs && node ../../../tests/console_shell_unavailable_source_ui_validation.mjs && node ../../../tests/console_cockpit_decision_validation.mjs && node ../../../tests/console_frontend_i18n_validation.mjs",
"dev:synthetic": "node scripts/dev-synthetic.mjs",
"test:performance": "node --experimental-strip-types ../../../tests/console_performance_math_validation.mjs && node --experimental-strip-types ../../../tests/console_performance_data_validation.mjs && node --experimental-strip-types ../../../tests/console_calendar_pending_ui_validation.mjs && node ../../../tests/website_currency_ranking_validation.mjs && node ../../../tests/website_summary_request_validation.mjs && node ../../../tests/console_compact_summary_validation.mjs && node ../../../tests/native_daily_gap_ui_validation.mjs"
},
diff --git a/web/strategy-switch-console/frontend/src/api/cockpit.ts b/web/strategy-switch-console/frontend/src/api/cockpit.ts
index 020e8a0a..3711fa5d 100644
--- a/web/strategy-switch-console/frontend/src/api/cockpit.ts
+++ b/web/strategy-switch-console/frontend/src/api/cockpit.ts
@@ -149,12 +149,17 @@ export interface DigestStatus {
targets: DigestTargetCount[];
}
+export interface AuditSubject {
+ kind: string;
+ id: string;
+}
+
export interface AuditEvent {
event_id: string;
occurred_at: string;
actor: string | null;
action: string;
- subject: string | null;
+ subject: AuditSubject | null;
revision: number | null;
decision_id: string | null;
operation_id: string | null;
@@ -326,6 +331,22 @@ function asNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
+/** Audit subjects stay structured: kind is translated and id labeled separately at render. */
+function asAuditSubject(value: unknown): AuditSubject | null {
+ const raw = asRecord(value);
+ if (raw) {
+ const kind = asString(raw.kind);
+ const id = asString(raw.id);
+ if (kind === null && id === null) return null;
+ return { kind: kind ?? "unknown", id: id ?? "" };
+ }
+ const text = asString(value);
+ if (text === null) return null;
+ const separator = text.indexOf(":");
+ if (separator < 0) return { kind: "unknown", id: text };
+ return { kind: text.slice(0, separator).trim() || "unknown", id: text.slice(separator + 1).trim() };
+}
+
function normalizeEvidence(value: unknown): EvidenceSummary | null {
const raw = asRecord(value);
if (!raw) return null;
@@ -737,7 +758,7 @@ export async function loadAuditEvents(cursor: string | null = null, synthetic =
occurred_at: asString(event.occurred_at) ?? "",
actor: asString(event.actor),
action: asString(event.action) ?? "unknown",
- subject: asString(event.subject),
+ subject: asAuditSubject(event.subject),
revision: asNumber(event.revision),
decision_id: asString(event.decision_id),
operation_id: asString(event.operation_id),
@@ -902,7 +923,7 @@ function syntheticDigest(): DigestStatus {
function syntheticAudit(): AuditEvent[] {
return [
- { event_id: "synthetic-event-1", occurred_at: "2026-11-01T10:00:00Z", actor: "owner", action: "approval_recorded", subject: "synthetic-decision-001", revision: 3, decision_id: "synthetic-decision-001", operation_id: "synthetic-operation-1", outcome: "recorded" },
- { event_id: "synthetic-event-2", occurred_at: "2026-11-01T10:00:05Z", actor: "platform", action: "application_dispatched", subject: "demo-target", revision: 3, decision_id: "synthetic-decision-001", operation_id: "synthetic-operation-1", outcome: "applying" },
+ { event_id: "synthetic-event-1", occurred_at: "2026-11-01T10:00:00Z", actor: "owner", action: "approval_recorded", subject: { kind: "owner_decision", id: "synthetic-decision-001" }, revision: 3, decision_id: "synthetic-decision-001", operation_id: "synthetic-operation-1", outcome: "recorded" },
+ { event_id: "synthetic-event-2", occurred_at: "2026-11-01T10:00:05Z", actor: "platform", action: "application_dispatched", subject: { kind: "paper_account_application", id: "demo-target" }, revision: 3, decision_id: "synthetic-decision-001", operation_id: "synthetic-operation-1", outcome: "applying" },
];
}
diff --git a/web/strategy-switch-console/frontend/src/components/DiffTable.tsx b/web/strategy-switch-console/frontend/src/components/DiffTable.tsx
index e9958ab5..e6045e20 100644
--- a/web/strategy-switch-console/frontend/src/components/DiffTable.tsx
+++ b/web/strategy-switch-console/frontend/src/components/DiffTable.tsx
@@ -34,7 +34,7 @@ export function DiffTable({ rows }: { rows: DiffRowLike[] }) {
{rows.map(row => (
- | {row.label} |
+ {row.label} |
|
|
diff --git a/web/strategy-switch-console/frontend/src/components/cockpit.tsx b/web/strategy-switch-console/frontend/src/components/cockpit.tsx
index 6de76de7..86738687 100644
--- a/web/strategy-switch-console/frontend/src/components/cockpit.tsx
+++ b/web/strategy-switch-console/frontend/src/components/cockpit.tsx
@@ -13,7 +13,7 @@ import type {
} from "../api/cockpit";
import { expiryState, isRiskTier } from "../api/cockpit";
import { formatDateTime } from "../lib/format";
-import { useLanguage, useT } from "../i18n";
+import { useLanguage, useT, hasTranslation } from "../i18n";
import { Badge, Skeleton } from "./primitives";
import "./cockpit.css";
@@ -24,13 +24,28 @@ export function tierLabel(value: string | null | undefined, t: ReturnType): string {
- if (value === "risk_reduction_only") return t("cockpit.regime.reduction_only");
- return value.replace(/_/g, " ");
+ return t(`cockpit.lifecycle.regime_effect.${value}`, {}, "cockpit.lifecycle.regime_effect.unknown");
+}
+
+/** Application reasons may be codes or free text; only vetted codes reach the screen. */
+export function operationReason(reason: string, t: ReturnType): string {
+ const reasonKey = `reason.${reason}`;
+ if (hasTranslation(reasonKey)) return t(reasonKey);
+ const explicitKey = `cockpit.operation.reason.${reason}`;
+ if (hasTranslation(explicitKey)) return t(explicitKey);
+ return t("cockpit.operation.reason.unknown");
+}
+
+/** Pause/resume unavailability keeps its own fallback: a blocked control is not a pending operation. */
+export function pauseReasonLabel(reason: string | null | undefined, t: ReturnType): string {
+ if (reason && hasTranslation(`reason.${reason}`)) return t(`reason.${reason}`);
+ return t("cockpit.pause.unavailable");
}
export function blockerLabel(reason: string, t: ReturnType): string {
@@ -320,7 +335,8 @@ function PolicyValueCell({ value }: { value: PolicyValue }) {
if (value.status !== "known" || value.value === null) {
return {t("cockpit.policy.unknown_value")};
}
- return {String(value.value)}{value.unit ? ` ${value.unit}` : ""};
+ const unit = value.unit === "percent" ? t("cockpit.policy.unit.percent") : "";
+ return {String(value.value)}{unit};
}
export function EffectivePolicyPanel({
@@ -389,7 +405,9 @@ export function EffectivePolicyPanel({
{restriction?.active ? (
<>
- {restriction.reason || t("cockpit.unknown")}
+ {restriction.reason === "reduction_only" || restriction.reason === "Reduction only"
+ ? t("cockpit.policy.restriction.reason.reduction_only")
+ : t("cockpit.policy.restriction.reason.unknown")}
{since && · {t("cockpit.policy.since", { value: since.text })}}
>
) : t("cockpit.policy.restriction_none")}
@@ -408,11 +426,11 @@ export function EffectivePolicyPanel({
{binding.values.map(value => (
-
- | {value.label_key ? t(value.label_key) : value.key} |
+
+ | {value.label_key ? t(value.label_key, {}, `cockpit.policy.limit.${value.key}`) : t(`cockpit.policy.limit.${value.key}`)} |
- {value.source && · {t("cockpit.policy.source", { value: value.source })}}
+ {value.source && · {t("cockpit.policy.source", { value: t(`cockpit.policy.source.${value.source}`) })}}
|
))}
@@ -582,7 +600,7 @@ export function ApplicationStatus({
))}
{(operation.failure_reason || operation.unknown_reason) && (
- {operation.failure_reason || operation.unknown_reason}
+ {operationReason(operation.failure_reason || operation.unknown_reason || "", t)}
)}
{operation.state === "unknown" && {t("cockpit.operation.no_retry")} }
@@ -655,7 +673,7 @@ export function PauseControl({
{pending && {t("cockpit.pause.pending")}}
{unavailable && (
- {reason || t("cockpit.pause.unavailable")}
+ {pauseReasonLabel(reason, t)}
)}
{t("cockpit.pause.note")}
{t("cockpit.pause.no_liquidation")}
@@ -690,15 +708,21 @@ export function AuditTimeline({
{events.map(event => {
const time = formatDateTime(event.occurred_at, language);
return (
-
+
- {event.action.replace(/_/g, " ")}
+ {t(`cockpit.change_log.action.${event.action}`, {}, "cockpit.change_log.action.other")}
{event.actor ? t("cockpit.change_log.actor", { value: event.actor }) : t("cockpit.unknown")}
- {event.outcome && ` · ${t("cockpit.change_log.outcome", { value: event.outcome })}`}
+ {event.outcome && ` · ${t("cockpit.change_log.outcome", { value: t(`cockpit.change_log.outcome.${event.outcome}`, {}, "cockpit.change_log.outcome.unknown") })}`}
- {event.subject && {t("cockpit.change_log.subject", { value: event.subject })}}
+ {event.subject && (
+
+ {t("cockpit.change_log.subject", {
+ value: `${t(`cockpit.change_log.subject_kind.${event.subject.kind}`, {}, "cockpit.change_log.subject_kind.other")}${event.subject.id ? ` · ${event.subject.id}` : ""}`,
+ })}
+
+ )}
{event.revision !== null && {t("cockpit.change_log.revision", { value: event.revision })}}
@@ -867,7 +891,7 @@ export function DecisionCard({
onSelect: () => void;
}) {
const t = useT();
- const title = t(decision.title_key);
+ const title = t(decision.title_key, {}, "decisions.unknown_title");
const blocked = blockedReasons.length > 0;
return (
@@ -880,7 +904,7 @@ export function DecisionCard({
{blocked && {t("cockpit.decision.blocked")}}
- {title === decision.title_key ? decision.short_id : title}
+ {title}
{decision.account_alias && (
diff --git a/web/strategy-switch-console/frontend/src/i18n/index.tsx b/web/strategy-switch-console/frontend/src/i18n/index.tsx
index c5b2c5fa..d140af62 100644
--- a/web/strategy-switch-console/frontend/src/i18n/index.tsx
+++ b/web/strategy-switch-console/frontend/src/i18n/index.tsx
@@ -46,15 +46,60 @@ for (const key of Object.keys(enMessages)) {
export type MessageKey = CoreMessageKey | (string & {});
export type TranslateParams = Record ;
-export type Translate = (key: MessageKey, params?: TranslateParams) => string;
+export type Translate = (key: MessageKey, params?: TranslateParams, fallbackKey?: MessageKey) => string;
-export function translate(language: Language, key: MessageKey, params: TranslateParams = {}): string {
- const table = language === "zh" ? zh : enMessages;
- const fallbackKey = key.startsWith("state.") ? "state.unknown_status"
- : key.startsWith("reason.") ? "reason.unknown"
- : key.startsWith("research.state.") ? "state.unknown_status" : null;
- const template = table[key] ?? (language === "zh" ? enMessages[key] : zh[key])
- ?? (fallbackKey ? table[fallbackKey] : key);
+const lookup = (table: Record, key: string): string | undefined =>
+ Object.prototype.hasOwnProperty.call(table, key) ? table[key] : undefined;
+
+/** True when the key has its own vetted copy (parity guarantees both languages). */
+export function hasTranslation(key: string): boolean {
+ return lookup(zh, key) !== undefined;
+}
+
+/** Ordered longest-prefix-first namespace fallbacks; unclassified keys end at common.unknown. */
+const NAMESPACE_FALLBACKS: readonly [prefix: string, fallbackKey: string][] = [
+ ["research.state.", "state.unknown_status"],
+ ["state.", "state.unknown_status"],
+ ["reason.", "reason.unknown"],
+ ["decisions.action.", "decisions.action.unknown"],
+ ["strategies.execution.", "strategies.execution.unknown"],
+ ["research.source.", "research.source.other"],
+ ["initial_enrollment.fact.", "initial_enrollment.fact.other"],
+ ["initial_enrollment.", "initial_enrollment.value.unknown"],
+ ["accounts.field.", "accounts.field.other"],
+ ["cockpit.change_log.action.", "cockpit.change_log.action.other"],
+ ["cockpit.change_log.outcome.", "cockpit.change_log.outcome.unknown"],
+ ["cockpit.change_log.subject_kind.", "cockpit.change_log.subject_kind.other"],
+ ["cockpit.policy.limit.", "cockpit.policy.limit.other"],
+ ["cockpit.policy.source.", "cockpit.policy.source.unknown"],
+ ["cockpit.policy.restriction.reason.", "cockpit.policy.restriction.reason.unknown"],
+ ["cockpit.lifecycle.regime_effect.", "cockpit.lifecycle.regime_effect.unknown"],
+ ["cockpit.operation.reason.", "cockpit.operation.reason.unknown"],
+];
+
+export function translate(
+ language: Language,
+ key: MessageKey,
+ params: TranslateParams = {},
+ fallbackKey?: MessageKey,
+): string {
+ const tables = language === "zh" ? [zh, enMessages] : [enMessages, zh];
+ const namespaceFallback = (candidate: string): string | undefined =>
+ NAMESPACE_FALLBACKS.find(([prefix]) => candidate.startsWith(prefix))?.[1];
+ const candidates: string[] = [key];
+ if (fallbackKey) candidates.push(fallbackKey);
+ for (const candidate of [key, fallbackKey]) {
+ if (!candidate) continue;
+ const routed = namespaceFallback(candidate);
+ if (routed && !candidates.includes(routed)) candidates.push(routed);
+ }
+ candidates.push("common.unknown");
+ let template: string | undefined;
+ for (const candidate of candidates) {
+ template = tables.map(table => lookup(table, candidate)).find(value => value !== undefined);
+ if (template !== undefined) break;
+ }
+ if (template === undefined) template = lookup(tables[0], "common.unknown") ?? "Unknown";
return template.replace(/\{(\w+)\}/g, (match, name: string) =>
name in params ? String(params[name]) : match,
);
@@ -72,5 +117,5 @@ export function useLanguage(): Language {
export function useT(): Translate {
const language = useLanguage();
- return (key, params) => translate(language, key, params);
+ return (key, params, fallbackKey) => translate(language, key, params, fallbackKey);
}
diff --git a/web/strategy-switch-console/frontend/src/i18n/pages/accounts.ts b/web/strategy-switch-console/frontend/src/i18n/pages/accounts.ts
index 7d7f9fbe..29d83282 100644
--- a/web/strategy-switch-console/frontend/src/i18n/pages/accounts.ts
+++ b/web/strategy-switch-console/frontend/src/i18n/pages/accounts.ts
@@ -50,6 +50,7 @@ export const zh: Record = {
"accounts.field.reserved_cash_ratio": "预留现金比例",
"accounts.field.income_layer_enabled": "收入层",
"accounts.field.option_overlay_enabled": "期权层",
+ "accounts.field.other": "其他参数",
"accounts.bool.on": "开启",
"accounts.bool.off": "关闭",
@@ -160,6 +161,7 @@ export const en: Record = {
"accounts.field.reserved_cash_ratio": "Reserved cash ratio",
"accounts.field.income_layer_enabled": "Income layer",
"accounts.field.option_overlay_enabled": "Option overlay",
+ "accounts.field.other": "Other parameter",
"accounts.bool.on": "On",
"accounts.bool.off": "Off",
diff --git a/web/strategy-switch-console/frontend/src/i18n/pages/cockpit.ts b/web/strategy-switch-console/frontend/src/i18n/pages/cockpit.ts
index 069800c7..1141bf47 100644
--- a/web/strategy-switch-console/frontend/src/i18n/pages/cockpit.ts
+++ b/web/strategy-switch-console/frontend/src/i18n/pages/cockpit.ts
@@ -93,12 +93,21 @@ export const zh: Record = {
"cockpit.policy.current": "映射已是最新",
"cockpit.policy.restriction_none": "无",
"cockpit.policy.since": "自 {value}",
+ "cockpit.policy.limit.max_gross_exposure": "最大总敞口",
+ "cockpit.policy.limit.cash_floor": "现金下限",
+ "cockpit.policy.limit.other": "其他限制",
+ "cockpit.policy.source.server": "服务端",
+ "cockpit.policy.source.unknown": "来源未知",
+ "cockpit.policy.unit.percent": "%",
+ "cockpit.policy.restriction.reason.reduction_only": "仅允许降低风险",
+ "cockpit.policy.restriction.reason.unknown": "已启用临时限制,原因未知",
"cockpit.policy.effective_note": "已选偏好与生效策略分开显示;观察确认前不会视为已生效。",
"cockpit.tier.title": "风险档位",
"cockpit.tier.capital": "资本保全",
"cockpit.tier.balanced": "平衡复利",
"cockpit.tier.growth": "成长复利",
+ "cockpit.tier.unknown": "未知档位",
"cockpit.tier.capital_note": "偏好较低风险;适用限制由已批准策略计算。",
"cockpit.tier.balanced_note": "兼顾成长与约束;适用限制由已批准策略计算。",
"cockpit.tier.growth_note": "接受较高风险;适用限制由已批准策略计算。",
@@ -139,7 +148,8 @@ export const zh: Record = {
"cockpit.lifecycle.note": "阶段、授权、实际观察和运行状态分别显示;界面不会自行提升阶段。",
"cockpit.lifecycle.not_provided": "未提供该目标的生命周期阶段信息。",
"cockpit.lifecycle.identity": "版本 {release} · 目标 {target}",
- "cockpit.regime.reduction_only": "市场状态限制:仅允许降低风险的操作。",
+ "cockpit.lifecycle.regime_effect.risk_reduction_only": "仅允许降低风险",
+ "cockpit.lifecycle.regime_effect.unknown": "市场状态影响未知",
"cockpit.operation.title": "申请状态",
"cockpit.operation.decision": "决定",
@@ -168,6 +178,7 @@ export const zh: Record = {
"cockpit.operation.stage.complete": "已完成",
"cockpit.operation.stage.failed": "已失败",
"cockpit.operation.stage.unavailable": "无法独立回读",
+ "cockpit.operation.reason.unknown": "操作结果待确认",
"cockpit.pause.title": "运行控制",
"cockpit.pause.pause": "暂停",
@@ -186,9 +197,26 @@ export const zh: Record = {
"cockpit.change_log.load_failed": "活动记录读取失败。",
"cockpit.change_log.more": "加载更多",
"cockpit.change_log.actor": "操作者 {value}",
- "cockpit.change_log.subject": "对象 {value}",
- "cockpit.change_log.revision": "修订 {value}",
+ "cockpit.change_log.subject": "对象:{value}",
+ "cockpit.change_log.revision": "记录修订 {value}",
"cockpit.change_log.outcome": "结果 {value}",
+ "cockpit.change_log.action.record_owner_decision": "记录负责人决定",
+ "cockpit.change_log.action.approval_recorded": "记录审批",
+ "cockpit.change_log.action.application_dispatched": "派发应用请求",
+ "cockpit.change_log.action.other": "其他操作",
+ "cockpit.change_log.outcome.decided": "已决定",
+ "cockpit.change_log.outcome.recorded": "已记录",
+ "cockpit.change_log.outcome.applying": "应用中",
+ "cockpit.change_log.outcome.applied": "已应用",
+ "cockpit.change_log.outcome.rejected": "已拒绝",
+ "cockpit.change_log.outcome.failed": "失败",
+ "cockpit.change_log.outcome.unknown": "未知",
+ "cockpit.change_log.subject_kind.owner_decision": "负责人决定",
+ "cockpit.change_log.subject_kind.strategy_promotion": "策略晋级",
+ "cockpit.change_log.subject_kind.reconciliation_recovery": "对账恢复",
+ "cockpit.change_log.subject_kind.paper_account_application": "模拟账户应用",
+ "cockpit.change_log.subject_kind.application": "模拟账户应用",
+ "cockpit.change_log.subject_kind.other": "其他对象",
"cockpit.digest.title": "每日摘要发送",
"cockpit.digest.accepted": "已接受投递",
@@ -308,12 +336,21 @@ export const en: Record = {
"cockpit.policy.current": "Mapping current",
"cockpit.policy.restriction_none": "None",
"cockpit.policy.since": "Since {value}",
+ "cockpit.policy.limit.max_gross_exposure": "Maximum gross exposure",
+ "cockpit.policy.limit.cash_floor": "Cash floor",
+ "cockpit.policy.limit.other": "Other limit",
+ "cockpit.policy.source.server": "Server",
+ "cockpit.policy.source.unknown": "Unknown source",
+ "cockpit.policy.unit.percent": "%",
+ "cockpit.policy.restriction.reason.reduction_only": "Risk reduction only",
+ "cockpit.policy.restriction.reason.unknown": "Temporary restriction active; reason unknown",
"cockpit.policy.effective_note": "Selected preference and effective policy stay separate; a selection is not effective before observation.",
"cockpit.tier.title": "Risk tier",
"cockpit.tier.capital": "Capital preservation",
"cockpit.tier.balanced": "Balanced compounding",
"cockpit.tier.growth": "Growth compounding",
+ "cockpit.tier.unknown": "Unknown tier",
"cockpit.tier.capital_note": "Prefer lower risk; the approved policy computes the applicable limits.",
"cockpit.tier.balanced_note": "Balance growth and restraint; the approved policy computes the applicable limits.",
"cockpit.tier.growth_note": "Accept higher risk; the approved policy computes the applicable limits.",
@@ -354,7 +391,8 @@ export const en: Record = {
"cockpit.lifecycle.note": "Stage, authority, observation and operating state stay separate; the UI never promotes a stage.",
"cockpit.lifecycle.not_provided": "Lifecycle stages were not provided for this target.",
"cockpit.lifecycle.identity": "Release {release} · target {target}",
- "cockpit.regime.reduction_only": "Regime limit: only risk-reducing actions are allowed.",
+ "cockpit.lifecycle.regime_effect.risk_reduction_only": "Risk reduction only",
+ "cockpit.lifecycle.regime_effect.unknown": "Regime effect unknown",
"cockpit.operation.title": "Application status",
"cockpit.operation.decision": "Decision",
@@ -374,6 +412,7 @@ export const en: Record = {
"cockpit.operation.stage.complete": "Complete",
"cockpit.operation.stage.failed": "Failed",
"cockpit.operation.stage.unavailable": "Independent readback unavailable",
+ "cockpit.operation.reason.unknown": "Operation outcome pending confirmation",
"cockpit.operation.confirmed": "Runtime confirmed",
"cockpit.operation.refresh": "Refresh status",
"cockpit.operation.id": "Operation {value}",
@@ -401,9 +440,26 @@ export const en: Record = {
"cockpit.change_log.load_failed": "Activity history could not be read.",
"cockpit.change_log.more": "Load more",
"cockpit.change_log.actor": "Actor {value}",
- "cockpit.change_log.subject": "Subject {value}",
- "cockpit.change_log.revision": "Revision {value}",
+ "cockpit.change_log.subject": "Subject: {value}",
+ "cockpit.change_log.revision": "Record revision {value}",
"cockpit.change_log.outcome": "Outcome {value}",
+ "cockpit.change_log.action.record_owner_decision": "Owner decision recorded",
+ "cockpit.change_log.action.approval_recorded": "Approval recorded",
+ "cockpit.change_log.action.application_dispatched": "Application dispatched",
+ "cockpit.change_log.action.other": "Other action",
+ "cockpit.change_log.outcome.decided": "Decided",
+ "cockpit.change_log.outcome.recorded": "Recorded",
+ "cockpit.change_log.outcome.applying": "Applying",
+ "cockpit.change_log.outcome.applied": "Applied",
+ "cockpit.change_log.outcome.rejected": "Rejected",
+ "cockpit.change_log.outcome.failed": "Failed",
+ "cockpit.change_log.outcome.unknown": "Unknown",
+ "cockpit.change_log.subject_kind.owner_decision": "Owner decision",
+ "cockpit.change_log.subject_kind.strategy_promotion": "Strategy promotion",
+ "cockpit.change_log.subject_kind.reconciliation_recovery": "Reconciliation recovery",
+ "cockpit.change_log.subject_kind.paper_account_application": "Paper account application",
+ "cockpit.change_log.subject_kind.application": "Paper account application",
+ "cockpit.change_log.subject_kind.other": "Other subject",
"cockpit.digest.title": "Daily digest delivery",
"cockpit.digest.accepted": "Accepted for delivery",
diff --git a/web/strategy-switch-console/frontend/src/i18n/pages/decisions.ts b/web/strategy-switch-console/frontend/src/i18n/pages/decisions.ts
index 202aa9c9..00526de3 100644
--- a/web/strategy-switch-console/frontend/src/i18n/pages/decisions.ts
+++ b/web/strategy-switch-console/frontend/src/i18n/pages/decisions.ts
@@ -157,6 +157,30 @@ export const zh: Record = {
"initial_enrollment.fact.SettledCash": "已结算现金",
"initial_enrollment.fact.$LEDGER-CashBalance": "账本现金余额",
"initial_enrollment.fact.$LEDGER-TotalCashBalance": "账本现金总额",
+ "decisions.unknown_title": "其他事项",
+ "decisions.action.unknown": "未知操作",
+ "initial_enrollment.fact.other": "其他字段",
+ "initial_enrollment.value.unknown": "未知",
+ "initial_enrollment.enum.action.BUY": "买入",
+ "initial_enrollment.enum.action.SELL": "卖出",
+ "initial_enrollment.enum.side.BUY": "买入",
+ "initial_enrollment.enum.side.SELL": "卖出",
+ "initial_enrollment.enum.status.Submitted": "已提交",
+ "initial_enrollment.enum.status.Filled": "已成交",
+ "initial_enrollment.enum.status.Cancelled": "已取消",
+ "initial_enrollment.enum.status.ApiCancelled": "已取消",
+ "initial_enrollment.enum.status.PendingSubmit": "提交中",
+ "initial_enrollment.enum.status.PreSubmitted": "预提交",
+ "initial_enrollment.enum.status.PendingCancel": "取消中",
+ "initial_enrollment.enum.status.Inactive": "未激活",
+ "initial_enrollment.enum.order_type.LMT": "限价",
+ "initial_enrollment.enum.order_type.MKT": "市价",
+ "initial_enrollment.enum.right.CALL": "看涨",
+ "initial_enrollment.enum.right.C": "看涨",
+ "initial_enrollment.enum.right.PUT": "看跌",
+ "initial_enrollment.enum.right.P": "看跌",
+ "initial_enrollment.enum.sec_type.STK": "股票",
+ "initial_enrollment.enum.sec_type.OPT": "期权",
};
export const en: Record = {
@@ -317,5 +341,29 @@ export const en: Record = {
"initial_enrollment.fact.SettledCash": "Settled cash",
"initial_enrollment.fact.$LEDGER-CashBalance": "Ledger cash balance",
"initial_enrollment.fact.$LEDGER-TotalCashBalance": "Ledger total cash balance",
+ "decisions.unknown_title": "Other item",
+ "decisions.action.unknown": "Unknown action",
+ "initial_enrollment.fact.other": "Other field",
+ "initial_enrollment.value.unknown": "Unknown",
+ "initial_enrollment.enum.action.BUY": "Buy",
+ "initial_enrollment.enum.action.SELL": "Sell",
+ "initial_enrollment.enum.side.BUY": "Buy",
+ "initial_enrollment.enum.side.SELL": "Sell",
+ "initial_enrollment.enum.status.Submitted": "Submitted",
+ "initial_enrollment.enum.status.Filled": "Filled",
+ "initial_enrollment.enum.status.Cancelled": "Cancelled",
+ "initial_enrollment.enum.status.ApiCancelled": "Cancelled",
+ "initial_enrollment.enum.status.PendingSubmit": "Submitting",
+ "initial_enrollment.enum.status.PreSubmitted": "Pre-submitted",
+ "initial_enrollment.enum.status.PendingCancel": "Cancelling",
+ "initial_enrollment.enum.status.Inactive": "Inactive",
+ "initial_enrollment.enum.order_type.LMT": "Limit",
+ "initial_enrollment.enum.order_type.MKT": "Market",
+ "initial_enrollment.enum.right.CALL": "Call",
+ "initial_enrollment.enum.right.C": "Call",
+ "initial_enrollment.enum.right.PUT": "Put",
+ "initial_enrollment.enum.right.P": "Put",
+ "initial_enrollment.enum.sec_type.STK": "Stock",
+ "initial_enrollment.enum.sec_type.OPT": "Option",
};
diff --git a/web/strategy-switch-console/frontend/src/i18n/pages/strategies.ts b/web/strategy-switch-console/frontend/src/i18n/pages/strategies.ts
index e7fa52a3..fc0a55b6 100644
--- a/web/strategy-switch-console/frontend/src/i18n/pages/strategies.ts
+++ b/web/strategy-switch-console/frontend/src/i18n/pages/strategies.ts
@@ -18,6 +18,8 @@ export const zh: Record = {
"research.source.generic": "研究来源",
"research.source.quant-advisor-research": "量化研究信号",
"research.source.quant_advisor_research": "量化研究信号",
+ "research.source.research-mirror": "研究镜像",
+ "research.source.other": "其他研究来源",
"research.state.context_only": "仅作背景",
"research.state.candidate": "研究候选",
"research.state.source_verification_required": "来源待核实",
@@ -54,6 +56,8 @@ export const en: Record = {
"research.source.generic": "Research source",
"research.source.quant-advisor-research": "Quant research signal",
"research.source.quant_advisor_research": "Quant research signal",
+ "research.source.research-mirror": "Research mirror",
+ "research.source.other": "Other research source",
"research.state.context_only": "Context only",
"research.state.candidate": "Research candidate",
"research.state.source_verification_required": "Source verification required",
diff --git a/web/strategy-switch-console/frontend/src/pages/accounts/ControlsTab.tsx b/web/strategy-switch-console/frontend/src/pages/accounts/ControlsTab.tsx
index 58eeda95..a79ccde7 100644
--- a/web/strategy-switch-console/frontend/src/pages/accounts/ControlsTab.tsx
+++ b/web/strategy-switch-console/frontend/src/pages/accounts/ControlsTab.tsx
@@ -73,11 +73,7 @@ export function ControlsTab({
if (data.account_alias !== account.account_alias || data.platform !== account.platform) return ;
const isRuntimeControlPlatform = RUNTIME_CONTROL_PLATFORMS.has(account.platform.toLowerCase());
- const reasonText = (reason: string): string => {
- const key = `reason.${reason}`;
- const translated = t(key);
- return translated === key ? reason : translated;
- };
+ const reasonText = (reason: string): string => t(`reason.${reason}`);
const control = data.runtime_control;
const controlLocked = Boolean(pendingControl?.targetKey === targetKey) || Boolean(control && CONTROL_LOCKED_PHASES.has(control.phase || ""));
diff --git a/web/strategy-switch-console/frontend/src/pages/accounts/helpers.ts b/web/strategy-switch-console/frontend/src/pages/accounts/helpers.ts
index 4dd877c5..eed81b8e 100644
--- a/web/strategy-switch-console/frontend/src/pages/accounts/helpers.ts
+++ b/web/strategy-switch-console/frontend/src/pages/accounts/helpers.ts
@@ -28,10 +28,9 @@ export function profileOptions(config: ConsoleConfig | null, language: Language)
.map(item => ({ profile: item.profile, label: strategyName(item.profile, language, language === "zh" ? item.label_zh : item.label_en) }));
}
-/** Backend label_key first; fall back to the page dictionary when the key is not translated. */
-export function fieldLabel(t: Translate, row: Pick): string {
- const translated = t(row.label_key);
- return translated === row.label_key ? t(`accounts.field.${row.field}`) : translated;
+/** Backend label_key first; unknown keys fall back to the account-field copy, never the raw field. */
+export function fieldLabel(t: Translate, row: { label_key: string; field: string }): string {
+ return t(row.label_key, {}, `accounts.field.${row.field}`);
}
export function sectionLabel(t: Translate, section: SettingsSection): string {
diff --git a/web/strategy-switch-console/frontend/src/pages/decisions/DecisionsPage.tsx b/web/strategy-switch-console/frontend/src/pages/decisions/DecisionsPage.tsx
index a86db46b..7d9c9cf7 100644
--- a/web/strategy-switch-console/frontend/src/pages/decisions/DecisionsPage.tsx
+++ b/web/strategy-switch-console/frontend/src/pages/decisions/DecisionsPage.tsx
@@ -35,6 +35,7 @@ import { expiryState, isRiskTier, loadOperation } from "../../api/cockpit";
import type { EvidenceSummary as EvidenceSummaryData } from "../../api/cockpit";
import { ApplicationStatus, EvidenceSummary, ExpiryIndicator, tierLabel } from "../../components/cockpit";
import { resolveAccountChoice } from "./helpers";
+import { fieldLabel } from "../accounts/helpers";
import { sourceConsequenceKey, sourceLabelKey } from "./sources";
import { InitialEnrollmentFacts } from "./InitialEnrollmentFacts";
import { ReconciliationMaterials } from "./ReconciliationMaterials";
@@ -617,13 +618,13 @@ export default function DecisionsPage({ decision }: { decision: string | null })
? ` · ${tierLabel(risk.risk_profile, t)}${risk.policy_revision ? ` · ${t("cockpit.policy.revision", { value: risk.policy_revision })}` : ""}`
: "";
const target = account
- ? `${itemTitle(item, t)} · ${account.label} (${account.platform} / ${account.key} / ${account.mode})${riskText}`
+ ? `${itemTitle(item, t)} · ${account.label} (${names.platform(account.platform)} / ${account.key} / ${t(`strategies.execution.${account.mode}`, {}, "strategies.execution.unknown")})${riskText}`
: `${itemTitle(item, t)}${riskText}`;
return confirm({
title: t(action === "adopt" ? "decisions.confirm_adopt_title" : "decisions.confirm_decline_title"),
target,
summary: t(paperAdopt ? "decisions.apply_note" : "decisions.confirm_body"),
- body: ({ ...row, label: t(row.label_key) === row.label_key ? row.field : t(row.label_key), affectsLive: row.affects_live }))} />,
+ body: ({ ...row, label: fieldLabel(t, row), affectsLive: row.affects_live }))} />,
consequence: t("decisions.intent_note"),
tone: action === "adopt" ? "normal" : "danger",
confirmLabel: t(action === "adopt" ? "decisions.confirm_adopt_label" : "decisions.confirm_decline_label"),
@@ -663,8 +664,7 @@ export default function DecisionsPage({ decision }: { decision: string | null })
function itemTitle(item: InboxItem, translate: ReturnType): string {
if (isInitialEnrollmentItem(item)) return translate("decisions.initial_enrollment_title");
- const translated = translate(item.title_key);
- return translated === item.title_key ? item.short_id : translated;
+ return translate(item.title_key, {}, "decisions.unknown_title");
}
/* ---------- render ---------- */
@@ -680,8 +680,7 @@ export default function DecisionsPage({ decision }: { decision: string | null })
const expired = isExpired(item);
const pending = item.status === "pending" && !expired;
const diffRows = item.diff.map(row => {
- const label = t(row.label_key);
- return { field: row.field, label: label === row.label_key ? row.field : label, before: row.before, after: row.after, affectsLive: row.affects_live };
+ return { field: row.field, label: fieldLabel(t, row), before: row.before, after: row.after, affectsLive: row.affects_live };
});
const busyIntent = Boolean(busy[`intent:${item.id}`]);
const busyApply = Boolean(busy[`apply:${subjectIdOf(item)}`]);
diff --git a/web/strategy-switch-console/frontend/src/pages/decisions/InitialEnrollmentFacts.tsx b/web/strategy-switch-console/frontend/src/pages/decisions/InitialEnrollmentFacts.tsx
index 2d090fc3..d8d8b605 100644
--- a/web/strategy-switch-console/frontend/src/pages/decisions/InitialEnrollmentFacts.tsx
+++ b/web/strategy-switch-console/frontend/src/pages/decisions/InitialEnrollmentFacts.tsx
@@ -97,17 +97,17 @@ function displaySurface(raw: unknown): DisplaySurface {
return { status: "unknown", items: null, scope };
}
-function humanizeKey(key: string): string {
- const text = key.replace(/_/g, " ").replace(/\s+/g, " ").trim();
- return text || key;
+/** Known source fields have locale copy; unknown fields use the shared fallback. */
+function factLabel(key: string, t: Translate): string {
+ return t(`initial_enrollment.fact.${key}`, {}, "initial_enrollment.fact.other");
}
-/** Known source fields have locale copy; an unknown source field is only
- * humanized, never rendered as a JSON fragment. */
-function factLabel(key: string, t: Translate): string {
- const messageKey = `initial_enrollment.fact.${key}`;
- const translated = t(messageKey);
- return translated === messageKey ? humanizeKey(key) : translated;
+const ENUM_FACT_FIELDS = ["action", "side", "status", "order_type", "right", "sec_type"] as const;
+
+function enumValue(field: string, value: string, t: Translate): string {
+ return (ENUM_FACT_FIELDS as readonly string[]).includes(field)
+ ? t(`initial_enrollment.enum.${field}.${value}`, {}, "initial_enrollment.value.unknown")
+ : value;
}
function scopeValue(field: ScopeField, value: string | null, t: Translate): string {
@@ -115,19 +115,18 @@ function scopeValue(field: ScopeField, value: string | null, t: Translate): stri
if (field === "truncated") {
if (value === "true") return t("initial_enrollment.value.true");
if (value === "false") return t("initial_enrollment.value.false");
+ return t("initial_enrollment.value.unknown");
}
- if (field === "gateway_availability" && value === "current_broker_day") return t("initial_enrollment.current_broker_day");
- if (field === "kind") {
- const messageKey = `initial_enrollment.scope_kind.${value}`;
- const translated = t(messageKey);
- if (translated !== messageKey) return translated;
+ if (field === "gateway_availability") {
+ return value === "current_broker_day" ? t("initial_enrollment.current_broker_day") : t("initial_enrollment.value.unknown");
}
+ if (field === "kind") return t(`initial_enrollment.scope_kind.${value}`, {}, "initial_enrollment.value.unknown");
return value;
}
-function FactValue({ value, t }: { value: InitialEnrollmentFactValue; t: Translate }) {
+function FactValue({ value, t, field }: { value: InitialEnrollmentFactValue; t: Translate; field?: string }) {
if (value === null) return <>{t("common.unknown")}>;
- if (typeof value === "string") return <>{value || t("common.na")}>;
+ if (typeof value === "string") return <>{value ? enumValue(field ?? "", value, t) : t("common.na")}>;
if (typeof value === "number") {
return <>{Number.isFinite(value) ? String(value) : t("common.unknown")}>;
}
@@ -139,7 +138,7 @@ function FactValue({ value, t }: { value: InitialEnrollmentFactValue; t: Transla
return (
{value.map((nested, index) => (
-
+
))}
);
@@ -149,9 +148,9 @@ function FactValue({ value, t }: { value: InitialEnrollmentFactValue; t: Transla
return (
{entries.map(([key, nested]) => (
-
+
- {factLabel(key, t)}
-
+
))}
diff --git a/web/strategy-switch-console/frontend/src/pages/strategies/StrategiesPage.tsx b/web/strategy-switch-console/frontend/src/pages/strategies/StrategiesPage.tsx
index 85899b45..14e16a4e 100644
--- a/web/strategy-switch-console/frontend/src/pages/strategies/StrategiesPage.tsx
+++ b/web/strategy-switch-console/frontend/src/pages/strategies/StrategiesPage.tsx
@@ -347,13 +347,9 @@ function ResearchTab({ refreshEpoch }: { refreshEpoch: number }) {
{entry.subject.identifier}
{entry.observations.map((observation, observationIndex) => {
- const sourceLabels = observation.source_ids.map(id => {
- const key = `research.source.${id}`;
- const label = t(key);
- return label === key ? id : label;
- });
+ const sourceLabels = observation.source_ids.map(id => t(`research.source.${id}`));
return (
-
+
\n \n \n \n \n \n \n 总览 · QuantStrategyLab\n \n \n \n \n \n \n \n |