feat(i18n): add zh-TW onboarding (wizard + README)

This commit is contained in:
前田信文 2026-01-30 06:13:18 +08:00
parent 4583f88626
commit e742a984ff
4 changed files with 232 additions and 56 deletions

View File

@ -23,6 +23,30 @@ If you want a personal, single-user assistant that feels local, fast, and always
[Website](https://molt.bot) · [Docs](https://docs.molt.bot) · [Getting Started](https://docs.molt.bot/start/getting-started) · [Updating](https://docs.molt.bot/install/updating) · [Showcase](https://docs.molt.bot/start/showcase) · [FAQ](https://docs.molt.bot/start/faq) · [Wizard](https://docs.molt.bot/start/wizard) · [Nix](https://github.com/moltbot/nix-clawdbot) · [Docker](https://docs.molt.bot/install/docker) · [Discord](https://discord.gg/clawd) [Website](https://molt.bot) · [Docs](https://docs.molt.bot) · [Getting Started](https://docs.molt.bot/start/getting-started) · [Updating](https://docs.molt.bot/install/updating) · [Showcase](https://docs.molt.bot/start/showcase) · [FAQ](https://docs.molt.bot/start/faq) · [Wizard](https://docs.molt.bot/start/wizard) · [Nix](https://github.com/moltbot/nix-clawdbot) · [Docker](https://docs.molt.bot/install/docker) · [Discord](https://discord.gg/clawd)
## 繁體中文zh-TW
此 fork 目標是把 **CLI 安裝/初始化流程(`moltbot onboard` / wizard** 加入繁體中文介面。
### 啟用繁中介面
macOS / Linuxzsh/bash
```bash
export MOLTBOT_LANG=zh-TW
moltbot onboard --install-daemon
```
WindowsPowerShell
```powershell
$env:MOLTBOT_LANG = "zh-TW"
moltbot onboard --install-daemon
```
也支援從系統語系自動判斷(例如 `LANG=zh_TW.UTF-8`)。
---
Preferred setup: run the onboarding wizard (`moltbot onboard`). It walks through gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**. Preferred setup: run the onboarding wizard (`moltbot onboard`). It walks through gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
Works with npm, pnpm, or bun. Works with npm, pnpm, or bun.
New install? Start here: [Getting started](https://docs.molt.bot/start/getting-started) New install? Start here: [Getting started](https://docs.molt.bot/start/getting-started)

View File

@ -61,11 +61,13 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv =
} }
if (process.platform === "win32") { if (process.platform === "win32") {
const { resolveLocaleFromEnv, t } = await import("../i18n/i18n.js");
const locale = resolveLocaleFromEnv();
runtime.log( runtime.log(
[ [
"Windows detected.", t(locale, "windows.detected"),
"WSL2 is strongly recommended; native Windows is untested and more problematic.", t(locale, "windows.recommend"),
"Guide: https://docs.molt.bot/windows", t(locale, "windows.guide"),
].join("\n"), ].join("\n"),
); );
} }

116
src/i18n/i18n.ts Normal file
View File

@ -0,0 +1,116 @@
export type MoltbotLocale = "en" | "zh-TW";
export function resolveLocaleFromEnv(env: NodeJS.ProcessEnv = process.env): MoltbotLocale {
const raw = (env.MOLTBOT_LANG ?? env.CLAWDBOT_LANG ?? env.LANG ?? env.LC_ALL ?? "").trim();
const norm = raw.replace("_", "-");
if (!norm) return "en";
// Common forms:
// - zh_TW.UTF-8
// - zh-TW
// - zh-Hant
// - zh-Hant-TW
const lower = norm.toLowerCase();
if (lower.startsWith("zh")) {
if (lower.includes("tw") || lower.includes("hant")) return "zh-TW";
// If user explicitly asked for Chinese, default to Traditional.
return "zh-TW";
}
return "en";
}
const dict: Record<MoltbotLocale, Record<string, string>> = {
en: {
"onboard.intro": "Moltbot onboarding",
"onboard.mode": "Onboarding mode",
"onboard.mode.quickstart": "QuickStart",
"onboard.mode.manual": "Manual",
"onboard.quickstart.title": "QuickStart",
"onboard.quickstart.keepExisting": "Keeping your current gateway settings:",
"onboard.quickstart.direct": "Direct to chat channels.",
"onboard.whatToSetup": "What do you want to set up?",
"onboard.localGateway": "Local gateway (this machine)",
"onboard.remoteGateway": "Remote gateway (info-only)",
"onboard.configExisting": "Existing config detected",
"onboard.configInvalid": "Invalid config",
"onboard.configIssues": "Config issues",
"onboard.configHandling": "Config handling",
"onboard.config.keep": "Use existing values",
"onboard.config.modify": "Update values",
"onboard.config.reset": "Reset",
"onboard.resetScope": "Reset scope",
"onboard.reset.config": "Config only",
"onboard.reset.configCredsSessions": "Config + creds + sessions",
"onboard.reset.full": "Full reset (config + creds + sessions + workspace)",
"onboard.workspaceDir": "Workspace directory",
"security.title": "Security",
"security.noteTitle": "Security warning — please read.",
"security.confirm": "I understand this is powerful and inherently risky. Continue?",
"windows.detected": "Windows detected.",
"windows.recommend": "WSL2 is strongly recommended; native Windows is untested and more problematic.",
"windows.guide": "Guide: https://docs.molt.bot/windows",
"onboard.skipChannels": "Skipping channel setup.",
"onboard.channelsTitle": "Channels",
"onboard.skipSkills": "Skipping skills setup.",
"onboard.skillsTitle": "Skills",
"onboard.remoteConfigured": "Remote gateway configured.",
},
"zh-TW": {
"onboard.intro": "Moltbot 初始設定Onboarding",
"onboard.mode": "安裝/初始化模式",
"onboard.mode.quickstart": "快速開始QuickStart",
"onboard.mode.manual": "手動設定Manual",
"onboard.quickstart.title": "快速開始QuickStart",
"onboard.quickstart.keepExisting": "保留你目前的 Gateway 設定:",
"onboard.quickstart.direct": "直接導向聊天管道設定。",
"onboard.whatToSetup": "你想要設定哪一種?",
"onboard.localGateway": "本機 Gateway這台機器",
"onboard.remoteGateway": "遠端 Gateway僅寫入設定/不做本機安裝)",
"onboard.configExisting": "偵測到既有設定",
"onboard.configInvalid": "設定檔不合法",
"onboard.configIssues": "設定問題",
"onboard.configHandling": "設定檔處理方式",
"onboard.config.keep": "沿用既有值",
"onboard.config.modify": "更新設定",
"onboard.config.reset": "重置",
"onboard.resetScope": "重置範圍",
"onboard.reset.config": "只重置設定檔",
"onboard.reset.configCredsSessions": "設定檔 + 憑證 + sessions",
"onboard.reset.full": "完整重置(設定檔 + 憑證 + sessions + workspace",
"onboard.workspaceDir": "Workspace 目錄",
"security.title": "安全性",
"security.noteTitle": "安全警告 — 請務必先閱讀",
"security.confirm": "我了解這很強大且有風險,仍要繼續嗎?",
"windows.detected": "偵測到 Windows。",
"windows.recommend": "強烈建議使用 WSL2原生 Windows 尚未完整測試,問題也比較多。",
"windows.guide": "指南https://docs.molt.bot/windows",
"onboard.skipChannels": "略過聊天管道設定。",
"onboard.channelsTitle": "聊天管道Channels",
"onboard.skipSkills": "略過技能Skills安裝。",
"onboard.skillsTitle": "技能Skills",
"onboard.remoteConfigured": "已完成遠端 Gateway 設定。",
},
};
export function t(locale: MoltbotLocale, key: string): string {
return dict[locale]?.[key] ?? dict.en[key] ?? key;
}

View File

@ -38,6 +38,7 @@ import { logConfigUpdated } from "../config/logging.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { resolveUserPath } from "../utils.js"; import { resolveUserPath } from "../utils.js";
import { resolveLocaleFromEnv, t } from "../i18n/i18n.js";
import { finalizeOnboardingWizard } from "./onboarding.finalize.js"; import { finalizeOnboardingWizard } from "./onboarding.finalize.js";
import { configureGatewayForOnboarding } from "./onboarding.gateway-config.js"; import { configureGatewayForOnboarding } from "./onboarding.gateway-config.js";
import type { QuickstartGatewayDefaults, WizardFlow } from "./onboarding.types.js"; import type { QuickstartGatewayDefaults, WizardFlow } from "./onboarding.types.js";
@ -49,8 +50,9 @@ async function requireRiskAcknowledgement(params: {
}) { }) {
if (params.opts.acceptRisk === true) return; if (params.opts.acceptRisk === true) return;
await params.prompter.note( const locale = resolveLocaleFromEnv();
[
const securityBodyEn = [
"Security warning — please read.", "Security warning — please read.",
"", "",
"Moltbot is a hobby project and still in beta. Expect sharp edges.", "Moltbot is a hobby project and still in beta. Expect sharp edges.",
@ -71,12 +73,38 @@ async function requireRiskAcknowledgement(params: {
"moltbot security audit --fix", "moltbot security audit --fix",
"", "",
"Must read: https://docs.molt.bot/gateway/security", "Must read: https://docs.molt.bot/gateway/security",
].join("\n"), ];
"Security",
const securityBodyZhTw = [
"安全警告 — 請務必先閱讀。",
"",
"Moltbot 是一個興趣專案,目前仍在 beta請預期會有一些粗糙邊角。",
"當你啟用工具tools這個 bot 可能具備讀檔/執行動作的能力。",
"不良提示prompt可能誘導它做出不安全的操作。",
"",
"如果你不熟悉基本資安與存取控制,建議不要直接在生產環境跑 Moltbot。",
"在啟用工具或把它暴露到網路前,請找有經驗的人協助檢視設定。",
"",
"建議底線baseline",
"- Pairing/allowlists + mention gating配對/白名單 + 只回應@提及)",
"- Sandbox + 最小權限工具",
"- 不要把機密放在 agent 可讀到的檔案系統",
"- 有工具或會讀不受信任訊息時,請用你能用到的最強模型",
"",
"建議定期執行:",
"moltbot security audit --deep",
"moltbot security audit --fix",
"",
"必讀https://docs.molt.bot/gateway/security",
];
await params.prompter.note(
(locale === "zh-TW" ? securityBodyZhTw : securityBodyEn).join("\n"),
t(locale, "security.title"),
); );
const ok = await params.prompter.confirm({ const ok = await params.prompter.confirm({
message: "I understand this is powerful and inherently risky. Continue?", message: t(locale, "security.confirm"),
initialValue: false, initialValue: false,
}); });
if (!ok) { if (!ok) {
@ -89,23 +117,27 @@ export async function runOnboardingWizard(
runtime: RuntimeEnv = defaultRuntime, runtime: RuntimeEnv = defaultRuntime,
prompter: WizardPrompter, prompter: WizardPrompter,
) { ) {
const locale = resolveLocaleFromEnv();
printWizardHeader(runtime); printWizardHeader(runtime);
await prompter.intro("Moltbot onboarding"); await prompter.intro(t(locale, "onboard.intro"));
await requireRiskAcknowledgement({ opts, prompter }); await requireRiskAcknowledgement({ opts, prompter });
const snapshot = await readConfigFileSnapshot(); const snapshot = await readConfigFileSnapshot();
let baseConfig: MoltbotConfig = snapshot.valid ? snapshot.config : {}; let baseConfig: MoltbotConfig = snapshot.valid ? snapshot.config : {};
if (snapshot.exists && !snapshot.valid) { if (snapshot.exists && !snapshot.valid) {
await prompter.note(summarizeExistingConfig(baseConfig), "Invalid config"); await prompter.note(summarizeExistingConfig(baseConfig), t(locale, "onboard.configInvalid"));
if (snapshot.issues.length > 0) { if (snapshot.issues.length > 0) {
await prompter.note( await prompter.note(
[ [
...snapshot.issues.map((iss) => `- ${iss.path}: ${iss.message}`), ...snapshot.issues.map((iss) => `- ${iss.path}: ${iss.message}`),
"", "",
"Docs: https://docs.molt.bot/gateway/configuration", locale === "zh-TW"
? "文件https://docs.molt.bot/gateway/configuration"
: "Docs: https://docs.molt.bot/gateway/configuration",
].join("\n"), ].join("\n"),
"Config issues", t(locale, "onboard.configIssues"),
); );
} }
await prompter.outro( await prompter.outro(
@ -135,47 +167,49 @@ export async function runOnboardingWizard(
let flow: WizardFlow = let flow: WizardFlow =
explicitFlow ?? explicitFlow ??
((await prompter.select({ ((await prompter.select({
message: "Onboarding mode", message: t(locale, "onboard.mode"),
options: [ options: [
{ value: "quickstart", label: "QuickStart", hint: quickstartHint }, { value: "quickstart", label: t(locale, "onboard.mode.quickstart"), hint: quickstartHint },
{ value: "advanced", label: "Manual", hint: manualHint }, { value: "advanced", label: t(locale, "onboard.mode.manual"), hint: manualHint },
], ],
initialValue: "quickstart", initialValue: "quickstart",
})) as "quickstart" | "advanced"); })) as "quickstart" | "advanced");
if (opts.mode === "remote" && flow === "quickstart") { if (opts.mode === "remote" && flow === "quickstart") {
await prompter.note( await prompter.note(
"QuickStart only supports local gateways. Switching to Manual mode.", locale === "zh-TW"
"QuickStart", ? "快速開始QuickStart只支援本機 Gateway將切換為手動模式Manual。"
: "QuickStart only supports local gateways. Switching to Manual mode.",
t(locale, "onboard.quickstart.title"),
); );
flow = "advanced"; flow = "advanced";
} }
if (snapshot.exists) { if (snapshot.exists) {
await prompter.note(summarizeExistingConfig(baseConfig), "Existing config detected"); await prompter.note(summarizeExistingConfig(baseConfig), t(locale, "onboard.configExisting"));
const action = (await prompter.select({ const action = (await prompter.select({
message: "Config handling", message: t(locale, "onboard.configHandling"),
options: [ options: [
{ value: "keep", label: "Use existing values" }, { value: "keep", label: t(locale, "onboard.config.keep") },
{ value: "modify", label: "Update values" }, { value: "modify", label: t(locale, "onboard.config.modify") },
{ value: "reset", label: "Reset" }, { value: "reset", label: t(locale, "onboard.config.reset") },
], ],
})) as "keep" | "modify" | "reset"; })) as "keep" | "modify" | "reset";
if (action === "reset") { if (action === "reset") {
const workspaceDefault = baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE; const workspaceDefault = baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE;
const resetScope = (await prompter.select({ const resetScope = (await prompter.select({
message: "Reset scope", message: t(locale, "onboard.resetScope"),
options: [ options: [
{ value: "config", label: "Config only" }, { value: "config", label: t(locale, "onboard.reset.config") },
{ {
value: "config+creds+sessions", value: "config+creds+sessions",
label: "Config + creds + sessions", label: t(locale, "onboard.reset.configCredsSessions"),
}, },
{ {
value: "full", value: "full",
label: "Full reset (config + creds + sessions + workspace)", label: t(locale, "onboard.reset.full"),
}, },
], ],
})) as ResetScope; })) as ResetScope;
@ -254,7 +288,7 @@ export async function runOnboardingWizard(
}; };
const quickstartLines = quickstartGateway.hasExisting const quickstartLines = quickstartGateway.hasExisting
? [ ? [
"Keeping your current gateway settings:", t(locale, "onboard.quickstart.keepExisting"),
`Gateway port: ${quickstartGateway.port}`, `Gateway port: ${quickstartGateway.port}`,
`Gateway bind: ${formatBind(quickstartGateway.bind)}`, `Gateway bind: ${formatBind(quickstartGateway.bind)}`,
...(quickstartGateway.bind === "custom" && quickstartGateway.customBindHost ...(quickstartGateway.bind === "custom" && quickstartGateway.customBindHost
@ -262,16 +296,16 @@ export async function runOnboardingWizard(
: []), : []),
`Gateway auth: ${formatAuth(quickstartGateway.authMode)}`, `Gateway auth: ${formatAuth(quickstartGateway.authMode)}`,
`Tailscale exposure: ${formatTailscale(quickstartGateway.tailscaleMode)}`, `Tailscale exposure: ${formatTailscale(quickstartGateway.tailscaleMode)}`,
"Direct to chat channels.", t(locale, "onboard.quickstart.direct"),
] ]
: [ : [
`Gateway port: ${DEFAULT_GATEWAY_PORT}`, `Gateway port: ${DEFAULT_GATEWAY_PORT}`,
"Gateway bind: Loopback (127.0.0.1)", "Gateway bind: Loopback (127.0.0.1)",
"Gateway auth: Token (default)", "Gateway auth: Token (default)",
"Tailscale exposure: Off", "Tailscale exposure: Off",
"Direct to chat channels.", t(locale, "onboard.quickstart.direct"),
]; ];
await prompter.note(quickstartLines.join("\n"), "QuickStart"); await prompter.note(quickstartLines.join("\n"), t(locale, "onboard.quickstart.title"));
} }
const localPort = resolveGatewayPort(baseConfig); const localPort = resolveGatewayPort(baseConfig);
@ -294,18 +328,18 @@ export async function runOnboardingWizard(
(flow === "quickstart" (flow === "quickstart"
? "local" ? "local"
: ((await prompter.select({ : ((await prompter.select({
message: "What do you want to set up?", message: t(locale, "onboard.whatToSetup"),
options: [ options: [
{ {
value: "local", value: "local",
label: "Local gateway (this machine)", label: t(locale, "onboard.localGateway"),
hint: localProbe.ok hint: localProbe.ok
? `Gateway reachable (${localUrl})` ? `Gateway reachable (${localUrl})`
: `No gateway detected (${localUrl})`, : `No gateway detected (${localUrl})`,
}, },
{ {
value: "remote", value: "remote",
label: "Remote gateway (info-only)", label: t(locale, "onboard.remoteGateway"),
hint: !remoteUrl hint: !remoteUrl
? "No remote URL configured yet" ? "No remote URL configured yet"
: remoteProbe?.ok : remoteProbe?.ok
@ -320,7 +354,7 @@ export async function runOnboardingWizard(
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode }); nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
await writeConfigFile(nextConfig); await writeConfigFile(nextConfig);
logConfigUpdated(runtime); logConfigUpdated(runtime);
await prompter.outro("Remote gateway configured."); await prompter.outro(t(locale, "onboard.remoteConfigured"));
return; return;
} }
@ -329,7 +363,7 @@ export async function runOnboardingWizard(
(flow === "quickstart" (flow === "quickstart"
? (baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE) ? (baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE)
: await prompter.text({ : await prompter.text({
message: "Workspace directory", message: t(locale, "onboard.workspaceDir"),
initialValue: baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE, initialValue: baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE,
})); }));
@ -403,7 +437,7 @@ export async function runOnboardingWizard(
const settings = gateway.settings; const settings = gateway.settings;
if (opts.skipChannels ?? opts.skipProviders) { if (opts.skipChannels ?? opts.skipProviders) {
await prompter.note("Skipping channel setup.", "Channels"); await prompter.note(t(locale, "onboard.skipChannels"), t(locale, "onboard.channelsTitle"));
} else { } else {
const quickstartAllowFromChannels = const quickstartAllowFromChannels =
flow === "quickstart" flow === "quickstart"
@ -427,7 +461,7 @@ export async function runOnboardingWizard(
}); });
if (opts.skipSkills) { if (opts.skipSkills) {
await prompter.note("Skipping skills setup.", "Skills"); await prompter.note(t(locale, "onboard.skipSkills"), t(locale, "onboard.skillsTitle"));
} else { } else {
nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter); nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter);
} }