This commit is contained in:
superssr 2026-01-29 19:00:16 +00:00 committed by GitHub
commit 919a6480b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 621 additions and 58 deletions

149
ui/src/i18n/index.ts Normal file
View File

@ -0,0 +1,149 @@
/**
* Lightweight i18n module for Control UI
*
* Usage:
* import { t, setLocale, getLocale } from './i18n';
*
* // Get translated string
* t('nav.chat') // => "Chat" or "聊天"
*
* // Change locale
* setLocale('zh-CN');
*
* // Get current locale
* getLocale() // => 'zh-CN'
*/
import type { Locale, TranslationKey, TranslationKeys } from './types';
import { en } from './locales/en';
import { zhCN } from './locales/zh-CN';
const STORAGE_KEY = 'moltbot-locale';
const locales: Record<Locale, TranslationKeys> = {
'en': en,
'zh-CN': zhCN,
};
let currentLocale: Locale = 'en';
/**
* Detect the best locale based on browser settings
*/
function detectLocale(): Locale {
// Check localStorage first
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored && isValidLocale(stored)) {
return stored as Locale;
}
} catch {
// localStorage not available
}
// Check browser language
const browserLang = navigator.language || (navigator as any).userLanguage || 'en';
// Check for exact match
if (isValidLocale(browserLang)) {
return browserLang as Locale;
}
// Check for language code match (e.g., 'zh' matches 'zh-CN')
const langCode = browserLang.split('-')[0].toLowerCase();
if (langCode === 'zh') {
return 'zh-CN';
}
return 'en';
}
/**
* Check if a locale is valid
*/
function isValidLocale(locale: string): locale is Locale {
return locale in locales;
}
/**
* Get the current locale
*/
export function getLocale(): Locale {
return currentLocale;
}
/**
* Get all available locales
*/
export function getAvailableLocales(): Locale[] {
return Object.keys(locales) as Locale[];
}
/**
* Get locale display name
*/
export function getLocaleDisplayName(locale: Locale): string {
switch (locale) {
case 'en':
return 'English';
case 'zh-CN':
return '简体中文';
default:
return locale;
}
}
/**
* Set the current locale
*/
export function setLocale(locale: Locale): void {
if (!isValidLocale(locale)) {
console.warn(`Invalid locale: ${locale}, falling back to 'en'`);
locale = 'en';
}
currentLocale = locale;
// Persist to localStorage
try {
localStorage.setItem(STORAGE_KEY, locale);
} catch {
// localStorage not available
}
// Dispatch event for components to react
window.dispatchEvent(new CustomEvent('locale-changed', { detail: { locale } }));
}
/**
* Get a translated string
*/
export function t(key: TranslationKey): string {
const translations = locales[currentLocale] ?? locales['en'];
return translations[key] ?? key;
}
/**
* Get a translated string with interpolation
*
* Usage:
* tf('greeting', { name: 'World' }) // "Hello, {name}!" => "Hello, World!"
*/
export function tf(key: TranslationKey, params: Record<string, string | number>): string {
let result = t(key);
for (const [param, value] of Object.entries(params)) {
result = result.replace(new RegExp(`\\{${param}\\}`, 'g'), String(value));
}
return result;
}
/**
* Initialize i18n with auto-detection
*/
export function initI18n(): Locale {
currentLocale = detectLocale();
return currentLocale;
}
// Auto-initialize on module load
initI18n();

151
ui/src/i18n/locales/en.ts Normal file
View File

