// --------------------------------------------------------------------------- // i18n — centralized translations. Add a language by appending a table that // satisfies `Translation`; the compiler enforces key completeness. // --------------------------------------------------------------------------- export type LangCode = "zh" | "en" | "ja" | "ko" const ZH_T = { title: "缓存统计", hit: "命中率", totalHit: "总命中:", read: "缓存读:", write: "缓存写:", miss: "未命中:", out: "输出:", cost: "费用:", saved: "累计节省:", model: "模型:", provider: "提供商:", rate: "单价:", hitFolded: "命中", inputRate: "输入", cacheRate: "缓存", writeRate: "写入", noData: "等待缓存数据...", tok: "tok", distTitle: "估算 Token 分布", distSys: "系统提示:", distUser: "用户:", distAgent: "子代理指令:", distTool: "Tool 调用:", distRes: "Tool 结果:", distReason: "推理:", stepsCount: "本回合 {n} 次调用:", secDetail: "明细", secModel: "模型", secSkills: "已加载技能", balTotal: "总余额:", balNoKey: "未配置 {p} API Key", balLoading: "查询中...", balError: "查询失败", balErr401: "API Key 无效", balErr403: "余额查询被拒绝", balErrEmpty:"未获取到余额数据", balErrTimeout: "查询超时", balUnsupported: "当前提供商不支持余额查询", balDetailPlan: "套餐", balDetailUsed: "已用", balDetailRemaining: "剩余", balDetailWindow: "周期", balDetailReset: "重置", balDetailCodeReview: "Code Review", balDetailCredits: "Credits", balDetailResetCredits: "重置次数", balUnlimited: "无限", balDay: "天", balHour: "小时", balMinute: "分钟", balResetSoon: "即将重置", barHit: "命中率", barBal: "余额", barTok: "Tokens", // ── /cache-section 面板 ── secToggle: "切换区块", secBalance: "余额", secBottom: "底部状态栏", secBorder: "面板边框", // ── toast ── keySaved: "API Key 已保存,正在查询余额...", keyCleared: "API Key 已清除", currencySet: "币种: {v} ({s}), 汇率: {r}", rateSet: "汇率已设为 {r}", panelConfigTitle: "缓存面板配置", panelConfigMsg: "币种: {c} | 汇率: {r} | 明细: {d} | 模型: {m} | 分布: {t} | 技能: {k} | 余额: {b} | 底部: {f}", borderShown: "面板边框 已显示", borderHidden: "面板边框 已隐藏", sectionShown: "{s} 已显示", sectionHidden: "{s} 已隐藏", bottomRestartOn: "底部状态栏已开启,重启 TUI 后生效", bottomRestartOff: "底部状态栏已关闭,重启 TUI 后生效", langSwitched: "语言已切换为中文", autoSwitchOn: "自动切换余额提供商: 开", autoSwitchOff: "自动切换余额提供商: 关", providerManual: "余额提供商: {p}(自动切换已关闭)", runInSession: "请在会话内运行此命令", backToMain: "已切回主会话", subAgentSwitched: "已切换至子代理: {s}", // ── 对话框 / 菜单 ── langTitle: "显示语言", subPrefix: "子代理: ", keyUser: "(用户 key)", keyOpenCode: "(OpenCode)", keyNotSet: "(未配置)", balKeyPrompt: "输入 {p} API Key 以显示账户余额(留空清除)", balProvTitle: "余额提供商 / 自动切换", autoSwitchOpt: "自动切换提供商", balSelectTitle: "选择余额提供商", backToMainTitle: "回到主会话", subSelectTitle: "选择子代理", subSwitchTitle: "切换子代理", subViewTitle: "查看子代理缓存", subNoFound: "未找到子代理,请手动粘贴 Session ID", } as const /** 结构约束:值放宽为 string,键集合来自中文表(新增语言缺 key 会编译报错)。 */ export type Translation = { [K in keyof typeof ZH_T]: string } const EN_T: Translation = { title: "Token Cache", hit: "Hit", totalHit: "Total Hit:", read: "Read:", write: "Write:", miss: "Miss:", out: "Out:", cost: "Cost:", saved: "Total Saved:", model: "Model:", provider: "Provider:", rate: "Rate:", hitFolded: "hit", inputRate: "in", cacheRate: "cache", writeRate: "write", noData: "Waiting for cache data...", tok: "tok", distTitle: "Estimated Token Dist.", distSys: "System:", distUser: "User:", distAgent: "Sub-Agent Instr:", distTool: "Tool Call:", distRes: "Tool Result:", distReason: "Reasoning:", stepsCount: "Turn #{n} calls:", secDetail: "Detail", secModel: "Model", secSkills: "Loaded Skills", balTotal: "Total:", balNoKey: "{p} API Key not set", balLoading: "Fetching...", balError: "Fetch failed", balErr401: "Invalid API Key", balErr403: "Balance request rejected", balErrEmpty:"No balance data", balErrTimeout: "Request timed out", balUnsupported: "Balance query unsupported", balDetailPlan: "Plan", balDetailUsed: "Used", balDetailRemaining: "Remaining", balDetailWindow: "Window", balDetailReset: "Reset", balDetailCodeReview: "Code Review", balDetailCredits: "Credits", balDetailResetCredits: "Reset credits", balUnlimited: "Unlimited", balDay: "d", balHour: "h", balMinute: "m", balResetSoon: "soon", barHit: "Hit", barBal: "Balance", barTok: "Tokens", // ── /cache-section panel ── secToggle: "Toggle Section", secBalance: "Balance", secBottom: "Bottom Bar", secBorder: "Panel Border", // ── toasts ── keySaved: "API Key saved, fetching balance...", keyCleared: "API Key cleared", currencySet: "Currency: {v} ({s}), rate: {r}", rateSet: "Exchange rate set to {r}", panelConfigTitle: "Cache Panel Config", panelConfigMsg: "Currency: {c} | Rate: {r} | Detail: {d} | Model: {m} | Dist: {t} | Skills: {k} | Balance: {b} | Bottom: {f}", borderShown: "Panel border shown", borderHidden: "Panel border hidden", sectionShown: "{s} section shown", sectionHidden: "{s} section hidden", bottomRestartOn: "Bottom bar enabled; restart the TUI to apply", bottomRestartOff: "Bottom bar disabled; restart the TUI to apply", langSwitched: "Switched to English", autoSwitchOn: "Auto-switch balance provider: ON", autoSwitchOff: "Auto-switch balance provider: OFF", providerManual: "Balance provider: {p} (auto-switch off)", runInSession: "Please run this command inside a session", backToMain: "Switched to main session", subAgentSwitched: "Showing sub-agent: {s}", // ── dialogs / menus ── langTitle: "Display Language", subPrefix: "Sub: ", keyUser: " (user key)", keyOpenCode: " (OpenCode)", keyNotSet: " (not set)", balKeyPrompt: "Enter your {p} API key to show account balance (leave empty to clear)", balProvTitle: "Balance Provider / Auto-switch", autoSwitchOpt: "Auto-switch provider", balSelectTitle: "Select Balance Provider", backToMainTitle: "Back to Main", subSelectTitle: "Select Sub-Agent", subSwitchTitle: "Switch Sub", subViewTitle: "View Sub Cache", subNoFound: "No sub-agents found. Paste a Session ID manually", } const JA_T: Translation = { title: "キャッシュ統計", hit: "ヒット率", totalHit: "総ヒット:", read: "キャッシュ読込:", write: "キャッシュ書込:", miss: "未ヒット:", out: "出力:", cost: "費用:", saved: "累計節約:", model: "モデル:", provider: "プロバイダ:", rate: "単価:", hitFolded: "ヒット", inputRate: "入力", cacheRate: "キャッシュ", writeRate: "書込", noData: "キャッシュデータ待機中...", tok: "tok", distTitle: "Token 分布(推定)", distSys: "システム:", distUser: "ユーザー:", distAgent: "サブエージェント指示:", distTool: "Tool 呼び出し:", distRes: "Tool 結果:", distReason: "推論:", stepsCount: "今回 {n} 回呼出:", secDetail: "明細", secModel: "モデル", secSkills: "読み込み済みスキル", balTotal: "総残高:", balNoKey: "{p} の API Key 未設定", balLoading: "取得中...", balError: "取得失敗", balErr401: "API Key 無効", balErr403: "残高照会が拒否されました", balErrEmpty:"残高データなし", balErrTimeout: "タイムアウト", balUnsupported: "このプロバイダは残高照会非対応", balDetailPlan: "プラン", balDetailUsed: "使用済み", balDetailRemaining: "残り", balDetailWindow: "期間", balDetailReset: "リセット", balDetailCodeReview: "Code Review", balDetailCredits: "Credits", balDetailResetCredits: "リセット回数", balUnlimited: "無制限", balDay: "日", balHour: "時間", balMinute: "分", balResetSoon: "まもなく", barHit: "ヒット率", barBal: "残高", barTok: "Tokens", // ── /cache-section パネル ── secToggle: "セクション切替", secBalance: "残高", secBottom: "下部ステータスバー", secBorder: "パネル枠線", // ── toast ── keySaved: "API Key 保存済み、残高を取得中...", keyCleared: "API Key をクリアしました", currencySet: "通貨: {v} ({s}), レート: {r}", rateSet: "レートを {r} に設定しました", panelConfigTitle: "パネル設定", panelConfigMsg: "通貨: {c} | レート: {r} | 明細: {d} | モデル: {m} | 分布: {t} | スキル: {k} | 残高: {b} | 下部: {f}", borderShown: "パネル枠線 表示", borderHidden: "パネル枠線 非表示", sectionShown: "{s} 表示", sectionHidden: "{s} 非表示", bottomRestartOn: "下部ステータスバーを有効化しました。TUI 再起動後に反映されます", bottomRestartOff: "下部ステータスバーを無効化しました。TUI 再起動後に反映されます", langSwitched: "日本語に切り替えました", autoSwitchOn: "残高プロバイダ自動切替: オン", autoSwitchOff: "残高プロバイダ自動切替: オフ", providerManual: "残高プロバイダ: {p}(自動切替オフ)", runInSession: "セッション内で実行してください", backToMain: "メインセッションに戻りました", subAgentSwitched: "サブエージェントへ切替: {s}", // ── ダイアログ / メニュー ── langTitle: "表示言語", subPrefix: "サブ: ", keyUser: "(ユーザー key)", keyOpenCode: "(OpenCode)", keyNotSet: "(未設定)", balKeyPrompt: "{p} の API Key を入力して残高を表示(空欄でクリア)", balProvTitle: "残高プロバイダ / 自動切替", autoSwitchOpt: "自動切替プロバイダ", balSelectTitle: "残高プロバイダ選択", backToMainTitle: "メインに戻る", subSelectTitle: "サブエージェント選択", subSwitchTitle: "サブ切替", subViewTitle: "サブキャッシュ表示", subNoFound: "サブエージェントが見つかりません。Session ID を手動で貼り付けてください", } const KO_T: Translation = { title: "캐시 통계", hit: "히트율", totalHit: "총 히트:", read: "캐시 읽기:", write: "캐시 쓰기:", miss: "미히트:", out: "출력:", cost: "비용:", saved: "누적 절약:", model: "모델:", provider: "프로바이더:", rate: "단가:", hitFolded: "히트", inputRate: "입력", cacheRate: "캐시", writeRate: "쓰기", noData: "캐시 데이터 대기 중...", tok: "tok", distTitle: "Token 분포(추정)", distSys: "시스템:", distUser: "사용자:", distAgent: "서브 에이전트 지시:", distTool: "Tool 호출:", distRes: "Tool 결과:", distReason: "추론:", stepsCount: "이번 턴 {n}회 호출:", secDetail: "상세", secModel: "모델", secSkills: "로드된 스킬", balTotal: "총 잔액:", balNoKey: "{p} API Key 미설정", balLoading: "조회 중...", balError: "조회 실패", balErr401: "API Key 무효", balErr403: "잔액 조회가 거부되었습니다", balErrEmpty:"잔액 데이터 없음", balErrTimeout: "시간 초과", balUnsupported: "이 프로바이더는 잔액 조회 미지원", balDetailPlan: "플랜", balDetailUsed: "사용", balDetailRemaining: "잔여", balDetailWindow: "주기", balDetailReset: "재설정", balDetailCodeReview: "Code Review", balDetailCredits: "Credits", balDetailResetCredits: "재설정 횟수", balUnlimited: "무제한", balDay: "일", balHour: "시간", balMinute: "분", balResetSoon: "곧 재설정", barHit: "히트율", barBal: "잔액", barTok: "Tokens", // ── /cache-section 패널 ── secToggle: "섹션 전환", secBalance: "잔액", secBottom: "하단 상태바", secBorder: "패널 테두리", // ── toast ── keySaved: "API Key 저장됨, 잔액 조회 중...", keyCleared: "API Key 삭제됨", currencySet: "통화: {v} ({s}), 환율: {r}", rateSet: "환율을 {r}(으)로 설정했습니다", panelConfigTitle: "패널 설정", panelConfigMsg: "통화: {c} | 환율: {r} | 상세: {d} | 모델: {m} | 분포: {t} | 스킬: {k} | 잔액: {b} | 하단: {f}", borderShown: "패널 테두리 표시", borderHidden: "패널 테두리 숨김", sectionShown: "{s} 표시", sectionHidden: "{s} 숨김", bottomRestartOn: "하단 상태바를 켰습니다. TUI 재시작 후 적용됩니다", bottomRestartOff: "하단 상태바를 껐습니다. TUI 재시작 후 적용됩니다", langSwitched: "한국어로 전환했습니다", autoSwitchOn: "잔액 프로바이더 자동 전환: 켜짐", autoSwitchOff: "잔액 프로바이더 자동 전환: 꺼짐", providerManual: "잔액 프로바이더: {p}(자동 전환 꺼짐)", runInSession: "세션 내에서 실행해 주세요", backToMain: "메인 세션으로 돌아갔습니다", subAgentSwitched: "서브 에이전트로 전환: {s}", // ── 다이얼로그 / 메뉴 ── langTitle: "표시 언어", subPrefix: "서브: ", keyUser: "(사용자 key)", keyOpenCode: "(OpenCode)", keyNotSet: "(미설정)", balKeyPrompt: "{p} API Key를 입력하여 잔액 표시(비우면 삭제)", balProvTitle: "잔액 프로바이더 / 자동 전환", autoSwitchOpt: "자동 전환 프로바이더", balSelectTitle: "잔액 프로바이더 선택", backToMainTitle: "메인으로 돌아가기", subSelectTitle: "서브 에이전트 선택", subSwitchTitle: "서브 전환", subViewTitle: "서브 캐시 보기", subNoFound: "서브 에이전트를 찾을 수 없습니다. Session ID를 직접 붙여넣으세요", } export const LANGS: Record = { zh: ZH_T, en: EN_T, ja: JA_T, ko: KO_T } /** 语言元数据:/cache-lang 选项与自动检测共用。 */ export const LANG_META: { code: LangCode; label: string }[] = [ { code: "zh", label: "中文" }, { code: "en", label: "English" }, { code: "ja", label: "日本語" }, { code: "ko", label: "한국어" }, ] /** * 模板参数替换:`{key}` 占位符统一在此处理。 * 未提供的参数保留原占位符,避免静默丢失。 */ export function applyParams(tpl: string, params?: Record): string { if (!params) return tpl return tpl.replace(/\{(\w+)\}/g, (m, k: string) => k in params ? String(params[k]) : m, ) } /** * 翻译查找函数工厂:`t("key")` 返回当前语言的文本,`t("key", { p })` 附带模板参数。 * `getCode` 读取语言信号——在 SolidJS 渲染/memo 上下文中调用时自动建立响应式依赖。 */ export function createT(getCode: () => LangCode) { return (key: keyof Translation, params?: Record): string => applyParams(LANGS[getCode()][key], params) } /** 按系统 locale 自动检测语言(zh → 中文,ja → 日语,ko → 韩语,其余 → 英文)。 */ export function detectLang(): LangCode { try { const loc = Intl.DateTimeFormat().resolvedOptions().locale.toLowerCase() if (loc.startsWith("zh")) return "zh" if (loc.startsWith("ja")) return "ja" if (loc.startsWith("ko")) return "ko" return "en" } catch { return "en" } }