mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 00:20:22 +00:00
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:
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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: "已断开与网关的连接。",
|
||||
|
||||
@@ -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: "已斷開與網關的連接。",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user