@ -0,0 +1,151 @@
import type { TranslationKeys } from '../types';
export const en: TranslationKeys = {
// Navigation groups
'nav.chat': 'Chat',
'nav.control': 'Control',
'nav.agent': 'Agent',
'nav.settings': 'Settings',
// Tab titles
'tab.overview': 'Overview',
'tab.channels': 'Channels',
'tab.instances': 'Instances',
'tab.sessions': 'Sessions',
'tab.cron': 'Cron Jobs',
'tab.skills': 'Skills',
'tab.nodes': 'Nodes',
'tab.chat': 'Chat',
'tab.config': 'Config',
'tab.debug': 'Debug',
'tab.logs': 'Logs',
// Tab subtitles/descriptions
'tab.overview.desc': 'Gateway status, entry points, and a fast health read.',
'tab.channels.desc': 'Manage channels and settings.',
'tab.instances.desc': 'Presence beacons from connected clients and nodes.',
'tab.sessions.desc': 'Inspect active sessions and adjust per-session defaults.',
'tab.cron.desc': 'Schedule wakeups and recurring agent runs.',
'tab.skills.desc': 'Manage skill availability and API key injection.',
'tab.nodes.desc': 'Paired devices, capabilities, and command exposure.',
'tab.chat.desc': 'Direct gateway chat session for quick interventions.',
'tab.config.desc': 'Edit ~/.clawdbot/clawdbot.json safely.',
'tab.debug.desc': 'Gateway snapshots, events, and manual RPC calls.',
'tab.logs.desc': 'Live tail of the gateway file logs.',
// Common actions
'action.save': 'Save',
'action.cancel': 'Cancel',
'action.apply': 'Apply',
'action.reset': 'Reset',
'action.delete': 'Delete',
'action.edit': 'Edit',
'action.add': 'Add',
'action.remove': 'Remove',
'action.refresh': 'Refresh',
'action.copy': 'Copy',
'action.close': 'Close',
'action.confirm': 'Confirm',
'action.send': 'Send',
'action.stop': 'Stop',
'action.retry': 'Retry',
// Status
'status.online': 'Online',
'status.offline': 'Offline',
'status.connected': 'Connected',
'status.disconnected': 'Disconnected',
'status.loading': 'Loading...',
'status.error': 'Error',
'status.success': 'Success',
'status.pending': 'Pending',
'status.idle': 'Idle',
'status.running': 'Running',
'status.ok': 'OK',
// Header
'header.health': 'Health',
'header.brand.title': 'MOLTBOT',
'header.brand.sub': 'Gateway Dashboard',
'header.expandSidebar': 'Expand sidebar',
'header.collapseSidebar': 'Collapse sidebar',
// Theme
'theme.light': 'Light',
'theme.dark': 'Dark',
'theme.system': 'System',
// Chat
'chat.placeholder': 'Type a message...',
'chat.send': 'Send',
'chat.thinking': 'Thinking...',
'chat.attachFile': 'Attach file',
'chat.clearHistory': 'Clear history',
// Channels
'channels.whatsapp': 'WhatsApp',
'channels.telegram': 'Telegram',
'channels.discord': 'Discord',
'channels.slack': 'Slack',
'channels.signal': 'Signal',
'channels.imessage': 'iMessage',
'channels.nostr': 'Nostr',
'channels.googlechat': 'Google Chat',
// Sessions
'sessions.title': 'Sessions',
'sessions.active': 'Active',
'sessions.tokens': 'Tokens',
'sessions.model': 'Model',
'sessions.lastActivity': 'Last Activity',
// Cron
'cron.title': 'Cron Jobs',
'cron.schedule': 'Schedule',
'cron.nextRun': 'Next Run',
'cron.lastRun': 'Last Run',
'cron.enabled': 'Enabled',
'cron.disabled': 'Disabled',
'cron.addJob': 'Add Job',
// Config
'config.title': 'Configuration',
'config.saved': 'Saved',
'config.unsaved': 'Unsaved changes',
'config.saveChanges': 'Save Changes',
'config.discardChanges': 'Discard Changes',
// Logs
'logs.title': 'Logs',
'logs.level': 'Level',
'logs.filter': 'Filter',
'logs.export': 'Export',
'logs.clear': 'Clear',
// Skills
'skills.title': 'Skills',
'skills.installed': 'Installed',
'skills.available': 'Available',
'skills.install': 'Install',
'skills.uninstall': 'Uninstall',
// Nodes
'nodes.title': 'Nodes',
'nodes.paired': 'Paired',
'nodes.pending': 'Pending',
'nodes.approve': 'Approve',
'nodes.reject': 'Reject',
// Errors
'error.connection': 'Connection error',
'error.timeout': 'Request timed out',
'error.unknown': 'An unknown error occurred',
'error.invalidInput': 'Invalid input',
// Misc
'misc.noData': 'No data',
'misc.loading': 'Loading...',
'misc.never': 'Never',
'misc.justNow': 'Just now',
'misc.ago': 'ago',
};

View File

