feat(ui): add i18n internationalization support with Chinese translation
- Add i18n core module with locale management and translation helpers - Add English (en.json) and Chinese (zh.json) translation files - Add language-switcher component for the top bar - Update navigation.ts to use localized strings - Update app-render.ts to include language switcher and i18n - Update app.ts to subscribe to locale changes for reactivity Features: - Auto-detect browser language preference - Persist language choice in localStorage - Support for nested translation keys (e.g., nav.tabs.chat) - Fallback to English for missing translations Translations include: - Navigation menu items and descriptions - Status indicators (connected, offline, etc.) - Action buttons (save, cancel, apply, etc.) - Configuration categories - Common UI elements
This commit is contained in:
parent
9688454a30
commit
a4dee5cba8
134
ui/src/i18n/index.ts
Normal file
134
ui/src/i18n/index.ts
Normal file
@ -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<string, string | TranslationDict>;
|
||||
|
||||
const locales: Record<Locale, TranslationDict> = { 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();
|
||||
205
ui/src/i18n/locales/en.json
Normal file
205
ui/src/i18n/locales/en.json
Normal file
@ -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": "中文"
|
||||
}
|
||||
}
|
||||
205
ui/src/i18n/locales/zh.json
Normal file
205
ui/src/i18n/locales/zh.json
Normal file
@ -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": "中文"
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
<div class="topbar-status">
|
||||
<div class="pill">
|
||||
<span class="statusDot ${state.connected ? "ok" : ""}"></span>
|
||||
<span>Health</span>
|
||||
<span class="mono">${state.connected ? "OK" : "Offline"}</span>
|
||||
<span>${t("status.health", "Health")}</span>
|
||||
<span class="mono">${state.connected ? t("status.ok", "OK") : t("status.offline", "Offline")}</span>
|
||||
</div>
|
||||
${renderThemeToggle(state)}
|
||||
<language-switcher></language-switcher>
|
||||
</div>
|
||||
</header>
|
||||
<aside class="nav ${state.settings.navCollapsed ? "nav--collapsed" : ""}">
|
||||
@ -164,7 +168,7 @@ export function renderApp(state: AppViewState) {
|
||||
}}
|
||||
aria-expanded=${!isGroupCollapsed}
|
||||
>
|
||||
<span class="nav-label__text">${group.label}</span>
|
||||
<span class="nav-label__text">${localizedGroupLabel(group.label)}</span>
|
||||
<span class="nav-label__chevron">${isGroupCollapsed ? "+" : "−"}</span>
|
||||
</button>
|
||||
<div class="nav-group__items">
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import { subscribeLocale, type Locale, getLocale } from "../i18n/index.js";
|
||||
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway";
|
||||
import { resolveInjectedAssistantIdentity } from "./assistant-identity";
|
||||
import { loadSettings, type UiSettings } from "./storage";
|
||||
@ -108,9 +109,11 @@ export class MoltbotApp extends LitElement {
|
||||
@state() hello: GatewayHelloOk | null = null;
|
||||
@state() lastError: string | null = null;
|
||||
@state() eventLog: EventLogEntry[] = [];
|
||||
@state() locale: Locale = getLocale();
|
||||
private eventLogBuffer: EventLogEntry[] = [];
|
||||
private toolStreamSyncTimer: number | null = null;
|
||||
private sidebarCloseTimer: number | null = null;
|
||||
private unsubscribeLocale?: () => void;
|
||||
|
||||
@state() assistantName = injectedAssistantIdentity.name;
|
||||
@state() assistantAvatar = injectedAssistantIdentity.avatar;
|
||||
@ -273,6 +276,10 @@ export class MoltbotApp extends LitElement {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
handleConnected(this as unknown as Parameters<typeof handleConnected>[0]);
|
||||
// Subscribe to locale changes for i18n
|
||||
this.unsubscribeLocale = subscribeLocale(() => {
|
||||
this.locale = getLocale();
|
||||
});
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
@ -281,6 +288,7 @@ export class MoltbotApp extends LitElement {
|
||||
|
||||
disconnectedCallback() {
|
||||
handleDisconnected(this as unknown as Parameters<typeof handleDisconnected>[0]);
|
||||
this.unsubscribeLocale?.();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
|
||||
127
ui/src/ui/components/language-switcher.ts
Normal file
127
ui/src/ui/components/language-switcher.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import {
|
||||
getLocale,
|
||||
setLocale,
|
||||
subscribeLocale,
|
||||
getAvailableLocales,
|
||||
type Locale,
|
||||
} from "../../i18n/index.js";
|
||||
|
||||
/**
|
||||
* Language switcher component for Clawdbot Control UI
|
||||
*
|
||||
* Usage: <language-switcher></language-switcher>
|
||||
*/
|
||||
@customElement("language-switcher")
|
||||
export class LanguageSwitcher extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--md-sys-color-surface-container, #1e1e1e);
|
||||
border: 1px solid var(--md-sys-color-outline-variant, #444);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.switcher:hover {
|
||||
background: var(--md-sys-color-surface-container-high, #2a2a2a);
|
||||
border-color: var(--md-sys-color-outline, #666);
|
||||
}
|
||||
|
||||
.globe {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
select {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--md-sys-color-on-surface, #e0e0e0);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
select option {
|
||||
background: var(--md-sys-color-surface-container, #1e1e1e);
|
||||
color: var(--md-sys-color-on-surface, #e0e0e0);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
opacity: 0.6;
|
||||
margin-left: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
@state()
|
||||
private locale: Locale = getLocale();
|
||||
|
||||
private unsubscribe?: () => void;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.unsubscribe = subscribeLocale(() => {
|
||||
this.locale = getLocale();
|
||||
});
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.unsubscribe?.();
|
||||
}
|
||||
|
||||
private handleChange(e: Event): void {
|
||||
const select = e.target as HTMLSelectElement;
|
||||
setLocale(select.value as Locale);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const locales = getAvailableLocales();
|
||||
|
||||
return html`
|
||||
<div class="switcher">
|
||||
<svg class="globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>
|
||||
<select .value=${this.locale} @change=${this.handleChange}>
|
||||
${locales.map(
|
||||
(l) => html`
|
||||
<option value=${l.code} ?selected=${l.code === this.locale}>
|
||||
${l.name}
|
||||
</option>
|
||||
`,
|
||||
)}
|
||||
</select>
|
||||
<svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"language-switcher": LanguageSwitcher;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,13 @@
|
||||
import type { IconName } from "./icons.js";
|
||||
import { t } from "../i18n/index.js";
|
||||
|
||||
// Tab group labels (used as keys for i18n)
|
||||
const TAB_GROUP_LABELS = {
|
||||
Chat: "nav.groups.chat",
|
||||
Control: "nav.groups.control",
|
||||
Agent: "nav.groups.agent",
|
||||
Settings: "nav.groups.settings",
|
||||
} as const;
|
||||
|
||||
export const TAB_GROUPS = [
|
||||
{ label: "Chat", tabs: ["chat"] },
|
||||
@ -10,6 +19,12 @@ export const TAB_GROUPS = [
|
||||
{ label: "Settings", tabs: ["config", "debug", "logs"] },
|
||||
] as const;
|
||||
|
||||
/** Get localized label for a tab group */
|
||||
export function localizedGroupLabel(label: string): string {
|
||||
const key = TAB_GROUP_LABELS[label as keyof typeof TAB_GROUP_LABELS];
|
||||
return key ? t(key) : label;
|
||||
}
|
||||
|
||||
export type Tab =
|
||||
| "overview"
|
||||
| "channels"
|
||||
@ -129,60 +144,10 @@ export function iconForTab(tab: Tab): IconName {
|
||||
}
|
||||
}
|
||||
|
||||
export function titleForTab(tab: Tab) {
|
||||
switch (tab) {
|
||||
case "overview":
|
||||
return "Overview";
|
||||
case "channels":
|
||||
return "Channels";
|
||||
case "instances":
|
||||
return "Instances";
|
||||
case "sessions":
|
||||
return "Sessions";
|
||||
case "cron":
|
||||
return "Cron Jobs";
|
||||
case "skills":
|
||||
return "Skills";
|
||||
case "nodes":
|
||||
return "Nodes";
|
||||
case "chat":
|
||||
return "Chat";
|
||||
case "config":
|
||||
return "Config";
|
||||
case "debug":
|
||||
return "Debug";
|
||||
case "logs":
|
||||
return "Logs";
|
||||
default:
|
||||
return "Control";
|
||||
}
|
||||
export function titleForTab(tab: Tab): string {
|
||||
return t(`nav.tabs.${tab}`);
|
||||
}
|
||||
|
||||
export function subtitleForTab(tab: Tab) {
|
||||
switch (tab) {
|
||||
case "overview":
|
||||
return "Gateway status, entry points, and a fast health read.";
|
||||
case "channels":
|
||||
return "Manage channels and settings.";
|
||||
case "instances":
|
||||
return "Presence beacons from connected clients and nodes.";
|
||||
case "sessions":
|
||||
return "Inspect active sessions and adjust per-session defaults.";
|
||||
case "cron":
|
||||
return "Schedule wakeups and recurring agent runs.";
|
||||
case "skills":
|
||||
return "Manage skill availability and API key injection.";
|
||||
case "nodes":
|
||||
return "Paired devices, capabilities, and command exposure.";
|
||||
case "chat":
|
||||
return "Direct gateway chat session for quick interventions.";
|
||||
case "config":
|
||||
return "Edit ~/.clawdbot/moltbot.json safely.";
|
||||
case "debug":
|
||||
return "Gateway snapshots, events, and manual RPC calls.";
|
||||
case "logs":
|
||||
return "Live tail of the gateway file logs.";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
export function subtitleForTab(tab: Tab): string {
|
||||
return t(`nav.subtitles.${tab}`);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user