diff --git a/README.md b/README.md index ec970bb5b..f4e7002f0 100644 --- a/README.md +++ b/README.md @@ -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) +## 繁體中文(zh-TW) + +此 fork 目標是把 **CLI 安裝/初始化流程(`moltbot onboard` / wizard)** 加入繁體中文介面。 + +### 啟用繁中介面 + +macOS / Linux(zsh/bash): + +```bash +export MOLTBOT_LANG=zh-TW +moltbot onboard --install-daemon +``` + +Windows(PowerShell): + +```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)**. Works with npm, pnpm, or bun. New install? Start here: [Getting started](https://docs.molt.bot/start/getting-started) diff --git a/src/commands/onboard.ts b/src/commands/onboard.ts index 48113d5b1..737290b17 100644 --- a/src/commands/onboard.ts +++ b/src/commands/onboard.ts @@ -61,11 +61,13 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = } if (process.platform === "win32") { + const { resolveLocaleFromEnv, t } = await import("../i18n/i18n.js"); + const locale = resolveLocaleFromEnv(); runtime.log( [ - "Windows detected.", - "WSL2 is strongly recommended; native Windows is untested and more problematic.", - "Guide: https://docs.molt.bot/windows", + t(locale, "windows.detected"), + t(locale, "windows.recommend"), + t(locale, "windows.guide"), ].join("\n"), ); } diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts new file mode 100644 index 000000000..1b164cc8e --- /dev/null +++ b/src/i18n/i18n.ts @@ -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> = { + 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; +} diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts index 75543ca19..4d6d970fa 100644 --- a/src/wizard/onboarding.ts +++ b/src/wizard/onboarding.ts @@ -38,6 +38,7 @@ import { logConfigUpdated } from "../config/logging.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { resolveUserPath } from "../utils.js"; +import { resolveLocaleFromEnv, t } from "../i18n/i18n.js"; import { finalizeOnboardingWizard } from "./onboarding.finalize.js"; import { configureGatewayForOnboarding } from "./onboarding.gateway-config.js"; import type { QuickstartGatewayDefaults, WizardFlow } from "./onboarding.types.js"; @@ -49,34 +50,61 @@ async function requireRiskAcknowledgement(params: { }) { if (params.opts.acceptRisk === true) return; + const locale = resolveLocaleFromEnv(); + + const securityBodyEn = [ + "Security warning — please read.", + "", + "Moltbot is a hobby project and still in beta. Expect sharp edges.", + "This bot can read files and run actions if tools are enabled.", + "A bad prompt can trick it into doing unsafe things.", + "", + "If you’re not comfortable with basic security and access control, don’t run Moltbot.", + "Ask someone experienced to help before enabling tools or exposing it to the internet.", + "", + "Recommended baseline:", + "- Pairing/allowlists + mention gating.", + "- Sandbox + least-privilege tools.", + "- Keep secrets out of the agent’s reachable filesystem.", + "- Use the strongest available model for any bot with tools or untrusted inboxes.", + "", + "Run regularly:", + "moltbot security audit --deep", + "moltbot security audit --fix", + "", + "Must read: https://docs.molt.bot/gateway/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( - [ - "Security warning — please read.", - "", - "Moltbot is a hobby project and still in beta. Expect sharp edges.", - "This bot can read files and run actions if tools are enabled.", - "A bad prompt can trick it into doing unsafe things.", - "", - "If you’re not comfortable with basic security and access control, don’t run Moltbot.", - "Ask someone experienced to help before enabling tools or exposing it to the internet.", - "", - "Recommended baseline:", - "- Pairing/allowlists + mention gating.", - "- Sandbox + least-privilege tools.", - "- Keep secrets out of the agent’s reachable filesystem.", - "- Use the strongest available model for any bot with tools or untrusted inboxes.", - "", - "Run regularly:", - "moltbot security audit --deep", - "moltbot security audit --fix", - "", - "Must read: https://docs.molt.bot/gateway/security", - ].join("\n"), - "Security", + (locale === "zh-TW" ? securityBodyZhTw : securityBodyEn).join("\n"), + t(locale, "security.title"), ); const ok = await params.prompter.confirm({ - message: "I understand this is powerful and inherently risky. Continue?", + message: t(locale, "security.confirm"), initialValue: false, }); if (!ok) { @@ -89,23 +117,27 @@ export async function runOnboardingWizard( runtime: RuntimeEnv = defaultRuntime, prompter: WizardPrompter, ) { + const locale = resolveLocaleFromEnv(); + printWizardHeader(runtime); - await prompter.intro("Moltbot onboarding"); + await prompter.intro(t(locale, "onboard.intro")); await requireRiskAcknowledgement({ opts, prompter }); const snapshot = await readConfigFileSnapshot(); let baseConfig: MoltbotConfig = snapshot.valid ? snapshot.config : {}; 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) { await prompter.note( [ ...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"), - "Config issues", + t(locale, "onboard.configIssues"), ); } await prompter.outro( @@ -135,47 +167,49 @@ export async function runOnboardingWizard( let flow: WizardFlow = explicitFlow ?? ((await prompter.select({ - message: "Onboarding mode", + message: t(locale, "onboard.mode"), options: [ - { value: "quickstart", label: "QuickStart", hint: quickstartHint }, - { value: "advanced", label: "Manual", hint: manualHint }, + { value: "quickstart", label: t(locale, "onboard.mode.quickstart"), hint: quickstartHint }, + { value: "advanced", label: t(locale, "onboard.mode.manual"), hint: manualHint }, ], initialValue: "quickstart", })) as "quickstart" | "advanced"); if (opts.mode === "remote" && flow === "quickstart") { await prompter.note( - "QuickStart only supports local gateways. Switching to Manual mode.", - "QuickStart", + locale === "zh-TW" + ? "快速開始(QuickStart)只支援本機 Gateway,將切換為手動模式(Manual)。" + : "QuickStart only supports local gateways. Switching to Manual mode.", + t(locale, "onboard.quickstart.title"), ); flow = "advanced"; } 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({ - message: "Config handling", + message: t(locale, "onboard.configHandling"), options: [ - { value: "keep", label: "Use existing values" }, - { value: "modify", label: "Update values" }, - { value: "reset", label: "Reset" }, + { value: "keep", label: t(locale, "onboard.config.keep") }, + { value: "modify", label: t(locale, "onboard.config.modify") }, + { value: "reset", label: t(locale, "onboard.config.reset") }, ], })) as "keep" | "modify" | "reset"; if (action === "reset") { const workspaceDefault = baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE; const resetScope = (await prompter.select({ - message: "Reset scope", + message: t(locale, "onboard.resetScope"), options: [ - { value: "config", label: "Config only" }, + { value: "config", label: t(locale, "onboard.reset.config") }, { value: "config+creds+sessions", - label: "Config + creds + sessions", + label: t(locale, "onboard.reset.configCredsSessions"), }, { value: "full", - label: "Full reset (config + creds + sessions + workspace)", + label: t(locale, "onboard.reset.full"), }, ], })) as ResetScope; @@ -254,7 +288,7 @@ export async function runOnboardingWizard( }; const quickstartLines = quickstartGateway.hasExisting ? [ - "Keeping your current gateway settings:", + t(locale, "onboard.quickstart.keepExisting"), `Gateway port: ${quickstartGateway.port}`, `Gateway bind: ${formatBind(quickstartGateway.bind)}`, ...(quickstartGateway.bind === "custom" && quickstartGateway.customBindHost @@ -262,16 +296,16 @@ export async function runOnboardingWizard( : []), `Gateway auth: ${formatAuth(quickstartGateway.authMode)}`, `Tailscale exposure: ${formatTailscale(quickstartGateway.tailscaleMode)}`, - "Direct to chat channels.", + t(locale, "onboard.quickstart.direct"), ] : [ `Gateway port: ${DEFAULT_GATEWAY_PORT}`, "Gateway bind: Loopback (127.0.0.1)", "Gateway auth: Token (default)", "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); @@ -294,18 +328,18 @@ export async function runOnboardingWizard( (flow === "quickstart" ? "local" : ((await prompter.select({ - message: "What do you want to set up?", + message: t(locale, "onboard.whatToSetup"), options: [ { value: "local", - label: "Local gateway (this machine)", + label: t(locale, "onboard.localGateway"), hint: localProbe.ok ? `Gateway reachable (${localUrl})` : `No gateway detected (${localUrl})`, }, { value: "remote", - label: "Remote gateway (info-only)", + label: t(locale, "onboard.remoteGateway"), hint: !remoteUrl ? "No remote URL configured yet" : remoteProbe?.ok @@ -320,7 +354,7 @@ export async function runOnboardingWizard( nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode }); await writeConfigFile(nextConfig); logConfigUpdated(runtime); - await prompter.outro("Remote gateway configured."); + await prompter.outro(t(locale, "onboard.remoteConfigured")); return; } @@ -329,7 +363,7 @@ export async function runOnboardingWizard( (flow === "quickstart" ? (baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE) : await prompter.text({ - message: "Workspace directory", + message: t(locale, "onboard.workspaceDir"), initialValue: baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE, })); @@ -403,7 +437,7 @@ export async function runOnboardingWizard( const settings = gateway.settings; if (opts.skipChannels ?? opts.skipProviders) { - await prompter.note("Skipping channel setup.", "Channels"); + await prompter.note(t(locale, "onboard.skipChannels"), t(locale, "onboard.channelsTitle")); } else { const quickstartAllowFromChannels = flow === "quickstart" @@ -427,7 +461,7 @@ export async function runOnboardingWizard( }); if (opts.skipSkills) { - await prompter.note("Skipping skills setup.", "Skills"); + await prompter.note(t(locale, "onboard.skipSkills"), t(locale, "onboard.skillsTitle")); } else { nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter); }