@ -0,0 +1,151 @@
import type { TranslationKeys } from '../types';
export const zhCN: TranslationKeys = {
// Navigation groups
'nav.chat': '聊天',
'nav.control': '控制',
'nav.agent': '代理',
'nav.settings': '设置',
// Tab titles
'tab.overview': '概览',
'tab.channels': '通道',
'tab.instances': '实例',
'tab.sessions': '会话',
'tab.cron': '定时任务',
'tab.skills': '技能',
'tab.nodes': '节点',
'tab.chat': '聊天',
'tab.config': '配置',
'tab.debug': '调试',
'tab.logs': '日志',
// Tab subtitles/descriptions
'tab.overview.desc': '网关状态、入口点和快速健康检查。',
'tab.channels.desc': '管理通道和设置。',
'tab.instances.desc': '来自已连接客户端和节点的状态信标。',
'tab.sessions.desc': '检查活跃会话并调整每会话默认设置。',
'tab.cron.desc': '安排唤醒和定期代理运行。',
'tab.skills.desc': '管理技能可用性和 API 密钥注入。',
'tab.nodes.desc': '已配对设备、功能和命令暴露。',
'tab.chat.desc': '直接网关聊天会话,用于快速干预。',
'tab.config.desc': '安全编辑 ~/.clawdbot/clawdbot.json。',
'tab.debug.desc': '网关快照、事件和手动 RPC 调用。',
'tab.logs.desc': '实时查看网关文件日志。',
// Common actions
'action.save': '保存',
'action.cancel': '取消',
'action.apply': '应用',
'action.reset': '重置',
'action.delete': '删除',
'action.edit': '编辑',
'action.add': '添加',
'action.remove': '移除',
'action.refresh': '刷新',
'action.copy': '复制',
'action.close': '关闭',
'action.confirm': '确认',
'action.send': '发送',
'action.stop': '停止',
'action.retry': '重试',
// Status
'status.online': '在线',
'status.offline': '离线',
'status.connected': '已连接',
'status.disconnected': '已断开',
'status.loading': '加载中...',
'status.error': '错误',
'status.success': '成功',
'status.pending': '等待中',
'status.idle': '空闲',
'status.running': '运行中',
'status.ok': '正常',
// Header
'header.health': '健康状态',
'header.brand.title': 'MOLTBOT',
'header.brand.sub': '网关控制台',
'header.expandSidebar': '展开侧边栏',
'header.collapseSidebar': '收起侧边栏',
// Theme
'theme.light': '浅色',
'theme.dark': '深色',
'theme.system': '跟随系统',
// Chat
'chat.placeholder': '输入消息...',
'chat.send': '发送',
'chat.thinking': '思考中...',
'chat.attachFile': '添加附件',
'chat.clearHistory': '清除历史',
// Channels
'channels.whatsapp': 'WhatsApp',
'channels.telegram': 'Telegram',
'channels.discord': 'Discord',
'channels.slack': 'Slack',
'channels.signal': 'Signal',
'channels.imessage': 'iMessage',
'channels.nostr': 'Nostr',
'channels.googlechat': 'Google Chat',
// Sessions
'sessions.title': '会话',
'sessions.active': '活跃',
'sessions.tokens': 'Token 数',
'sessions.model': '模型',
'sessions.lastActivity': '最后活动',
// Cron
'cron.title': '定时任务',
'cron.schedule': '调度',
'cron.nextRun': '下次运行',
'cron.lastRun': '上次运行',
'cron.enabled': '已启用',
'cron.disabled': '已禁用',
'cron.addJob': '添加任务',
// Config
'config.title': '配置',
'config.saved': '已保存',
'config.unsaved': '有未保存的更改',
'config.saveChanges': '保存更改',
'config.discardChanges': '放弃更改',
// Logs
'logs.title': '日志',
'logs.level': '级别',
'logs.filter': '筛选',
'logs.export': '导出',
'logs.clear': '清空',
// Skills
'skills.title': '技能',
'skills.installed': '已安装',
'skills.available': '可用',
'skills.install': '安装',
'skills.uninstall': '卸载',
// Nodes
'nodes.title': '节点',
'nodes.paired': '已配对',
'nodes.pending': '等待中',
'nodes.approve': '批准',
'nodes.reject': '拒绝',
// Errors
'error.connection': '连接错误',
'error.timeout': '请求超时',
'error.unknown': '发生未知错误',
'error.invalidInput': '输入无效',
// Misc
'misc.noData': '暂无数据',
'misc.loading': '加载中...',
'misc.never': '从未',
'misc.justNow': '刚刚',
'misc.ago': '前',
};

157
ui/src/i18n/types.ts Normal file
View File

