miki-moni 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/README.zh-CN.md +275 -0
- package/README.zh-TW.md +275 -0
- package/bin/miki.mjs +49 -0
- package/dist/web/assets/favicon-DFpLtP36.svg +13 -0
- package/dist/web/assets/index--89DkyV1.css +1 -0
- package/dist/web/assets/index-CyPlxvOn.js +64 -0
- package/dist/web/index.html +20 -0
- package/dist/web/pair-info.html +138 -0
- package/dist/web-phone/assets/app-CyQWCdKZ.js +64 -0
- package/dist/web-phone/assets/index-D5BUh7Uf.js +1 -0
- package/dist/web-phone/assets/index-D8vY_9ld.css +1 -0
- package/dist/web-phone/index.html +20 -0
- package/hooks/miki-emit.ps1 +56 -0
- package/package.json +89 -0
- package/shared/i18n.ts +915 -0
- package/src/cli/i18n-cli.ts +149 -0
- package/src/cli/miki.ts +168 -0
- package/src/cli/pair.ts +534 -0
- package/src/cli/prompt.ts +6 -0
- package/src/cli/pushable-iter.ts +45 -0
- package/src/cli/setup-self-host.ts +292 -0
- package/src/cli/setup-wizard.ts +130 -0
- package/src/cli/wrap.ts +742 -0
- package/src/config.ts +121 -0
- package/src/crypto.ts +66 -0
- package/src/data-dir.ts +31 -0
- package/src/ext-registry.ts +47 -0
- package/src/hook-handler.ts +86 -0
- package/src/index.ts +279 -0
- package/src/install-hooks.ts +107 -0
- package/src/notifier.ts +21 -0
- package/src/pairing.ts +100 -0
- package/src/protocol-ext.ts +46 -0
- package/src/relay-client.ts +468 -0
- package/src/relay-protocol.ts +57 -0
- package/src/server.ts +1134 -0
- package/src/session-resolver.ts +437 -0
- package/src/session-store.ts +131 -0
- package/src/types.ts +33 -0
- package/src/vscode-bridge.ts +407 -0
- package/src/wrap-process.ts +183 -0
- package/tools/tray.ps1 +286 -0
- package/worker/package.json +24 -0
- package/worker/src/daemon-relay.ts +348 -0
- package/worker/src/env.ts +11 -0
- package/worker/src/handshake.ts +63 -0
- package/worker/src/index.ts +81 -0
- package/worker/src/pairing-code.ts +39 -0
- package/worker/src/pairing-coordinator.ts +145 -0
- package/worker/wrangler-selfhost.toml +36 -0
- package/worker/wrangler.toml +29 -0
package/shared/i18n.ts
ADDED
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
// ── i18n: zh-TW / zh-CN / en ───────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Lightweight i18n for the cc-hub web dashboard. Three locales, single source
|
|
4
|
+
// of truth = zh-TW (matches the original hardcoded strings). Locale is
|
|
5
|
+
// persisted to localStorage; first-load fallback inspects navigator.language.
|
|
6
|
+
//
|
|
7
|
+
// Public surface:
|
|
8
|
+
// - useLocale() : Preact hook → [locale, setLocale]
|
|
9
|
+
// - t("key", params?) : translate; params interpolate "{name}" tokens
|
|
10
|
+
// - LOCALES, LOCALE_LABELS
|
|
11
|
+
//
|
|
12
|
+
// Components re-render on locale change via a tiny pub/sub on the module
|
|
13
|
+
// scope (so we don't have to thread a Context through every component).
|
|
14
|
+
|
|
15
|
+
import { useEffect, useState } from "preact/hooks";
|
|
16
|
+
|
|
17
|
+
export type Locale = "zh-TW" | "zh-CN" | "en";
|
|
18
|
+
|
|
19
|
+
export const LOCALES: readonly Locale[] = ["zh-TW", "zh-CN", "en"] as const;
|
|
20
|
+
|
|
21
|
+
export const LOCALE_LABELS: Record<Locale, string> = {
|
|
22
|
+
"zh-TW": "繁體中文",
|
|
23
|
+
"zh-CN": "简体中文",
|
|
24
|
+
"en": "English",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const LS_KEY = "miki-moni:locale";
|
|
28
|
+
|
|
29
|
+
function detectInitial(): Locale {
|
|
30
|
+
try {
|
|
31
|
+
const raw = localStorage.getItem(LS_KEY);
|
|
32
|
+
if (raw === "zh-TW" || raw === "zh-CN" || raw === "en") return raw;
|
|
33
|
+
} catch { /* disabled */ }
|
|
34
|
+
try {
|
|
35
|
+
const nav = (navigator.language || "").toLowerCase();
|
|
36
|
+
if (nav.startsWith("zh-cn") || nav.startsWith("zh-hans") || nav === "zh") return "zh-CN";
|
|
37
|
+
if (nav.startsWith("zh")) return "zh-TW";
|
|
38
|
+
if (nav.startsWith("en")) return "en";
|
|
39
|
+
} catch { /* SSR / no navigator */ }
|
|
40
|
+
return "zh-TW";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let currentLocale: Locale = detectInitial();
|
|
44
|
+
const subs = new Set<(l: Locale) => void>();
|
|
45
|
+
|
|
46
|
+
export function getLocale(): Locale { return currentLocale; }
|
|
47
|
+
export function setLocale(l: Locale): void {
|
|
48
|
+
if (l === currentLocale) return;
|
|
49
|
+
currentLocale = l;
|
|
50
|
+
try { localStorage.setItem(LS_KEY, l); } catch { /* quota / disabled */ }
|
|
51
|
+
subs.forEach((fn) => fn(l));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function useLocale(): [Locale, (l: Locale) => void] {
|
|
55
|
+
const [l, setL] = useState<Locale>(currentLocale);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const fn = (next: Locale) => setL(next);
|
|
58
|
+
subs.add(fn);
|
|
59
|
+
return () => { subs.delete(fn); };
|
|
60
|
+
}, []);
|
|
61
|
+
return [l, setLocale];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Dictionaries ───────────────────────────────────────────────────────────
|
|
65
|
+
// Flat dotted keys for trivial lookup. zh-TW is the source; zh-CN and en
|
|
66
|
+
// mirror the exact same key set. Missing keys fall back to the key string
|
|
67
|
+
// itself so a typo never crashes a render.
|
|
68
|
+
|
|
69
|
+
type Dict = Record<string, string>;
|
|
70
|
+
|
|
71
|
+
const zhTW: Dict = {
|
|
72
|
+
// status
|
|
73
|
+
"status.active": "進行中",
|
|
74
|
+
"status.waiting": "等你回應",
|
|
75
|
+
"status.idle": "閒置",
|
|
76
|
+
"status.stale": "已斷線",
|
|
77
|
+
|
|
78
|
+
// time-ago
|
|
79
|
+
"time.secondsAgo": "{n} 秒前",
|
|
80
|
+
"time.minutesAgo": "{n} 分鐘前",
|
|
81
|
+
"time.hoursAgo": "{n} 小時前",
|
|
82
|
+
"time.daysAgo": "{n} 天前",
|
|
83
|
+
|
|
84
|
+
// ask card
|
|
85
|
+
"ask.atLeastOne": "請至少回答一題",
|
|
86
|
+
"ask.claudeAsking": "❓ Claude 在問你問題",
|
|
87
|
+
"ask.placeholder": "或自己打答案(會跟勾選一起送)…",
|
|
88
|
+
"ask.prev": "← 上一題",
|
|
89
|
+
"ask.next": "下一題 →",
|
|
90
|
+
"ask.submitting": "送出中…",
|
|
91
|
+
"ask.submit": "Submit",
|
|
92
|
+
"ask.removeThis": "移除這張",
|
|
93
|
+
|
|
94
|
+
// tool-use expand toggles
|
|
95
|
+
"expand.collapse": "收合",
|
|
96
|
+
"expand.expandIn": "展開 IN",
|
|
97
|
+
"expand.expandOut": "展開 OUT",
|
|
98
|
+
"expand.expand": "展開",
|
|
99
|
+
|
|
100
|
+
// transcript
|
|
101
|
+
"transcript.empty": "(transcript 沒有可顯示的訊息)",
|
|
102
|
+
"transcript.conversation": "對話",
|
|
103
|
+
"transcript.countItems": "{n} 條",
|
|
104
|
+
"transcript.fileSize": "{kb} KB",
|
|
105
|
+
"transcript.lastModified": "最後修改 {time}",
|
|
106
|
+
"model.switchHint": "點擊切換目前 wrap session 的模型(SDK 動態切換,不會中斷對話)",
|
|
107
|
+
"model.menuTitle": "模型",
|
|
108
|
+
"model.defaultDesc": "用 SDK 預設模型(CLAUDE_DEFAULT_MODEL / Anthropic 預設)",
|
|
109
|
+
"model.aliasDesc": "切到 {id}",
|
|
110
|
+
"model.customPlaceholder": "claude-opus-4-7-20251122",
|
|
111
|
+
"model.applyCustom": "套用",
|
|
112
|
+
"transcript.viewOptions": "對話檢視選項",
|
|
113
|
+
"transcript.showTool": "顯示 TOOL",
|
|
114
|
+
"transcript.items10": "10 條",
|
|
115
|
+
"transcript.items20": "20 條",
|
|
116
|
+
"transcript.items50": "50 條",
|
|
117
|
+
"transcript.items100": "100 條",
|
|
118
|
+
"transcript.items200": "200 條",
|
|
119
|
+
"transcript.items500": "500 條",
|
|
120
|
+
"transcript.itemsAll": "全部",
|
|
121
|
+
"transcript.loadAll": "📜 載入全部",
|
|
122
|
+
"transcript.loadAllTitle": "把整個 session JSONL 全部讀進來(最多 10000 turn)",
|
|
123
|
+
"transcript.loading": "讀取中…",
|
|
124
|
+
"transcript.reload": "重新讀取",
|
|
125
|
+
|
|
126
|
+
// send / focus result
|
|
127
|
+
"send.focusing": "叫起視窗:{label}",
|
|
128
|
+
"send.httpOk": "OK · HTTP {status}",
|
|
129
|
+
"send.httpFail": "失敗 HTTP {status}",
|
|
130
|
+
"send.claudeReplied": "Claude 已回覆 ({ms} ms)",
|
|
131
|
+
"send.sentToVSCode": "已送出到 VSCode panel(Enter 已自動按)",
|
|
132
|
+
"send.sendFailed": "送出失敗:{label}",
|
|
133
|
+
|
|
134
|
+
// focus / wrapper badges
|
|
135
|
+
"focus.cliNotSupported": "🚫 CLI session 不支援 focus(URI handler 只開 VSCode)",
|
|
136
|
+
"focus.bringVSCode": "叫起 VSCode 視窗(focus)",
|
|
137
|
+
"focus.wrappedBadge": "🔌 wrapped",
|
|
138
|
+
"focus.wrappedTitle": "wrapper 接管中(CLI session)",
|
|
139
|
+
"focus.wrapperRunning": "wrapper 正在: {activity}",
|
|
140
|
+
|
|
141
|
+
// composer
|
|
142
|
+
"composer.imageIgnored": "⚠️ 圖片在這個 session 模式下會被忽略(只有 wrapped 才能傳圖)",
|
|
143
|
+
"composer.uploadImage": "附加圖片(從手機相簿/相機選擇)",
|
|
144
|
+
"composer.cliNotWrapped": "CLI session 沒接管 — 先在 terminal 跑:miki claude -r {short}…",
|
|
145
|
+
"composer.vscodeDisabledWrap": "VSCode panel mode 已停用 — 請改用 wrap:miki claude -r {short}…",
|
|
146
|
+
"composer.vscodeDisabledWrap2": "VSCode panel mode 已停用 — 請改 wrap:miki claude -r {short}…",
|
|
147
|
+
"composer.inputPrompt": "輸入 prompt(Ctrl+V 可貼圖)→ push 進 wrapper 的 query()…",
|
|
148
|
+
"composer.inputPromptShort": "輸入訊息…",
|
|
149
|
+
"composer.cliNotWrappedShort": "未接管 — 先 wrap:-r {short}…",
|
|
150
|
+
"composer.vscodeDisabledShort": "需要 wrap:-r {short}…",
|
|
151
|
+
"composer.interruptLong": "中斷目前 Claude 的回應(呼叫 SDK Query.interrupt())",
|
|
152
|
+
"composer.interruptShort": "中斷目前 Claude 的回應",
|
|
153
|
+
"composer.needWrapHint": "先 `miki claude -r <uuid>` 接管 (wrap-cli)",
|
|
154
|
+
"composer.needWrapHint2": "請先 `miki claude -r <uuid>` 接管 (wrap-cli)",
|
|
155
|
+
"composer.wrapWSHint": "走 wrap WS push 進 terminal 的 query()(免費,可帶圖)",
|
|
156
|
+
"composer.wrapWSShort": "走 wrap WS push 進 terminal 的 query()",
|
|
157
|
+
"composer.needWrapBadge": "需 wrap",
|
|
158
|
+
"composer.cliPanelNotWrapped": "📟",
|
|
159
|
+
"composer.cliNotWrappedStrong": "CLI session 沒接管",
|
|
160
|
+
"composer.pleaseUse": ":請改用",
|
|
161
|
+
"composer.toTakeOver": "接管。",
|
|
162
|
+
"composer.advancedToggle": "advanced:切換送出模式",
|
|
163
|
+
"composer.headlessReal": "真送出模式(headless `claude -r -p`,會花 API 費用、不走你 panel session)",
|
|
164
|
+
"composer.keyEnter": "Enter 送出 · Shift+Enter 換行",
|
|
165
|
+
"composer.keyCtrlEnter": "⌘/Ctrl+Enter 送出 · Enter 換行",
|
|
166
|
+
"composer.escClose": " · Esc 關閉",
|
|
167
|
+
"composer.sendBusy": "⌛",
|
|
168
|
+
"composer.sendWrapped": "送出 🔌",
|
|
169
|
+
"composer.sendNeedWrap": "送出 (需 wrap)",
|
|
170
|
+
|
|
171
|
+
// header / stats
|
|
172
|
+
"header.all": "全覽",
|
|
173
|
+
"header.live": "進行中",
|
|
174
|
+
"header.idle": "閒置",
|
|
175
|
+
"header.stale": "已斷線",
|
|
176
|
+
"header.onlyShow": "只看 {label}",
|
|
177
|
+
"header.running": "目前正在運行",
|
|
178
|
+
"header.filtered": "· 已過濾 ({filter}) · 點 TOTAL 重設",
|
|
179
|
+
"header.liveLong": "進行中 / 等回應",
|
|
180
|
+
"header.wsConnected": "WS connected",
|
|
181
|
+
"header.wsConnecting": "connecting…",
|
|
182
|
+
"header.wsDisconnected":"WS disconnected",
|
|
183
|
+
"header.settingsBtn": "⚙️ 設定",
|
|
184
|
+
"header.settingsTitle": "設定",
|
|
185
|
+
|
|
186
|
+
// session card / row
|
|
187
|
+
"session.copyRestart": "複製重啟指令:pnpm --dir D:\\code\\cc-hub miki claude -r {uuid}",
|
|
188
|
+
"session.cliMarkTooltip": "標記為 CLI session — 預填送出已停用(URI handler 不支援 terminal)。點一下改回 VSCode。",
|
|
189
|
+
"session.vscodeMarkTooltip":"標記為 VSCode session — 點一下改成 CLI。",
|
|
190
|
+
"session.waitingBadge": "🔔 待回應",
|
|
191
|
+
"session.waitingTooltip": "Claude 還在等你回答 — 點開重新顯示問題",
|
|
192
|
+
"session.openTab": "開到 tab",
|
|
193
|
+
"session.wrappedDetailed": "這個 session 被 `miki claude` wrapper 接管(CLI session)— 送出走 push、不再 spawn / 不花 $$",
|
|
194
|
+
"session.empty": "(尚未開始對話)",
|
|
195
|
+
"session.quickSend": "快速送出(彈出小卡片,不展開 transcript)",
|
|
196
|
+
"session.openCli": "開 CLI",
|
|
197
|
+
"session.openCliPending": "...",
|
|
198
|
+
"session.openCliFailed": "失敗:{err}",
|
|
199
|
+
"session.openCliTooltip": "彈一個 Windows Terminal 跑 miki claude -r — 把這個 session 變成 wrapped",
|
|
200
|
+
"wrapNotice.title": "CLI 已啟動 — 請手動關閉 VSCode panel",
|
|
201
|
+
"wrapNotice.body": "{project} 現在被 CLI wrapper 接管。VSCode panel 還是「擁有」這個 session 的話,兩邊各打字會把 JSONL 切成分支、session 狀態就糊了。建議:把 VSCode 那個 Claude Code panel 關掉,之後只用 dashboard / CLI 互動。",
|
|
202
|
+
"wrapNotice.confirmCheck": "我已關閉對應的 VSCode panel",
|
|
203
|
+
"wrapNotice.confirm": "確認",
|
|
204
|
+
"wrapNotice.later": "稍後處理",
|
|
205
|
+
"wrapNotice.dismiss": "知道了",
|
|
206
|
+
"header.newCli": "新增 CLI",
|
|
207
|
+
"header.newCliTitle": "在指定資料夾開新的 CLI(miki claude --fresh)",
|
|
208
|
+
"newCli.heading": "新增 CLI",
|
|
209
|
+
"newCli.cwdLabel": "資料夾路徑",
|
|
210
|
+
"newCli.cwdPlaceholder": "路徑",
|
|
211
|
+
"newCli.recentCwds": "最近用過的路徑",
|
|
212
|
+
"newCli.submit": "開啟",
|
|
213
|
+
"newCli.submitting": "啟動中…",
|
|
214
|
+
"newCli.success": "已啟動 — 等 wrap 連回 daemon",
|
|
215
|
+
"newCli.error": "失敗:{err}",
|
|
216
|
+
"newCli.hint": "會在新的 Windows Terminal 裡跑 miki claude --fresh。先送一句 \"hi\" 啟動 SDK 後接管。",
|
|
217
|
+
"spawnPending.title": "啟動中…",
|
|
218
|
+
"spawnPending.body": "在 {cwd} 跑 miki claude — 等 SDK init 完成、wrap 連回 daemon 後新卡片就會出現。約 3–5 秒。",
|
|
219
|
+
"spawnPending.timeout": "已超過 30 秒沒連回 — 看一下 wt 視窗是否有錯誤。",
|
|
220
|
+
"spawnPending.dismiss": "關閉",
|
|
221
|
+
"session.closeTab": "關閉 tab",
|
|
222
|
+
"session.closeKey": "關閉",
|
|
223
|
+
"session.modalClose": "關閉 (Esc)",
|
|
224
|
+
"session.claudeWaitingHere":"🔔 Claude 還在等你回答這個 session 的問題",
|
|
225
|
+
"session.showQuestion": "重新顯示問題",
|
|
226
|
+
|
|
227
|
+
// permission modes (locked tooltips + menu rows)
|
|
228
|
+
"mode.lockedAcceptTitle": "Auto-accept edits mode:所有 edit/write 直接套用、不再確認。`miki claude --permission-mode acceptEdits` 啟動鎖定。",
|
|
229
|
+
"mode.lockedBypassTitle": "Bypass permissions:所有工具都不問就執行。極度危險,僅在 sandbox 使用。`miki claude --bypass-permissions` 啟動鎖定。",
|
|
230
|
+
"mode.lockedPlanTitle": "Plan mode:只規劃、不執行 mutation 類工具。`miki claude --permission-mode plan` 啟動鎖定。",
|
|
231
|
+
"mode.defaultDesc": "每次 edit 都問你",
|
|
232
|
+
"mode.defaultTitle": "Ask before edits:每個 mutation 工具會問你。",
|
|
233
|
+
"mode.acceptDesc": "Edit / write 直接套用",
|
|
234
|
+
"mode.acceptTitle": "Edit automatically:所有 edit/write 直接套用、不再確認。",
|
|
235
|
+
"mode.planDesc": "只規劃、不動檔案",
|
|
236
|
+
"mode.planTitle": "Plan mode:只規劃、不執行 mutation 類工具。",
|
|
237
|
+
"mode.autoDesc": "Claude 自己挑 mode",
|
|
238
|
+
"mode.autoTitle": "Auto mode:Claude 自動依任務選最適合的 mode。",
|
|
239
|
+
"mode.bypassDesc": "全部工具直接跑(危險)",
|
|
240
|
+
"mode.bypassTitle": "Bypass permissions:所有工具都不問就執行。極度危險,只在 sandbox 使用。",
|
|
241
|
+
"mode.switchHint": " · 點一下切換 mode",
|
|
242
|
+
|
|
243
|
+
// log panel
|
|
244
|
+
"log.activity": "活動紀錄",
|
|
245
|
+
"log.syncedConsole": "同步輸出到 F12 Console",
|
|
246
|
+
"log.clear": "清空",
|
|
247
|
+
"log.empty": "尚無活動,按任何按鈕或等 hook 事件就會冒出來。",
|
|
248
|
+
|
|
249
|
+
// overview / empty states
|
|
250
|
+
"overview.title": "🗺️ 全覽",
|
|
251
|
+
"overview.noSessions": "目前沒有 session。",
|
|
252
|
+
"overview.openPanelHint": "在任何 VSCode 視窗開 Claude Code panel 就會冒出來。",
|
|
253
|
+
"overview.runHooks": "沒反應的話:",
|
|
254
|
+
"overview.sessionGone": "(session 不存在了,可能 daemon 重啟過)",
|
|
255
|
+
|
|
256
|
+
// settings panel
|
|
257
|
+
"settings.title": "設定",
|
|
258
|
+
"settings.sendKeySection": "送出鍵(全域)",
|
|
259
|
+
"settings.enterLabel": "Enter",
|
|
260
|
+
"settings.enterDesc": " 送出 · Shift+Enter 換行",
|
|
261
|
+
"settings.ctrlEnterLabel": "Ctrl/⌘ + Enter",
|
|
262
|
+
"settings.ctrlEnterDesc": " 送出 · Enter 換行",
|
|
263
|
+
"settings.sendKeyHelp": "套用到 dashboard 快速送出視窗 + 每個 tab 的 composer。Enter 模式下仍可用 Ctrl/⌘+Enter 強制送出。",
|
|
264
|
+
"settings.appearance": "外觀",
|
|
265
|
+
"settings.themeLight": "☀️ Light",
|
|
266
|
+
"settings.themeDark": "🌙 Dark",
|
|
267
|
+
"settings.themeSystem": "🖥 System",
|
|
268
|
+
"settings.themeSystemTitle": "跟隨 OS 設定(prefers-color-scheme)",
|
|
269
|
+
"settings.themeDarkTitle": "深色模式",
|
|
270
|
+
"settings.themeLightTitle": "淺色模式",
|
|
271
|
+
"settings.sortMode": "卡片排序",
|
|
272
|
+
"settings.sortPriorityLabel": "🔔 優先級",
|
|
273
|
+
"settings.sortPriorityTitle": "需要回應的 session 優先(waiting → active → idle → stale),同組內依 cwd 排序",
|
|
274
|
+
"settings.sortUuidLabel": "🆔 UUID 序",
|
|
275
|
+
"settings.sortUuidTitle": "依 session_uuid 排序;每個 session 永遠卡同一格,F5 / 重啟都不會位移(位置與專案名無關)",
|
|
276
|
+
"settings.sortRecentLabel": "⏱ 最近活動",
|
|
277
|
+
"settings.sortRecentTitle": "最近有事件的排前面(last_event_at DESC),會隨活動跳動",
|
|
278
|
+
"settings.sortHelp": "影響首頁卡片順序。F5 後位置會記住(除了「最近活動」模式,這個本來就會隨活動移動)。",
|
|
279
|
+
"settings.close": "關閉",
|
|
280
|
+
"settings.language": "語言",
|
|
281
|
+
|
|
282
|
+
// startup logs
|
|
283
|
+
"startup.starting": "啟動 — 抓 /sessions + previews",
|
|
284
|
+
"startup.getSessionsFailed": "GET /sessions 失敗",
|
|
285
|
+
"startup.gotSessions": "GET /sessions 200 — 收到 {n} 個 session",
|
|
286
|
+
"startup.wsConnecting": "WS 連線中",
|
|
287
|
+
|
|
288
|
+
// ── web-phone (mobile client) ────────────────────────────────────────────
|
|
289
|
+
"phone.pair.title": "miki-moni 配對",
|
|
290
|
+
"phone.pair.subtitle": "把這台裝置跟你電腦上的 miki-moni daemon 配對",
|
|
291
|
+
"phone.pair.instruction": "在電腦終端機跑 {cmd},輸入它顯示的 16 碼配對碼。",
|
|
292
|
+
"phone.pair.codeLabel": "配對碼",
|
|
293
|
+
"phone.pair.codePlaceholder": "XXXX-XXXX-XXXX-XXXX",
|
|
294
|
+
"phone.pair.errorBadCode": "Code must be 16 chars from the Crockford base32 alphabet",
|
|
295
|
+
"phone.pair.errorScannedQr": "掃到了但不是配對 QR:{text}",
|
|
296
|
+
"phone.pair.busy": "配對中…",
|
|
297
|
+
"phone.pair.submit": "開始配對",
|
|
298
|
+
"phone.pair.or": "或",
|
|
299
|
+
"phone.pair.scan": "掃 QR Code",
|
|
300
|
+
"phone.pair.autoTitle": "配對中…",
|
|
301
|
+
"phone.pair.autoHint": "正在跟 daemon 完成 handshake",
|
|
302
|
+
"phone.pair.relayLabel": "relay:",
|
|
303
|
+
"phone.tunnel.connecting": "連線中…",
|
|
304
|
+
"phone.tunnel.connectingHint": "透過 relay 連到 daemon · {daemon}…",
|
|
305
|
+
"phone.tunnel.error": "連線錯誤",
|
|
306
|
+
"phone.tunnel.resetAndRepair": "清除狀態並重新配對",
|
|
307
|
+
|
|
308
|
+
"phone.conn.connected": "已連線",
|
|
309
|
+
"phone.conn.connecting": "連線中…",
|
|
310
|
+
"phone.conn.reconnecting": "重新連線中…",
|
|
311
|
+
"phone.conn.error": "連線錯誤",
|
|
312
|
+
|
|
313
|
+
"phone.session.noUuid": "null(沒抓到,叫起/送出可能會開錯 session)",
|
|
314
|
+
"phone.session.focus": "叫起視窗",
|
|
315
|
+
"phone.session.promptPlaceholder": "輸入 prompt 送給這個 session…",
|
|
316
|
+
"phone.session.send": "送出",
|
|
317
|
+
|
|
318
|
+
"phone.empty.title": "⚪ 連上 relay,但還沒有 session",
|
|
319
|
+
"phone.empty.twoOptions": "兩個可能:",
|
|
320
|
+
"phone.empty.daemonNotRunning": "電腦的 daemon 沒在跑 → 終端機跑 {cmd}",
|
|
321
|
+
"phone.empty.noClaude": "daemon 有跑,但你還沒開任何 Claude Code session → 開一個就會出現",
|
|
322
|
+
"phone.empty.hooksHint": "若 hooks 還沒裝:{cmd}",
|
|
323
|
+
"phone.empty.waitingConn": "等待連線…",
|
|
324
|
+
|
|
325
|
+
"phone.header.unpair": "解除配對",
|
|
326
|
+
"phone.header.unpairing": "解除中…",
|
|
327
|
+
|
|
328
|
+
"phone.log.title": "活動紀錄",
|
|
329
|
+
"phone.log.consoleNote": "(同步輸出到 F12 Console)",
|
|
330
|
+
"phone.log.clear": "清空",
|
|
331
|
+
"phone.log.empty": "尚無活動。WS 連上、配對後送出 cmd、收到 event 都會即時顯示。",
|
|
332
|
+
"phone.log.wsError": "WS error",
|
|
333
|
+
"phone.log.wsCloseReconnect": "WS close — {delay}ms 後重連",
|
|
334
|
+
"phone.log.wsCloseReason": "(無)",
|
|
335
|
+
"phone.log.wsNonJson": "WS 收到非 JSON",
|
|
336
|
+
"phone.log.wsConnectingRelay": "WS 連線中 (relay)",
|
|
337
|
+
"phone.log.wsConnectingLegacy": "WS 連線中 (legacy)",
|
|
338
|
+
"phone.log.wsOpenSnapshot": "WS open — 送 request_snapshot",
|
|
339
|
+
"phone.log.unknownKind": "不認識的訊息 kind",
|
|
340
|
+
"phone.log.snapshotReceived": "state_snapshot — 收到 {n} 個 session",
|
|
341
|
+
"phone.log.eventReceived": "event — {project}",
|
|
342
|
+
"phone.log.envelopeFail": "envelope 解密失敗 — 可能對方不是這個 peer",
|
|
343
|
+
"phone.log.identityFail": "無法載入 identity",
|
|
344
|
+
"phone.log.sendFailWsClosed": "{label}: WS 未連線,無法送出",
|
|
345
|
+
"phone.log.encryptedSend": "加密送出 {label}",
|
|
346
|
+
"phone.log.registerPeer": "送 register_peer_id",
|
|
347
|
+
"phone.log.revokeRelay": "送 revoke_self 給 relay",
|
|
348
|
+
"phone.log.revokeOk": "relay 確認 revoke 完成,清除本地狀態",
|
|
349
|
+
"phone.log.revokeKicked": "daemon 主動解除配對,回到配對畫面",
|
|
350
|
+
"phone.log.unpairOffline": "WS 未連線;直接清本地(relay 端可能仍有 paired_phones entry,下次連會被擋掉)",
|
|
351
|
+
"phone.log.focusClick": "click 叫起視窗",
|
|
352
|
+
"phone.log.sendClick": "click 送出 prompt",
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const zhCN: Dict = {
|
|
356
|
+
"status.active": "进行中",
|
|
357
|
+
"status.waiting": "等你回应",
|
|
358
|
+
"status.idle": "闲置",
|
|
359
|
+
"status.stale": "已断线",
|
|
360
|
+
|
|
361
|
+
"time.secondsAgo": "{n} 秒前",
|
|
362
|
+
"time.minutesAgo": "{n} 分钟前",
|
|
363
|
+
"time.hoursAgo": "{n} 小时前",
|
|
364
|
+
"time.daysAgo": "{n} 天前",
|
|
365
|
+
|
|
366
|
+
"ask.atLeastOne": "请至少回答一题",
|
|
367
|
+
"ask.claudeAsking": "❓ Claude 在问你问题",
|
|
368
|
+
"ask.placeholder": "或自己打答案(会跟勾选一起送)…",
|
|
369
|
+
"ask.prev": "← 上一题",
|
|
370
|
+
"ask.next": "下一题 →",
|
|
371
|
+
"ask.submitting": "送出中…",
|
|
372
|
+
"ask.submit": "Submit",
|
|
373
|
+
"ask.removeThis": "移除这张",
|
|
374
|
+
|
|
375
|
+
"expand.collapse": "收合",
|
|
376
|
+
"expand.expandIn": "展开 IN",
|
|
377
|
+
"expand.expandOut": "展开 OUT",
|
|
378
|
+
"expand.expand": "展开",
|
|
379
|
+
|
|
380
|
+
"transcript.empty": "(transcript 没有可显示的讯息)",
|
|
381
|
+
"transcript.conversation": "对话",
|
|
382
|
+
"transcript.countItems": "{n} 条",
|
|
383
|
+
"transcript.fileSize": "{kb} KB",
|
|
384
|
+
"transcript.lastModified": "最后修改 {time}",
|
|
385
|
+
"model.switchHint": "点击切换目前 wrap session 的模型(SDK 动态切换,不会中断对话)",
|
|
386
|
+
"model.menuTitle": "模型",
|
|
387
|
+
"model.defaultDesc": "用 SDK 默认模型(CLAUDE_DEFAULT_MODEL / Anthropic 默认)",
|
|
388
|
+
"model.aliasDesc": "切到 {id}",
|
|
389
|
+
"model.customPlaceholder": "claude-opus-4-7-20251122",
|
|
390
|
+
"model.applyCustom": "套用",
|
|
391
|
+
"transcript.viewOptions": "对话检视选项",
|
|
392
|
+
"transcript.showTool": "显示 TOOL",
|
|
393
|
+
"transcript.items10": "10 条",
|
|
394
|
+
"transcript.items20": "20 条",
|
|
395
|
+
"transcript.items50": "50 条",
|
|
396
|
+
"transcript.items100": "100 条",
|
|
397
|
+
"transcript.items200": "200 条",
|
|
398
|
+
"transcript.items500": "500 条",
|
|
399
|
+
"transcript.itemsAll": "全部",
|
|
400
|
+
"transcript.loadAll": "📜 加载全部",
|
|
401
|
+
"transcript.loadAllTitle": "把整个 session JSONL 全部读进来(最多 10000 turn)",
|
|
402
|
+
"transcript.loading": "读取中…",
|
|
403
|
+
"transcript.reload": "重新读取",
|
|
404
|
+
|
|
405
|
+
"send.focusing": "叫起窗口:{label}",
|
|
406
|
+
"send.httpOk": "OK · HTTP {status}",
|
|
407
|
+
"send.httpFail": "失败 HTTP {status}",
|
|
408
|
+
"send.claudeReplied": "Claude 已回复 ({ms} ms)",
|
|
409
|
+
"send.sentToVSCode": "已送出到 VSCode panel(Enter 已自动按)",
|
|
410
|
+
"send.sendFailed": "送出失败:{label}",
|
|
411
|
+
|
|
412
|
+
"focus.cliNotSupported": "🚫 CLI session 不支持 focus(URI handler 只开 VSCode)",
|
|
413
|
+
"focus.bringVSCode": "叫起 VSCode 窗口(focus)",
|
|
414
|
+
"focus.wrappedBadge": "🔌 wrapped",
|
|
415
|
+
"focus.wrappedTitle": "wrapper 接管中(CLI session)",
|
|
416
|
+
"focus.wrapperRunning": "wrapper 正在: {activity}",
|
|
417
|
+
|
|
418
|
+
"composer.imageIgnored": "⚠️ 图片在这个 session 模式下会被忽略(只有 wrapped 才能传图)",
|
|
419
|
+
"composer.uploadImage": "附加图片(从手机相册/相机选择)",
|
|
420
|
+
"composer.cliNotWrapped": "CLI session 没接管 — 先在 terminal 跑:miki claude -r {short}…",
|
|
421
|
+
"composer.vscodeDisabledWrap": "VSCode panel mode 已停用 — 请改用 wrap:miki claude -r {short}…",
|
|
422
|
+
"composer.vscodeDisabledWrap2": "VSCode panel mode 已停用 — 请改 wrap:miki claude -r {short}…",
|
|
423
|
+
"composer.inputPrompt": "输入 prompt(Ctrl+V 可贴图)→ push 进 wrapper 的 query()…",
|
|
424
|
+
"composer.inputPromptShort": "输入讯息…",
|
|
425
|
+
"composer.cliNotWrappedShort": "未接管 — 先 wrap:-r {short}…",
|
|
426
|
+
"composer.vscodeDisabledShort": "需要 wrap:-r {short}…",
|
|
427
|
+
"composer.interruptLong": "中断目前 Claude 的回应(呼叫 SDK Query.interrupt())",
|
|
428
|
+
"composer.interruptShort": "中断目前 Claude 的回应",
|
|
429
|
+
"composer.needWrapHint": "先 `miki claude -r <uuid>` 接管 (wrap-cli)",
|
|
430
|
+
"composer.needWrapHint2": "请先 `miki claude -r <uuid>` 接管 (wrap-cli)",
|
|
431
|
+
"composer.wrapWSHint": "走 wrap WS push 进 terminal 的 query()(免费,可带图)",
|
|
432
|
+
"composer.wrapWSShort": "走 wrap WS push 进 terminal 的 query()",
|
|
433
|
+
"composer.needWrapBadge": "需 wrap",
|
|
434
|
+
"composer.cliPanelNotWrapped": "📟",
|
|
435
|
+
"composer.cliNotWrappedStrong": "CLI session 没接管",
|
|
436
|
+
"composer.pleaseUse": ":请改用",
|
|
437
|
+
"composer.toTakeOver": "接管。",
|
|
438
|
+
"composer.advancedToggle": "advanced:切换送出模式",
|
|
439
|
+
"composer.headlessReal": "真送出模式(headless `claude -r -p`,会花 API 费用、不走你 panel session)",
|
|
440
|
+
"composer.keyEnter": "Enter 送出 · Shift+Enter 换行",
|
|
441
|
+
"composer.keyCtrlEnter": "⌘/Ctrl+Enter 送出 · Enter 换行",
|
|
442
|
+
"composer.escClose": " · Esc 关闭",
|
|
443
|
+
"composer.sendBusy": "⌛",
|
|
444
|
+
"composer.sendWrapped": "送出 🔌",
|
|
445
|
+
"composer.sendNeedWrap": "送出 (需 wrap)",
|
|
446
|
+
|
|
447
|
+
"header.all": "全览",
|
|
448
|
+
"header.live": "进行中",
|
|
449
|
+
"header.idle": "闲置",
|
|
450
|
+
"header.stale": "已断线",
|
|
451
|
+
"header.onlyShow": "只看 {label}",
|
|
452
|
+
"header.running": "目前正在运行",
|
|
453
|
+
"header.filtered": "· 已过滤 ({filter}) · 点 TOTAL 重设",
|
|
454
|
+
"header.liveLong": "进行中 / 等回应",
|
|
455
|
+
"header.wsConnected": "WS connected",
|
|
456
|
+
"header.wsConnecting": "connecting…",
|
|
457
|
+
"header.wsDisconnected":"WS disconnected",
|
|
458
|
+
"header.settingsBtn": "⚙️ 设置",
|
|
459
|
+
"header.settingsTitle": "设置",
|
|
460
|
+
|
|
461
|
+
"session.copyRestart": "复制重启指令:pnpm --dir D:\\code\\cc-hub miki claude -r {uuid}",
|
|
462
|
+
"session.cliMarkTooltip": "标记为 CLI session — 预填送出已停用(URI handler 不支持 terminal)。点一下改回 VSCode。",
|
|
463
|
+
"session.vscodeMarkTooltip": "标记为 VSCode session — 点一下改成 CLI。",
|
|
464
|
+
"session.waitingBadge": "🔔 待回应",
|
|
465
|
+
"session.waitingTooltip": "Claude 还在等你回答 — 点开重新显示问题",
|
|
466
|
+
"session.openTab": "开到 tab",
|
|
467
|
+
"session.wrappedDetailed": "这个 session 被 `miki claude` wrapper 接管(CLI session)— 送出走 push、不再 spawn / 不花 $$",
|
|
468
|
+
"session.empty": "(尚未开始对话)",
|
|
469
|
+
"session.quickSend": "快速送出(弹出小卡片,不展开 transcript)",
|
|
470
|
+
"session.openCli": "开 CLI",
|
|
471
|
+
"session.openCliPending": "...",
|
|
472
|
+
"session.openCliFailed": "失败:{err}",
|
|
473
|
+
"session.openCliTooltip": "弹一个 Windows Terminal 跑 miki claude -r — 把这个 session 变成 wrapped",
|
|
474
|
+
"wrapNotice.title": "CLI 已启动 — 请手动关闭 VSCode panel",
|
|
475
|
+
"wrapNotice.body": "{project} 现在被 CLI wrapper 接管。VSCode panel 还是「拥有」这个 session 的话,两边各打字会把 JSONL 切成分支、session 状态就糊了。建议:把 VSCode 那个 Claude Code panel 关掉,之后只用 dashboard / CLI 互动。",
|
|
476
|
+
"wrapNotice.confirmCheck": "我已关闭对应的 VSCode panel",
|
|
477
|
+
"wrapNotice.confirm": "确认",
|
|
478
|
+
"wrapNotice.later": "稍后处理",
|
|
479
|
+
"wrapNotice.dismiss": "知道了",
|
|
480
|
+
"header.newCli": "新增 CLI",
|
|
481
|
+
"header.newCliTitle": "在指定文件夹开新的 CLI(miki claude --fresh)",
|
|
482
|
+
"newCli.heading": "新增 CLI",
|
|
483
|
+
"newCli.cwdLabel": "文件夹路径",
|
|
484
|
+
"newCli.cwdPlaceholder": "路径",
|
|
485
|
+
"newCli.recentCwds": "最近用过的路径",
|
|
486
|
+
"newCli.submit": "开启",
|
|
487
|
+
"newCli.submitting": "启动中…",
|
|
488
|
+
"newCli.success": "已启动 — 等 wrap 连回 daemon",
|
|
489
|
+
"newCli.error": "失败:{err}",
|
|
490
|
+
"newCli.hint": "会在新的 Windows Terminal 里跑 miki claude --fresh。先送一句 \"hi\" 启动 SDK 后接管。",
|
|
491
|
+
"spawnPending.title": "启动中…",
|
|
492
|
+
"spawnPending.body": "在 {cwd} 跑 miki claude — 等 SDK init 完成、wrap 连回 daemon 后新卡片就会出现。约 3–5 秒。",
|
|
493
|
+
"spawnPending.timeout": "已超过 30 秒没连回 — 看一下 wt 视窗是否有错误。",
|
|
494
|
+
"spawnPending.dismiss": "关闭",
|
|
495
|
+
"session.closeTab": "关闭 tab",
|
|
496
|
+
"session.closeKey": "关闭",
|
|
497
|
+
"session.modalClose": "关闭 (Esc)",
|
|
498
|
+
"session.claudeWaitingHere": "🔔 Claude 还在等你回答这个 session 的问题",
|
|
499
|
+
"session.showQuestion": "重新显示问题",
|
|
500
|
+
|
|
501
|
+
"mode.lockedAcceptTitle": "Auto-accept edits mode:所有 edit/write 直接套用、不再确认。`miki claude --permission-mode acceptEdits` 启动锁定。",
|
|
502
|
+
"mode.lockedBypassTitle": "Bypass permissions:所有工具都不问就执行。极度危险,仅在 sandbox 使用。`miki claude --bypass-permissions` 启动锁定。",
|
|
503
|
+
"mode.lockedPlanTitle": "Plan mode:只规划、不执行 mutation 类工具。`miki claude --permission-mode plan` 启动锁定。",
|
|
504
|
+
"mode.defaultDesc": "每次 edit 都问你",
|
|
505
|
+
"mode.defaultTitle": "Ask before edits:每个 mutation 工具会问你。",
|
|
506
|
+
"mode.acceptDesc": "Edit / write 直接套用",
|
|
507
|
+
"mode.acceptTitle": "Edit automatically:所有 edit/write 直接套用、不再确认。",
|
|
508
|
+
"mode.planDesc": "只规划、不动文件",
|
|
509
|
+
"mode.planTitle": "Plan mode:只规划、不执行 mutation 类工具。",
|
|
510
|
+
"mode.autoDesc": "Claude 自己挑 mode",
|
|
511
|
+
"mode.autoTitle": "Auto mode:Claude 自动依任务选最适合的 mode。",
|
|
512
|
+
"mode.bypassDesc": "全部工具直接跑(危险)",
|
|
513
|
+
"mode.bypassTitle": "Bypass permissions:所有工具都不问就执行。极度危险,只在 sandbox 使用。",
|
|
514
|
+
"mode.switchHint": " · 点一下切换 mode",
|
|
515
|
+
|
|
516
|
+
"log.activity": "活动记录",
|
|
517
|
+
"log.syncedConsole": "同步输出到 F12 Console",
|
|
518
|
+
"log.clear": "清空",
|
|
519
|
+
"log.empty": "尚无活动,按任何按钮或等 hook 事件就会冒出来。",
|
|
520
|
+
|
|
521
|
+
"overview.title": "🗺️ 全览",
|
|
522
|
+
"overview.noSessions": "目前没有 session。",
|
|
523
|
+
"overview.openPanelHint": "在任何 VSCode 窗口开 Claude Code panel 就会冒出来。",
|
|
524
|
+
"overview.runHooks": "没反应的话:",
|
|
525
|
+
"overview.sessionGone": "(session 不存在了,可能 daemon 重启过)",
|
|
526
|
+
|
|
527
|
+
"settings.title": "设置",
|
|
528
|
+
"settings.sendKeySection": "送出键(全局)",
|
|
529
|
+
"settings.enterLabel": "Enter",
|
|
530
|
+
"settings.enterDesc": " 送出 · Shift+Enter 换行",
|
|
531
|
+
"settings.ctrlEnterLabel": "Ctrl/⌘ + Enter",
|
|
532
|
+
"settings.ctrlEnterDesc": " 送出 · Enter 换行",
|
|
533
|
+
"settings.sendKeyHelp": "应用到 dashboard 快速送出窗口 + 每个 tab 的 composer。Enter 模式下仍可用 Ctrl/⌘+Enter 强制送出。",
|
|
534
|
+
"settings.appearance": "外观",
|
|
535
|
+
"settings.themeLight": "☀️ Light",
|
|
536
|
+
"settings.themeDark": "🌙 Dark",
|
|
537
|
+
"settings.themeSystem": "🖥 System",
|
|
538
|
+
"settings.themeSystemTitle": "跟随 OS 设置(prefers-color-scheme)",
|
|
539
|
+
"settings.themeDarkTitle": "深色模式",
|
|
540
|
+
"settings.themeLightTitle": "浅色模式",
|
|
541
|
+
"settings.sortMode": "卡片排序",
|
|
542
|
+
"settings.sortPriorityLabel": "🔔 优先级",
|
|
543
|
+
"settings.sortPriorityTitle": "需要回应的 session 优先(waiting → active → idle → stale),同组内依 cwd 排序",
|
|
544
|
+
"settings.sortUuidLabel": "🆔 UUID 序",
|
|
545
|
+
"settings.sortUuidTitle": "依 session_uuid 排序;每个 session 永远卡同一格,F5 / 重启都不会位移(位置与项目名无关)",
|
|
546
|
+
"settings.sortRecentLabel": "⏱ 最近活动",
|
|
547
|
+
"settings.sortRecentTitle": "最近有事件的排前面(last_event_at DESC),会随活动跳动",
|
|
548
|
+
"settings.sortHelp": "影响首页卡片顺序。F5 后位置会记住(除了「最近活动」模式,这个本来就会随活动移动)。",
|
|
549
|
+
"settings.close": "关闭",
|
|
550
|
+
"settings.language": "语言",
|
|
551
|
+
|
|
552
|
+
"startup.starting": "启动 — 抓 /sessions + previews",
|
|
553
|
+
"startup.getSessionsFailed": "GET /sessions 失败",
|
|
554
|
+
"startup.gotSessions": "GET /sessions 200 — 收到 {n} 个 session",
|
|
555
|
+
"startup.wsConnecting": "WS 连线中",
|
|
556
|
+
|
|
557
|
+
// ── web-phone (mobile client) ────────────────────────────────────────────
|
|
558
|
+
"phone.pair.title": "miki-moni 配对",
|
|
559
|
+
"phone.pair.subtitle": "把这台设备跟你电脑上的 miki-moni daemon 配对",
|
|
560
|
+
"phone.pair.instruction": "在电脑终端机跑 {cmd},输入它显示的 16 码配对码。",
|
|
561
|
+
"phone.pair.codeLabel": "配对码",
|
|
562
|
+
"phone.pair.codePlaceholder": "XXXX-XXXX-XXXX-XXXX",
|
|
563
|
+
"phone.pair.errorBadCode": "Code must be 16 chars from the Crockford base32 alphabet",
|
|
564
|
+
"phone.pair.errorScannedQr": "扫到了但不是配对 QR:{text}",
|
|
565
|
+
"phone.pair.busy": "配对中…",
|
|
566
|
+
"phone.pair.submit": "开始配对",
|
|
567
|
+
"phone.pair.or": "或",
|
|
568
|
+
"phone.pair.scan": "扫 QR Code",
|
|
569
|
+
"phone.pair.autoTitle": "配对中…",
|
|
570
|
+
"phone.pair.autoHint": "正在跟 daemon 完成 handshake",
|
|
571
|
+
"phone.pair.relayLabel": "relay:",
|
|
572
|
+
"phone.tunnel.connecting": "连线中…",
|
|
573
|
+
"phone.tunnel.connectingHint": "通过 relay 连到 daemon · {daemon}…",
|
|
574
|
+
"phone.tunnel.error": "连线错误",
|
|
575
|
+
"phone.tunnel.resetAndRepair": "清除状态并重新配对",
|
|
576
|
+
|
|
577
|
+
"phone.conn.connected": "已连线",
|
|
578
|
+
"phone.conn.connecting": "连线中…",
|
|
579
|
+
"phone.conn.reconnecting": "重新连线中…",
|
|
580
|
+
"phone.conn.error": "连线错误",
|
|
581
|
+
|
|
582
|
+
"phone.session.noUuid": "null(没抓到,叫起/送出可能会开错 session)",
|
|
583
|
+
"phone.session.focus": "叫起窗口",
|
|
584
|
+
"phone.session.promptPlaceholder": "输入 prompt 送给这个 session…",
|
|
585
|
+
"phone.session.send": "送出",
|
|
586
|
+
|
|
587
|
+
"phone.empty.title": "⚪ 连上 relay,但还没有 session",
|
|
588
|
+
"phone.empty.twoOptions": "两个可能:",
|
|
589
|
+
"phone.empty.daemonNotRunning": "电脑的 daemon 没在跑 → 终端机跑 {cmd}",
|
|
590
|
+
"phone.empty.noClaude": "daemon 有跑,但你还没开任何 Claude Code session → 开一个就会出现",
|
|
591
|
+
"phone.empty.hooksHint": "若 hooks 还没装:{cmd}",
|
|
592
|
+
"phone.empty.waitingConn": "等待连线…",
|
|
593
|
+
|
|
594
|
+
"phone.header.unpair": "解除配对",
|
|
595
|
+
"phone.header.unpairing": "解除中…",
|
|
596
|
+
|
|
597
|
+
"phone.log.title": "活动记录",
|
|
598
|
+
"phone.log.consoleNote": "(同步输出到 F12 Console)",
|
|
599
|
+
"phone.log.clear": "清空",
|
|
600
|
+
"phone.log.empty": "尚无活动。WS 连上、配对后送出 cmd、收到 event 都会即时显示。",
|
|
601
|
+
"phone.log.wsError": "WS error",
|
|
602
|
+
"phone.log.wsCloseReconnect": "WS close — {delay}ms 后重连",
|
|
603
|
+
"phone.log.wsCloseReason": "(无)",
|
|
604
|
+
"phone.log.wsNonJson": "WS 收到非 JSON",
|
|
605
|
+
"phone.log.wsConnectingRelay": "WS 连线中 (relay)",
|
|
606
|
+
"phone.log.wsConnectingLegacy": "WS 连线中 (legacy)",
|
|
607
|
+
"phone.log.wsOpenSnapshot": "WS open — 送 request_snapshot",
|
|
608
|
+
"phone.log.unknownKind": "不认识的讯息 kind",
|
|
609
|
+
"phone.log.snapshotReceived": "state_snapshot — 收到 {n} 个 session",
|
|
610
|
+
"phone.log.eventReceived": "event — {project}",
|
|
611
|
+
"phone.log.envelopeFail": "envelope 解密失败 — 可能对方不是这个 peer",
|
|
612
|
+
"phone.log.identityFail": "无法加载 identity",
|
|
613
|
+
"phone.log.sendFailWsClosed": "{label}: WS 未连线,无法送出",
|
|
614
|
+
"phone.log.encryptedSend": "加密送出 {label}",
|
|
615
|
+
"phone.log.registerPeer": "送 register_peer_id",
|
|
616
|
+
"phone.log.revokeRelay": "送 revoke_self 给 relay",
|
|
617
|
+
"phone.log.revokeOk": "relay 确认 revoke 完成,清除本地状态",
|
|
618
|
+
"phone.log.revokeKicked": "daemon 主动解除配对,回到配对画面",
|
|
619
|
+
"phone.log.unpairOffline": "WS 未连线;直接清本地(relay 端可能仍有 paired_phones entry,下次连会被挡掉)",
|
|
620
|
+
"phone.log.focusClick": "click 叫起窗口",
|
|
621
|
+
"phone.log.sendClick": "click 送出 prompt",
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const en: Dict = {
|
|
625
|
+
"status.active": "Active",
|
|
626
|
+
"status.waiting": "Awaiting you",
|
|
627
|
+
"status.idle": "Idle",
|
|
628
|
+
"status.stale": "Disconnected",
|
|
629
|
+
|
|
630
|
+
"time.secondsAgo": "{n}s ago",
|
|
631
|
+
"time.minutesAgo": "{n}m ago",
|
|
632
|
+
"time.hoursAgo": "{n}h ago",
|
|
633
|
+
"time.daysAgo": "{n}d ago",
|
|
634
|
+
|
|
635
|
+
"ask.atLeastOne": "Please answer at least one question",
|
|
636
|
+
"ask.claudeAsking": "❓ Claude is asking you a question",
|
|
637
|
+
"ask.placeholder": "Or type your own answer (sent with the checked options)…",
|
|
638
|
+
"ask.prev": "← Prev",
|
|
639
|
+
"ask.next": "Next →",
|
|
640
|
+
"ask.submitting": "Submitting…",
|
|
641
|
+
"ask.submit": "Submit",
|
|
642
|
+
"ask.removeThis": "Remove this",
|
|
643
|
+
|
|
644
|
+
"expand.collapse": "Collapse",
|
|
645
|
+
"expand.expandIn": "Expand IN",
|
|
646
|
+
"expand.expandOut": "Expand OUT",
|
|
647
|
+
"expand.expand": "Expand",
|
|
648
|
+
|
|
649
|
+
"transcript.empty": "(no displayable messages in transcript)",
|
|
650
|
+
"transcript.conversation": "Conversation",
|
|
651
|
+
"transcript.countItems": "{n}",
|
|
652
|
+
"transcript.fileSize": "{kb} KB",
|
|
653
|
+
"transcript.lastModified": "Last modified {time}",
|
|
654
|
+
"model.switchHint": "Switch the current wrap session's model (SDK dynamic swap, doesn't break the conversation)",
|
|
655
|
+
"model.menuTitle": "Model",
|
|
656
|
+
"model.defaultDesc": "Use SDK default (CLAUDE_DEFAULT_MODEL / Anthropic default)",
|
|
657
|
+
"model.aliasDesc": "Switch to {id}",
|
|
658
|
+
"model.customPlaceholder": "claude-opus-4-7-20251122",
|
|
659
|
+
"model.applyCustom": "Apply",
|
|
660
|
+
"transcript.viewOptions": "Transcript view options",
|
|
661
|
+
"transcript.showTool": "Show TOOL",
|
|
662
|
+
"transcript.items10": "10",
|
|
663
|
+
"transcript.items20": "20",
|
|
664
|
+
"transcript.items50": "50",
|
|
665
|
+
"transcript.items100": "100",
|
|
666
|
+
"transcript.items200": "200",
|
|
667
|
+
"transcript.items500": "500",
|
|
668
|
+
"transcript.itemsAll": "All",
|
|
669
|
+
"transcript.loadAll": "📜 Load all",
|
|
670
|
+
"transcript.loadAllTitle": "Load the entire session JSONL (up to 10000 turns)",
|
|
671
|
+
"transcript.loading": "Loading…",
|
|
672
|
+
"transcript.reload": "Reload",
|
|
673
|
+
|
|
674
|
+
"send.focusing": "Focus window: {label}",
|
|
675
|
+
"send.httpOk": "OK · HTTP {status}",
|
|
676
|
+
"send.httpFail": "Failed HTTP {status}",
|
|
677
|
+
"send.claudeReplied": "Claude replied ({ms} ms)",
|
|
678
|
+
"send.sentToVSCode": "Sent to VSCode panel (Enter auto-pressed)",
|
|
679
|
+
"send.sendFailed": "Send failed: {label}",
|
|
680
|
+
|
|
681
|
+
"focus.cliNotSupported": "🚫 CLI session does not support focus (URI handler only opens VSCode)",
|
|
682
|
+
"focus.bringVSCode": "Bring up VSCode window (focus)",
|
|
683
|
+
"focus.wrappedBadge": "🔌 wrapped",
|
|
684
|
+
"focus.wrappedTitle": "Wrapper has taken over (CLI session)",
|
|
685
|
+
"focus.wrapperRunning": "wrapper running: {activity}",
|
|
686
|
+
|
|
687
|
+
"composer.imageIgnored": "⚠️ Images are ignored in this session mode (only wrapped sessions accept images)",
|
|
688
|
+
"composer.uploadImage": "Attach image (pick from gallery or camera)",
|
|
689
|
+
"composer.cliNotWrapped": "CLI session not wrapped — first run in terminal: miki claude -r {short}…",
|
|
690
|
+
"composer.vscodeDisabledWrap": "VSCode panel mode disabled — please wrap instead: miki claude -r {short}…",
|
|
691
|
+
"composer.vscodeDisabledWrap2": "VSCode panel mode disabled — please wrap: miki claude -r {short}…",
|
|
692
|
+
"composer.inputPrompt": "Type prompt (Ctrl+V to paste image) → push into wrapper's query()…",
|
|
693
|
+
"composer.inputPromptShort": "Type a message…",
|
|
694
|
+
"composer.cliNotWrappedShort": "Not wrapped — run: -r {short}…",
|
|
695
|
+
"composer.vscodeDisabledShort": "Needs wrap: -r {short}…",
|
|
696
|
+
"composer.interruptLong": "Interrupt current Claude response (calls SDK Query.interrupt())",
|
|
697
|
+
"composer.interruptShort": "Interrupt current Claude response",
|
|
698
|
+
"composer.needWrapHint": "First `miki claude -r <uuid>` to take over (wrap-cli)",
|
|
699
|
+
"composer.needWrapHint2": "Please `miki claude -r <uuid>` to take over (wrap-cli)",
|
|
700
|
+
"composer.wrapWSHint": "Use wrap WS push into terminal's query() (free, supports images)",
|
|
701
|
+
"composer.wrapWSShort": "Use wrap WS push into terminal's query()",
|
|
702
|
+
"composer.needWrapBadge": "needs wrap",
|
|
703
|
+
"composer.cliPanelNotWrapped": "📟",
|
|
704
|
+
"composer.cliNotWrappedStrong": "CLI session not wrapped",
|
|
705
|
+
"composer.pleaseUse": ": please use",
|
|
706
|
+
"composer.toTakeOver": " to take over.",
|
|
707
|
+
"composer.advancedToggle": "advanced: toggle send mode",
|
|
708
|
+
"composer.headlessReal": "Real send mode (headless `claude -r -p`, costs API $$ and bypasses your panel session)",
|
|
709
|
+
"composer.keyEnter": "Enter to send · Shift+Enter for newline",
|
|
710
|
+
"composer.keyCtrlEnter": "⌘/Ctrl+Enter to send · Enter for newline",
|
|
711
|
+
"composer.escClose": " · Esc to close",
|
|
712
|
+
"composer.sendBusy": "⌛",
|
|
713
|
+
"composer.sendWrapped": "Send 🔌",
|
|
714
|
+
"composer.sendNeedWrap": "Send (needs wrap)",
|
|
715
|
+
|
|
716
|
+
"header.all": "All",
|
|
717
|
+
"header.live": "Live",
|
|
718
|
+
"header.idle": "Idle",
|
|
719
|
+
"header.stale": "Disconnected",
|
|
720
|
+
"header.onlyShow": "Only show {label}",
|
|
721
|
+
"header.running": "Currently running",
|
|
722
|
+
"header.filtered": "· filtered ({filter}) · click TOTAL to reset",
|
|
723
|
+
"header.liveLong": "Live / awaiting",
|
|
724
|
+
"header.wsConnected": "WS connected",
|
|
725
|
+
"header.wsConnecting": "connecting…",
|
|
726
|
+
"header.wsDisconnected":"WS disconnected",
|
|
727
|
+
"header.settingsBtn": "⚙️ Settings",
|
|
728
|
+
"header.settingsTitle": "Settings",
|
|
729
|
+
|
|
730
|
+
"session.copyRestart": "Copy restart command: pnpm --dir D:\\code\\cc-hub miki claude -r {uuid}",
|
|
731
|
+
"session.cliMarkTooltip": "Marked as CLI session — prefilled send is disabled (URI handler doesn't support terminal). Click to switch back to VSCode.",
|
|
732
|
+
"session.vscodeMarkTooltip": "Marked as VSCode session — click to switch to CLI.",
|
|
733
|
+
"session.waitingBadge": "🔔 Awaiting reply",
|
|
734
|
+
"session.waitingTooltip": "Claude is still waiting for your answer — click to re-show the question",
|
|
735
|
+
"session.openTab": "Open in tab",
|
|
736
|
+
"session.wrappedDetailed": "This session is taken over by the `miki claude` wrapper (CLI session) — sends go via push, no spawn / no $$",
|
|
737
|
+
"session.empty": "(no conversation yet)",
|
|
738
|
+
"session.quickSend": "Quick send (pops a small card, no transcript expand)",
|
|
739
|
+
"session.openCli": "Open CLI",
|
|
740
|
+
"session.openCliPending": "...",
|
|
741
|
+
"session.openCliFailed": "Failed: {err}",
|
|
742
|
+
"session.openCliTooltip": "Pop a Windows Terminal running miki claude -r — turns this session into a wrapped one",
|
|
743
|
+
"wrapNotice.title": "CLI started — please close the VSCode panel manually",
|
|
744
|
+
"wrapNotice.body": "{project} is now owned by the CLI wrapper. The VSCode panel still thinks it owns this session — typing in both will fork the JSONL transcript and corrupt session state. Recommended: close that Claude Code panel in VSCode, then interact only via dashboard / CLI.",
|
|
745
|
+
"wrapNotice.confirmCheck": "I've closed the corresponding VSCode panel",
|
|
746
|
+
"wrapNotice.confirm": "Confirm",
|
|
747
|
+
"wrapNotice.later": "Cancel",
|
|
748
|
+
"wrapNotice.dismiss": "Got it",
|
|
749
|
+
"header.newCli": "New CLI",
|
|
750
|
+
"header.newCliTitle": "Open a new CLI in a chosen folder (miki claude --fresh)",
|
|
751
|
+
"newCli.heading": "New CLI",
|
|
752
|
+
"newCli.cwdLabel": "Folder path",
|
|
753
|
+
"newCli.cwdPlaceholder": "Path",
|
|
754
|
+
"newCli.recentCwds": "Recent paths",
|
|
755
|
+
"newCli.submit": "Open",
|
|
756
|
+
"newCli.submitting": "Starting…",
|
|
757
|
+
"newCli.success": "Started — waiting for wrap to connect back",
|
|
758
|
+
"newCli.error": "Failed: {err}",
|
|
759
|
+
"newCli.hint": "Spawns miki claude --fresh in a new Windows Terminal. SDK initializes via a synthetic \"hi\" then takes over.",
|
|
760
|
+
"spawnPending.title": "Starting…",
|
|
761
|
+
"spawnPending.body": "Running miki claude in {cwd} — once SDK init completes and wrap connects back, the new card will appear here. About 3–5s.",
|
|
762
|
+
"spawnPending.timeout": "Over 30s with no callback — check the wt window for errors.",
|
|
763
|
+
"spawnPending.dismiss": "Close",
|
|
764
|
+
"session.closeTab": "Close tab",
|
|
765
|
+
"session.closeKey": "Close",
|
|
766
|
+
"session.modalClose": "Close (Esc)",
|
|
767
|
+
"session.claudeWaitingHere": "🔔 Claude is still waiting for your answer on this session",
|
|
768
|
+
"session.showQuestion": "Re-show question",
|
|
769
|
+
|
|
770
|
+
"mode.lockedAcceptTitle": "Auto-accept edits mode: all edits/writes apply directly, no confirmation. Locked at start via `miki claude --permission-mode acceptEdits`.",
|
|
771
|
+
"mode.lockedBypassTitle": "Bypass permissions: all tools run without asking. Extremely dangerous, sandbox only. Locked at start via `miki claude --bypass-permissions`.",
|
|
772
|
+
"mode.lockedPlanTitle": "Plan mode: planning only, no mutation tools. Locked at start via `miki claude --permission-mode plan`.",
|
|
773
|
+
"mode.defaultDesc": "Ask before every edit",
|
|
774
|
+
"mode.defaultTitle": "Ask before edits: every mutation tool prompts you.",
|
|
775
|
+
"mode.acceptDesc": "Apply edits / writes directly",
|
|
776
|
+
"mode.acceptTitle": "Edit automatically: all edits/writes apply directly, no confirmation.",
|
|
777
|
+
"mode.planDesc": "Plan only, no file changes",
|
|
778
|
+
"mode.planTitle": "Plan mode: planning only, no mutation tools.",
|
|
779
|
+
"mode.autoDesc": "Claude picks the mode itself",
|
|
780
|
+
"mode.autoTitle": "Auto mode: Claude picks the most appropriate mode per task.",
|
|
781
|
+
"mode.bypassDesc": "All tools run (dangerous)",
|
|
782
|
+
"mode.bypassTitle": "Bypass permissions: all tools run without asking. Extremely dangerous, sandbox only.",
|
|
783
|
+
"mode.switchHint": " · click to switch mode",
|
|
784
|
+
|
|
785
|
+
"log.activity": "Activity log",
|
|
786
|
+
"log.syncedConsole": "Mirrored to F12 Console",
|
|
787
|
+
"log.clear": "Clear",
|
|
788
|
+
"log.empty": "No activity yet. Hit any button or wait for hook events.",
|
|
789
|
+
|
|
790
|
+
"overview.title": "🗺️ Overview",
|
|
791
|
+
"overview.noSessions": "No sessions yet.",
|
|
792
|
+
"overview.openPanelHint": "Open a Claude Code panel in any VSCode window — it shows up here.",
|
|
793
|
+
"overview.runHooks": "Nothing happening? Try:",
|
|
794
|
+
"overview.sessionGone": "(session no longer exists, daemon may have restarted)",
|
|
795
|
+
|
|
796
|
+
"settings.title": "Settings",
|
|
797
|
+
"settings.sendKeySection": "Send key (global)",
|
|
798
|
+
"settings.enterLabel": "Enter",
|
|
799
|
+
"settings.enterDesc": " to send · Shift+Enter for newline",
|
|
800
|
+
"settings.ctrlEnterLabel": "Ctrl/⌘ + Enter",
|
|
801
|
+
"settings.ctrlEnterDesc": " to send · Enter for newline",
|
|
802
|
+
"settings.sendKeyHelp": "Applies to the dashboard quick-send card + every tab's composer. In Enter mode, Ctrl/⌘+Enter still force-sends.",
|
|
803
|
+
"settings.appearance": "Appearance",
|
|
804
|
+
"settings.themeLight": "☀️ Light",
|
|
805
|
+
"settings.themeDark": "🌙 Dark",
|
|
806
|
+
"settings.themeSystem": "🖥 System",
|
|
807
|
+
"settings.themeSystemTitle": "Follow OS setting (prefers-color-scheme)",
|
|
808
|
+
"settings.themeDarkTitle": "Dark mode",
|
|
809
|
+
"settings.themeLightTitle": "Light mode",
|
|
810
|
+
"settings.sortMode": "Card sort",
|
|
811
|
+
"settings.sortPriorityLabel": "🔔 Priority",
|
|
812
|
+
"settings.sortPriorityTitle": "Sessions awaiting reply first (waiting → active → idle → stale), then by cwd",
|
|
813
|
+
"settings.sortUuidLabel": "🆔 UUID",
|
|
814
|
+
"settings.sortUuidTitle": "Sort by session_uuid; each session stays in its slot forever, no drift on F5/restart (independent of project name)",
|
|
815
|
+
"settings.sortRecentLabel": "⏱ Recent",
|
|
816
|
+
"settings.sortRecentTitle": "Most recent activity first (last_event_at DESC), shuffles as events arrive",
|
|
817
|
+
"settings.sortHelp": "Affects overview card ordering. Position persists across F5 (except for the Recent mode, which is meant to shuffle).",
|
|
818
|
+
"settings.close": "Close",
|
|
819
|
+
"settings.language": "Language",
|
|
820
|
+
|
|
821
|
+
"startup.starting": "Starting — fetching /sessions + previews",
|
|
822
|
+
"startup.getSessionsFailed": "GET /sessions failed",
|
|
823
|
+
"startup.gotSessions": "GET /sessions 200 — received {n} sessions",
|
|
824
|
+
"startup.wsConnecting": "WS connecting",
|
|
825
|
+
|
|
826
|
+
// ── web-phone (mobile client) ────────────────────────────────────────────
|
|
827
|
+
"phone.pair.title": "miki-moni pairing",
|
|
828
|
+
"phone.pair.subtitle": "Pair this device with the miki-moni daemon on your computer",
|
|
829
|
+
"phone.pair.instruction": "Run {cmd} in your computer's terminal and enter the 16-char pairing code it shows.",
|
|
830
|
+
"phone.pair.codeLabel": "Pairing code",
|
|
831
|
+
"phone.pair.codePlaceholder": "XXXX-XXXX-XXXX-XXXX",
|
|
832
|
+
"phone.pair.errorBadCode": "Code must be 16 chars from the Crockford base32 alphabet",
|
|
833
|
+
"phone.pair.errorScannedQr": "Scanned, but not a pairing QR: {text}",
|
|
834
|
+
"phone.pair.busy": "Pairing…",
|
|
835
|
+
"phone.pair.submit": "Start pairing",
|
|
836
|
+
"phone.pair.or": "or",
|
|
837
|
+
"phone.pair.scan": "Scan QR Code",
|
|
838
|
+
"phone.pair.autoTitle": "Pairing…",
|
|
839
|
+
"phone.pair.autoHint": "Completing handshake with daemon",
|
|
840
|
+
"phone.pair.relayLabel": "relay:",
|
|
841
|
+
"phone.tunnel.connecting": "Connecting…",
|
|
842
|
+
"phone.tunnel.connectingHint": "Connecting through relay to daemon · {daemon}…",
|
|
843
|
+
"phone.tunnel.error": "Connection error",
|
|
844
|
+
"phone.tunnel.resetAndRepair": "Clear state and re-pair",
|
|
845
|
+
|
|
846
|
+
"phone.conn.connected": "Connected",
|
|
847
|
+
"phone.conn.connecting": "Connecting…",
|
|
848
|
+
"phone.conn.reconnecting": "Reconnecting…",
|
|
849
|
+
"phone.conn.error": "Connection error",
|
|
850
|
+
|
|
851
|
+
"phone.session.noUuid": "null (not detected — focus/send may open the wrong session)",
|
|
852
|
+
"phone.session.focus": "Bring up window",
|
|
853
|
+
"phone.session.promptPlaceholder": "Type a prompt to send to this session…",
|
|
854
|
+
"phone.session.send": "Send",
|
|
855
|
+
|
|
856
|
+
"phone.empty.title": "⚪ Connected to relay, but no sessions yet",
|
|
857
|
+
"phone.empty.twoOptions": "Two possibilities:",
|
|
858
|
+
"phone.empty.daemonNotRunning": "Computer's daemon isn't running → in terminal run {cmd}",
|
|
859
|
+
"phone.empty.noClaude": "daemon is running but you haven't opened any Claude Code session yet → open one and it'll show up",
|
|
860
|
+
"phone.empty.hooksHint": "If hooks aren't installed yet: {cmd}",
|
|
861
|
+
"phone.empty.waitingConn": "Waiting for connection…",
|
|
862
|
+
|
|
863
|
+
"phone.header.unpair": "Unpair",
|
|
864
|
+
"phone.header.unpairing": "Unpairing…",
|
|
865
|
+
|
|
866
|
+
"phone.log.title": "Activity log",
|
|
867
|
+
"phone.log.consoleNote": " (mirrored to F12 Console)",
|
|
868
|
+
"phone.log.clear": "Clear",
|
|
869
|
+
"phone.log.empty": "No activity yet. Once WS connects, sends + events show up here in real time.",
|
|
870
|
+
"phone.log.wsError": "WS error",
|
|
871
|
+
"phone.log.wsCloseReconnect": "WS close — reconnecting in {delay}ms",
|
|
872
|
+
"phone.log.wsCloseReason": "(none)",
|
|
873
|
+
"phone.log.wsNonJson": "WS received non-JSON",
|
|
874
|
+
"phone.log.wsConnectingRelay": "WS connecting (relay)",
|
|
875
|
+
"phone.log.wsConnectingLegacy": "WS connecting (legacy)",
|
|
876
|
+
"phone.log.wsOpenSnapshot": "WS open — sending request_snapshot",
|
|
877
|
+
"phone.log.unknownKind": "Unknown message kind",
|
|
878
|
+
"phone.log.snapshotReceived": "state_snapshot — received {n} sessions",
|
|
879
|
+
"phone.log.eventReceived": "event — {project}",
|
|
880
|
+
"phone.log.envelopeFail": "envelope decryption failed — peer may not be this device",
|
|
881
|
+
"phone.log.identityFail": "Failed to load identity",
|
|
882
|
+
"phone.log.sendFailWsClosed": "{label}: WS not connected, can't send",
|
|
883
|
+
"phone.log.encryptedSend": "Encrypted send {label}",
|
|
884
|
+
"phone.log.registerPeer": "Sending register_peer_id",
|
|
885
|
+
"phone.log.revokeRelay": "Sending revoke_self to relay",
|
|
886
|
+
"phone.log.revokeOk": "Relay confirmed revoke, clearing local state",
|
|
887
|
+
"phone.log.revokeKicked": "Daemon force-unpaired us, returning to pair screen",
|
|
888
|
+
"phone.log.unpairOffline": "WS offline; cleared local state directly (relay may still have a paired_phones entry that blocks the next connection)",
|
|
889
|
+
"phone.log.focusClick": "click bring-up window",
|
|
890
|
+
"phone.log.sendClick": "click send prompt",
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
const MESSAGES: Record<Locale, Dict> = {
|
|
894
|
+
"zh-TW": zhTW,
|
|
895
|
+
"zh-CN": zhCN,
|
|
896
|
+
"en": en,
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
function interpolate(template: string, params?: Record<string, string | number>): string {
|
|
900
|
+
if (!params) return template;
|
|
901
|
+
return template.replace(/\{(\w+)\}/g, (_, k) => {
|
|
902
|
+
const v = params[k];
|
|
903
|
+
return v === undefined || v === null ? `{${k}}` : String(v);
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Translate a flat dotted key against the current locale.
|
|
909
|
+
* Missing key → falls back to zh-TW, then to the key string itself.
|
|
910
|
+
*/
|
|
911
|
+
export function t(key: string, params?: Record<string, string | number>): string {
|
|
912
|
+
const tbl = MESSAGES[currentLocale];
|
|
913
|
+
const raw = tbl[key] ?? MESSAGES["zh-TW"][key] ?? key;
|
|
914
|
+
return interpolate(raw, params);
|
|
915
|
+
}
|