Add frontend Chinese English i18n
This commit is contained in:
@@ -119,6 +119,7 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
|
||||
- latest_decision_assistant_digest_browser_slice: digest_history_browser_review_sequence
|
||||
- latest_decision_assistant_digest_preference_slice: digest_preference_profile_local_mode
|
||||
- latest_trader_mode_shell_slice: unified_workbench_left_tree_route
|
||||
- latest_frontend_i18n_slice: zh_cn_default_en_switcher
|
||||
- latest_decision_assistant_workflow_queue_slice: assistant_loop_queue
|
||||
- latest_decision_assistant_workspace_loop_slice: workflow_section_navigation_loop_queue
|
||||
- latest_decision_assistant_learning_focus_slice: learning_focus_queue
|
||||
@@ -802,6 +803,7 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
|
||||
- Trader Mode usefulness compression passed on 2026-05-20: `/trader-mode` and the embedded Decision Assistant watchlist now prioritize trader-language first-screen answers: `Why No Trade`, `Watch Next`, `Focus Reason`, `Data State`, and `No Candidate`. Watchlist queue cards now say why not to trade, what to watch next, and why the unique focus deserves attention instead of exposing internal reason buckets first. The stale/freshness gate no longer freezes the page just because an old candidate timestamp exists when fresh market/review timestamps are available, and read-only runtime fallback now treats market stream / broker sync degradation as warnings for analysis-only mode while strict execution/readiness gates stay unchanged. Profile filtering now preserves the server-generated readout when the current focus already matches the trader profile, avoiding fallback text that degraded `FVG retrace` into generic `confirmation`. State-change alerts now show readable trader messages instead of raw internal signatures. Validation passed 23 focused operator/static tests, `node --check`, Python compile checks, full unit discovery (`787` tests), secret static checks, script static checks, `git diff --check` with only LF/CRLF warnings, deploy smoke over 11 endpoints, standard remote ops smoke including `unattended_harness`, remote `run_readonly_live_analysis_smoke.py` with `ready_for_readonly_live_analysis=true` and `must_not_trade=true`, and gstack browser screenshots at `output/playwright/trader-mode-usefulness-readout-remote-final3-390x844.png` plus `output/playwright/trader-mode-usefulness-readout-remote-final3-1366x768.png`.
|
||||
- First real Trader Mode R2 use loop executed on 2026-05-20: used remote `/trader-mode` as the primary screen, observed `Observe Only: Watch Candidate #4610: FVG retrace`, and recorded a real `No Trade` action from the UI because Candidate `4610` remained `blocked` / `executable=false`. This correctly updated `shadow_paper_observations` instead of creating a fake paper ticket: `operator_feedback_count` increased from `1` to `2`, `shadow_observation_count` increased from `14` to `15`, while `paper_ticket_count=0` and `paper_execution_count=0` stayed unchanged. Saved after-close draft note `operator_notes.id=3` with scope `weekly_review_draft`; `/trading/journal/generate` returned `created=0`, so `weekly_review_entry_count` remains `0` until a real execution-derived journal entry exists. Evidence screenshot saved at `output/playwright/trader-mode-r2-real-use-loop-390x844.png`. Current R2 gate remains `paper_ticket_sample_gate`; do not mark R2 completed.
|
||||
- Trader Mode unified shell closure passed on 2026-05-20: `/trader-mode` now resolves to the same left-menu/right-workspace `index.html` shell instead of the standalone page, with the Trader Mode section active from the route and the sidebar `Trader Mode` tree item selected. The embedded Trader Mode surface keeps the fast-read status, live readout, fast queue, freshness, focus, action buttons, after-close draft, R2 evidence, and read-only boundary controls, while the generic workbench filter bar is hidden only for this section. `trader_mode.js` is scoped so it can run inside the workbench without leaking globals into `app.js`; keyboard shortcuts and auto-refresh are active only while the Trader Mode section is visible. Remote validation passed deploy smoke over 11 endpoints, gstack browser checks on `http://192.168.3.90:8000/trader-mode` (`bodySection=trader-mode`, active nav=`Trader Mode`, sidebar present, generic controls hidden, loaded status=`Observe Only`), `run_readonly_live_analysis_smoke.py`, and `run_remote_ops_smoke.py`. Evidence screenshot saved at `output/playwright/trader-mode-unified-shell-1366x768-loaded.png`. No change was made to `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, or operator confirmation protocol.
|
||||
- Frontend Chinese/English i18n closure passed on 2026-05-20: added shared `/static/i18n.js`, default locale `zh-CN`, persistent `ai_ict_locale_v1`, topbar language switchers on the workbench, standalone Trader Mode fallback page, and training page. `app.js`, `trader_mode.js`, and `training.js` now route dynamic `setText` output through the same translation layer, so both static shell labels and live Trader Mode status/readout labels can switch between Chinese and English without changing backend payloads. Mutation handling was hardened to avoid self-triggered translation loops during language changes. Remote validation confirmed default Chinese with no stored locale (`lang=zh-CN`, heading `AI ICT 交易工作台`, status `仅观察`, readout `为什么不交易`) and English switching (`lang=en`, heading `AI ICT Trading Workbench`, status `Observe Only`), with no browser console errors. Evidence screenshots saved at `output/playwright/i18n-trader-mode-zh-default-1366x768.png` and `output/playwright/i18n-trader-mode-en-1366x768.png`. Local validation passed i18n/app/trader/training JS syntax checks, focused static/read-only/operator tests, script static checks, full unit discovery (`789` tests), deploy smoke over 11 endpoints, `run_remote_ops_smoke.py`, and `run_readonly_live_analysis_smoke.py`. No change was made to `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, or operator confirmation protocol.
|
||||
- Current assisted-analysis next gate: keep R1/R2 focused on `decision_support_ready` trust/sample/quality evidence and shadow/paper/operator feedback collection. OKX trade-permission sandbox smoke is deferred until the project explicitly re-enters execution adapter or real-trading preparation.
|
||||
|
||||
## Exact Next Step
|
||||
|
||||
@@ -163,6 +163,7 @@
|
||||
- [x] Workbench frontend split-navigation closure: converted the long root workbench into a left-menu/right-workspace layout, added hash/localStorage section switching, added frontend request timeout fallback so slow review/readiness APIs no longer freeze the page, and validated desktop/narrow rendering
|
||||
- [x] Live usefulness blocker recovery: root workbench split-navigation is usable, slow frontend requests time out instead of freezing the page, remote DB/compose/deploy/migration/proxy/runtime blockers were recovered, standard remote ops smoke including `unattended_harness` passed, and readonly live analysis smoke passed without enabling order submission
|
||||
- [x] Trader Mode unified workbench shell closure: `/trader-mode` now opens inside the same left-tree/right-workspace framework with the Trader Mode tree item active, hides the generic workbench filters only for this section, scopes Trader Mode JS to avoid app globals/hidden-section shortcuts, deploy smoke passed, remote gstack browser verification passed, readonly live analysis smoke passed, and remote ops smoke passed
|
||||
- [x] Frontend Chinese/English i18n closure: added shared `/static/i18n.js`, default Chinese locale, persistent language selector, dynamic `setText` translation in workbench / Trader Mode / training pages, remote browser evidence for Chinese default and English switch, full unit validation, deploy smoke, remote ops smoke, and readonly live analysis smoke
|
||||
- [ ] Next trader-usefulness refinement: compress Trader Mode / watchlist language around why no trade, what to watch next, unique focus quality, stale data, and no-candidate states so the first screen answers live operator questions faster
|
||||
- [ ] R2 next collection action after readonly launch: wait for or surface a real executable candidate, then build shadow paper ticket / confirm paper execution / record operator feedback / add weekly review evidence without changing execution/live gates
|
||||
- [x] R3 OKX sandbox preflight starter implemented and deployed: signed dry-run request previews, idempotency key preview, error normalization catalog, `/trading/okx-sandbox-preflight`, live-readiness summary, cockpit surface, deploy smoke, remote ops smoke, and remote field/UI smoke
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@@ -140,6 +140,10 @@ function downloadJsonFile(filename, payload) {
|
||||
|
||||
function setText(element, value) {
|
||||
if (!element) return;
|
||||
if (window.I18N?.setText) {
|
||||
window.I18N.setText(element, value);
|
||||
return;
|
||||
}
|
||||
element.textContent = value == null ? '' : String(value);
|
||||
}
|
||||
|
||||
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
(() => {
|
||||
const STORAGE_KEY = 'ai_ict_locale_v1';
|
||||
const DEFAULT_LOCALE = 'zh-CN';
|
||||
const SUPPORTED_LOCALES = new Set(['zh-CN', 'en']);
|
||||
const textNodeSources = new WeakMap();
|
||||
|
||||
const zhExact = {
|
||||
'AI ICT Trading Workbench': 'AI ICT 交易工作台',
|
||||
'Live plan, candidate, ticket, position, and review flow.': '实时计划、候选、票据、仓位与复盘流程。',
|
||||
'Open Trader Mode': '打开交易员模式',
|
||||
'Language': '语言',
|
||||
'中文': '中文',
|
||||
'English': 'English',
|
||||
'Workspace': '工作区',
|
||||
'Trader Mode': '交易员模式',
|
||||
'Overview': '总览',
|
||||
'Decision Assistant': '决策助手',
|
||||
'Trading Status': '交易状态',
|
||||
'Runtime Supervisor': '运行监督',
|
||||
'Decision Packet': '决策包',
|
||||
'Readiness': '就绪状态',
|
||||
'Session Focus': '交易时段焦点',
|
||||
'Pre-Market Plan': '盘前计划',
|
||||
'Intraday Candidates': '盘中候选',
|
||||
'Execution Tickets': '执行票据',
|
||||
'Alerts': '提醒',
|
||||
'Visual Reviews': '视觉复核',
|
||||
'Position Management': '仓位管理',
|
||||
'Operator Console': '操作台',
|
||||
'Post-Trade Review': '盘后复盘',
|
||||
'Candles': 'K线',
|
||||
'Instrument ID': '品种 ID',
|
||||
'Timeframe': '周期',
|
||||
'Limit': '数量限制',
|
||||
'Auto Refresh': '自动刷新',
|
||||
'Execution Status': '执行状态',
|
||||
'Trust Class': '信任分组',
|
||||
'Search': '搜索',
|
||||
'Refresh': '刷新',
|
||||
'All': '全部',
|
||||
'All Sessions': '全部时段',
|
||||
'Whitelist': '白名单',
|
||||
'Observe': '观察',
|
||||
'Blacklist': '黑名单',
|
||||
'Open': '打开',
|
||||
'Closed': '已关闭',
|
||||
'Pending': '待处理',
|
||||
'Rejected': '已拒绝',
|
||||
'Cancelled': '已取消',
|
||||
'Filters: none': '筛选:无',
|
||||
'Loading workspace...': '正在加载工作台...',
|
||||
'Loading Trader Mode...': '正在加载交易员模式...',
|
||||
'Read-only live brief for fast intraday review.': '只读盘中速读简报,用于快速复核。',
|
||||
'Status': '状态',
|
||||
'Decision Now': '当前判断',
|
||||
'Next Action': '下一步',
|
||||
'Loading': '加载中',
|
||||
'Waiting for live brief.': '等待实时简报。',
|
||||
'n/a': '无',
|
||||
'Live Readout': '实时速读',
|
||||
'Fast Queue': '速读队列',
|
||||
'Profile': '偏好模板',
|
||||
'Profile Name': '模板名称',
|
||||
'Sessions': '时段',
|
||||
'Setups': '形态',
|
||||
'Save Profile': '保存模板',
|
||||
'Reset Profile': '重置模板',
|
||||
'Enable Alerts': '启用提醒',
|
||||
'Disable Alerts': '停用提醒',
|
||||
'Freshness': '数据新鲜度',
|
||||
'Market Last Updated': '行情最后更新',
|
||||
'Review Packet Updated': '复核包更新时间',
|
||||
'Runtime Online': '运行状态在线',
|
||||
'Stale Seconds': '过期秒数',
|
||||
'Chart Bridge': '图表桥接',
|
||||
'Duty Panel': '值班面板',
|
||||
'Focus': '焦点',
|
||||
'Three Reasons': '三条原因',
|
||||
'Recovery': '恢复动作',
|
||||
'Record Action': '记录盘中动作',
|
||||
'Agree': '同意',
|
||||
'Disagree': '不同意',
|
||||
'No Trade': '不交易',
|
||||
'Need More Evidence': '需要更多证据',
|
||||
'Write Quick Note': '写快速备注',
|
||||
'No action recorded in this browser session.': '当前浏览器会话尚未记录动作。',
|
||||
'Optional quick note for this live brief.': '可选:为当前速读简报记录快速备注。',
|
||||
'After-Close Draft': '盘后草稿',
|
||||
'Save Draft Note': '保存草稿备注',
|
||||
'R2 Evidence Window': 'R2 证据窗口',
|
||||
'Boundary': '边界',
|
||||
'Review Now': '立即复核',
|
||||
'Observe Only': '仅观察',
|
||||
'Do Not Use': '不要使用',
|
||||
'Why No Trade': '为什么不交易',
|
||||
'Watch Next': '下一步观察',
|
||||
'Focus Reason': '焦点原因',
|
||||
'Data State': '数据状态',
|
||||
'No Candidate': '无候选',
|
||||
'Read-only analysis': '只读分析',
|
||||
'No order submission': '不提交订单',
|
||||
'Not a buy/sell instruction': '不是买卖指令',
|
||||
'Execution disabled': '执行已禁用',
|
||||
'Training': '训练',
|
||||
'AI ICT Training Console': 'AI ICT 训练控制台',
|
||||
'Public training cases, attempts, annotations, and progress.': '公开训练案例、尝试、标注与进度。',
|
||||
'Import / Export': '导入 / 导出',
|
||||
'Cases': '案例',
|
||||
'Case Detail': '案例详情',
|
||||
'Progress': '进度',
|
||||
'Training workflow': '训练流程',
|
||||
'Case list': '案例列表',
|
||||
'Case detail': '案例详情',
|
||||
'Attempt flow': '尝试流程',
|
||||
'Import case package JSON': '导入案例包 JSON',
|
||||
'Paste case package JSON': '粘贴案例包 JSON',
|
||||
'Import cases': '导入案例',
|
||||
'Export cases': '导出案例',
|
||||
'Reload training data': '重新加载训练数据',
|
||||
'Refresh progress': '刷新进度',
|
||||
'Training annotation note': '训练标注备注',
|
||||
'Annotation note': '标注备注',
|
||||
'Save annotation': '保存标注',
|
||||
'Submit attempt': '提交尝试',
|
||||
'Start selected attempt': '开始所选尝试',
|
||||
'No active attempt.': '没有进行中的尝试。',
|
||||
};
|
||||
|
||||
const zhPhrases = [
|
||||
['Observe Only', '仅观察'],
|
||||
['Review Now', '立即复核'],
|
||||
['Do Not Use', '不要使用'],
|
||||
['Read-only Live Analysis', '只读实盘分析'],
|
||||
['Trader Mode / Live Brief', '交易员模式 / 实时简报'],
|
||||
['ready_for_readonly_live_analysis', '只读实盘分析就绪'],
|
||||
['must_not_trade', '必须不交易'],
|
||||
['Execution remains disabled', '执行保持禁用'],
|
||||
['Candidate present', '存在候选'],
|
||||
['Candidate #', '候选 #'],
|
||||
['Candidate', '候选'],
|
||||
['Decision packet', '决策包'],
|
||||
['No trade', '不交易'],
|
||||
['Watch Candidate', '观察候选'],
|
||||
['Watch Decision packet', '观察决策包'],
|
||||
['Watch', '观察'],
|
||||
['Focus:', '焦点:'],
|
||||
['soft watch', '轻观察'],
|
||||
['soft wait', '轻等待'],
|
||||
['wait FVG retrace', '等待 FVG 回撤'],
|
||||
['wait clean signal', '等待清晰信号'],
|
||||
['wait trust gate', '等待信任门禁'],
|
||||
['clean signal', '清晰信号'],
|
||||
['FVG retrace', 'FVG 回撤'],
|
||||
['trust gate', '信任门禁'],
|
||||
['restore market feed first', '先恢复行情源'],
|
||||
['Refresh runtime/feed first', '先刷新运行状态/行情源'],
|
||||
['restore runtime first', '先恢复运行状态'],
|
||||
['until status leaves', '直到状态离开'],
|
||||
['ignore', '忽略'],
|
||||
['setup output', '形态输出'],
|
||||
['ignore setup output until status leaves Do Not Use', '状态离开“不要使用”前忽略形态输出'],
|
||||
['runtime', '运行状态'],
|
||||
['stream connecting', '行情流连接中'],
|
||||
['runtime ok', '运行正常'],
|
||||
[' ok', ' 正常'],
|
||||
['market ok', '行情正常'],
|
||||
['drill down only if needed', '仅在需要时下钻'],
|
||||
['read-only', '只读'],
|
||||
['review packet', '复核包'],
|
||||
['operator feedback', '操作员反馈'],
|
||||
['paper ticket', '纸面票据'],
|
||||
['paper execution', '纸面执行'],
|
||||
['weekly review', '周复盘'],
|
||||
['blocked', '已阻塞'],
|
||||
['fresh', '新鲜'],
|
||||
['stale', '过期'],
|
||||
['available', '可用'],
|
||||
['unavailable', '不可用'],
|
||||
['unknown', '未知'],
|
||||
['active', '激活'],
|
||||
['monitor', '监控'],
|
||||
['queued', '已排队'],
|
||||
['review', '复核'],
|
||||
['warning', '警告'],
|
||||
['passed', '通过'],
|
||||
['failed', '失败'],
|
||||
['yes', '是'],
|
||||
['no', '否'],
|
||||
['none', '无'],
|
||||
['Next step:', '下一步:'],
|
||||
['Loading case list.', '正在加载案例列表。'],
|
||||
['Loading progress.', '正在加载进度。'],
|
||||
['Select a case to review its public context.', '选择一个案例查看公开上下文。'],
|
||||
];
|
||||
|
||||
const zhPatterns = [
|
||||
[/^Observe Only: Watch Candidate #(\d+): (.+)\.$/i, '仅观察:观察候选 #$1:$2。'],
|
||||
[/^Review Now: Watch Candidate #(\d+): (.+)\.$/i, '立即复核:观察候选 #$1:$2。'],
|
||||
[/^Watch Candidate #(\d+): (.+)\.$/i, '观察候选 #$1:$2。'],
|
||||
[/^No trade: (.+)\.$/i, '不交易:$1。'],
|
||||
[/^Focus: (.+)$/i, '焦点:$1'],
|
||||
[/^Last updated: (.+)$/i, '最后更新:$1'],
|
||||
[/^Filters: (.+)$/i, '筛选:$1'],
|
||||
[/^API online \/ (.+)$/i, 'API 在线 / $1'],
|
||||
[/^Profile saved: (.+)$/i, '模板已保存:$1'],
|
||||
[/^(.+) recorded at (.+)\.$/i, '$1 已记录于 $2。'],
|
||||
[/^After-close draft saved at (.+)\.$/i, '盘后草稿已保存于 $1。'],
|
||||
[/^Shortcut: (.+)$/i, '快捷键:$1'],
|
||||
[/^Candidate #(\d+)$/i, '候选 #$1'],
|
||||
[/^ID (.+)$/i, 'ID $1'],
|
||||
[/^quality: (.+)$/i, '质量:$1'],
|
||||
[/^title: (.+)$/i, '标题:$1'],
|
||||
[/^case id: (.+)$/i, '案例 ID:$1'],
|
||||
[/^status: (.+)$/i, '状态:$1'],
|
||||
[/^skill: (.+)$/i, '技能:$1'],
|
||||
[/^timeframe: (.+)$/i, '周期:$1'],
|
||||
];
|
||||
|
||||
function storedLocale() {
|
||||
try {
|
||||
const value = window.localStorage?.getItem(STORAGE_KEY);
|
||||
return SUPPORTED_LOCALES.has(value) ? value : DEFAULT_LOCALE;
|
||||
} catch {
|
||||
return DEFAULT_LOCALE;
|
||||
}
|
||||
}
|
||||
|
||||
let currentLocale = storedLocale();
|
||||
|
||||
function applyPhraseTranslations(source) {
|
||||
let translated = source;
|
||||
for (const [from, to] of zhPhrases) {
|
||||
translated = translated.replaceAll(from, to);
|
||||
}
|
||||
return translated;
|
||||
}
|
||||
|
||||
function translateText(value, locale = currentLocale) {
|
||||
const source = value == null ? '' : String(value);
|
||||
if (locale === 'en' || !source.trim()) return source;
|
||||
const exact = zhExact[source.trim()];
|
||||
if (exact && source.trim() === source) return exact;
|
||||
for (const [pattern, replacement] of zhPatterns) {
|
||||
if (pattern.test(source)) return applyPhraseTranslations(source.replace(pattern, replacement));
|
||||
}
|
||||
return applyPhraseTranslations(source);
|
||||
}
|
||||
|
||||
function setDocumentLanguage() {
|
||||
document.documentElement.lang = currentLocale;
|
||||
if (!document.documentElement.dataset.i18nTitleSource) {
|
||||
document.documentElement.dataset.i18nTitleSource = document.title || '';
|
||||
}
|
||||
const translatedTitle = translateText(document.documentElement.dataset.i18nTitleSource || document.title);
|
||||
if (document.title !== translatedTitle) {
|
||||
document.title = translatedTitle;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldSkipNode(node) {
|
||||
const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
||||
if (!element) return true;
|
||||
return Boolean(element.closest('script, style, textarea, code, pre'));
|
||||
}
|
||||
|
||||
function translateTextNode(node) {
|
||||
if (!node || shouldSkipNode(node)) return;
|
||||
const parentSource = node.parentElement?.dataset?.i18nSourceText;
|
||||
if (parentSource != null) {
|
||||
textNodeSources.set(node, parentSource);
|
||||
const translated = translateText(parentSource);
|
||||
if (node.nodeValue !== translated) {
|
||||
node.nodeValue = translated;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!textNodeSources.has(node)) {
|
||||
textNodeSources.set(node, node.nodeValue || '');
|
||||
}
|
||||
const translated = translateText(textNodeSources.get(node));
|
||||
if (node.nodeValue !== translated) {
|
||||
node.nodeValue = translated;
|
||||
}
|
||||
}
|
||||
|
||||
function translateAttributes(element) {
|
||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
|
||||
for (const attr of ['placeholder', 'title', 'aria-label']) {
|
||||
if (!element.hasAttribute(attr)) continue;
|
||||
const sourceAttr = `data-i18n-source-${attr}`;
|
||||
if (!element.hasAttribute(sourceAttr)) {
|
||||
element.setAttribute(sourceAttr, element.getAttribute(attr) || '');
|
||||
}
|
||||
const translated = translateText(element.getAttribute(sourceAttr) || '');
|
||||
if (element.getAttribute(attr) !== translated) {
|
||||
element.setAttribute(attr, translated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function translateTree(root = document.body) {
|
||||
if (!root) return;
|
||||
if (root.nodeType === Node.TEXT_NODE) {
|
||||
translateTextNode(root);
|
||||
return;
|
||||
}
|
||||
if (root.nodeType !== Node.ELEMENT_NODE && root.nodeType !== Node.DOCUMENT_NODE) return;
|
||||
if (root.nodeType === Node.ELEMENT_NODE) translateAttributes(root);
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
|
||||
let node = walker.nextNode();
|
||||
while (node) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
translateTextNode(node);
|
||||
} else {
|
||||
translateAttributes(node);
|
||||
}
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function setText(element, value) {
|
||||
if (!element) return;
|
||||
const source = value == null ? '' : String(value);
|
||||
element.dataset.i18nSourceText = source;
|
||||
const translated = translateText(source);
|
||||
if (element.textContent !== translated) {
|
||||
element.textContent = translated;
|
||||
}
|
||||
}
|
||||
|
||||
function setLocale(locale) {
|
||||
currentLocale = SUPPORTED_LOCALES.has(locale) ? locale : DEFAULT_LOCALE;
|
||||
try {
|
||||
window.localStorage?.setItem(STORAGE_KEY, currentLocale);
|
||||
} catch {
|
||||
// Ignore storage failures; default locale still applies for the current page.
|
||||
}
|
||||
setDocumentLanguage();
|
||||
translateTree(document.body);
|
||||
document.querySelectorAll('[data-i18n-source-text]').forEach((element) => {
|
||||
const translated = translateText(element.dataset.i18nSourceText || '');
|
||||
if (element.textContent !== translated) {
|
||||
element.textContent = translated;
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('[data-i18n-language-select]').forEach((select) => {
|
||||
select.value = currentLocale;
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('ai-ict-locale-change', { detail: { locale: currentLocale } }));
|
||||
}
|
||||
|
||||
function installLanguageSelect() {
|
||||
document.querySelectorAll('#languageSelect, [data-i18n-language-select]').forEach((select) => {
|
||||
select.dataset.i18nLanguageSelect = 'true';
|
||||
select.value = currentLocale;
|
||||
if (select.dataset.i18nBound === 'true') return;
|
||||
select.dataset.i18nBound = 'true';
|
||||
select.addEventListener('change', () => setLocale(select.value));
|
||||
});
|
||||
}
|
||||
|
||||
function observeDom() {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === 'characterData') {
|
||||
translateTextNode(mutation.target);
|
||||
}
|
||||
mutation.addedNodes.forEach((node) => translateTree(node));
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
setDocumentLanguage();
|
||||
installLanguageSelect();
|
||||
translateTree(document.body);
|
||||
observeDom();
|
||||
}
|
||||
|
||||
window.I18N = {
|
||||
apply: translateTree,
|
||||
getLocale: () => currentLocale,
|
||||
setLocale,
|
||||
setText,
|
||||
t: translateText,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initialize, { once: true });
|
||||
} else {
|
||||
initialize();
|
||||
}
|
||||
})();
|
||||
@@ -13,6 +13,13 @@
|
||||
<p>Live plan, candidate, ticket, position, and review flow.</p>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<label class="language-switcher" for="languageSelect">
|
||||
<span>Language</span>
|
||||
<select id="languageSelect" data-i18n-language-select>
|
||||
<option value="zh-CN">中文</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</label>
|
||||
<a class="nav-link" href="/trader-mode">Open Trader Mode</a>
|
||||
<div id="healthStatus" class="health-card">Loading workspace...</div>
|
||||
</div>
|
||||
@@ -320,6 +327,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/i18n.js"></script>
|
||||
<script src="/static/app.js"></script>
|
||||
<script src="/static/trader_mode.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -37,6 +37,26 @@ body {
|
||||
text-decoration: none;
|
||||
background: #0f1530;
|
||||
}
|
||||
.language-switcher {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 40px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #31436c;
|
||||
border-radius: 8px;
|
||||
background: #0f1530;
|
||||
color: #c7d2fe;
|
||||
font-size: 13px;
|
||||
}
|
||||
.language-switcher select {
|
||||
min-width: 92px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #31436c;
|
||||
border-radius: 8px;
|
||||
background: #0d1430;
|
||||
color: #fff;
|
||||
}
|
||||
.health-card {
|
||||
padding: 12px 16px;
|
||||
min-width: 220px;
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
<p>Read-only live brief for fast intraday review.</p>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<label class="language-switcher" for="languageSelect">
|
||||
<span>Language</span>
|
||||
<select id="languageSelect" data-i18n-language-select>
|
||||
<option value="zh-CN">中文</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</label>
|
||||
<a class="nav-link" href="/">Workbench</a>
|
||||
<div id="traderModeHealth" class="health-card">Loading Trader Mode...</div>
|
||||
</div>
|
||||
@@ -141,6 +148,7 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/static/i18n.js"></script>
|
||||
<script src="/static/trader_mode.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -99,6 +99,10 @@ let traderAlertsEnabled = localStorage.getItem(TRADER_ALERTS_KEY) === '1';
|
||||
|
||||
function setText(element, value) {
|
||||
if (!element) return;
|
||||
if (window.I18N?.setText) {
|
||||
window.I18N.setText(element, value);
|
||||
return;
|
||||
}
|
||||
element.textContent = value == null ? '' : String(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,15 @@
|
||||
<h1>AI ICT Training Console</h1>
|
||||
<p>Public training cases, attempts, annotations, and progress.</p>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<label class="language-switcher" for="languageSelect">
|
||||
<span>Language</span>
|
||||
<select id="languageSelect" data-i18n-language-select>
|
||||
<option value="zh-CN">中文</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="grid">
|
||||
@@ -90,6 +99,7 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/static/i18n.js"></script>
|
||||
<script src="/static/training.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -50,6 +50,10 @@ function clearNode(node) {
|
||||
|
||||
function setText(node, value) {
|
||||
if (!node) return;
|
||||
if (window.I18N?.setText) {
|
||||
window.I18N.setText(node, value);
|
||||
return;
|
||||
}
|
||||
node.textContent = value == null ? '' : String(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class ApiStaticServerTests(unittest.TestCase):
|
||||
self.assertIsNotNone(index_file)
|
||||
index_html = index_file.read_text(encoding="utf-8")
|
||||
|
||||
for asset in ("/static/styles.css", "/static/app.js", "/static/trader_mode.js"):
|
||||
for asset in ("/static/styles.css", "/static/i18n.js", "/static/app.js", "/static/trader_mode.js"):
|
||||
with self.subTest(asset=asset):
|
||||
self.assertIn(asset, index_html)
|
||||
asset_file = _resolve_static_file(asset)
|
||||
@@ -48,7 +48,7 @@ class ApiStaticServerTests(unittest.TestCase):
|
||||
self.assertIsNotNone(training_file)
|
||||
training_html = training_file.read_text(encoding="utf-8")
|
||||
|
||||
for asset in ("/static/styles.css", "/static/training.js"):
|
||||
for asset in ("/static/styles.css", "/static/i18n.js", "/static/training.js"):
|
||||
with self.subTest(asset=asset):
|
||||
self.assertIn(asset, training_html)
|
||||
asset_file = _resolve_static_file(asset)
|
||||
@@ -60,7 +60,7 @@ class ApiStaticServerTests(unittest.TestCase):
|
||||
self.assertIsNotNone(trader_mode_file)
|
||||
trader_mode_html = trader_mode_file.read_text(encoding="utf-8")
|
||||
|
||||
for asset in ("/static/styles.css", "/static/app.js", "/static/trader_mode.js"):
|
||||
for asset in ("/static/styles.css", "/static/i18n.js", "/static/app.js", "/static/trader_mode.js"):
|
||||
with self.subTest(asset=asset):
|
||||
self.assertIn(asset, trader_mode_html)
|
||||
asset_file = _resolve_static_file(asset)
|
||||
@@ -85,6 +85,15 @@ class ApiStaticServerTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr + result.stdout)
|
||||
|
||||
@unittest.skipIf(shutil.which("node") is None, "node is required for JS syntax checks")
|
||||
def test_i18n_javascript_syntax_is_valid(self) -> None:
|
||||
i18n_file = _resolve_static_file("/static/i18n.js")
|
||||
self.assertIsNotNone(i18n_file)
|
||||
|
||||
result = subprocess.run(["node", "--check", str(i18n_file)], capture_output=True, text=True, check=False)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr + result.stdout)
|
||||
|
||||
@unittest.skipIf(shutil.which("node") is None, "node is required for JS syntax checks")
|
||||
def test_trader_mode_javascript_syntax_is_valid(self) -> None:
|
||||
trader_mode_file = _resolve_static_file("/static/trader_mode.js")
|
||||
@@ -131,6 +140,8 @@ class ApiStaticServerTests(unittest.TestCase):
|
||||
for expected in (
|
||||
'class="workbench-layout"',
|
||||
'id="workbenchNav"',
|
||||
'id="languageSelect"',
|
||||
'data-i18n-language-select',
|
||||
'data-workbench-target="trader-mode"',
|
||||
'data-workbench-section="trader-mode"',
|
||||
'id="traderStatusPanel"',
|
||||
@@ -156,9 +167,40 @@ class ApiStaticServerTests(unittest.TestCase):
|
||||
".workbench-sidebar",
|
||||
".workbench-nav button.is-active",
|
||||
".workspace-section.is-active",
|
||||
".language-switcher",
|
||||
):
|
||||
self.assertIn(expected, styles_css)
|
||||
|
||||
def test_frontend_exposes_default_chinese_i18n(self) -> None:
|
||||
index_file = _resolve_static_file("/")
|
||||
self.assertIsNotNone(index_file)
|
||||
index_html = index_file.read_text(encoding="utf-8")
|
||||
i18n_file = _resolve_static_file("/static/i18n.js")
|
||||
self.assertIsNotNone(i18n_file)
|
||||
i18n_js = i18n_file.read_text(encoding="utf-8")
|
||||
|
||||
for expected in (
|
||||
"DEFAULT_LOCALE = 'zh-CN'",
|
||||
"SUPPORTED_LOCALES",
|
||||
"AI ICT 交易工作台",
|
||||
"交易员模式",
|
||||
"决策助手",
|
||||
"仅观察",
|
||||
"不要使用",
|
||||
"只读实盘分析",
|
||||
"ai_ict_locale_v1",
|
||||
"ai-ict-locale-change",
|
||||
):
|
||||
self.assertIn(expected, i18n_js)
|
||||
|
||||
for expected in (
|
||||
"/static/i18n.js",
|
||||
'id="languageSelect"',
|
||||
'value="zh-CN"',
|
||||
'value="en"',
|
||||
):
|
||||
self.assertIn(expected, index_html)
|
||||
|
||||
def test_dashboard_uses_canonical_trading_routes(self) -> None:
|
||||
app_file = _resolve_static_file("/static/app.js")
|
||||
self.assertIsNotNone(app_file)
|
||||
|
||||
Reference in New Issue
Block a user