diff --git a/README.md b/README.md
index 49085c76f..6e7b4027c 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# 🦞 OpenClaw — Personal AI Assistant
+English | [简体中文](README.zh-CN.md)
+
diff --git a/README.zh-CN.md b/README.zh-CN.md
new file mode 100644
index 000000000..4d2eb2c97
--- /dev/null
+++ b/README.zh-CN.md
@@ -0,0 +1,171 @@
+# 🦞 OpenClaw CN — 个人 AI 助手 (中文增强版)
+
+
+
+
+
+
+
+
+**OpenClaw CN** 是 OpenClaw 的中文增强版本,为中国用户提供更好的使用体验,并集成了国内主流的通信渠道和大模型提供商。
+
+## 🎯 特性增强
+
+### 🇨🇳 全面中文化
+- ✅ **Web UI 汉化**:完整的中文用户界面
+- ✅ **中文文档**:安装指南、配置说明和使用教程
+- ✅ **本地化体验**:针对中文用户优化的交互设计
+
+### 🤖 新增大模型支持
+- ✅ **DeepSeek**:完整支持 DeepSeek API
+ - `deepseek-chat` (V3.2 非思考模式)
+ - `deepseek-reasoner` (V3.2 思考模式)
+ - 完全兼容 OpenAI API 格式
+ - 极具竞争力的价格
+
+### 💬 新增通信渠道
+
+#### 已实现
+- 🔵 **企业渠道**(基于官方 API)
+ - 钉钉 (DingTalk)
+ - 飞书 (Feishu/Lark)
+ - 企业微信 (WeChat Work)
+
+#### 计划中
+- 🟡 **个人渠道**(基于 OneBot 协议)
+ - 微信(通过 OneBot 适配器如 Gewechat)
+ - QQ(通过 OneBot 适配器如 NapCatQQ)
+ - 华为畅连(开发中)
+
+> **注意**:个人微信和 QQ 需要运行独立的 OneBot 客户端。推荐方案:
+> - **QQ**: [NapCatQQ](https://github.com/NapNeko/NapCatQQ), [Lagrange](https://github.com/LagrangeDev/Lagrange.Core)
+> - **微信**: [Gewechat](https://github.com/Devo919/Gewechat)
+
+## 核心功能 (继承自 OpenClaw)
+
+- **多渠道消息支持**:连接 WhatsApp、Telegram、Slack、Discord、Google Chat、Signal、iMessage、WebChat 等
+- **本地优先网关**:统一的控制平面,管理会话、渠道、工具和事件
+- **语音唤醒与对话**:支持 macOS/iOS/Android 上的语音交互
+- **实时画布**:代理驱动的可视化工作空间
+- **浏览器控制**:通过 Chrome/Chromium 实现网页自动化
+- **技能平台**:可扩展的技能系统,支持自定义工具
+
+[完整功能列表](https://docs.openclaw.ai)
+
+## 快速开始
+
+### 环境要求
+- **Node.js** ≥ 22
+- **操作系统**: macOS, Linux, Windows (推荐 WSL2)
+
+### 安装
+
+```bash
+npm install -g openclaw@latest
+
+# 运行安装向导(推荐)
+openclaw onboard --install-daemon
+```
+
+### DeepSeek 配置示例
+
+在 `~/.openclaw/openclaw.json` 中添加:
+
+```json
+{
+ "models": {
+ "providers": {
+ "deepseek": {
+ "baseUrl": "https://api.deepseek.com/v1",
+ "apiKey": "${DEEPSEEK_API_KEY}",
+ "api": "openai-responses",
+ "models": [
+ {
+ "id": "deepseek-chat",
+ "name": "DeepSeek Chat",
+ "contextWindow": 64000,
+ "maxTokens": 8000
+ },
+ {
+ "id": "deepseek-reasoner",
+ "name": "DeepSeek Reasoner (思考模式)",
+ "reasoning": true,
+ "contextWindow": 64000,
+ "maxTokens": 8000
+ }
+ ]
+ }
+ }
+ },
+ "agent": {
+ "model": "deepseek/deepseek-chat"
+ }
+}
+```
+
+或使用环境变量:
+
+```bash
+export DEEPSEEK_API_KEY=sk-xxxxxxxxx
+```
+
+### 启动网关
+
+```bash
+# 启动网关
+openclaw gateway --port 18789 --verbose
+
+# 发送消息
+openclaw agent --message "你好,请介绍一下自己" --model deepseek/deepseek-chat
+```
+
+## 文档
+
+- 📘 [DeepSeek 配置指南](docs/zh-CN/deepseek-guide.md)
+- 📘 [钉钉配置指南](docs/zh-CN/dingtalk-guide.md) *(即将推出)*
+- 📘 [飞书配置指南](docs/zh-CN/feishu-guide.md) *(即将推出)*
+- 📘 [官方文档](https://docs.openclaw.ai) (英文)
+
+## 价格优势
+
+DeepSeekV3.2 定价(/百万 tokens):
+
+| 模型 | 输入 | 输出 | 缓存读 | 缓存写 |
+|------|------|------|--------|--------|
+| deepseek-chat | $0.14 | $0.28 | $0.014 | $0.14 |
+| deepseek-reasoner | $0.55 | $2.19 | - | - |
+
+**相比 Claude 和 GPT 系列,DeepSeek 价格仅为其几分之一,同时性能优异!**
+
+## 安全性
+
+- **默认安全**:DM 配对机制,未知发送者需要批准
+- **沙箱模式**:支持 Docker 沙箱运行非主会话
+- **权限控制**:基于白名单的渠道访问控制
+
+详见 [安全指南](https://docs.openclaw.ai/gateway/security)
+
+## 贡献
+
+欢迎提交 Pull Request!查看 [CONTRIBUTING.md](CONTRIBUTING.md) 了解贡献指南。
+
+## 开源协议
+
+MIT License - 详见 [LICENSE](LICENSE)
+
+## 致谢
+
+- 感谢 [OpenClaw 官方团队](https://github.com/openclaw/openclaw)
+- 感谢 Peter Steinberger 和社区贡献者
+- 特别感谢 DeepSeek 提供优秀的开源大模型
+
+## 链接
+
+- [OpenClaw 官网](https://openclaw.ai)
+- [官方文档](https://docs.openclaw.ai)
+- [Discord 社区](https://discord.gg/clawd)
+- [GitHub 仓库](https://github.com/openclaw/openclaw)
+
+---
+
+**⚠️ 注意**: 本项目是 OpenClaw 的社区增强版本,主要面向中文用户。如需使用原版 OpenClaw,请访问 [官方仓库](https://github.com/openclaw/openclaw)。
diff --git a/docs/examples/deepseek.json b/docs/examples/deepseek.json
new file mode 100644
index 000000000..140fa5086
--- /dev/null
+++ b/docs/examples/deepseek.json
@@ -0,0 +1,43 @@
+{
+ "models": {
+ "providers": {
+ "deepseek": {
+ "baseUrl": "https://api.deepseek.com/v1",
+ "apiKey": "${DEEPSEEK_API_KEY}",
+ "api": "openai-responses",
+ "models": [
+ {
+ "id": "deepseek-chat",
+ "name": "Deep Seek Chat (V3.2 非思考模式)",
+ "contextWindow": 64000,
+ "maxTokens": 8000,
+ "cost": {
+ "input": 0.14,
+ "output": 0.28,
+ "cacheRead": 0.014,
+ "cacheWrite": 0.14
+ },
+ "compat": {
+ "supportsStore": false
+ }
+ },
+ {
+ "id": "deepseek-reasoner",
+ "name": "DeepSeek Reasoner (V3.2 思考模式)",
+ "reasoning": true,
+ "contextWindow": 64000,
+ "maxTokens": 8000,
+ "cost": {
+ "input": 0.55,
+ "output": 2.19
+ },
+ "compat": {
+ "supportsStore": false,
+ "supportsReasoningEffort": true
+ }
+ }
+ ]
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/zh-CN/deepseek-guide.md b/docs/zh-CN/deepseek-guide.md
new file mode 100644
index 000000000..dfd9c4c3b
--- /dev/null
+++ b/docs/zh-CN/deepseek-guide.md
@@ -0,0 +1,91 @@
+# DeepSeek 大模型配置指南
+
+本文档说明如何在 OpenClaw 中配置 DeepSeek API。
+
+## 获取 API 密钥
+1. 访问 [DeepSeek 开放平台](https://platform.deepseek.com/)
+2. 注册并登录您的账户
+3. 在 API Keys 页面创建新的 API 密钥
+
+## 配置步骤
+
+### 方式一:环境变量
+在您的环境中设置 `DEEPSEEK_API_KEY`:
+
+```bash
+export DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxx
+```
+
+### 方式二:配置文件
+在 `~/.openclaw/openclaw.json` 中添加以下配置:
+
+```json
+{
+ "models": {
+ "providers": {
+ "deepseek": {
+ "baseUrl": "https://api.deepseek.com/v1",
+ "apiKey": "sk-xxxxxxxxxxxxxxxxxx",
+ "api": "openai-responses",
+ "models": [
+ {
+ "id": "deepseek-chat",
+ "name": "DeepSeek Chat (V3.2)",
+ "contextWindow": 64000,
+ "maxTokens": 8000
+ },
+ {
+ "id": "deepseek-reasoner",
+ "name": "DeepSeek Reasoner (V3.2 思考模式)",
+ "reasoning": true,
+ "contextWindow": 64000,
+ "maxTokens": 8000
+ }
+ ]
+ }
+ }
+ }
+}
+```
+
+## 使用模型
+
+配置完成后,您可以在 OpenClaw 中使用以下模型标识符:
+
+- `deepseek/deepseek-chat` - 标准对话模型(无思考模式)
+- `deepseek/deepseek-reasoner` - 推理模型(启用思考模式)
+
+示例:
+
+```bash
+openclaw chat --model deepseek/deepseek-chat "你好,请介绍一下自己"
+```
+
+## 定价说明
+
+DeepSeek V3.2 定价(以每百万 tokens 计):
+
+**deepseek-chat** (非思考模式):
+- 输入:$0.14/M tokens
+- 输出:$0.28/M tokens
+- 缓存读取:$0.014/M tokens
+- 缓存写入:$0.14/M tokens
+
+**deepseek-reasoner** (思考模式):
+- 输入:$0.55/M tokens
+- 输出:$2.19/M tokens
+
+## 注意事项
+
+1. DeepSeek API 完全兼容 OpenAI API 格式
+2. 推荐使用 `deepseek-chat` 进行日常对话
+3. 对于复杂推理任务,使用 `deepseek-reasoner`
+
+## 故障排查
+
+如果遇到问题,请检查:
+1. API 密钥是否正确设置
+2. 网络是否能访问 `api.deepseek.com`
+3. 配置文件 JSON 格式是否正确
+
+更多信息请访问 [DeepSeek API 文档](https://platform.deepseek.com/docs)。
diff --git a/src/agents/auth-health.ts b/src/agents/auth-health.ts
index 4f281b7bb..29bcabffc 100644
--- a/src/agents/auth-health.ts
+++ b/src/agents/auth-health.ts
@@ -44,14 +44,14 @@ export function resolveAuthProfileSource(_profileId: string): AuthProfileSource
}
export function formatRemainingShort(remainingMs?: number): string {
- if (remainingMs === undefined || Number.isNaN(remainingMs)) return "unknown";
- if (remainingMs <= 0) return "0m";
+ if (remainingMs === undefined || Number.isNaN(remainingMs)) return "未知";
+ if (remainingMs <= 0) return "0分";
const minutes = Math.max(1, Math.round(remainingMs / 60_000));
- if (minutes < 60) return `${minutes}m`;
+ if (minutes < 60) return `${minutes}分`;
const hours = Math.round(minutes / 60);
- if (hours < 48) return `${hours}h`;
+ if (hours < 48) return `${hours}小时`;
const days = Math.round(hours / 24);
- return `${days}d`;
+ return `${days}天`;
}
function resolveOAuthStatus(
diff --git a/src/commands/configure.daemon.ts b/src/commands/configure.daemon.ts
index c0431c9f1..84ff08f27 100644
--- a/src/commands/configure.daemon.ts
+++ b/src/commands/configure.daemon.ts
@@ -12,6 +12,7 @@ import {
import { guardCancel } from "./onboard-helpers.js";
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
import { loadConfig } from "../config/config.js";
+import { t } from "../wizard/i18n.js";
export async function maybeInstallDaemon(params: {
runtime: RuntimeEnv;
@@ -27,11 +28,11 @@ export async function maybeInstallDaemon(params: {
if (loaded) {
const action = guardCancel(
await select({
- message: "Gateway service already installed",
+ message: t("configure.daemon.alreadyInstalled"),
options: [
- { value: "restart", label: "Restart" },
- { value: "reinstall", label: "Reinstall" },
- { value: "skip", label: "Skip" },
+ { value: "restart", label: t("configure.daemon.restart") },
+ { value: "reinstall", label: t("configure.daemon.reinstall") },
+ { value: "skip", label: t("configure.daemon.skip") },
],
}),
params.runtime,
@@ -40,12 +41,12 @@ export async function maybeInstallDaemon(params: {
await withProgress(
{ label: "Gateway service", indeterminate: true, delayMs: 0 },
async (progress) => {
- progress.setLabel("Restarting Gateway service…");
+ progress.setLabel(t("configure.daemon.restarting"));
await service.restart({
env: process.env,
stdout: process.stdout,
});
- progress.setLabel("Gateway service restarted.");
+ progress.setLabel(t("configure.daemon.restarted"));
},
);
shouldCheckLinger = true;
@@ -56,9 +57,9 @@ export async function maybeInstallDaemon(params: {
await withProgress(
{ label: "Gateway service", indeterminate: true, delayMs: 0 },
async (progress) => {
- progress.setLabel("Uninstalling Gateway service…");
+ progress.setLabel(t("configure.daemon.uninstalling"));
await service.uninstall({ env: process.env, stdout: process.stdout });
- progress.setLabel("Gateway service uninstalled.");
+ progress.setLabel(t("configure.daemon.uninstalled"));
},
);
}
@@ -72,7 +73,7 @@ export async function maybeInstallDaemon(params: {
} else {
daemonRuntime = guardCancel(
await select({
- message: "Gateway service runtime",
+ message: t("configure.daemon.selectRuntime"),
options: GATEWAY_DAEMON_RUNTIME_OPTIONS,
initialValue: DEFAULT_GATEWAY_DAEMON_RUNTIME,
}),
@@ -83,7 +84,7 @@ export async function maybeInstallDaemon(params: {
await withProgress(
{ label: "Gateway service", indeterminate: true, delayMs: 0 },
async (progress) => {
- progress.setLabel("Preparing Gateway service…");
+ progress.setLabel(t("configure.daemon.preparing"));
const cfg = loadConfig();
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
@@ -95,7 +96,7 @@ export async function maybeInstallDaemon(params: {
config: cfg,
});
- progress.setLabel("Installing Gateway service…");
+ progress.setLabel(t("configure.daemon.installing"));
try {
await service.install({
env: process.env,
@@ -104,15 +105,15 @@ export async function maybeInstallDaemon(params: {
workingDirectory,
environment,
});
- progress.setLabel("Gateway service installed.");
+ progress.setLabel(t("configure.daemon.installed"));
} catch (err) {
installError = err instanceof Error ? err.message : String(err);
- progress.setLabel("Gateway service install failed.");
+ progress.setLabel(t("configure.daemon.installFailed"));
}
},
);
if (installError) {
- note("Gateway service install failed: " + installError, "Gateway");
+ note(t("configure.daemon.installFailedNote") + installError, "Gateway");
note(gatewayInstallErrorHint(), "Gateway");
return;
}
@@ -127,7 +128,7 @@ export async function maybeInstallDaemon(params: {
note,
},
reason:
- "Linux installs use a systemd user service. Without lingering, systemd stops the user session on logout/idle and kills the Gateway.",
+ t("configure.daemon.lingerReason"),
requireConfirm: true,
});
}
diff --git a/src/commands/configure.gateway-auth.ts b/src/commands/configure.gateway-auth.ts
index 508ed6cf0..88b4c04bf 100644
--- a/src/commands/configure.gateway-auth.ts
+++ b/src/commands/configure.gateway-auth.ts
@@ -11,6 +11,7 @@ import {
promptDefaultModel,
promptModelAllowlist,
} from "./model-picker.js";
+import { t } from "../wizard/i18n.js";
type GatewayAuthChoice = "token" | "password";
@@ -80,7 +81,7 @@ export async function promptAuthConfig(
prompter,
allowedKeys: anthropicOAuth ? ANTHROPIC_OAUTH_MODEL_KEYS : undefined,
initialSelections: anthropicOAuth ? ["anthropic/claude-opus-4-5"] : undefined,
- message: anthropicOAuth ? "Anthropic OAuth models" : undefined,
+ message: anthropicOAuth ? t("configure.auth.anthropicOAuthModels") : undefined,
});
if (allowlistSelection.models) {
next = applyModelAllowlist(next, allowlistSelection.models);
diff --git a/src/commands/configure.gateway.ts b/src/commands/configure.gateway.ts
index 9b66a7b34..8b209b49b 100644
--- a/src/commands/configure.gateway.ts
+++ b/src/commands/configure.gateway.ts
@@ -4,6 +4,7 @@ import { findTailscaleBinary } from "../infra/tailscale.js";
import type { RuntimeEnv } from "../runtime.js";
import { note } from "../terminal/note.js";
import { buildGatewayAuthConfig } from "./configure.gateway-auth.js";
+import { t } from "../wizard/i18n.js";
import { confirm, select, text } from "./configure.shared.js";
import { guardCancel, randomToken } from "./onboard-helpers.js";
@@ -19,9 +20,9 @@ export async function promptGatewayConfig(
}> {
const portRaw = guardCancel(
await text({
- message: "Gateway port",
+ message: t("onboarding.gatewayConfig.port"),
initialValue: String(resolveGatewayPort(cfg)),
- validate: (value) => (Number.isFinite(Number(value)) ? undefined : "Invalid port"),
+ validate: (value) => (Number.isFinite(Number(value)) ? undefined : t("onboarding.gatewayConfig.invalidPort")),
}),
runtime,
);
@@ -29,31 +30,31 @@ export async function promptGatewayConfig(
let bind = guardCancel(
await select({
- message: "Gateway bind mode",
+ message: t("onboarding.gatewayConfig.bind"),
options: [
{
value: "loopback",
- label: "Loopback (Local only)",
- hint: "Bind to 127.0.0.1 - secure, local-only access",
+ label: t("onboarding.gateway.bindLoopback"),
+ hint: t("onboarding.gatewayConfig.bindLoopbackHint") || "Bind to 127.0.0.1 - secure, local-only access",
},
{
value: "tailnet",
- label: "Tailnet (Tailscale IP)",
- hint: "Bind to your Tailscale IP only (100.x.x.x)",
+ label: t("onboarding.gateway.bindTailnet"),
+ hint: t("onboarding.gatewayConfig.bindTailnetHint") || "Bind to your Tailscale IP only (100.x.x.x)",
},
{
value: "auto",
- label: "Auto (Loopback → LAN)",
+ label: t("onboarding.gateway.bindAuto"),
hint: "Prefer loopback; fall back to all interfaces if unavailable",
},
{
value: "lan",
- label: "LAN (All interfaces)",
- hint: "Bind to 0.0.0.0 - accessible from anywhere on your network",
+ label: t("onboarding.gateway.bindLan"),
+ hint: t("onboarding.gatewayConfig.bindLanHint") || "Bind to 0.0.0.0 - accessible from anywhere on your network",
},
{
value: "custom",
- label: "Custom IP",
+ label: t("onboarding.gateway.bindCustom"),
hint: "Specify a specific IP address, with 0.0.0.0 fallback if unavailable",
},
],
@@ -65,13 +66,13 @@ export async function promptGatewayConfig(
if (bind === "custom") {
const input = guardCancel(
await text({
- message: "Custom IP address",
+ message: t("onboarding.gatewayConfig.customIp"),
placeholder: "192.168.1.100",
validate: (value) => {
- if (!value) return "IP address is required for custom bind mode";
+ if (!value) return t("onboarding.gatewayConfig.customIpRequired");
const trimmed = value.trim();
const parts = trimmed.split(".");
- if (parts.length !== 4) return "Invalid IPv4 address (e.g., 192.168.1.100)";
+ if (parts.length !== 4) return t("onboarding.gatewayConfig.invalidIp") + " (e.g., 192.168.1.100)";
if (
parts.every((part) => {
const n = parseInt(part, 10);
@@ -79,7 +80,7 @@ export async function promptGatewayConfig(
})
)
return undefined;
- return "Invalid IPv4 address (each octet must be 0-255)";
+ return t("onboarding.gatewayConfig.invalidIpOctet");
},
}),
runtime,
@@ -89,10 +90,10 @@ export async function promptGatewayConfig(
let authMode = guardCancel(
await select({
- message: "Gateway auth",
+ message: t("onboarding.gatewayConfig.auth"),
options: [
- { value: "token", label: "Token", hint: "Recommended default" },
- { value: "password", label: "Password" },
+ { value: "token", label: t("onboarding.gatewayConfig.authToken"), hint: t("onboarding.gatewayConfig.authTokenHint") },
+ { value: "password", label: t("onboarding.gatewayConfig.authPassword") },
],
initialValue: "token",
}),
@@ -101,18 +102,18 @@ export async function promptGatewayConfig(
const tailscaleMode = guardCancel(
await select({
- message: "Tailscale exposure",
+ message: t("onboarding.gatewayConfig.tsExposure"),
options: [
- { value: "off", label: "Off", hint: "No Tailscale exposure" },
+ { value: "off", label: t("onboarding.gatewayConfig.tsOff"), hint: t("onboarding.gatewayConfig.tsOffHint") },
{
value: "serve",
- label: "Serve",
- hint: "Private HTTPS for your tailnet (devices on Tailscale)",
+ label: t("onboarding.gatewayConfig.tsServe"),
+ hint: t("onboarding.gatewayConfig.tsServeHint"),
},
{
value: "funnel",
- label: "Funnel",
- hint: "Public HTTPS via Tailscale Funnel (internet)",
+ label: t("onboarding.gatewayConfig.tsFunnel"),
+ hint: t("onboarding.gatewayConfig.tsFunnelHint"),
},
],
}),
@@ -124,14 +125,8 @@ export async function promptGatewayConfig(
const tailscaleBin = await findTailscaleBinary();
if (!tailscaleBin) {
note(
- [
- "Tailscale binary not found in PATH or /Applications.",
- "Ensure Tailscale is installed from:",
- " https://tailscale.com/download/mac",
- "",
- "You can continue setup, but serve/funnel will fail at runtime.",
- ].join("\n"),
- "Tailscale Warning",
+ t("onboarding.gatewayConfig.tsNotFound"),
+ t("onboarding.gatewayConfig.tsWarningTitle"),
);
}
}
@@ -147,7 +142,7 @@ export async function promptGatewayConfig(
tailscaleResetOnExit = Boolean(
guardCancel(
await confirm({
- message: "Reset Tailscale serve/funnel on exit?",
+ message: t("onboarding.gatewayConfig.tsResetConfirm"),
initialValue: false,
}),
runtime,
@@ -156,12 +151,12 @@ export async function promptGatewayConfig(
}
if (tailscaleMode !== "off" && bind !== "loopback") {
- note("Tailscale requires bind=loopback. Adjusting bind to loopback.", "Note");
+ note(t("onboarding.gatewayConfig.tsAdjustBind"), "Note");
bind = "loopback";
}
if (tailscaleMode === "funnel" && authMode !== "password") {
- note("Tailscale funnel requires password auth.", "Note");
+ note(t("onboarding.gatewayConfig.tsFunnelAuth"), "Note");
authMode = "password";
}
@@ -172,7 +167,7 @@ export async function promptGatewayConfig(
if (authMode === "token") {
const tokenInput = guardCancel(
await text({
- message: "Gateway token (blank to generate)",
+ message: t("onboarding.gatewayConfig.tokenPlaceholder"),
initialValue: randomToken(),
}),
runtime,
@@ -183,8 +178,8 @@ export async function promptGatewayConfig(
if (authMode === "password") {
const password = guardCancel(
await text({
- message: "Gateway password",
- validate: (value) => (value?.trim() ? undefined : "Required"),
+ message: t("onboarding.gatewayConfig.passwordLabel"),
+ validate: (value) => (value?.trim() ? undefined : t("onboarding.gatewayConfig.passwordRequired")),
}),
runtime,
);
diff --git a/src/commands/configure.shared.ts b/src/commands/configure.shared.ts
index bc89529d8..35002a745 100644
--- a/src/commands/configure.shared.ts
+++ b/src/commands/configure.shared.ts
@@ -7,6 +7,7 @@ import {
} from "@clack/prompts";
import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js";
+import { t } from "../wizard/i18n.js";
export const CONFIGURE_WIZARD_SECTIONS = [
"workspace",
@@ -33,27 +34,27 @@ export const CONFIGURE_SECTION_OPTIONS: Array<{
label: string;
hint: string;
}> = [
- { value: "workspace", label: "Workspace", hint: "Set workspace + sessions" },
- { value: "model", label: "Model", hint: "Pick provider + credentials" },
- { value: "web", label: "Web tools", hint: "Configure Brave search + fetch" },
- { value: "gateway", label: "Gateway", hint: "Port, bind, auth, tailscale" },
- {
- value: "daemon",
- label: "Daemon",
- hint: "Install/manage the background service",
- },
- {
- value: "channels",
- label: "Channels",
- hint: "Link WhatsApp/Telegram/etc and defaults",
- },
- { value: "skills", label: "Skills", hint: "Install/enable workspace skills" },
- {
- value: "health",
- label: "Health check",
- hint: "Run gateway + channel checks",
- },
-];
+ { value: "workspace", label: t("configure.sections.workspace"), hint: t("configure.sections.workspaceHint") },
+ { value: "model", label: t("configure.sections.model"), hint: t("configure.sections.modelHint") },
+ { value: "web", label: t("configure.sections.web"), hint: t("configure.sections.webHint") },
+ { value: "gateway", label: t("configure.sections.gateway"), hint: t("configure.sections.gatewayHint") },
+ {
+ value: "daemon",
+ label: t("configure.sections.daemon"),
+ hint: t("configure.sections.daemonHint"),
+ },
+ {
+ value: "channels",
+ label: t("configure.sections.channels"),
+ hint: t("configure.sections.channelsHint"),
+ },
+ { value: "skills", label: t("configure.sections.skills"), hint: t("configure.sections.skillsHint") },
+ {
+ value: "health",
+ label: t("configure.sections.health"),
+ hint: t("configure.sections.healthHint"),
+ },
+ ];
export const intro = (message: string) => clackIntro(stylePromptTitle(message) ?? message);
export const outro = (message: string) => clackOutro(stylePromptTitle(message) ?? message);
diff --git a/src/commands/configure.wizard.ts b/src/commands/configure.wizard.ts
index 505fb7760..23159bc22 100644
--- a/src/commands/configure.wizard.ts
+++ b/src/commands/configure.wizard.ts
@@ -9,6 +9,7 @@ import { note } from "../terminal/note.js";
import { resolveUserPath } from "../utils.js";
import { createClackPrompter } from "../wizard/clack-prompter.js";
import { WizardCancelledError } from "../wizard/prompts.js";
+import { t } from "../wizard/i18n.js";
import { removeChannelConfigWizard } from "./configure.channels.js";
import { maybeInstallDaemon } from "./configure.daemon.js";
import { promptGatewayConfig } from "./configure.gateway.js";
@@ -51,13 +52,13 @@ async function promptConfigureSection(
): Promise {
return guardCancel(
await select({
- message: "Select sections to configure",
+ message: t("configure.sections.title"),
options: [
...CONFIGURE_SECTION_OPTIONS,
{
value: "__continue",
- label: "Continue",
- hint: hasSelection ? "Done" : "Skip for now",
+ label: t("configure.sections.continue"),
+ hint: hasSelection ? t("configure.sections.continueHint") : t("configure.sections.skipHint"),
},
],
initialValue: CONFIGURE_SECTION_OPTIONS[0]?.value,
@@ -69,17 +70,17 @@ async function promptConfigureSection(
async function promptChannelMode(runtime: RuntimeEnv): Promise {
return guardCancel(
await select({
- message: "Channels",
+ message: t("configure.sections.channels"),
options: [
{
value: "configure",
- label: "Configure/link",
- hint: "Add/update channels; disable unselected accounts",
+ label: t("configure.channels.configure"),
+ hint: t("configure.channels.configureHint"),
},
{
value: "remove",
- label: "Remove channel config",
- hint: "Delete channel tokens/settings from openclaw.json",
+ label: t("configure.channels.remove"),
+ hint: t("configure.channels.removeHint"),
},
],
initialValue: "configure",
@@ -107,7 +108,7 @@ async function promptWebToolsConfig(
const enableSearch = guardCancel(
await confirm({
- message: "Enable web_search (Brave Search)?",
+ message: t("configure.web.enableSearch"),
initialValue: existingSearch?.enabled ?? hasSearchKey,
}),
runtime,
@@ -122,9 +123,9 @@ async function promptWebToolsConfig(
const keyInput = guardCancel(
await text({
message: hasSearchKey
- ? "Brave Search API key (leave blank to keep current or use BRAVE_API_KEY)"
- : "Brave Search API key (paste it here; leave blank to use BRAVE_API_KEY)",
- placeholder: hasSearchKey ? "Leave blank to keep current" : "BSA...",
+ ? t("configure.web.keyPrompt")
+ : t("configure.web.keyPromptEmpty"),
+ placeholder: hasSearchKey ? t("configure.web.placeholderKey") : t("configure.web.placeholderKeyEmpty"),
}),
runtime,
);
@@ -133,19 +134,15 @@ async function promptWebToolsConfig(
nextSearch = { ...nextSearch, apiKey: key };
} else if (!hasSearchKey) {
note(
- [
- "No key stored yet, so web_search will stay unavailable.",
- "Store a key here or set BRAVE_API_KEY in the Gateway environment.",
- "Docs: https://docs.openclaw.ai/tools/web",
- ].join("\n"),
- "Web search",
+ t("configure.web.noKeyWarning"),
+ t("configure.sections.web"),
);
}
}
const enableFetch = guardCancel(
await confirm({
- message: "Enable web_fetch (keyless HTTP fetch)?",
+ message: t("configure.web.enableFetch"),
initialValue: existingFetch?.enabled ?? true,
}),
runtime,
@@ -175,7 +172,7 @@ export async function runConfigureWizard(
) {
try {
printWizardHeader(runtime);
- intro(opts.command === "update" ? "OpenClaw update wizard" : "OpenClaw configure");
+ intro(opts.command === "update" ? t("configure.updateTitle") : t("configure.title"));
const prompter = createClackPrompter();
const snapshot = await readConfigFileSnapshot();
@@ -212,30 +209,30 @@ export async function runConfigureWizard(
const remoteUrl = baseConfig.gateway?.remote?.url?.trim() ?? "";
const remoteProbe = remoteUrl
? await probeGatewayReachable({
- url: remoteUrl,
- token: baseConfig.gateway?.remote?.token,
- })
+ url: remoteUrl,
+ token: baseConfig.gateway?.remote?.token,
+ })
: null;
const mode = guardCancel(
await select({
- message: "Where will the Gateway run?",
+ message: t("configure.gateway.modeSelect"),
options: [
{
value: "local",
- label: "Local (this machine)",
+ label: t("configure.gateway.local"),
hint: localProbe.ok
- ? `Gateway reachable (${localUrl})`
- : `No gateway detected (${localUrl})`,
+ ? t("configure.gateway.localHintReachable", { url: localUrl })
+ : t("configure.gateway.localHintMissing", { url: localUrl }),
},
{
value: "remote",
- label: "Remote (info-only)",
+ label: t("configure.gateway.remote"),
hint: !remoteUrl
- ? "No remote URL configured yet"
+ ? t("configure.gateway.remoteHintNoUrl")
: remoteProbe?.ok
- ? `Gateway reachable (${remoteUrl})`
- : `Configured but unreachable (${remoteUrl})`,
+ ? t("configure.gateway.remoteHintReachable", { url: remoteUrl })
+ : t("configure.gateway.remoteHintConfigured", { url: remoteUrl }),
},
],
}),
@@ -250,7 +247,7 @@ export async function runConfigureWizard(
});
await writeConfigFile(remoteConfig);
logConfigUpdated(runtime);
- outro("Remote gateway configured.");
+ outro(t("configure.gateway.remoteConfigured"));
return;
}
@@ -288,7 +285,7 @@ export async function runConfigureWizard(
if (opts.sections) {
const selected = opts.sections;
if (!selected || selected.length === 0) {
- outro("No changes selected.");
+ outro(t("configure.gateway.noChanges"));
return;
}
@@ -530,10 +527,10 @@ export async function runConfigureWizard(
if (!ranSection) {
if (didSetGatewayMode) {
await persistConfig();
- outro("Gateway mode set to local.");
+ outro(t("configure.gateway.modeSetLocal"));
return;
}
- outro("No changes selected.");
+ outro(t("configure.gateway.noChanges"));
return;
}
}
@@ -582,7 +579,7 @@ export async function runConfigureWizard(
"Control UI",
);
- outro("Configure complete.");
+ outro(t("configure.gateway.configureComplete"));
} catch (err) {
if (err instanceof WizardCancelledError) {
runtime.exit(0);
diff --git a/src/commands/doctor-auth.ts b/src/commands/doctor-auth.ts
index fc62a21c2..4ce0e8083 100644
--- a/src/commands/doctor-auth.ts
+++ b/src/commands/doctor-auth.ts
@@ -30,9 +30,9 @@ export async function maybeRepairAnthropicOAuthProfileId(
});
if (!repair.migrated || repair.changes.length === 0) return cfg;
- note(repair.changes.map((c) => `- ${c}`).join("\n"), "Auth profiles");
+ note(repair.changes.map((c) => `- ${c}`).join("\n"), "身份验证配置文件");
const apply = await prompter.confirm({
- message: "Update Anthropic OAuth profile id in config now?",
+ message: "立即更新配置中的 Anthropic OAuth 配置文件 ID?",
initialValue: true,
});
if (!apply) return cfg;
@@ -80,10 +80,10 @@ function pruneAuthProfiles(
const nextAuth =
nextProfiles || prunedOrder.next
? {
- ...cfg.auth,
- profiles: nextProfiles && Object.keys(nextProfiles).length > 0 ? nextProfiles : undefined,
- order: prunedOrder.next,
- }
+ ...cfg.auth,
+ profiles: nextProfiles && Object.keys(nextProfiles).length > 0 ? nextProfiles : undefined,
+ order: prunedOrder.next,
+ }
: undefined;
return {
@@ -110,23 +110,23 @@ export async function maybeRemoveDeprecatedCliAuthProfiles(
if (deprecated.size === 0) return cfg;
- const lines = ["Deprecated external CLI auth profiles detected (no longer supported):"];
+ const lines = ["检测到已弃用的外部 CLI 身份验证配置文件(不再支持):"];
if (deprecated.has(CLAUDE_CLI_PROFILE_ID)) {
lines.push(
- `- ${CLAUDE_CLI_PROFILE_ID} (Anthropic): use setup-token → ${formatCliCommand("openclaw models auth setup-token")}`,
+ `- ${CLAUDE_CLI_PROFILE_ID} (Anthropic): 请使用 setup-token → ${formatCliCommand("openclaw models auth setup-token")}`,
);
}
if (deprecated.has(CODEX_CLI_PROFILE_ID)) {
lines.push(
- `- ${CODEX_CLI_PROFILE_ID} (OpenAI Codex): use OAuth → ${formatCliCommand(
+ `- ${CODEX_CLI_PROFILE_ID} (OpenAI Codex): 请使用 OAuth → ${formatCliCommand(
"openclaw models auth login --provider openai-codex",
)}`,
);
}
- note(lines.join("\n"), "Auth profiles");
+ note(lines.join("\n"), "身份验证配置文件");
const shouldRemove = await prompter.confirmRepair({
- message: "Remove deprecated CLI auth profiles now?",
+ message: "立即移除已弃用的 CLI 身份验证配置文件?",
initialValue: true,
});
if (!shouldRemove) return cfg;
@@ -175,7 +175,7 @@ export async function maybeRemoveDeprecatedCliAuthProfiles(
Array.from(deprecated.values())
.map((id) => `- removed ${id} from config`)
.join("\n"),
- "Doctor changes",
+ "医生修改",
);
}
return pruned.next;
@@ -190,16 +190,16 @@ type AuthIssue = {
function formatAuthIssueHint(issue: AuthIssue): string | null {
if (issue.provider === "anthropic" && issue.profileId === CLAUDE_CLI_PROFILE_ID) {
- return `Deprecated profile. Use ${formatCliCommand("openclaw models auth setup-token")} or ${formatCliCommand(
+ return `已弃用的配置文件。请使用 ${formatCliCommand("openclaw models auth setup-token")} 或 ${formatCliCommand(
"openclaw configure",
- )}.`;
+ )}。`;
}
if (issue.provider === "openai-codex" && issue.profileId === CODEX_CLI_PROFILE_ID) {
- return `Deprecated profile. Use ${formatCliCommand(
+ return `已弃用的配置文件。请使用 ${formatCliCommand(
"openclaw models auth login --provider openai-codex",
- )} or ${formatCliCommand("openclaw configure")}.`;
+ )} 或 ${formatCliCommand("openclaw configure")}。`;
}
- return `Re-auth via \`${formatCliCommand("openclaw configure")}\` or \`${formatCliCommand("openclaw onboard")}\`.`;
+ return `通过 \`${formatCliCommand("openclaw configure")}\` 或 \`${formatCliCommand("openclaw onboard")}\` 重新验证。`;
}
function formatAuthIssueLine(issue: AuthIssue): string {
@@ -227,18 +227,18 @@ export async function noteAuthProfileHealth(params: {
const remaining = formatRemainingShort(until - now);
const kind =
typeof stats?.disabledUntil === "number" && now < stats.disabledUntil
- ? `disabled${stats.disabledReason ? `:${stats.disabledReason}` : ""}`
- : "cooldown";
- const hint = kind.startsWith("disabled:billing")
- ? "Top up credits (provider billing) or switch provider."
- : "Wait for cooldown or switch provider.";
+ ? `已禁用${stats.disabledReason ? `:${stats.disabledReason}` : ""}`
+ : "冷却中";
+ const hint = kind.startsWith("已禁用:billing")
+ ? "充值(提供商计费)或切换提供商。"
+ : "等待冷却或切换提供商。";
out.push(`- ${profileId}: ${kind} (${remaining})${hint ? ` — ${hint}` : ""}`);
}
return out;
})();
if (unusable.length > 0) {
- note(unusable.join("\n"), "Auth profile cooldowns");
+ note(unusable.join("\n"), "身份验证配置文件冷却");
}
let summary = buildAuthHealthSummary({
@@ -260,7 +260,7 @@ export async function noteAuthProfileHealth(params: {
if (issues.length === 0) return;
const shouldRefresh = await params.prompter.confirmRepair({
- message: "Refresh expiring OAuth tokens now? (static tokens need re-auth)",
+ message: "立即刷新过期的 OAuth 令牌?(静态令牌需要重新验证)",
initialValue: true,
});
@@ -282,7 +282,7 @@ export async function noteAuthProfileHealth(params: {
}
}
if (errors.length > 0) {
- note(errors.join("\n"), "OAuth refresh errors");
+ note(errors.join("\n"), "OAuth 刷新错误");
}
summary = buildAuthHealthSummary({
store: ensureAuthProfileStore(undefined, {
@@ -306,7 +306,7 @@ export async function noteAuthProfileHealth(params: {
}),
)
.join("\n"),
- "Model auth",
+ "模型验证",
);
}
}
diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts
index 774893213..aba1441e6 100644
--- a/src/commands/onboard-helpers.ts
+++ b/src/commands/onboard-helpers.ts
@@ -27,10 +27,11 @@ import {
} from "../utils.js";
import { VERSION } from "../version.js";
import type { NodeManagerChoice, OnboardMode, ResetScope } from "./onboard-types.js";
+import { t } from "../wizard/i18n.js";
export function guardCancel(value: T | symbol, runtime: RuntimeEnv): T {
if (isCancel(value)) {
- cancel(stylePromptTitle("Setup cancelled.") ?? "Setup cancelled.");
+ cancel(stylePromptTitle(t("onboarding.helpers.cancelled")) ?? t("onboarding.helpers.cancelled"));
runtime.exit(0);
}
return value as T;
@@ -55,7 +56,7 @@ export function summarizeExistingConfig(config: OpenClawConfig): string {
if (config.skills?.install?.nodeManager) {
rows.push(shortenHomeInString(`skills.nodeManager: ${config.skills.install.nodeManager}`));
}
- return rows.length ? rows.join("\n") : "No key settings detected.";
+ return rows.length ? rows.join("\n") : t("onboarding.helpers.noSettings");
}
export function randomToken(): string {
@@ -172,7 +173,7 @@ export function formatControlUiSshHint(params: {
const authedUrl = params.token ? `${localUrl}${tokenParam}` : undefined;
const sshTarget = resolveSshTargetHint();
return [
- "No GUI detected. Open from your computer:",
+ t("onboarding.helpers.sshHint"),
`ssh -N -L ${params.port}:127.0.0.1:${params.port} ${sshTarget}`,
"Then open:",
localUrl,
@@ -242,10 +243,10 @@ export async function ensureWorkspaceAndSessions(
dir: workspaceDir,
ensureBootstrapFiles: !options?.skipBootstrap,
});
- runtime.log(`Workspace OK: ${shortenHomePath(ws.dir)}`);
+ runtime.log(`${t("onboarding.helpers.workspaceOk")}: ${shortenHomePath(ws.dir)}`);
const sessionsDir = resolveSessionTranscriptsDirForAgent(options?.agentId);
await fs.mkdir(sessionsDir, { recursive: true });
- runtime.log(`Sessions OK: ${shortenHomePath(sessionsDir)}`);
+ runtime.log(`${t("onboarding.helpers.sessionsOk")}: ${shortenHomePath(sessionsDir)}`);
}
export function resolveNodeManagerOptions(): Array<{
@@ -268,9 +269,9 @@ export async function moveToTrash(pathname: string, runtime: RuntimeEnv): Promis
}
try {
await runCommandWithTimeout(["trash", pathname], { timeoutMs: 5000 });
- runtime.log(`Moved to Trash: ${shortenHomePath(pathname)}`);
+ runtime.log(`${t("onboarding.helpers.trashOk")}: ${shortenHomePath(pathname)}`);
} catch {
- runtime.log(`Failed to move to Trash (manual delete): ${shortenHomePath(pathname)}`);
+ runtime.log(`${t("onboarding.helpers.trashFail")}: ${shortenHomePath(pathname)}`);
}
}
diff --git a/src/commands/onboard.ts b/src/commands/onboard.ts
index 915097d83..73dfaa32f 100644
--- a/src/commands/onboard.ts
+++ b/src/commands/onboard.ts
@@ -8,6 +8,7 @@ import { runInteractiveOnboarding } from "./onboard-interactive.js";
import { runNonInteractiveOnboarding } from "./onboard-non-interactive.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { OnboardOptions } from "./onboard-types.js";
+import { t } from "../wizard/i18n.js";
export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = defaultRuntime) {
assertSupportedRuntime(runtime);
@@ -21,18 +22,18 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv =
if (opts.nonInteractive && (authChoice === "claude-cli" || authChoice === "codex-cli")) {
runtime.error(
[
- `Auth choice "${authChoice}" is deprecated.`,
- 'Use "--auth-choice token" (Anthropic setup-token) or "--auth-choice openai-codex".',
+ t("onboarding.cli.deprecatedAuth").replace("{authChoice}", authChoice),
+ t("onboarding.cli.useAuthToken"),
].join("\n"),
);
runtime.exit(1);
return;
}
if (authChoice === "claude-cli") {
- runtime.log('Auth choice "claude-cli" is deprecated; using setup-token flow instead.');
+ runtime.log(t("onboarding.cli.authTokenFlow"));
}
if (authChoice === "codex-cli") {
- runtime.log('Auth choice "codex-cli" is deprecated; using OpenAI Codex OAuth instead.');
+ runtime.log(t("onboarding.cli.authCodexFlow"));
}
const flow = opts.flow === "manual" ? ("advanced" as const) : opts.flow;
const normalizedOpts =
@@ -43,8 +44,7 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv =
if (normalizedOpts.nonInteractive && normalizedOpts.acceptRisk !== true) {
runtime.error(
[
- "Non-interactive onboarding requires explicit risk acknowledgement.",
- "Read: https://docs.openclaw.ai/security",
+ t("onboarding.cli.nonInteractiveRisk"),
`Re-run with: ${formatCliCommand("openclaw onboard --non-interactive --accept-risk ...")}`,
].join("\n"),
);
@@ -61,13 +61,7 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv =
}
if (process.platform === "win32") {
- runtime.log(
- [
- "Windows detected.",
- "WSL2 is strongly recommended; native Windows is untested and more problematic.",
- "Guide: https://docs.openclaw.ai/windows",
- ].join("\n"),
- );
+ runtime.log(t("onboarding.cli.winWarning"));
}
if (normalizedOpts.nonInteractive) {
diff --git a/src/wizard/i18n.ts b/src/wizard/i18n.ts
new file mode 100644
index 000000000..adf64f3d2
--- /dev/null
+++ b/src/wizard/i18n.ts
@@ -0,0 +1,26 @@
+
+import { zhCN } from "./locales/zh-CN.js";
+
+const currentLocale = "zh-CN"; // Default to Chinese
+const locales: Record = {
+ "zh-CN": zhCN,
+};
+
+export function t(key: string, args?: Record): string {
+ const keys = key.split(".");
+ let value = locales[currentLocale];
+ for (const k of keys) {
+ if (value && typeof value === "object") {
+ value = value[k];
+ } else {
+ return key;
+ }
+ }
+ let str = typeof value === "string" ? value : key;
+ if (args) {
+ for (const [k, v] of Object.entries(args)) {
+ str = str.replace(new RegExp(`{${k}}`, "g"), String(v));
+ }
+ }
+ return str;
+}
diff --git a/src/wizard/locales/zh-CN.ts b/src/wizard/locales/zh-CN.ts
new file mode 100644
index 000000000..f436f23fd
--- /dev/null
+++ b/src/wizard/locales/zh-CN.ts
@@ -0,0 +1,301 @@
+
+export const zhCN = {
+ onboarding: {
+ title: "OpenClaw 引导安装",
+ intro: "欢迎使用 OpenClaw 引导安装",
+ security: {
+ title: "安全警告",
+ note: [
+ "安全警告 — 请仔细阅读。",
+ "",
+ "OpenClaw 是一个个人爱好项目,目前处于 Beta 阶段。请做好遇到问题的心理准备。",
+ "如果启用了技能工具,机器人可以读取您的文件并执行操作。",
+ "恶意提示(Prompt Injection)可能会诱使机器人执行不安全的操作。",
+ "",
+ "如果您对基础安全和访问控制感到不放心,请不要运行 OpenClaw。",
+ "在启用工具或将其暴露在互联网之前,请先向有经验的人寻求帮助。",
+ "",
+ "推荐的安全基准:",
+ "- 开启配对/白名单机制 + 提及触发(Mention Gating)。",
+ "- 在沙箱中运行 + 最小权限原则。",
+ "- 不要让代理程序能接触到敏感的系统密钥和凭证。",
+ "- 对拥有工具权限或监听不信任渠道的机器人,务必使用最强大的模型。",
+ "",
+ "定期运行审计命令:",
+ "openclaw security audit --deep",
+ "openclaw security audit --fix",
+ "",
+ "必读文档: https://docs.openclaw.ai/gateway/security",
+ ].join("\n"),
+ confirm: "我理解这是非常强大的工具,并且具有内在风险。是否继续?",
+ cancelled: "未接受安全风险,已取消。",
+ },
+ config: {
+ invalid: "配置文件无效",
+ issues: "配置问题提示",
+ repair: "配置无效。请运行 `openclaw doctor` 进行修复,然后重新运行引导安装。",
+ },
+ flow: {
+ modeSelect: "选择安装模式",
+ quickstart: "快速上手 (QuickStart)",
+ quickstartHint: "稍后可以通过 `openclaw configure` 进行详细调整。",
+ manual: "手动配置 (Manual)",
+ manualHint: "详细配置端口、网络、Tailscale 及认证选项。",
+ invalidFlow: "无效的 --flow 参数(请使用 quickstart, manual 或 advanced)。",
+ remoteSwitch: "快速上手模式仅支持本地网关。正在切换到手动模式。",
+ },
+ existingConfig: {
+ title: "检测到现有配置",
+ action: "配置处理方式",
+ keep: "使用现有值",
+ modify: "更新配置值",
+ reset: "重置 (Reset)",
+ resetScope: "重置范围",
+ scopeConfig: "仅重置基本配置",
+ scopeConfigCreds: "重置基本配置 + 凭证 + 会话",
+ scopeFull: "完整重置 (配置 + 凭证 + 会话 + 工作区)",
+ },
+ gateway: {
+ keepSettings: "保留当前的网关设置:",
+ port: "网关端口",
+ bind: "网关绑定",
+ auth: "网关认证",
+ tailscale: "Tailscale 暴露",
+ chatChannels: "直接前往聊天渠道配置。",
+ bindLoopback: "本地回环 (127.0.0.1)",
+ bindLan: "局域网 (LAN)",
+ bindCustom: "自定义 IP",
+ bindTailnet: "Tailnet (Tailscale IP)",
+ bindAuto: "自动",
+ authToken: "令牌 Token (默认)",
+ authPassword: "密码 Password",
+ tsOff: "关闭",
+ tsServe: "Serve 模式",
+ tsFunnel: "Funnel 模式",
+ },
+ setup: {
+ question: "您想设置什么?",
+ local: "本地网关 (Local gateway - 当前机器)",
+ localOk: "网关可达",
+ localFail: "未检测到网关",
+ remote: "远程网关 (Remote gateway - 仅配置信息)",
+ remoteNoUrl: "尚未配置远程 URL",
+ remoteOk: "远程网关可达",
+ remoteFail: "已配置但无法连接",
+ remoteDone: "远程网关配置完成。",
+ workspaceDir: "工作区目录",
+ skippingChannels: "跳过渠道设置。",
+ skills: "技能 (Skills)",
+ skippingSkills: "跳过技能设置。",
+ },
+ cli: {
+ winWarning: [
+ "检测到 Windows 系统。",
+ "强烈建议使用 WSL2;原生 Windows 环境未经充分测试,可能存在兼容性问题。",
+ "指南: https://docs.openclaw.ai/windows",
+ ].join("\n"),
+ nonInteractiveRisk: [
+ "非交互式安装需要明确的技术风险说明(--accept-risk)。",
+ "详情请阅读: https://docs.openclaw.ai/security",
+ ].join("\n"),
+ deprecatedAuth: '身份验证选项 "{authChoice}" 已弃用。',
+ useAuthToken: '请使用 "--auth-choice token" (Anthropic) 或 "--auth-choice openai-codex"。',
+ authTokenFlow: '身份验证选项 "claude-cli" 已弃用,将使用令牌(token)流程。',
+ authCodexFlow: '身份验证选项 "codex-cli" 已弃用,将使用 OpenAI Codex 会话流程。',
+ },
+ helpers: {
+ cancelled: "设置已取消。",
+ noSettings: "未检测到关键配置。",
+ workspaceOk: "工作区确认",
+ sessionsOk: "会话目录确认",
+ trashOk: "已移至回收站",
+ trashFail: "移至回收站失败 (请手动删除)",
+ sshHint: "未检测到 GUI 环境。请从您的电脑上访问:",
+ },
+ finalize: {
+ systemdNote: "检测到 Linux,但当前用户似乎无法使用 Systemd。这可能会影响服务安装。",
+ systemdLinger: "为确保 OpenClaw 服务在您登出后继续运行,我们需要启用用户逗留 (Linger)。",
+ installService: "是否将 OpenClaw 安装为后台服务?",
+ serviceNoSystemd: "由于 Systemd不可用,跳过服务安装。您可以手动运行 OpenClaw。",
+ serviceInstalled: "服务管理",
+ serviceRuntime: "服务运行时 (Daemon Runtime)",
+ serviceRuntimeQuickstart: "快速启动模式下,我们将使用 Node.js 运行时。",
+ restarted: "服务已重启",
+ restarting: "正在重启服务...",
+ uninstalled: "服务已卸载",
+ uninstalling: "正在卸载服务...",
+ preparing: "正在准备安装...",
+ installing: "正在安装服务...",
+ installFail: "安装失败",
+ installSuccess: "安装成功",
+ healthHelp: "健康检查失败。请参考文档进行排查:",
+ healthDocsPrefix: "相关文档:",
+ optionalApps: "可选组件",
+ optionalAppsList: "OpenClaw 提供了 Web UI、TUI 等多种管理方式。",
+ controlUi: "控制面板 (Control UI)",
+ hatchTui: "启动 TUI (Moltbot)",
+ hatchTuiNote: [
+ "这是定义您的代理人的关键动作。",
+ "请花点时间。",
+ "您告诉它的信息越多,体验就越好。",
+ '我们将发送: "唤醒吧,我的朋友!"',
+ ].join("\n"),
+ hatchWeb: "打开 Web UI",
+ hatchLater: "以后再说",
+ hatchQuestion: "您想现在启动哪个界面?",
+ tokenNote: [
+ "网关令牌 (Token):用于网关和控制面板的共享身份验证。",
+ "存储位置:~/.openclaw/openclaw.json (gateway.auth.token) 或环境变量 OPENCLAW_GATEWAY_TOKEN。",
+ "网页 UI 会在浏览器本地存储中保存一份副本。",
+ "随时获取带令牌的链接:openclaw dashboard --no-open",
+ ].join("\n"),
+ webUiSeeded: "网页 UI 已在后台初始化。稍后可通过以下命令打开:",
+ dashboardReady: "仪表板已就绪",
+ dashboardOpened: "已在浏览器中打开。请保留该标签页以控制 OpenClaw。",
+ dashboardCopy: "请将此 URL 复制到本机的浏览器中以控制 OpenClaw。",
+ backupNote: "请定期备份您的工作区目录,它包含您的所有 Agent 数据。",
+ webSearchOptional: "网页搜索功能 (可选)",
+ webSearchEnabled: "网页搜索已成功启用!代理人可以在需要时在线查询信息。",
+ webSearchDisabled: [
+ "如果您希望代理人能够搜索网页,则需要一个 API 密钥。",
+ "",
+ "OpenClaw 使用 Brave Search 进行网页搜索。如果没有 API 密钥,该工具将无法工作。",
+ "",
+ "设置方法:",
+ "- 运行: openclaw configure --section web",
+ "- 启用 web_search 并粘贴您的 Brave Search API 密钥",
+ "",
+ "或者:在网关环境变量中设置 BRAVE_API_KEY。",
+ ].join("\n"),
+ webSearchKeyConfig: "已使用配置文件中的 API Key。",
+ webSearchKeyEnv: "已使用系统环境变量 BRAVE_API_KEY。",
+ whatNow: "接下来可以做什么?",
+ onboardingComplete: "OpenClaw 初始化完成!",
+ onboardingCompleteOpened: "OpenClaw 初始化完成,仪表板已随令牌打开;请保留该标签页以控制 OpenClaw。",
+ onboardingCompleteSeeded: "OpenClaw 初始化完成,网页 UI 已在后台初始化;随时使用上面的链接打开。",
+ },
+ gatewayConfig: {
+ port: "网关端口",
+ invalidPort: "无效的端口号",
+ bind: "网关绑定 (Bind)",
+ customIp: "自定义 IP 地址",
+ customIpRequired: "自定义绑定模式需要提供 IP 地址",
+ invalidIp: "无效的 IPv4 地址",
+ invalidIpOctet: "无效的 IPv4 地址 (每段必须是 0-255)",
+ auth: "网关身份验证",
+ authToken: "令牌 (Token)",
+ authTokenHint: "推荐的默认方式 (支持本地和远程)",
+ authPassword: "密码 (Password)",
+ tsExposure: "Tailscale 暴露",
+ tsOff: "关闭",
+ tsOffHint: "不进行 Tailscale 暴露",
+ tsServe: "Serve 模式",
+ tsServeHint: "为您的 Tailnet 提供私有 HTTPS (仅限 Tailscale 里的设备)",
+ tsFunnel: "Funnel 模式",
+ tsFunnelHint: "通过 Tailscale Funnel 提供公共 HTTPS (互联网可访问)",
+ tsWarningTitle: "Tailscale 警告",
+ tsNotFound: [
+ "未在 PATH 中找到 Tailscale 二进制文件。",
+ "请确保已安装 Tailscale。",
+ "",
+ "您可以继续设置,但 serve/funnel 在运行时会失败。",
+ ].join("\n"),
+ tsResetConfirm: "退出时重置 Tailscale serve/funnel?",
+ tsAdjustBind: "Tailscale 需要 bind=loopback。正在自动调整网关绑定为 loopback。",
+ tsFunnelAuth: "Tailscale Funnel 需要使用密码验证方式。",
+ tokenPlaceholder: "网关令牌 (留空则自动生成)",
+ tokenHint: "多机访问或非 127.0.0.1 访问时需要此令牌",
+ passwordLabel: "网关密码",
+ passwordRequired: "必须填写密码",
+ }
+ },
+ configure: {
+ title: "OpenClaw 配置",
+ updateTitle: "OpenClaw 更新向导",
+ auth: {
+ anthropicOAuthModels: "Anthropic OAuth 模型",
+ },
+ sections: {
+ title: "选择要配置的部分",
+ continue: "继续",
+ continueHint: "完成",
+ skipHint: "暂时跳过",
+ workspace: "工作区 (Workspace)",
+ workspaceHint: "设置工作区 + 会话",
+ model: "模型 (Model)",
+ modelHint: "选择提供商 + 凭证",
+ web: "Web 工具",
+ webHint: "配置 Brave 搜索 + 用于抓取",
+ gateway: "网关 (Gateway)",
+ gatewayHint: "端口、绑定、认证、Tailscale",
+ daemon: "后台服务 (Daemon)",
+ daemonHint: "安装/管理后台服务",
+ channels: "渠道 (Channels)",
+ channelsHint: "连接 WhatsApp/Telegram 等",
+ skills: "技能 (Skills)",
+ skillsHint: "安装/启用工作区技能",
+ health: "健康检查 (Health check)",
+ healthHint: "运行网关 + 渠道检查",
+ },
+ gateway: {
+ modeSelect: "网关将在哪里运行?",
+ local: "本地 (这台机器)",
+ localHintReachable: "网关可达 ({url})",
+ localHintMissing: "未检测到网关 ({url})",
+ remote: "远程 (仅信息)",
+ remoteHintNoUrl: "尚未配置远程 URL",
+ remoteHintReachable: "网关可达 ({url})",
+ remoteHintConfigured: "已配置但无法连接 ({url})",
+ remoteConfigured: "远程网关已配置。",
+ modeSetLocal: "网关模式已设置为本地。",
+ noChanges: "未选择任何更改。",
+ configureComplete: "配置完成。",
+ },
+ channels: {
+ modeTitle: "渠道配置模式",
+ configure: "配置/连接",
+ configureHint: "添加/更新渠道;禁用未选中的账户",
+ remove: "移除渠道配置",
+ removeHint: "从 openclaw.json 中删除渠道令牌/设置",
+ },
+ web: {
+ title: "网页搜索",
+ desc: [
+ "网页搜索允许您的 Agent 使用 `web_search` 工具在线查找信息。",
+ "它需要 Brave Search API 密钥(您可以将其存储在配置中或在网关环境中设置 BRAVE_API_KEY)。",
+ "文档: https://docs.openclaw.ai/tools/web",
+ ].join("\n"),
+ enableSearch: "启用 web_search (Brave Search)?",
+ keyPrompt: "Brave Search API 密钥 (留空保留当前值或使用 BRAVE_API_KEY)",
+ keyPromptEmpty: "Brave Search API 密钥 (粘贴到这里;留空使用 BRAVE_API_KEY)",
+ placeholderKey: "留空以保留当前设置",
+ placeholderKeyEmpty: "BSA...",
+ noKeyWarning: [
+ "尚未存储密钥,因此 web_search 将不可用。",
+ "请在此处存储密钥或在网关环境中设置 BRAVE_API_KEY。",
+ "文档: https://docs.openclaw.ai/tools/web",
+ ].join("\n"),
+ enableFetch: "启用 web_fetch (无密钥 HTTP 抓取)?",
+ },
+ daemon: {
+ alreadyInstalled: "网关服务已安装",
+ restart: "重启 (Restart)",
+ reinstall: "重新安装 (Reinstall)",
+ skip: "跳过 (Skip)",
+ restarting: "正在重启网关服务…",
+ restarted: "网关服务已重启。",
+ uninstalling: "正在卸载网关服务…",
+ uninstalled: "网关服务已卸载。",
+ selectRuntime: "网关服务运行时",
+ preparing: "正在准备网关服务…",
+ installing: "正在安装网关服务…",
+ installed: "网关服务已安装。",
+ installFailed: "网关服务安装失败。",
+ installFailedNote: "网关服务安装失败: ",
+ lingerReason: "Linux 安装使用 systemd 用户服务。如果不启用 lingering,systemd 会在注销/空闲时停止用户会话并终止网关。",
+ }
+ },
+ common: {
+ configUpdated: "配置已更新。",
+ }
+};
diff --git a/src/wizard/onboarding.finalize.ts b/src/wizard/onboarding.finalize.ts
index c5b01d6bf..1f5e63e37 100644
--- a/src/wizard/onboarding.finalize.ts
+++ b/src/wizard/onboarding.finalize.ts
@@ -33,8 +33,9 @@ import {
} from "../commands/daemon-install-helpers.js";
import type { GatewayWizardSettings, WizardFlow } from "./onboarding.types.js";
import type { WizardPrompter } from "./prompts.js";
+import { t } from "./i18n.js";
-type FinalizeOnboardingOptions = {
+export type FinalizeOnboardingOptions = {
flow: WizardFlow;
opts: OnboardOptions;
baseConfig: OpenClawConfig;
@@ -65,7 +66,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
process.platform === "linux" ? await isSystemdUserServiceAvailable() : true;
if (process.platform === "linux" && !systemdAvailable) {
await prompter.note(
- "Systemd user services are unavailable. Skipping lingering checks and service install.",
+ t("onboarding.finalize.systemdNote"),
"Systemd",
);
}
@@ -78,8 +79,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
confirm: prompter.confirm,
note: prompter.note,
},
- reason:
- "Linux installs use a systemd user service by default. Without lingering, systemd stops the user session on logout/idle and kills the Gateway.",
+ reason: t("onboarding.finalize.systemdLinger"),
requireConfirm: false,
});
}
@@ -95,15 +95,15 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
installDaemon = true;
} else {
installDaemon = await prompter.confirm({
- message: "Install Gateway service (recommended)",
+ message: t("onboarding.finalize.installService"),
initialValue: true,
});
}
if (process.platform === "linux" && !systemdAvailable && installDaemon) {
await prompter.note(
- "Systemd user services are unavailable; skipping service install. Use your container supervisor or `docker compose up -d`.",
- "Gateway service",
+ t("onboarding.finalize.serviceNoSystemd"),
+ t("onboarding.finalize.serviceInstalled"),
);
installDaemon = false;
}
@@ -113,33 +113,33 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
flow === "quickstart"
? (DEFAULT_GATEWAY_DAEMON_RUNTIME as GatewayDaemonRuntime)
: ((await prompter.select({
- message: "Gateway service runtime",
- options: GATEWAY_DAEMON_RUNTIME_OPTIONS,
- initialValue: opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME,
- })) as GatewayDaemonRuntime);
+ message: t("onboarding.finalize.serviceRuntime"),
+ options: GATEWAY_DAEMON_RUNTIME_OPTIONS,
+ initialValue: opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME,
+ })) as GatewayDaemonRuntime);
if (flow === "quickstart") {
await prompter.note(
- "QuickStart uses Node for the Gateway service (stable + supported).",
- "Gateway service runtime",
+ t("onboarding.finalize.serviceRuntimeQuickstart"),
+ t("onboarding.finalize.serviceRuntime"),
);
}
const service = resolveGatewayService();
const loaded = await service.isLoaded({ env: process.env });
if (loaded) {
const action = (await prompter.select({
- message: "Gateway service already installed",
+ message: t("onboarding.finalize.serviceInstalled"),
options: [
- { value: "restart", label: "Restart" },
- { value: "reinstall", label: "Reinstall" },
- { value: "skip", label: "Skip" },
+ { value: "restart", label: "重启 (Restart)" },
+ { value: "reinstall", label: "重新安装 (Reinstall)" },
+ { value: "skip", label: "跳过 (Skip)" },
],
})) as "restart" | "reinstall" | "skip";
if (action === "restart") {
await withWizardProgress(
- "Gateway service",
- { doneMessage: "Gateway service restarted." },
+ t("onboarding.finalize.serviceInstalled"),
+ { doneMessage: t("onboarding.finalize.restarted") },
async (progress) => {
- progress.update("Restarting Gateway service…");
+ progress.update(t("onboarding.finalize.restarting"));
await service.restart({
env: process.env,
stdout: process.stdout,
@@ -148,10 +148,10 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
);
} else if (action === "reinstall") {
await withWizardProgress(
- "Gateway service",
- { doneMessage: "Gateway service uninstalled." },
+ t("onboarding.finalize.serviceInstalled"),
+ { doneMessage: t("onboarding.finalize.uninstalled") },
async (progress) => {
- progress.update("Uninstalling Gateway service…");
+ progress.update(t("onboarding.finalize.uninstalling"));
await service.uninstall({ env: process.env, stdout: process.stdout });
},
);
@@ -159,10 +159,10 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
}
if (!loaded || (loaded && (await service.isLoaded({ env: process.env })) === false)) {
- const progress = prompter.progress("Gateway service");
+ const progress = prompter.progress(t("onboarding.finalize.serviceInstalled"));
let installError: string | null = null;
try {
- progress.update("Preparing Gateway service…");
+ progress.update(t("onboarding.finalize.preparing"));
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
env: process.env,
port: settings.port,
@@ -172,7 +172,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
config: nextConfig,
});
- progress.update("Installing Gateway service…");
+ progress.update(t("onboarding.finalize.installing"));
await service.install({
env: process.env,
stdout: process.stdout,
@@ -184,11 +184,11 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
installError = err instanceof Error ? err.message : String(err);
} finally {
progress.stop(
- installError ? "Gateway service install failed." : "Gateway service installed.",
+ installError ? t("onboarding.finalize.installFail") : t("onboarding.finalize.installSuccess"),
);
}
if (installError) {
- await prompter.note(`Gateway service install failed: ${installError}`, "Gateway");
+ await prompter.note(`${t("onboarding.finalize.installFail")}: ${installError}`, "Gateway");
await prompter.note(gatewayInstallErrorHint(), "Gateway");
}
}
@@ -213,11 +213,11 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
runtime.error(formatHealthCheckFailure(err));
await prompter.note(
[
- "Docs:",
+ t("onboarding.finalize.healthDocsPrefix"),
"https://docs.openclaw.ai/gateway/health",
"https://docs.openclaw.ai/gateway/troubleshooting",
].join("\n"),
- "Health check help",
+ t("onboarding.finalize.healthHelp"),
);
}
}
@@ -232,13 +232,8 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
}
await prompter.note(
- [
- "Add nodes for extra features:",
- "- macOS app (system + notifications)",
- "- iOS app (camera/canvas)",
- "- Android app (camera/canvas)",
- ].join("\n"),
- "Optional apps",
+ t("onboarding.finalize.optionalAppsList"),
+ t("onboarding.finalize.optionalApps"),
);
const controlUiBasePath =
@@ -260,8 +255,8 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
password: settings.authMode === "password" ? nextConfig.gateway?.auth?.password : "",
});
const gatewayStatusLine = gatewayProbe.ok
- ? "Gateway: reachable"
- : `Gateway: not detected${gatewayProbe.detail ? ` (${gatewayProbe.detail})` : ""}`;
+ ? t("onboarding.setup.localOk")
+ : `${t("onboarding.setup.localFail")}${gatewayProbe.detail ? ` (${gatewayProbe.detail})` : ""}`;
const bootstrapPath = path.join(
resolveUserPath(options.workspaceDir),
DEFAULT_BOOTSTRAP_FILENAME,
@@ -274,14 +269,14 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
await prompter.note(
[
`Web UI: ${links.httpUrl}`,
- tokenParam ? `Web UI (with token): ${authedUrl}` : undefined,
+ tokenParam ? `Web UI (${t("onboarding.gatewayConfig.authToken")}): ${authedUrl}` : undefined,
`Gateway WS: ${links.wsUrl}`,
gatewayStatusLine,
"Docs: https://docs.openclaw.ai/web/control-ui",
]
.filter(Boolean)
.join("\n"),
- "Control UI",
+ t("onboarding.finalize.controlUi"),
);
let controlUiOpened = false;
@@ -292,32 +287,22 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
if (!opts.skipUi && gatewayProbe.ok) {
if (hasBootstrap) {
await prompter.note(
- [
- "This is the defining action that makes your agent you.",
- "Please take your time.",
- "The more you tell it, the better the experience will be.",
- 'We will send: "Wake up, my friend!"',
- ].join("\n"),
- "Start TUI (best option!)",
+ t("onboarding.finalize.hatchTuiNote"),
+ t("onboarding.finalize.hatchTui"),
);
}
await prompter.note(
- [
- "Gateway token: shared auth for the Gateway + Control UI.",
- "Stored in: ~/.openclaw/openclaw.json (gateway.auth.token) or OPENCLAW_GATEWAY_TOKEN.",
- "Web UI stores a copy in this browser's localStorage (openclaw.control.settings.v1).",
- `Get the tokenized link anytime: ${formatCliCommand("openclaw dashboard --no-open")}`,
- ].join("\n"),
- "Token",
+ t("onboarding.finalize.tokenNote"),
+ t("onboarding.gatewayConfig.authToken"),
);
hatchChoice = (await prompter.select({
- message: "How do you want to hatch your bot?",
+ message: t("onboarding.finalize.hatchQuestion"),
options: [
- { value: "tui", label: "Hatch in TUI (recommended)" },
- { value: "web", label: "Open the Web UI" },
- { value: "later", label: "Do this later" },
+ { value: "tui", label: t("onboarding.finalize.hatchTui") },
+ { value: "web", label: t("onboarding.finalize.hatchWeb") },
+ { value: "later", label: t("onboarding.finalize.hatchLater") },
],
initialValue: "tui",
})) as "tui" | "web" | "later";
@@ -336,7 +321,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
}
if (seededInBackground) {
await prompter.note(
- `Web UI seeded in the background. Open later with: ${formatCliCommand(
+ `${t("onboarding.finalize.webUiSeeded")} ${formatCliCommand(
"openclaw dashboard --no-open",
)}`,
"Web UI",
@@ -362,37 +347,37 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
}
await prompter.note(
[
- `Dashboard link (with token): ${authedUrl}`,
+ `${t("onboarding.finalize.dashboardReady")} (${t("onboarding.gatewayConfig.authToken")}): ${authedUrl}`,
controlUiOpened
- ? "Opened in your browser. Keep that tab to control OpenClaw."
- : "Copy/paste this URL in a browser on this machine to control OpenClaw.",
+ ? t("onboarding.finalize.dashboardOpened")
+ : t("onboarding.finalize.dashboardCopy"),
controlUiOpenHint,
]
.filter(Boolean)
.join("\n"),
- "Dashboard ready",
+ t("onboarding.finalize.dashboardReady"),
);
} else {
await prompter.note(
- `When you're ready: ${formatCliCommand("openclaw dashboard --no-open")}`,
- "Later",
+ `${t("onboarding.finalize.hatchLater")}: ${formatCliCommand("openclaw dashboard --no-open")}`,
+ t("onboarding.finalize.hatchLater"),
);
}
} else if (opts.skipUi) {
- await prompter.note("Skipping Control UI/TUI prompts.", "Control UI");
+ await prompter.note("Skipping Control UI/TUI prompts.", t("onboarding.finalize.controlUi"));
}
await prompter.note(
[
- "Back up your agent workspace.",
+ t("onboarding.finalize.backupNote"),
"Docs: https://docs.openclaw.ai/concepts/agent-workspace",
].join("\n"),
- "Workspace backup",
+ t("onboarding.finalize.backupNote"),
);
await prompter.note(
"Running agents on your computer is risky — harden your setup: https://docs.openclaw.ai/security",
- "Security",
+ t("onboarding.security.title"),
);
const shouldOpenControlUi =
@@ -421,15 +406,15 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
await prompter.note(
[
- `Dashboard link (with token): ${authedUrl}`,
+ `${t("onboarding.finalize.dashboardReady")} (${t("onboarding.gatewayConfig.authToken")}): ${authedUrl}`,
controlUiOpened
- ? "Opened in your browser. Keep that tab to control OpenClaw."
- : "Copy/paste this URL in a browser on this machine to control OpenClaw.",
+ ? t("onboarding.finalize.dashboardOpened")
+ : t("onboarding.finalize.dashboardCopy"),
controlUiOpenHint,
]
.filter(Boolean)
.join("\n"),
- "Dashboard ready",
+ t("onboarding.finalize.dashboardReady"),
);
}
@@ -439,38 +424,32 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
await prompter.note(
hasWebSearchKey
? [
- "Web search is enabled, so your agent can look things up online when needed.",
- "",
- webSearchKey
- ? "API key: stored in config (tools.web.search.apiKey)."
- : "API key: provided via BRAVE_API_KEY env var (Gateway environment).",
- "Docs: https://docs.openclaw.ai/tools/web",
- ].join("\n")
+ t("onboarding.finalize.webSearchEnabled"),
+ "",
+ webSearchKey
+ ? t("onboarding.finalize.webSearchKeyConfig")
+ : t("onboarding.finalize.webSearchKeyEnv"),
+ "Docs: https://docs.openclaw.ai/tools/web",
+ ].join("\n")
: [
- "If you want your agent to be able to search the web, you’ll need an API key.",
- "",
- "OpenClaw uses Brave Search for the `web_search` tool. Without a Brave Search API key, web search won’t work.",
- "",
- "Set it up interactively:",
- `- Run: ${formatCliCommand("openclaw configure --section web")}`,
- "- Enable web_search and paste your Brave Search API key",
- "",
- "Alternative: set BRAVE_API_KEY in the Gateway environment (no config changes).",
- "Docs: https://docs.openclaw.ai/tools/web",
- ].join("\n"),
- "Web search (optional)",
+ t("onboarding.finalize.webSearchDisabled"),
+ "",
+ `设置命令: ${formatCliCommand("openclaw configure --section web")}`,
+ "Docs: https://docs.openclaw.ai/tools/web",
+ ].join("\n"),
+ t("onboarding.finalize.webSearchOptional"),
);
await prompter.note(
- 'What now: https://openclaw.ai/showcase ("What People Are Building").',
- "What now",
+ 'Showcase: https://openclaw.ai/showcase',
+ t("onboarding.finalize.whatNow"),
);
await prompter.outro(
controlUiOpened
- ? "Onboarding complete. Dashboard opened with your token; keep that tab to control OpenClaw."
+ ? t("onboarding.finalize.onboardingCompleteOpened")
: seededInBackground
- ? "Onboarding complete. Web UI seeded in the background; open it anytime with the tokenized link above."
- : "Onboarding complete. Use the tokenized dashboard link above to control OpenClaw.",
+ ? t("onboarding.finalize.onboardingCompleteSeeded")
+ : t("onboarding.finalize.onboardingComplete"),
);
}
diff --git a/src/wizard/onboarding.gateway-config.ts b/src/wizard/onboarding.gateway-config.ts
index d7dceae24..f85e6a0b5 100644
--- a/src/wizard/onboarding.gateway-config.ts
+++ b/src/wizard/onboarding.gateway-config.ts
@@ -9,6 +9,7 @@ import type {
WizardFlow,
} from "./onboarding.types.js";
import type { WizardPrompter } from "./prompts.js";
+import { t } from "./i18n.js";
type ConfigureGatewayOptions = {
flow: WizardFlow;
@@ -35,29 +36,29 @@ export async function configureGatewayForOnboarding(
flow === "quickstart"
? quickstartGateway.port
: Number.parseInt(
- String(
- await prompter.text({
- message: "Gateway port",
- initialValue: String(localPort),
- validate: (value) => (Number.isFinite(Number(value)) ? undefined : "Invalid port"),
- }),
- ),
- 10,
- );
+ String(
+ await prompter.text({
+ message: t("onboarding.gatewayConfig.port"),
+ initialValue: String(localPort),
+ validate: (value) => (Number.isFinite(Number(value)) ? undefined : t("onboarding.gatewayConfig.invalidPort")),
+ }),
+ ),
+ 10,
+ );
let bind = (
flow === "quickstart"
? quickstartGateway.bind
: ((await prompter.select({
- message: "Gateway bind",
- options: [
- { value: "loopback", label: "Loopback (127.0.0.1)" },
- { value: "lan", label: "LAN (0.0.0.0)" },
- { value: "tailnet", label: "Tailnet (Tailscale IP)" },
- { value: "auto", label: "Auto (Loopback → LAN)" },
- { value: "custom", label: "Custom IP" },
- ],
- })) as "loopback" | "lan" | "auto" | "custom" | "tailnet")
+ message: t("onboarding.gatewayConfig.bind"),
+ options: [
+ { value: "loopback", label: t("onboarding.gateway.bindLoopback") },
+ { value: "lan", label: t("onboarding.gateway.bindLan") },
+ { value: "tailnet", label: t("onboarding.gateway.bindTailnet") },
+ { value: "auto", label: t("onboarding.gateway.bindAuto") },
+ { value: "custom", label: t("onboarding.gateway.bindCustom") },
+ ],
+ })) as "loopback" | "lan" | "auto" | "custom" | "tailnet")
) as "loopback" | "lan" | "auto" | "custom" | "tailnet";
let customBindHost = quickstartGateway.customBindHost;
@@ -65,14 +66,14 @@ export async function configureGatewayForOnboarding(
const needsPrompt = flow !== "quickstart" || !customBindHost;
if (needsPrompt) {
const input = await prompter.text({
- message: "Custom IP address",
+ message: t("onboarding.gatewayConfig.customIp"),
placeholder: "192.168.1.100",
initialValue: customBindHost ?? "",
validate: (value) => {
- if (!value) return "IP address is required for custom bind mode";
+ if (!value) return t("onboarding.gatewayConfig.customIpRequired");
const trimmed = value.trim();
const parts = trimmed.split(".");
- if (parts.length !== 4) return "Invalid IPv4 address (e.g., 192.168.1.100)";
+ if (parts.length !== 4) return t("onboarding.gatewayConfig.invalidIp");
if (
parts.every((part) => {
const n = parseInt(part, 10);
@@ -80,7 +81,7 @@ export async function configureGatewayForOnboarding(
})
)
return undefined;
- return "Invalid IPv4 address (each octet must be 0-255)";
+ return t("onboarding.gatewayConfig.invalidIpOctet");
},
});
customBindHost = typeof input === "string" ? input.trim() : undefined;
@@ -91,38 +92,38 @@ export async function configureGatewayForOnboarding(
flow === "quickstart"
? quickstartGateway.authMode
: ((await prompter.select({
- message: "Gateway auth",
- options: [
- {
- value: "token",
- label: "Token",
- hint: "Recommended default (local + remote)",
- },
- { value: "password", label: "Password" },
- ],
- initialValue: "token",
- })) as GatewayAuthChoice)
+ message: t("onboarding.gatewayConfig.auth"),
+ options: [
+ {
+ value: "token",
+ label: t("onboarding.gatewayConfig.authToken"),
+ hint: t("onboarding.gatewayConfig.authTokenHint"),
+ },
+ { value: "password", label: t("onboarding.gatewayConfig.authPassword") },
+ ],
+ initialValue: "token",
+ })) as GatewayAuthChoice)
) as GatewayAuthChoice;
const tailscaleMode = (
flow === "quickstart"
? quickstartGateway.tailscaleMode
: ((await prompter.select({
- message: "Tailscale exposure",
- options: [
- { value: "off", label: "Off", hint: "No Tailscale exposure" },
- {
- value: "serve",
- label: "Serve",
- hint: "Private HTTPS for your tailnet (devices on Tailscale)",
- },
- {
- value: "funnel",
- label: "Funnel",
- hint: "Public HTTPS via Tailscale Funnel (internet)",
- },
- ],
- })) as "off" | "serve" | "funnel")
+ message: t("onboarding.gatewayConfig.tsExposure"),
+ options: [
+ { value: "off", label: t("onboarding.gatewayConfig.tsOff"), hint: t("onboarding.gatewayConfig.tsOffHint") },
+ {
+ value: "serve",
+ label: t("onboarding.gatewayConfig.tsServe"),
+ hint: t("onboarding.gatewayConfig.tsServeHint"),
+ },
+ {
+ value: "funnel",
+ label: t("onboarding.gatewayConfig.tsFunnel"),
+ hint: t("onboarding.gatewayConfig.tsFunnelHint"),
+ },
+ ],
+ })) as "off" | "serve" | "funnel")
) as "off" | "serve" | "funnel";
// Detect Tailscale binary before proceeding with serve/funnel setup.
@@ -130,14 +131,8 @@ export async function configureGatewayForOnboarding(
const tailscaleBin = await findTailscaleBinary();
if (!tailscaleBin) {
await prompter.note(
- [
- "Tailscale binary not found in PATH or /Applications.",
- "Ensure Tailscale is installed from:",
- " https://tailscale.com/download/mac",
- "",
- "You can continue setup, but serve/funnel will fail at runtime.",
- ].join("\n"),
- "Tailscale Warning",
+ t("onboarding.gatewayConfig.tsNotFound"),
+ t("onboarding.gatewayConfig.tsWarningTitle"),
);
}
}
@@ -145,14 +140,14 @@ export async function configureGatewayForOnboarding(
let tailscaleResetOnExit = flow === "quickstart" ? quickstartGateway.tailscaleResetOnExit : false;
if (tailscaleMode !== "off" && flow !== "quickstart") {
await prompter.note(
- ["Docs:", "https://docs.openclaw.ai/gateway/tailscale", "https://docs.openclaw.ai/web"].join(
+ [t("onboarding.finalize.healthDocsPrefix"), "https://docs.openclaw.ai/gateway/tailscale", "https://docs.openclaw.ai/web"].join(
"\n",
),
- "Tailscale",
+ t("onboarding.gateway.tailscale"),
);
tailscaleResetOnExit = Boolean(
await prompter.confirm({
- message: "Reset Tailscale serve/funnel on exit?",
+ message: t("onboarding.gatewayConfig.tsResetConfirm"),
initialValue: false,
}),
);
@@ -162,13 +157,13 @@ export async function configureGatewayForOnboarding(
// - Tailscale wants bind=loopback so we never expose a non-loopback server + tailscale serve/funnel at once.
// - Funnel requires password auth.
if (tailscaleMode !== "off" && bind !== "loopback") {
- await prompter.note("Tailscale requires bind=loopback. Adjusting bind to loopback.", "Note");
+ await prompter.note(t("onboarding.gatewayConfig.tsAdjustBind"), t("onboarding.gateway.tailscale"));
bind = "loopback";
customBindHost = undefined;
}
if (tailscaleMode === "funnel" && authMode !== "password") {
- await prompter.note("Tailscale funnel requires password auth.", "Note");
+ await prompter.note(t("onboarding.gatewayConfig.tsFunnelAuth"), t("onboarding.gateway.tailscale"));
authMode = "password";
}
@@ -178,8 +173,8 @@ export async function configureGatewayForOnboarding(
gatewayToken = quickstartGateway.token ?? randomToken();
} else {
const tokenInput = await prompter.text({
- message: "Gateway token (blank to generate)",
- placeholder: "Needed for multi-machine or non-loopback access",
+ message: t("onboarding.gatewayConfig.tokenPlaceholder"),
+ placeholder: t("onboarding.gatewayConfig.tokenHint"),
initialValue: quickstartGateway.token ?? "",
});
gatewayToken = String(tokenInput).trim() || randomToken();
@@ -191,9 +186,9 @@ export async function configureGatewayForOnboarding(
flow === "quickstart" && quickstartGateway.password
? quickstartGateway.password
: await prompter.text({
- message: "Gateway password",
- validate: (value) => (value?.trim() ? undefined : "Required"),
- });
+ message: t("onboarding.gatewayConfig.passwordLabel"),
+ validate: (value) => (value?.trim() ? undefined : t("onboarding.gatewayConfig.passwordRequired")),
+ });
nextConfig = {
...nextConfig,
gateway: {
diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts
index ef2e349c6..7989b3f00 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 { t } from "./i18n.js";
import { finalizeOnboardingWizard } from "./onboarding.finalize.js";
import { configureGatewayForOnboarding } from "./onboarding.gateway-config.js";
import type { QuickstartGatewayDefaults, WizardFlow } from "./onboarding.types.js";
@@ -50,33 +51,12 @@ async function requireRiskAcknowledgement(params: {
if (params.opts.acceptRisk === true) return;
await params.prompter.note(
- [
- "Security warning — please read.",
- "",
- "OpenClaw 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 OpenClaw.",
- "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:",
- "openclaw security audit --deep",
- "openclaw security audit --fix",
- "",
- "Must read: https://docs.openclaw.ai/gateway/security",
- ].join("\n"),
- "Security",
+ t("onboarding.security.note"),
+ t("onboarding.security.title"),
);
const ok = await params.prompter.confirm({
- message: "I understand this is powerful and inherently risky. Continue?",
+ message: t("onboarding.security.confirm"),
initialValue: false,
});
if (!ok) {
@@ -90,14 +70,14 @@ export async function runOnboardingWizard(
prompter: WizardPrompter,
) {
printWizardHeader(runtime);
- await prompter.intro("OpenClaw onboarding");
+ await prompter.intro(t("onboarding.intro"));
await requireRiskAcknowledgement({ opts, prompter });
const snapshot = await readConfigFileSnapshot();
let baseConfig: OpenClawConfig = snapshot.valid ? snapshot.config : {};
if (snapshot.exists && !snapshot.valid) {
- await prompter.note(summarizeExistingConfig(baseConfig), "Invalid config");
+ await prompter.note(summarizeExistingConfig(baseConfig), t("onboarding.config.invalid"));
if (snapshot.issues.length > 0) {
await prompter.note(
[
@@ -105,18 +85,18 @@ export async function runOnboardingWizard(
"",
"Docs: https://docs.openclaw.ai/gateway/configuration",
].join("\n"),
- "Config issues",
+ t("onboarding.config.issues"),
);
}
await prompter.outro(
- `Config invalid. Run \`${formatCliCommand("openclaw doctor")}\` to repair it, then re-run onboarding.`,
+ t("onboarding.config.repair"),
);
runtime.exit(1);
return;
}
- const quickstartHint = `Configure details later via ${formatCliCommand("openclaw configure")}.`;
- const manualHint = "Configure port, network, Tailscale, and auth options.";
+ const quickstartHint = t("onboarding.flow.quickstartHint");
+ const manualHint = t("onboarding.flow.manualHint");
const explicitFlowRaw = opts.flow?.trim();
const normalizedExplicitFlow = explicitFlowRaw === "manual" ? "advanced" : explicitFlowRaw;
if (
@@ -124,7 +104,7 @@ export async function runOnboardingWizard(
normalizedExplicitFlow !== "quickstart" &&
normalizedExplicitFlow !== "advanced"
) {
- runtime.error("Invalid --flow (use quickstart, manual, or advanced).");
+ runtime.error(t("onboarding.flow.invalidFlow"));
runtime.exit(1);
return;
}
@@ -135,47 +115,47 @@ export async function runOnboardingWizard(
let flow: WizardFlow =
explicitFlow ??
((await prompter.select({
- message: "Onboarding mode",
+ message: t("onboarding.flow.modeSelect"),
options: [
- { value: "quickstart", label: "QuickStart", hint: quickstartHint },
- { value: "advanced", label: "Manual", hint: manualHint },
+ { value: "quickstart", label: t("onboarding.flow.quickstart"), hint: quickstartHint },
+ { value: "advanced", label: t("onboarding.flow.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",
+ t("onboarding.flow.remoteSwitch"),
+ t("onboarding.flow.quickstart"),
);
flow = "advanced";
}
if (snapshot.exists) {
- await prompter.note(summarizeExistingConfig(baseConfig), "Existing config detected");
+ await prompter.note(summarizeExistingConfig(baseConfig), t("onboarding.existingConfig.title"));
const action = (await prompter.select({
- message: "Config handling",
+ message: t("onboarding.existingConfig.action"),
options: [
- { value: "keep", label: "Use existing values" },
- { value: "modify", label: "Update values" },
- { value: "reset", label: "Reset" },
+ { value: "keep", label: t("onboarding.existingConfig.keep") },
+ { value: "modify", label: t("onboarding.existingConfig.modify") },
+ { value: "reset", label: t("onboarding.existingConfig.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("onboarding.existingConfig.resetScope"),
options: [
- { value: "config", label: "Config only" },
+ { value: "config", label: t("onboarding.existingConfig.scopeConfig") },
{
value: "config+creds+sessions",
- label: "Config + creds + sessions",
+ label: t("onboarding.existingConfig.scopeConfigCreds"),
},
{
value: "full",
- label: "Full reset (config + creds + sessions + workspace)",
+ label: t("onboarding.existingConfig.scopeFull"),
},
],
})) as ResetScope;
@@ -197,10 +177,10 @@ export async function runOnboardingWizard(
const bindRaw = baseConfig.gateway?.bind;
const bind =
bindRaw === "loopback" ||
- bindRaw === "lan" ||
- bindRaw === "auto" ||
- bindRaw === "custom" ||
- bindRaw === "tailnet"
+ bindRaw === "lan" ||
+ bindRaw === "auto" ||
+ bindRaw === "custom" ||
+ bindRaw === "tailnet"
? bindRaw
: "loopback";
@@ -237,41 +217,41 @@ export async function runOnboardingWizard(
if (flow === "quickstart") {
const formatBind = (value: "loopback" | "lan" | "auto" | "custom" | "tailnet") => {
- if (value === "loopback") return "Loopback (127.0.0.1)";
- if (value === "lan") return "LAN";
- if (value === "custom") return "Custom IP";
- if (value === "tailnet") return "Tailnet (Tailscale IP)";
- return "Auto";
+ if (value === "loopback") return t("onboarding.gateway.bindLoopback");
+ if (value === "lan") return t("onboarding.gateway.bindLan");
+ if (value === "custom") return t("onboarding.gateway.bindCustom");
+ if (value === "tailnet") return t("onboarding.gateway.bindTailnet");
+ return t("onboarding.gateway.bindAuto");
};
const formatAuth = (value: GatewayAuthChoice) => {
- if (value === "token") return "Token (default)";
- return "Password";
+ if (value === "token") return t("onboarding.gateway.authToken");
+ return t("onboarding.gateway.authPassword");
};
const formatTailscale = (value: "off" | "serve" | "funnel") => {
- if (value === "off") return "Off";
- if (value === "serve") return "Serve";
- return "Funnel";
+ if (value === "off") return t("onboarding.gateway.tsOff");
+ if (value === "serve") return t("onboarding.gateway.tsServe");
+ return t("onboarding.gateway.tsFunnel");
};
const quickstartLines = quickstartGateway.hasExisting
? [
- "Keeping your current gateway settings:",
- `Gateway port: ${quickstartGateway.port}`,
- `Gateway bind: ${formatBind(quickstartGateway.bind)}`,
- ...(quickstartGateway.bind === "custom" && quickstartGateway.customBindHost
- ? [`Gateway custom IP: ${quickstartGateway.customBindHost}`]
- : []),
- `Gateway auth: ${formatAuth(quickstartGateway.authMode)}`,
- `Tailscale exposure: ${formatTailscale(quickstartGateway.tailscaleMode)}`,
- "Direct to chat channels.",
- ]
+ t("onboarding.gateway.keepSettings"),
+ `${t("onboarding.gateway.port")}: ${quickstartGateway.port}`,
+ `${t("onboarding.gateway.bind")}: ${formatBind(quickstartGateway.bind)}`,
+ ...(quickstartGateway.bind === "custom" && quickstartGateway.customBindHost
+ ? [`${t("onboarding.gateway.bindCustom")}: ${quickstartGateway.customBindHost}`]
+ : []),
+ `${t("onboarding.gateway.auth")}: ${formatAuth(quickstartGateway.authMode)}`,
+ `${t("onboarding.gateway.tailscale")}: ${formatTailscale(quickstartGateway.tailscaleMode)}`,
+ t("onboarding.gateway.chatChannels"),
+ ]
: [
- `Gateway port: ${DEFAULT_GATEWAY_PORT}`,
- "Gateway bind: Loopback (127.0.0.1)",
- "Gateway auth: Token (default)",
- "Tailscale exposure: Off",
- "Direct to chat channels.",
- ];
- await prompter.note(quickstartLines.join("\n"), "QuickStart");
+ `${t("onboarding.gateway.port")}: ${DEFAULT_GATEWAY_PORT}`,
+ `${t("onboarding.gateway.bind")}: ${t("onboarding.gateway.bindLoopback")}`,
+ `${t("onboarding.gateway.auth")}: ${t("onboarding.gateway.authToken")}`,
+ `${t("onboarding.gateway.tailscale")}: ${t("onboarding.gateway.tsOff")}`,
+ t("onboarding.gateway.chatChannels"),
+ ];
+ await prompter.note(quickstartLines.join("\n"), t("onboarding.flow.quickstart"));
}
const localPort = resolveGatewayPort(baseConfig);
@@ -284,9 +264,9 @@ export async function runOnboardingWizard(
const remoteUrl = baseConfig.gateway?.remote?.url?.trim() ?? "";
const remoteProbe = remoteUrl
? await probeGatewayReachable({
- url: remoteUrl,
- token: baseConfig.gateway?.remote?.token,
- })
+ url: remoteUrl,
+ token: baseConfig.gateway?.remote?.token,
+ })
: null;
const mode =
@@ -294,33 +274,33 @@ export async function runOnboardingWizard(
(flow === "quickstart"
? "local"
: ((await prompter.select({
- message: "What do you want to set up?",
- options: [
- {
- value: "local",
- label: "Local gateway (this machine)",
- hint: localProbe.ok
- ? `Gateway reachable (${localUrl})`
- : `No gateway detected (${localUrl})`,
- },
- {
- value: "remote",
- label: "Remote gateway (info-only)",
- hint: !remoteUrl
- ? "No remote URL configured yet"
- : remoteProbe?.ok
- ? `Gateway reachable (${remoteUrl})`
- : `Configured but unreachable (${remoteUrl})`,
- },
- ],
- })) as OnboardMode));
+ message: t("onboarding.setup.question"),
+ options: [
+ {
+ value: "local",
+ label: t("onboarding.setup.local"),
+ hint: localProbe.ok
+ ? `${t("onboarding.setup.localOk")} (${localUrl})`
+ : `${t("onboarding.setup.localFail")} (${localUrl})`,
+ },
+ {
+ value: "remote",
+ label: t("onboarding.setup.remote"),
+ hint: !remoteUrl
+ ? t("onboarding.setup.remoteNoUrl")
+ : remoteProbe?.ok
+ ? `${t("onboarding.setup.remoteOk")} (${remoteUrl})`
+ : `${t("onboarding.setup.remoteFail")} (${remoteUrl})`,
+ },
+ ],
+ })) as OnboardMode));
if (mode === "remote") {
let nextConfig = await promptRemoteGatewayConfig(baseConfig, prompter);
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
await writeConfigFile(nextConfig);
logConfigUpdated(runtime);
- await prompter.outro("Remote gateway configured.");
+ await prompter.outro(t("onboarding.setup.remoteDone"));
return;
}
@@ -329,9 +309,9 @@ export async function runOnboardingWizard(
(flow === "quickstart"
? (baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE)
: await prompter.text({
- message: "Workspace directory",
- initialValue: baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE,
- }));
+ message: t("onboarding.setup.workspaceDir"),
+ initialValue: baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE,
+ }));
const workspaceDir = resolveUserPath(workspaceInput.trim() || DEFAULT_WORKSPACE);
@@ -403,13 +383,13 @@ export async function runOnboardingWizard(
const settings = gateway.settings;
if (opts.skipChannels ?? opts.skipProviders) {
- await prompter.note("Skipping channel setup.", "Channels");
+ await prompter.note(t("onboarding.setup.skippingChannels"), t("onboarding.gateway.chatChannels"));
} else {
const quickstartAllowFromChannels =
flow === "quickstart"
? listChannelPlugins()
- .filter((plugin) => plugin.meta.quickstartAllowFrom)
- .map((plugin) => plugin.id)
+ .filter((plugin) => plugin.meta.quickstartAllowFrom)
+ .map((plugin) => plugin.id)
: [];
nextConfig = await setupChannels(nextConfig, runtime, prompter, {
allowSignalInstall: true,
@@ -427,7 +407,7 @@ export async function runOnboardingWizard(
});
if (opts.skipSkills) {
- await prompter.note("Skipping skills setup.", "Skills");
+ await prompter.note(t("onboarding.setup.skippingSkills"), t("onboarding.setup.skills"));
} else {
nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter);
}
diff --git a/ui/package-lock.json b/ui/package-lock.json
new file mode 100644
index 000000000..230df7e09
--- /dev/null
+++ b/ui/package-lock.json
@@ -0,0 +1,1686 @@
+{
+ "name": "openclaw-control-ui",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "openclaw-control-ui",
+ "dependencies": {
+ "@noble/ed25519": "3.0.0",
+ "dompurify": "^3.3.1",
+ "lit": "^3.3.2",
+ "marked": "^17.0.1",
+ "vite": "7.3.1"
+ },
+ "devDependencies": {
+ "@vitest/browser-playwright": "4.0.18",
+ "playwright": "^1.58.0",
+ "typescript": "^5.9.3",
+ "vitest": "4.0.18"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
+ "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
+ "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
+ "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
+ "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
+ "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
+ "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
+ "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
+ "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
+ "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
+ "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
+ "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
+ "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
+ "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
+ "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
+ "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
+ "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
+ "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
+ "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
+ "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
+ "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
+ "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@lit-labs/ssr-dom-shim": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz",
+ "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@lit/reactive-element": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz",
+ "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.5.0"
+ }
+ },
+ "node_modules/@noble/ed25519": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.0.0.tgz",
+ "integrity": "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.29",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz",
+ "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz",
+ "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz",
+ "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz",
+ "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz",
+ "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz",
+ "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz",
+ "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz",
+ "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz",
+ "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz",
+ "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz",
+ "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz",
+ "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz",
+ "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz",
+ "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz",
+ "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz",
+ "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz",
+ "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz",
+ "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz",
+ "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz",
+ "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz",
+ "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz",
+ "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz",
+ "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz",
+ "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz",
+ "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT"
+ },
+ "node_modules/@vitest/browser": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.18.tgz",
+ "integrity": "sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/mocker": "4.0.18",
+ "@vitest/utils": "4.0.18",
+ "magic-string": "^0.30.21",
+ "pixelmatch": "7.1.0",
+ "pngjs": "^7.0.0",
+ "sirv": "^3.0.2",
+ "tinyrainbow": "^3.0.3",
+ "ws": "^8.18.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "vitest": "4.0.18"
+ }
+ },
+ "node_modules/@vitest/browser-playwright": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.18.tgz",
+ "integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/browser": "4.0.18",
+ "@vitest/mocker": "4.0.18",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "playwright": "*",
+ "vitest": "4.0.18"
+ },
+ "peerDependenciesMeta": {
+ "playwright": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
+ "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.0.18",
+ "@vitest/utils": "4.0.18",
+ "chai": "^6.2.1",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
+ "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.0.18",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
+ "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
+ "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.0.18",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
+ "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.0.18",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
+ "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
+ "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.0.18",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/dompurify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
+ "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
+ "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.2",
+ "@esbuild/android-arm": "0.27.2",
+ "@esbuild/android-arm64": "0.27.2",
+ "@esbuild/android-x64": "0.27.2",
+ "@esbuild/darwin-arm64": "0.27.2",
+ "@esbuild/darwin-x64": "0.27.2",
+ "@esbuild/freebsd-arm64": "0.27.2",
+ "@esbuild/freebsd-x64": "0.27.2",
+ "@esbuild/linux-arm": "0.27.2",
+ "@esbuild/linux-arm64": "0.27.2",
+ "@esbuild/linux-ia32": "0.27.2",
+ "@esbuild/linux-loong64": "0.27.2",
+ "@esbuild/linux-mips64el": "0.27.2",
+ "@esbuild/linux-ppc64": "0.27.2",
+ "@esbuild/linux-riscv64": "0.27.2",
+ "@esbuild/linux-s390x": "0.27.2",
+ "@esbuild/linux-x64": "0.27.2",
+ "@esbuild/netbsd-arm64": "0.27.2",
+ "@esbuild/netbsd-x64": "0.27.2",
+ "@esbuild/openbsd-arm64": "0.27.2",
+ "@esbuild/openbsd-x64": "0.27.2",
+ "@esbuild/openharmony-arm64": "0.27.2",
+ "@esbuild/sunos-x64": "0.27.2",
+ "@esbuild/win32-arm64": "0.27.2",
+ "@esbuild/win32-ia32": "0.27.2",
+ "@esbuild/win32-x64": "0.27.2"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/lit": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz",
+ "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit/reactive-element": "^2.1.0",
+ "lit-element": "^4.2.0",
+ "lit-html": "^3.3.0"
+ }
+ },
+ "node_modules/lit-element": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz",
+ "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.5.0",
+ "@lit/reactive-element": "^2.1.0",
+ "lit-html": "^3.3.0"
+ }
+ },
+ "node_modules/lit-html": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz",
+ "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/marked": {
+ "version": "17.0.1",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz",
+ "integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+ "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pixelmatch": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz",
+ "integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "pngjs": "^7.0.0"
+ },
+ "bin": {
+ "pixelmatch": "bin/pixelmatch"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.58.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz",
+ "integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.58.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.58.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz",
+ "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/pngjs": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
+ "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.19.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.57.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz",
+ "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.57.0",
+ "@rollup/rollup-android-arm64": "4.57.0",
+ "@rollup/rollup-darwin-arm64": "4.57.0",
+ "@rollup/rollup-darwin-x64": "4.57.0",
+ "@rollup/rollup-freebsd-arm64": "4.57.0",
+ "@rollup/rollup-freebsd-x64": "4.57.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.57.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.57.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.57.0",
+ "@rollup/rollup-linux-arm64-musl": "4.57.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.57.0",
+ "@rollup/rollup-linux-loong64-musl": "4.57.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.57.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.57.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.57.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.57.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.57.0",
+ "@rollup/rollup-linux-x64-gnu": "4.57.0",
+ "@rollup/rollup-linux-x64-musl": "4.57.0",
+ "@rollup/rollup-openbsd-x64": "4.57.0",
+ "@rollup/rollup-openharmony-arm64": "4.57.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.57.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.57.0",
+ "@rollup/rollup-win32-x64-gnu": "4.57.0",
+ "@rollup/rollup-win32-x64-msvc": "4.57.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/sirv": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
+ "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
+ "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
+ "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/totalist": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
+ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.0.18",
+ "@vitest/mocker": "4.0.18",
+ "@vitest/pretty-format": "4.0.18",
+ "@vitest/runner": "4.0.18",
+ "@vitest/snapshot": "4.0.18",
+ "@vitest/spy": "4.0.18",
+ "@vitest/utils": "4.0.18",
+ "es-module-lexer": "^1.7.0",
+ "expect-type": "^1.2.2",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^3.10.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.0.3",
+ "vite": "^6.0.0 || ^7.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.0.18",
+ "@vitest/browser-preview": "4.0.18",
+ "@vitest/browser-webdriverio": "4.0.18",
+ "@vitest/ui": "4.0.18",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts
index d692693cb..188d18a41 100644
--- a/ui/src/ui/app-render.ts
+++ b/ui/src/ui/app-render.ts
@@ -82,6 +82,7 @@ import {
import { loadCronRuns, toggleCronJob, runCronJob, removeCronJob, addCronJob } from "./controllers/cron";
import { loadDebug, callDebugMethod } from "./controllers/debug";
import { loadLogs } from "./controllers/logs";
+import { t } from "./i18n";
const AVATAR_DATA_RE = /^data:/i;
const AVATAR_HTTP_RE = /^https?:\/\//i;
@@ -105,7 +106,7 @@ export function renderApp(state: AppViewState) {
const presenceCount = state.presenceEntries.length;
const sessionsCount = state.sessionsResult?.count ?? null;
const cronNext = state.cronStatus?.nextWakeAtMs ?? null;
- const chatDisabledReason = state.connected ? null : "Disconnected from gateway.";
+ const chatDisabledReason = state.connected ? null : t("common.disconnected");
const isChat = state.tab === "chat";
const chatFocus = isChat && (state.settings.chatFocusMode || state.onboarding);
const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
@@ -119,12 +120,12 @@ export function renderApp(state: AppViewState) {
@@ -133,39 +134,43 @@ export function renderApp(state: AppViewState) {
-
OPENCLAW
-
Gateway Dashboard
+
${t("app.title")}
+
${t("app.subtitle")}
- Health
- ${state.connected ? "OK" : "Offline"}
+ ${t("app.health")}
+ ${state.connected ? t("app.healthOk") : t("app.healthOffline")}
${renderThemeToggle(state)}