feat(ui): utilities, theming, and i18n updates (slice 2/3 of dashboard-v2) (#41500)

* feat(ui): add utilities, theming, and i18n updates (slice 2 of dashboard-v2)

UI utilities and theming improvements extracted from dashboard-v2-structure:

Icons & formatting:
- icons.ts: expanded icon set for new dashboard views
- format.ts: date/number formatting helpers
- tool-labels.ts: human-readable tool name mappings

Theming:
- theme.ts: enhanced theme resolution and system theme support
- theme-transition.ts: simplified transition logic
- storage.ts: theme parsing improvements for settings persistence

Navigation & types:
- navigation.ts: extended tab definitions for dashboard-v2
- app-view-state.ts: expanded view state management
- types.ts: new type definitions (HealthSummary, ModelCatalogEntry, etc.)

Components:
- components/dashboard-header.ts: reusable header component

i18n:
- Updated en, pt-BR, zh-CN, zh-TW locales with new dashboard strings

All changes are additive or backwards-compatible. Build passes.
Part of #36853.

* ui: fix theme and locale review regressions

* ui: fix review follow-ups for dashboard tabs

* ui: allowlist locale password placeholder false positives

* ui: fix theme mode and locale regressions

* Vincentkoc code/pr 41500 route fix (#43829)

* UI: keep unfinished settings routes hidden

* UI: normalize light theme data token

* UI: restore cron type compatibility

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Val Alexander
2026-03-12 03:26:39 -05:00
committed by GitHub
parent 658bd54ecf
commit 46cb73da37
23 changed files with 1306 additions and 241 deletions

View File

@@ -12,7 +12,9 @@ export const en: TranslationMap = {
disabled: "Disabled",
na: "n/a",
docs: "Docs",
theme: "Theme",
resources: "Resources",
search: "Search",
},
nav: {
chat: "Chat",
@@ -21,6 +23,7 @@ export const en: TranslationMap = {
settings: "Settings",
expand: "Expand sidebar",
collapse: "Collapse sidebar",
resize: "Resize sidebar",
},
tabs: {
agents: "Agents",
@@ -34,23 +37,33 @@ export const en: TranslationMap = {
nodes: "Nodes",
chat: "Chat",
config: "Config",
communications: "Communications",
appearance: "Appearance",
automation: "Automation",
infrastructure: "Infrastructure",
aiAgents: "AI & Agents",
debug: "Debug",
logs: "Logs",
},
subtitles: {
agents: "Manage agent workspaces, tools, and identities.",
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.",
usage: "Monitor API usage and costs.",
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 ~/.openclaw/openclaw.json safely.",
debug: "Gateway snapshots, events, and manual RPC calls.",
logs: "Live tail of the gateway file logs.",
agents: "Workspaces, tools, identities.",
overview: "Status, entry points, health.",
channels: "Channels and settings.",
instances: "Connected clients and nodes.",
sessions: "Active sessions and defaults.",
usage: "API usage and costs.",
cron: "Wakeups and recurring runs.",
skills: "Skills and API keys.",
nodes: "Paired devices and commands.",
chat: "Gateway chat for quick interventions.",
config: "Edit openclaw.json.",
communications: "Channels, messages, and audio settings.",
appearance: "Theme, UI, and setup wizard settings.",
automation: "Commands, hooks, cron, and plugins.",
infrastructure: "Gateway, web, browser, and media settings.",
aiAgents: "Agents, models, skills, tools, memory, session.",
debug: "Snapshots, events, RPC.",
logs: "Live gateway logs.",
},
overview: {
access: {
@@ -105,6 +118,47 @@ export const en: TranslationMap = {
hint: "This page is HTTP, so the browser blocks device identity. Use HTTPS (Tailscale Serve) or open {url} on the gateway host.",
stayHttp: "If you must stay on HTTP, set {config} (token-only).",
},
connection: {
title: "How to connect",
step1: "Start the gateway on your host machine:",
step2: "Get a tokenized dashboard URL:",
step3: "Paste the WebSocket URL and token above, or open the tokenized URL directly.",
step4: "Or generate a reusable token:",
docsHint: "For remote access, Tailscale Serve is recommended. ",
docsLink: "Read the docs →",
},
cards: {
cost: "Cost",
skills: "Skills",
recentSessions: "Recent Sessions",
},
attention: {
title: "Attention",
},
eventLog: {
title: "Event Log",
},
logTail: {
title: "Gateway Logs",
},
quickActions: {
newSession: "New Session",
automation: "Automation",
refreshAll: "Refresh All",
terminal: "Terminal",
},
streamMode: {
active: "Stream mode — values redacted",
disable: "Disable",
},
palette: {
placeholder: "Type a command…",
noResults: "No results",
},
},
login: {
subtitle: "Gateway Dashboard",
passwordPlaceholder: "optional", // pragma: allowlist secret
},
chat: {
disconnected: "Disconnected from gateway.",

View File

@@ -12,7 +12,9 @@ export const pt_BR: TranslationMap = {
disabled: "Desativado",
na: "n/a",
docs: "Docs",
theme: "Tema",
resources: "Recursos",
search: "Pesquisar",
},
nav: {
chat: "Chat",
@@ -21,6 +23,7 @@ export const pt_BR: TranslationMap = {
settings: "Configurações",
expand: "Expandir barra lateral",
collapse: "Recolher barra lateral",
resize: "Redimensionar barra lateral",
},
tabs: {
agents: "Agentes",
@@ -34,23 +37,33 @@ export const pt_BR: TranslationMap = {
nodes: "Nós",
chat: "Chat",
config: "Config",
communications: "Comunicações",
appearance: "Aparência e Configuração",
automation: "Automação",
infrastructure: "Infraestrutura",
aiAgents: "IA e Agentes",
debug: "Debug",
logs: "Logs",
},
subtitles: {
agents: "Gerenciar espaços de trabalho, ferramentas e identidades de agentes.",
overview: "Status do gateway, pontos de entrada e leitura rápida de saúde.",
channels: "Gerenciar canais e configurações.",
instances: "Beacons de presença de clientes e nós conectados.",
sessions: "Inspecionar sessões ativas e ajustar padrões por sessão.",
usage: "Monitorar uso e custos da API.",
cron: "Agendar despertares e execuções recorrentes de agentes.",
skills: "Gerenciar disponibilidade de habilidades e injeção de chaves de API.",
nodes: "Dispositivos pareados, capacidades e exposição de comandos.",
chat: "Sessão de chat direta com o gateway para intervenções rápidas.",
config: "Editar ~/.openclaw/openclaw.json com segurança.",
debug: "Snapshots do gateway, eventos e chamadas RPC manuais.",
logs: "Acompanhamento ao vivo dos logs de arquivo do gateway.",
agents: "Espaços, ferramentas, identidades.",
overview: "Status, entrada, saúde.",
channels: "Canais e configurações.",
instances: "Clientes e nós conectados.",
sessions: "Sessões ativas e padrões.",
usage: "Uso e custos da API.",
cron: "Despertares e execuções.",
skills: "Habilidades e chaves API.",
nodes: "Dispositivos e comandos.",
chat: "Chat do gateway para intervenções rápidas.",
config: "Editar openclaw.json.",
communications: "Configurações de canais, mensagens e áudio.",
appearance: "Configurações de tema, UI e assistente de configuração.",
automation: "Configurações de comandos, hooks, cron e plugins.",
infrastructure: "Configurações de gateway, web, browser e mídia.",
aiAgents: "Configurações de agentes, modelos, habilidades, ferramentas, memória e sessão.",
debug: "Snapshots, eventos, RPC.",
logs: "Logs ao vivo do gateway.",
},
overview: {
access: {
@@ -107,6 +120,47 @@ export const pt_BR: TranslationMap = {
hint: "Esta página é HTTP, então o navegador bloqueia a identidade do dispositivo. Use HTTPS (Tailscale Serve) ou abra {url} no host do gateway.",
stayHttp: "Se você precisar permanecer em HTTP, defina {config} (apenas token).",
},
connection: {
title: "Como conectar",
step1: "Inicie o gateway na sua máquina host:",
step2: "Obtenha uma URL do painel com token:",
step3: "Cole a URL do WebSocket e o token acima, ou abra a URL com token diretamente.",
step4: "Ou gere um token reutilizável:",
docsHint: "Para acesso remoto, recomendamos o Tailscale Serve. ",
docsLink: "Leia a documentação →",
},
cards: {
cost: "Custo",
skills: "Habilidades",
recentSessions: "Sessões Recentes",
},
attention: {
title: "Atenção",
},
eventLog: {
title: "Log de Eventos",
},
logTail: {
title: "Logs do Gateway",
},
quickActions: {
newSession: "Nova Sessão",
automation: "Automação",
refreshAll: "Atualizar Tudo",
terminal: "Terminal",
},
streamMode: {
active: "Modo stream — valores ocultos",
disable: "Desativar",
},
palette: {
placeholder: "Digite um comando…",
noResults: "Sem resultados",
},
},
login: {
subtitle: "Painel do Gateway",
passwordPlaceholder: "opcional", // pragma: allowlist secret
},
chat: {
disconnected: "Desconectado do gateway.",

View File

@@ -12,7 +12,9 @@ export const zh_CN: TranslationMap = {
disabled: "已禁用",
na: "不适用",
docs: "文档",
theme: "主题",
resources: "资源",
search: "搜索",
},
nav: {
chat: "聊天",
@@ -21,6 +23,7 @@ export const zh_CN: TranslationMap = {
settings: "设置",
expand: "展开侧边栏",
collapse: "折叠侧边栏",
resize: "调整侧边栏大小",
},
tabs: {
agents: "代理",
@@ -34,23 +37,33 @@ export const zh_CN: TranslationMap = {
nodes: "节点",
chat: "聊天",
config: "配置",
communications: "通信",
appearance: "外观与设置",
automation: "自动化",
infrastructure: "基础设施",
aiAgents: "AI 与代理",
debug: "调试",
logs: "日志",
},
subtitles: {
agents: "管理代理工作区、工具身份。",
overview: "网关状态、入口点和快速健康读取。",
channels: "管理频道和设置。",
instances: "来自已连接客户端和节点的在线信号。",
sessions: "检查活动会话并调整每个会话的默认设置。",
usage: "监控 API 使用情况和成本。",
cron: "安排唤醒和重复的代理运行。",
skills: "管理技能可用性和 API 密钥注入。",
nodes: "配对设备、功能和命令公开。",
chat: "用于快速干预的直接网关聊天会话。",
config: "安全地编辑 ~/.openclaw/openclaw.json。",
debug: "网关快照、事件和手动 RPC 调用。",
logs: "网关文件日志的实时追踪。",
agents: "工作区、工具身份。",
overview: "状态、入口点、健康。",
channels: "频道和设置。",
instances: "已连接客户端和节点。",
sessions: "活动会话默认设置。",
usage: "API 使用情况和成本。",
cron: "唤醒和重复运行。",
skills: "技能和 API 密钥。",
nodes: "配对设备和命令。",
chat: "网关聊天,快速干预。",
config: "编辑 openclaw.json。",
communications: "频道、消息和音频设置。",
appearance: "主题、界面和设置向导设置。",
automation: "命令、钩子、定时任务和插件设置。",
infrastructure: "网关、Web、浏览器和媒体设置。",
aiAgents: "代理、模型、技能、工具、记忆和会话设置。",
debug: "快照、事件、RPC。",
logs: "实时网关日志。",
},
overview: {
access: {
@@ -104,6 +117,47 @@ export const zh_CN: TranslationMap = {
hint: "此页面为 HTTP因此浏览器阻止设备标识。请使用 HTTPS (Tailscale Serve) 或在网关主机上打开 {url}。",
stayHttp: "如果您必须保持 HTTP请设置 {config} (仅限令牌)。",
},
connection: {
title: "如何连接",
step1: "在主机上启动网关:",
step2: "获取带令牌的仪表盘 URL",
step3: "将 WebSocket URL 和令牌粘贴到上方,或直接打开带令牌的 URL。",
step4: "或生成可重复使用的令牌:",
docsHint: "如需远程访问,建议使用 Tailscale Serve。",
docsLink: "查看文档 →",
},
cards: {
cost: "费用",
skills: "技能",
recentSessions: "最近会话",
},
attention: {
title: "注意事项",
},
eventLog: {
title: "事件日志",
},
logTail: {
title: "网关日志",
},
quickActions: {
newSession: "新建会话",
automation: "自动化",
refreshAll: "全部刷新",
terminal: "终端",
},
streamMode: {
active: "流模式 — 数据已隐藏",
disable: "禁用",
},
palette: {
placeholder: "输入命令…",
noResults: "无结果",
},
},
login: {
subtitle: "网关仪表盘",
passwordPlaceholder: "可选",
},
chat: {
disconnected: "已断开与网关的连接。",

View File

@@ -12,7 +12,9 @@ export const zh_TW: TranslationMap = {
disabled: "已禁用",
na: "不適用",
docs: "文檔",
theme: "主題",
resources: "資源",
search: "搜尋",
},
nav: {
chat: "聊天",
@@ -21,6 +23,7 @@ export const zh_TW: TranslationMap = {
settings: "設置",
expand: "展開側邊欄",
collapse: "折疊側邊欄",
resize: "調整側邊欄大小",
},
tabs: {
agents: "代理",
@@ -34,23 +37,33 @@ export const zh_TW: TranslationMap = {
nodes: "節點",
chat: "聊天",
config: "配置",
communications: "通訊",
appearance: "外觀與設置",
automation: "自動化",
infrastructure: "基礎設施",
aiAgents: "AI 與代理",
debug: "調試",
logs: "日誌",
},
subtitles: {
agents: "管理代理工作區、工具身份。",
overview: "網關狀態、入口點和快速健康讀取。",
channels: "管理頻道和設置。",
instances: "來自已連接客戶端和節點的在線信號。",
sessions: "檢查活動會話並調整每個會話的默認設置。",
usage: "監控 API 使用情況和成本。",
cron: "安排喚醒和重複的代理運行。",
skills: "管理技能可用性和 API 密鑰注入。",
nodes: "配對設備、功能和命令公開。",
chat: "用於快速干預的直接網關聊天會話。",
config: "安全地編輯 ~/.openclaw/openclaw.json。",
debug: "網關快照、事件和手動 RPC 調用。",
logs: "網關文件日志的實時追蹤。",
agents: "工作區、工具身份。",
overview: "狀態、入口點、健康。",
channels: "頻道和設置。",
instances: "已連接客戶端和節點。",
sessions: "活動會話默認設置。",
usage: "API 使用情況和成本。",
cron: "喚醒和重複運行。",
skills: "技能和 API 密鑰。",
nodes: "配對設備和命令。",
chat: "網關聊天,快速干預。",
config: "編輯 openclaw.json。",
communications: "頻道、消息和音頻設置。",
appearance: "主題、界面和設置向導設置。",
automation: "命令、鉤子、定時任務和插件設置。",
infrastructure: "網關、Web、瀏覽器和媒體設置。",
aiAgents: "代理、模型、技能、工具、記憶和會話設置。",
debug: "快照、事件、RPC。",
logs: "實時網關日誌。",
},
overview: {
access: {
@@ -104,6 +117,47 @@ export const zh_TW: TranslationMap = {
hint: "此頁面為 HTTP因此瀏覽器阻止設備標識。請使用 HTTPS (Tailscale Serve) 或在網關主機上打開 {url}。",
stayHttp: "如果您必須保持 HTTP請設置 {config} (僅限令牌)。",
},
connection: {
title: "如何連接",
step1: "在主機上啟動閘道:",
step2: "取得帶令牌的儀表板 URL",
step3: "將 WebSocket URL 和令牌貼到上方,或直接開啟帶令牌的 URL。",
step4: "或產生可重複使用的令牌:",
docsHint: "如需遠端存取,建議使用 Tailscale Serve。",
docsLink: "查看文件 →",
},
cards: {
cost: "費用",
skills: "技能",
recentSessions: "最近會話",
},
attention: {
title: "注意事項",
},
eventLog: {
title: "事件日誌",
},
logTail: {
title: "閘道日誌",
},
quickActions: {
newSession: "新建會話",
automation: "自動化",
refreshAll: "全部刷新",
terminal: "終端",
},
streamMode: {
active: "串流模式 — 數據已隱藏",
disable: "禁用",
},
palette: {
placeholder: "輸入指令…",
noResults: "無結果",
},
},
login: {
subtitle: "閘道儀表板",
passwordPlaceholder: "可選",
},
chat: {
disconnected: "已斷開與網關的連接。",

View File

@@ -1,56 +1,100 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { i18n, t } from "../lib/translate.ts";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { pt_BR } from "../locales/pt-BR.ts";
import { zh_CN } from "../locales/zh-CN.ts";
import { zh_TW } from "../locales/zh-TW.ts";
type TranslateModule = typeof import("../lib/translate.ts");
function createStorageMock(): Storage {
const store = new Map<string, string>();
return {
get length() {
return store.size;
},
clear() {
store.clear();
},
getItem(key: string) {
return store.get(key) ?? null;
},
key(index: number) {
return Array.from(store.keys())[index] ?? null;
},
removeItem(key: string) {
store.delete(key);
},
setItem(key: string, value: string) {
store.set(key, String(value));
},
};
}
describe("i18n", () => {
let translate: TranslateModule;
beforeEach(async () => {
vi.resetModules();
vi.stubGlobal("localStorage", createStorageMock());
vi.stubGlobal("navigator", { language: "en-US" } as Navigator);
translate = await import("../lib/translate.ts");
localStorage.clear();
// Reset to English
await i18n.setLocale("en");
await translate.i18n.setLocale("en");
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("should return the key if translation is missing", () => {
expect(t("non.existent.key")).toBe("non.existent.key");
expect(translate.t("non.existent.key")).toBe("non.existent.key");
});
it("should return the correct English translation", () => {
expect(t("common.health")).toBe("Health");
expect(translate.t("common.health")).toBe("Health");
});
it("should replace parameters correctly", () => {
expect(t("overview.stats.cronNext", { time: "10:00" })).toBe("Next wake 10:00");
expect(translate.t("overview.stats.cronNext", { time: "10:00" })).toBe("Next wake 10:00");
});
it("should fallback to English if key is missing in another locale", async () => {
// We haven't registered other locales in the test environment yet,
// but the logic should fallback to 'en' map which is always there.
await i18n.setLocale("zh-CN");
await translate.i18n.setLocale("zh-CN");
// Since we don't mock the import, it might fail to load zh-CN,
// but let's assume it falls back to English for now.
expect(t("common.health")).toBeDefined();
expect(translate.t("common.health")).toBeDefined();
});
it("loads translations even when setting the same locale again", async () => {
const internal = i18n as unknown as {
const internal = translate.i18n as unknown as {
locale: string;
translations: Record<string, unknown>;
};
internal.locale = "zh-CN";
delete internal.translations["zh-CN"];
await i18n.setLocale("zh-CN");
expect(t("common.health")).toBe("健康状况");
await translate.i18n.setLocale("zh-CN");
expect(translate.t("common.health")).toBe("健康状况");
});
it("loads saved non-English locale on startup", async () => {
localStorage.setItem("openclaw.i18n.locale", "zh-CN");
vi.resetModules();
vi.stubGlobal("localStorage", createStorageMock());
vi.stubGlobal("navigator", { language: "en-US" } as Navigator);
localStorage.setItem("openclaw.i18n.locale", "zh-CN");
const fresh = await import("../lib/translate.ts");
for (let index = 0; index < 5 && fresh.i18n.getLocale() !== "zh-CN"; index += 1) {
await Promise.resolve();
}
await vi.waitFor(() => {
expect(fresh.i18n.getLocale()).toBe("zh-CN");
});
expect(fresh.i18n.getLocale()).toBe("zh-CN");
expect(fresh.t("common.health")).toBe("健康状况");
});
it("keeps the version label available in shipped locales", () => {
expect((pt_BR.common as { version?: string }).version).toBeTruthy();
expect((zh_CN.common as { version?: string }).version).toBeTruthy();
expect((zh_TW.common as { version?: string }).version).toBeTruthy();
});
});