@ -0,0 +1,157 @@
/**
* i18n type definitions for Control UI
*/
export type Locale = 'en' | 'zh-CN';
export interface TranslationKeys {
// Navigation groups
'nav.chat': string;
'nav.control': string;
'nav.agent': string;
'nav.settings': string;
// Tab titles
'tab.overview': string;
'tab.channels': string;
'tab.instances': string;
'tab.sessions': string;
'tab.cron': string;
'tab.skills': string;
'tab.nodes': string;
'tab.chat': string;
'tab.config': string;
'tab.debug': string;
'tab.logs': string;
// Tab subtitles/descriptions
'tab.overview.desc': string;
'tab.channels.desc': string;
'tab.instances.desc': string;
'tab.sessions.desc': string;
'tab.cron.desc': string;
'tab.skills.desc': string;
'tab.nodes.desc': string;
'tab.chat.desc': string;
'tab.config.desc': string;
'tab.debug.desc': string;
'tab.logs.desc': string;
// Common actions
'action.save': string;
'action.cancel': string;
'action.apply': string;
'action.reset': string;
'action.delete': string;
'action.edit': string;
'action.add': string;
'action.remove': string;
'action.refresh': string;
'action.copy': string;
'action.close': string;
'action.confirm': string;
'action.send': string;
'action.stop': string;
'action.retry': string;
// Status
'status.online': string;
'status.offline': string;
'status.connected': string;
'status.disconnected': string;
'status.loading': string;
'status.error': string;
'status.success': string;
'status.pending': string;
'status.idle': string;
'status.running': string;
'status.ok': string;
// Header
'header.health': string;
'header.brand.title': string;
'header.brand.sub': string;
'header.expandSidebar': string;
'header.collapseSidebar': string;
// Theme
'theme.light': string;
'theme.dark': string;
'theme.system': string;
// Chat
'chat.placeholder': string;
'chat.send': string;
'chat.thinking': string;
'chat.attachFile': string;
'chat.clearHistory': string;
// Channels
'channels.whatsapp': string;
'channels.telegram': string;
'channels.discord': string;
'channels.slack': string;
'channels.signal': string;
'channels.imessage': string;
'channels.nostr': string;
'channels.googlechat': string;
// Sessions
'sessions.title': string;
'sessions.active': string;
'sessions.tokens': string;
'sessions.model': string;
'sessions.lastActivity': string;
// Cron
'cron.title': string;
'cron.schedule': string;
'cron.nextRun': string;
'cron.lastRun': string;
'cron.enabled': string;
'cron.disabled': string;
'cron.addJob': string;
// Config
'config.title': string;
'config.saved': string;
'config.unsaved': string;
'config.saveChanges': string;
'config.discardChanges': string;
// Logs
'logs.title': string;
'logs.level': string;
'logs.filter': string;
'logs.export': string;
'logs.clear': string;
// Skills
'skills.title': string;
'skills.installed': string;
'skills.available': string;
'skills.install': string;
'skills.uninstall': string;
// Nodes
'nodes.title': string;
'nodes.paired': string;
'nodes.pending': string;
'nodes.approve': string;
'nodes.reject': string;
// Errors
'error.connection': string;
'error.timeout': string;
'error.unknown': string;
'error.invalidInput': string;
// Misc
'misc.noData': string;
'misc.loading': string;
'misc.never': string;
'misc.justNow': string;
'misc.ago': string;
}
export type TranslationKey = keyof TranslationKeys;

View File

@ -1,15 +1,20 @@
import type { IconName } from "./icons.js"; import type { IconName } from "./icons.js";
import { t } from "../i18n";
export const TAB_GROUPS = [ export const TAB_GROUPS = [
{ label: "Chat", tabs: ["chat"] }, { labelKey: "nav.chat" as const, tabs: ["chat"] },
{ {
label: "Control", labelKey: "nav.control" as const,
tabs: ["overview", "channels", "instances", "sessions", "cron"], tabs: ["overview", "channels", "instances", "sessions", "cron"],
}, },
{ label: "Agent", tabs: ["skills", "nodes"] }, { labelKey: "nav.agent" as const, tabs: ["skills", "nodes"] },
{ label: "Settings", tabs: ["config", "debug", "logs"] }, { labelKey: "nav.settings" as const, tabs: ["config", "debug", "logs"] },
] as const; ] as const;
export function getTabGroupLabel(labelKey: string): string {
return t(labelKey as any);
}
export type Tab = export type Tab =
| "overview" | "overview"
| "channels" | "channels"
@ -129,60 +134,10 @@ export function iconForTab(tab: Tab): IconName {
} }
} }
export function titleForTab(tab: Tab) { export function titleForTab(tab: Tab): string {
switch (tab) { return t(`tab.${tab}` as any);
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 subtitleForTab(tab: Tab) { export function subtitleForTab(tab: Tab): string {
switch (tab) { return t(`tab.${tab}.desc` as any);
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 "";
}
} }