diff --git a/ui/src/i18n/index.ts b/ui/src/i18n/index.ts new file mode 100644 index 000000000..95171d1bb --- /dev/null +++ b/ui/src/i18n/index.ts @@ -0,0 +1,134 @@ +/** + * Clawdbot Control UI - Internationalization (i18n) Module + * + * A lightweight i18n system for Lit web components. + * Supports dynamic locale switching and nested translation keys. + */ + +import en from "./locales/en.json" with { type: "json" }; +import zh from "./locales/zh.json" with { type: "json" }; + +export type Locale = "en" | "zh"; + +export type TranslationDict = Record; + +const locales: Record = { en, zh }; + +// Current locale state +let currentLocale: Locale = "en"; + +// Subscribers for locale changes +const subscribers = new Set<() => void>(); + +/** + * Get the current locale + */ +export function getLocale(): Locale { + return currentLocale; +} + +/** + * Set the current locale and notify subscribers + */ +export function setLocale(locale: Locale): void { + if (locale === currentLocale) return; + if (!locales[locale]) { + console.warn(`[i18n] Unknown locale: ${locale}, falling back to 'en'`); + locale = "en"; + } + currentLocale = locale; + localStorage.setItem("clawdbot-locale", locale); + subscribers.forEach((cb) => cb()); +} + +/** + * Initialize locale from localStorage or browser preference + */ +export function initLocale(): void { + const saved = localStorage.getItem("clawdbot-locale") as Locale | null; + if (saved && locales[saved]) { + currentLocale = saved; + return; + } + + // Auto-detect from browser + const browserLang = navigator.language.toLowerCase(); + if (browserLang.startsWith("zh")) { + currentLocale = "zh"; + } else { + currentLocale = "en"; + } +} + +/** + * Subscribe to locale changes (for Lit component updates) + */ +export function subscribeLocale(callback: () => void): () => void { + subscribers.add(callback); + return () => subscribers.delete(callback); +} + +/** + * Get a translation by key (supports nested keys with dot notation) + * Example: t("nav.chat") or t("common.save") + */ +export function t(key: string, fallback?: string): string { + const dict = locales[currentLocale] ?? locales.en; + const keys = key.split("."); + let value: string | TranslationDict | undefined = dict; + + for (const k of keys) { + if (typeof value === "object" && value !== null) { + value = value[k]; + } else { + value = undefined; + break; + } + } + + if (typeof value === "string") return value; + + // Fallback to English if not found in current locale + if (currentLocale !== "en") { + let enValue: string | TranslationDict | undefined = locales.en; + for (const k of keys) { + if (typeof enValue === "object" && enValue !== null) { + enValue = enValue[k]; + } else { + enValue = undefined; + break; + } + } + if (typeof enValue === "string") return enValue; + } + + return fallback ?? key; +} + +/** + * Template literal tag for translations + * Usage: i18n`nav.chat` or i18n`common.save` + */ +export function i18n( + strings: TemplateStringsArray, + ...values: unknown[] +): string { + const key = strings.reduce( + (acc, str, i) => acc + str + (values[i] ?? ""), + "", + ); + return t(key); +} + +/** + * Get all available locales with display names + */ +export function getAvailableLocales(): Array<{ code: Locale; name: string }> { + return [ + { code: "en", name: "English" }, + { code: "zh", name: "中文" }, + ]; +} + +// Initialize on load +initLocale(); diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json new file mode 100644 index 000000000..8107a13b4 --- /dev/null +++ b/ui/src/i18n/locales/en.json @@ -0,0 +1,205 @@ +{ + "app": { + "title": "Clawdbot Control", + "loading": "Loading…", + "error": "Error", + "version": "Version" + }, + "nav": { + "groups": { + "chat": "Chat", + "control": "Control", + "agent": "Agent", + "settings": "Settings" + }, + "tabs": { + "overview": "Overview", + "channels": "Channels", + "instances": "Instances", + "sessions": "Sessions", + "cron": "Cron Jobs", + "skills": "Skills", + "nodes": "Nodes", + "chat": "Chat", + "config": "Config", + "debug": "Debug", + "logs": "Logs" + }, + "subtitles": { + "overview": "Gateway status, entry points, and a fast health read.", + "channels": "Manage channels and settings.", + "instances": "Presence beacons from connected clients and nodes.", + "sessions": "Inspect active sessions and adjust per-session defaults.", + "cron": "Schedule wakeups and recurring agent runs.", + "skills": "Manage skill availability and API key injection.", + "nodes": "Paired devices, capabilities, and command exposure.", + "chat": "Direct gateway chat session for quick interventions.", + "config": "Edit ~/.clawdbot/clawdbot.json safely.", + "debug": "Gateway snapshots, events, and manual RPC calls.", + "logs": "Live tail of the gateway file logs." + } + }, + "sidebar": { + "collapse": "Collapse sidebar", + "expand": "Expand sidebar", + "close": "Close sidebar", + "exitFocus": "Exit focus mode" + }, + "chat": { + "placeholder": "Message (↩ to send, Shift+↩ for line breaks)", + "connect": "Connect to the gateway to start chatting…", + "agentMessage": "Agent message", + "agentMessageRequired": "Agent message required.", + "assistant": "Assistant", + "agent": "Agent", + "copyMarkdown": "Copy as markdown", + "copied": "Copied", + "copyFailed": "Copy failed", + "attach": "Attach", + "send": "Send" + }, + "status": { + "health": "Health", + "ok": "OK", + "offline": "Offline", + "connected": "Connected", + "disconnected": "Disconnected", + "disconnectedGateway": "Disconnected from gateway.", + "active": "Active", + "enabled": "Enabled", + "disabled": "Disabled", + "installed": "Installed", + "loading": "Loading…", + "applying": "Applying…", + "installing": "Installing…", + "importing": "Importing..." + }, + "actions": { + "apply": "Apply", + "save": "Save", + "cancel": "Cancel", + "edit": "Edit", + "delete": "Delete", + "add": "Add", + "addJob": "Add job", + "enable": "Enable", + "disable": "Disable", + "copy": "Copy", + "retry": "Retry", + "refresh": "Refresh", + "loadConfig": "Load config", + "loadApprovals": "Load approvals", + "importRelays": "Import from Relays", + "editProfile": "Edit Profile" + }, + "config": { + "categories": { + "gateway": "Gateway", + "agents": "Agents", + "authentication": "Authentication", + "models": "Models", + "tools": "Tools", + "channels": "Channels", + "messages": "Messages", + "commands": "Commands", + "browser": "Browser", + "audio": "Audio", + "broadcast": "Broadcast", + "hooks": "Hooks", + "logging": "Logging", + "skills": "Skills", + "plugins": "Plugins", + "discovery": "Discovery", + "bindings": "Bindings", + "environment": "Environment", + "canvasHost": "Canvas Host", + "cron": "Cron" + }, + "descriptions": { + "gateway": "Gateway server settings (port, auth, binding)", + "agents": "Agent configurations, models, and identities", + "authentication": "API keys and authentication profiles", + "models": "AI model configurations and providers", + "audio": "Audio input/output settings", + "browser": "Browser automation settings", + "broadcast": "Broadcast and notification settings", + "update": "Auto-update settings and release channel", + "messages": "Message handling and routing settings", + "logging": "Log levels and output configuration", + "canvasHost": "Canvas rendering and display", + "environment": "Environment variables passed to the gateway process" + }, + "noSettings": "No settings in this section", + "hideAdvanced": "Hide Advanced", + "showAdvanced": "Show Advanced", + "hashMissing": "Config hash missing; reload and retry.", + "approvalHashMissing": "Exec approvals hash missing; reload and retry." + }, + "channels": { + "whatsapp": "WhatsApp", + "telegram": "Telegram", + "discord": "Discord", + "slack": "Slack", + "signal": "Signal", + "imessage": "iMessage", + "bluebubbles": "BlueBubbles", + "msteams": "MS Teams", + "mattermost": "Mattermost" + }, + "security": { + "host": "Host", + "full": "Full", + "allowlist": "Allowlist", + "deny": "Deny", + "ask": "Ask", + "always": "Always", + "defaultSecurityMode": "Default security mode.", + "defaultPromptPolicy": "Default prompt policy.", + "allowSkillExecutables": "Allow skill executables listed by the Gateway." + }, + "cron": { + "cronRequired": "Cron expression required.", + "invalidInterval": "Invalid interval amount.", + "invalidRunTime": "Invalid run time." + }, + "profile": { + "displayName": "Display Name", + "avatarUrl": "Avatar URL", + "bannerUrl": "Banner URL", + "bio": "Bio", + "bioPlaceholder": "A brief bio or description", + "avatarPlaceholder": "HTTPS URL to your profile picture", + "bannerPlaceholder": "HTTPS URL to a banner image", + "lightningAddress": "Lightning Address", + "lightningPlaceholder": "Lightning address for tips (LUD-16)", + "picture": "Profile picture", + "picturePreview": "Profile picture preview", + "importedRelays": "Profile imported from relays. Review and publish.", + "imported": "Profile imported. Review and publish.", + "publishFailed": "Profile publish failed on all relays." + }, + "theme": { + "dark": "Dark", + "light": "Light", + "darkTheme": "Dark theme", + "lightTheme": "Light theme" + }, + "common": { + "key": "Key", + "value": "JSON value", + "enter": "Enter", + "apiKeySaved": "API key saved", + "loggedOut": "Logged out.", + "disabledOnboarding": "Disabled during onboarding", + "newDeviceToken": "New device token (copy and store securely):", + "environmentVariables": "Environment Variables", + "docsNewTab": "Docs (opens in new tab)", + "authDocsNewTab": "Control UI auth docs (opens in new tab)", + "insecureDocsNewTab": "Insecure HTTP docs (opens in new tab)" + }, + "language": { + "select": "Language", + "en": "English", + "zh": "中文" + } +} diff --git a/ui/src/i18n/locales/zh.json b/ui/src/i18n/locales/zh.json new file mode 100644 index 000000000..8f210a3a5 --- /dev/null +++ b/ui/src/i18n/locales/zh.json @@ -0,0 +1,205 @@ +{ + "app": { + "title": "Clawdbot 控制台", + "loading": "加载中…", + "error": "错误", + "version": "版本" + }, + "nav": { + "groups": { + "chat": "聊天", + "control": "控制", + "agent": "智能体", + "settings": "设置" + }, + "tabs": { + "overview": "概览", + "channels": "渠道", + "instances": "实例", + "sessions": "会话", + "cron": "定时任务", + "skills": "技能", + "nodes": "节点", + "chat": "聊天", + "config": "配置", + "debug": "调试", + "logs": "日志" + }, + "subtitles": { + "overview": "网关状态、入口点和快速健康检查。", + "channels": "管理渠道和设置。", + "instances": "来自已连接客户端和节点的在线状态。", + "sessions": "检查活动会话并调整每个会话的默认设置。", + "cron": "安排唤醒和定期运行智能体任务。", + "skills": "管理技能可用性和 API 密钥注入。", + "nodes": "配对设备、功能和命令暴露。", + "chat": "用于快速操作的直接网关聊天会话。", + "config": "安全编辑 ~/.clawdbot/clawdbot.json", + "debug": "网关快照、事件和手动 RPC 调用。", + "logs": "实时查看网关日志文件。" + } + }, + "sidebar": { + "collapse": "折叠侧边栏", + "expand": "展开侧边栏", + "close": "关闭侧边栏", + "exitFocus": "退出专注模式" + }, + "chat": { + "placeholder": "消息(↩ 发送,Shift+↩ 换行)", + "connect": "连接到网关开始聊天…", + "agentMessage": "智能体消息", + "agentMessageRequired": "需要智能体消息。", + "assistant": "助手", + "agent": "智能体", + "copyMarkdown": "复制为 Markdown", + "copied": "已复制", + "copyFailed": "复制失败", + "attach": "附加文件", + "send": "发送" + }, + "status": { + "health": "状态", + "ok": "正常", + "offline": "离线", + "connected": "已连接", + "disconnected": "已断开", + "disconnectedGateway": "已从网关断开。", + "active": "活跃", + "enabled": "已启用", + "disabled": "已禁用", + "installed": "已安装", + "loading": "加载中…", + "applying": "应用中…", + "installing": "安装中…", + "importing": "导入中..." + }, + "actions": { + "apply": "应用", + "save": "保存", + "cancel": "取消", + "edit": "编辑", + "delete": "删除", + "add": "添加", + "addJob": "添加任务", + "enable": "启用", + "disable": "禁用", + "copy": "复制", + "retry": "重试", + "refresh": "刷新", + "loadConfig": "加载配置", + "loadApprovals": "加载审批", + "importRelays": "从中继导入", + "editProfile": "编辑资料" + }, + "config": { + "categories": { + "gateway": "网关", + "agents": "智能体", + "authentication": "认证", + "models": "模型", + "tools": "工具", + "channels": "渠道", + "messages": "消息", + "commands": "命令", + "browser": "浏览器", + "audio": "音频", + "broadcast": "广播", + "hooks": "钩子", + "logging": "日志记录", + "skills": "技能", + "plugins": "插件", + "discovery": "发现", + "bindings": "绑定", + "environment": "环境变量", + "canvasHost": "画布主机", + "cron": "定时任务" + }, + "descriptions": { + "gateway": "网关服务器设置(端口、认证、绑定)", + "agents": "智能体配置、模型和身份", + "authentication": "API 密钥和认证配置", + "models": "AI 模型配置和提供商", + "audio": "音频输入/输出设置", + "browser": "浏览器自动化设置", + "broadcast": "广播和通知设置", + "update": "自动更新设置和发布渠道", + "messages": "消息处理和路由设置", + "logging": "日志级别和输出配置", + "canvasHost": "画布渲染和显示", + "environment": "传递给网关进程的环境变量" + }, + "noSettings": "此部分没有设置", + "hideAdvanced": "隐藏高级选项", + "showAdvanced": "显示高级选项", + "hashMissing": "配置哈希缺失;请重新加载并重试。", + "approvalHashMissing": "执行审批哈希缺失;请重新加载并重试。" + }, + "channels": { + "whatsapp": "WhatsApp", + "telegram": "Telegram", + "discord": "Discord", + "slack": "Slack", + "signal": "Signal", + "imessage": "iMessage", + "bluebubbles": "BlueBubbles", + "msteams": "微软 Teams", + "mattermost": "Mattermost" + }, + "security": { + "host": "主机", + "full": "完全", + "allowlist": "白名单", + "deny": "拒绝", + "ask": "询问", + "always": "始终", + "defaultSecurityMode": "默认安全模式。", + "defaultPromptPolicy": "默认提示策略。", + "allowSkillExecutables": "允许网关列出的技能可执行文件。" + }, + "cron": { + "cronRequired": "需要 Cron 表达式。", + "invalidInterval": "无效的间隔数量。", + "invalidRunTime": "无效的运行时间。" + }, + "profile": { + "displayName": "显示名称", + "avatarUrl": "头像 URL", + "bannerUrl": "横幅 URL", + "bio": "简介", + "bioPlaceholder": "简短的个人介绍或描述", + "avatarPlaceholder": "您头像的 HTTPS URL", + "bannerPlaceholder": "横幅图片的 HTTPS URL", + "lightningAddress": "闪电网络地址", + "lightningPlaceholder": "用于打赏的闪电网络地址 (LUD-16)", + "picture": "头像", + "picturePreview": "头像预览", + "importedRelays": "已从中继导入资料。请检查并发布。", + "imported": "资料已导入。请检查并发布。", + "publishFailed": "资料在所有中继上发布失败。" + }, + "theme": { + "dark": "深色", + "light": "浅色", + "darkTheme": "深色主题", + "lightTheme": "浅色主题" + }, + "common": { + "key": "键", + "value": "JSON 值", + "enter": "输入", + "apiKeySaved": "API 密钥已保存", + "loggedOut": "已登出。", + "disabledOnboarding": "引导期间禁用", + "newDeviceToken": "新设备令牌(请复制并安全存储):", + "environmentVariables": "环境变量", + "docsNewTab": "文档(在新标签页打开)", + "authDocsNewTab": "控制台认证文档(在新标签页打开)", + "insecureDocsNewTab": "不安全 HTTP 文档(在新标签页打开)" + }, + "language": { + "select": "语言", + "en": "English", + "zh": "中文" + } +} diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index a088c33ff..161e63efe 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -9,8 +9,11 @@ import { pathForTab, subtitleForTab, titleForTab, + localizedGroupLabel, type Tab, } from "./navigation"; +import { t } from "../i18n/index.js"; +import "./components/language-switcher.js"; import { icons } from "./icons"; import type { UiSettings } from "./storage"; import type { ThemeMode } from "./theme"; @@ -140,10 +143,11 @@ export function renderApp(state: AppViewState) {
- Health - ${state.connected ? "OK" : "Offline"} + ${t("status.health", "Health")} + ${state.connected ? t("status.ok", "OK") : t("status.offline", "Offline")}
${renderThemeToggle(state)} +