From ae17ea9c1bde931fdeee7d3752ca0e18b2b906ab Mon Sep 17 00:00:00 2001 From: Apple User Date: Wed, 28 Jan 2026 09:53:03 +0800 Subject: [PATCH] feat: add zh-CN localization and i18n support --- src/canvas-host/a2ui/.bundle.hash | 2 +- ui/src/styles/components.css | 7 + ui/src/ui/app-gateway.ts | 26 +- ui/src/ui/app-render.helpers.ts | 25 +- ui/src/ui/app-render.ts | 48 +- ui/src/ui/app-settings.ts | 11 + ui/src/ui/chat/grouped-render.ts | 13 +- ui/src/ui/chat/tool-cards.ts | 9 +- ui/src/ui/controllers/channels.ts | 3 +- ui/src/ui/controllers/cron.ts | 13 +- ui/src/ui/controllers/presence.ts | 13 +- ui/src/ui/controllers/sessions.ts | 5 +- ui/src/ui/format.ts | 27 +- ui/src/ui/i18n.ts | 84 ++ ui/src/ui/locales/README.md | 45 + ui/src/ui/locales/en.ts | 801 ++++++++++++++++++ ui/src/ui/locales/zh-CN.ts | 801 ++++++++++++++++++ ui/src/ui/navigation.ts | 56 +- ui/src/ui/presenter.ts | 43 +- ui/src/ui/storage.ts | 49 +- ui/src/ui/views/channels.config.ts | 11 +- ui/src/ui/views/channels.discord.ts | 25 +- ui/src/ui/views/channels.googlechat.ts | 61 +- ui/src/ui/views/channels.imessage.ts | 25 +- .../ui/views/channels.nostr-profile-form.ts | 53 +- ui/src/ui/views/channels.nostr.ts | 51 +- ui/src/ui/views/channels.shared.ts | 5 +- ui/src/ui/views/channels.signal.ts | 29 +- ui/src/ui/views/channels.slack.ts | 25 +- ui/src/ui/views/channels.telegram.ts | 41 +- ui/src/ui/views/channels.ts | 77 +- ui/src/ui/views/channels.whatsapp.ts | 45 +- ui/src/ui/views/chat.ts | 40 +- ui/src/ui/views/config-form.node.ts | 101 ++- ui/src/ui/views/config-form.render.ts | 91 +- ui/src/ui/views/config-form.shared.ts | 28 + ui/src/ui/views/config-uihints.overrides.ts | 142 ++++ ui/src/ui/views/config.ts | 121 ++- ui/src/ui/views/cron.ts | 167 ++-- ui/src/ui/views/debug.ts | 47 +- ui/src/ui/views/exec-approval.ts | 29 +- ui/src/ui/views/instances.ts | 23 +- ui/src/ui/views/logs.ts | 27 +- ui/src/ui/views/markdown-sidebar.ts | 9 +- ui/src/ui/views/nodes.ts | 219 ++--- ui/src/ui/views/overview.ts | 129 +-- ui/src/ui/views/sessions.ts | 55 +- ui/src/ui/views/skills.ts | 35 +- 48 files changed, 2995 insertions(+), 797 deletions(-) create mode 100644 ui/src/ui/i18n.ts create mode 100644 ui/src/ui/locales/README.md create mode 100644 ui/src/ui/locales/en.ts create mode 100644 ui/src/ui/locales/zh-CN.ts create mode 100644 ui/src/ui/views/config-uihints.overrides.ts diff --git a/src/canvas-host/a2ui/.bundle.hash b/src/canvas-host/a2ui/.bundle.hash index 19a232f5c..3a5f32dff 100644 --- a/src/canvas-host/a2ui/.bundle.hash +++ b/src/canvas-host/a2ui/.bundle.hash @@ -1 +1 @@ -2567ca5bbc065b922d96717a488d5db3120b5b033c5d0508682d1aa8fbba470a +9d9765de99758bf1005206ba11ab8f0e5ea80a1d46c974ae579779abc199e1b1 diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css index 27dfe62d1..7aaca220d 100644 --- a/ui/src/styles/components.css +++ b/ui/src/styles/components.css @@ -191,6 +191,13 @@ transition: border-color var(--duration-fast) ease; } +button.pill { + appearance: none; + font: inherit; + color: inherit; + cursor: pointer; +} + .pill:hover { border-color: var(--border-strong); } diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index 0df25bbdf..614aa5398 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -26,6 +26,7 @@ import { import type { ClawdbotApp } from "./app"; import type { ExecApprovalRequest } from "./controllers/exec-approval"; import { loadAssistantIdentity } from "./controllers/assistant-identity"; +import { t } from "./i18n"; type GatewayHost = { settings: UiSettings; @@ -108,6 +109,24 @@ function applySessionDefaults(host: GatewayHost, defaults?: SessionDefaultsSnaps } } +function localizeCloseReason(reason: string | undefined | null) { + const raw = typeof reason === "string" ? reason.trim() : ""; + if (!raw) return t("gateway.noReason"); + const lower = raw.toLowerCase(); + if (lower.includes("gateway token missing")) return t("gateway.reason.tokenMissing"); + if (lower.includes("gateway token mismatch")) return t("gateway.reason.tokenMismatch"); + if (lower.includes("gateway token not configured")) return t("gateway.reason.tokenNotConfigured"); + if (lower.includes("gateway password missing")) return t("gateway.reason.passwordMissing"); + if (lower.includes("gateway password mismatch")) return t("gateway.reason.passwordMismatch"); + if (lower.includes("gateway password not configured")) return t("gateway.reason.passwordNotConfigured"); + if (lower.includes("tailscale identity missing")) return t("gateway.reason.tailscaleIdentityMissing"); + if (lower.includes("tailscale proxy headers missing")) return t("gateway.reason.tailscaleProxyHeadersMissing"); + if (lower.includes("tailscale identity check failed")) return t("gateway.reason.tailscaleIdentityCheckFailed"); + if (lower.includes("tailscale identity mismatch")) return t("gateway.reason.tailscaleIdentityMismatch"); + if (lower === "unauthorized") return t("gateway.reason.unauthorized"); + return raw; +} + export function connectGateway(host: GatewayHost) { host.lastError = null; host.hello = null; @@ -143,12 +162,15 @@ export function connectGateway(host: GatewayHost) { host.connected = false; // Code 1012 = Service Restart (expected during config saves, don't show as error) if (code !== 1012) { - host.lastError = `disconnected (${code}): ${reason || "no reason"}`; + host.lastError = t("gateway.disconnected", { + code, + reason: localizeCloseReason(reason), + }); } }, onEvent: (evt) => handleGatewayEvent(host, evt), onGap: ({ expected, received }) => { - host.lastError = `event gap detected (expected seq ${expected}, got ${received}); refresh recommended`; + host.lastError = t("gateway.eventGap", { expected, received }); }, }); host.client.start(); diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 22f8d90db..5b348b2b0 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -9,6 +9,7 @@ import { syncUrlWithSessionKey } from "./app-settings"; import type { SessionsListResult } from "./types"; import type { ThemeMode } from "./theme"; import type { ThemeTransitionContext } from "./theme-transition"; +import { t } from "./i18n"; export function renderTab(state: AppViewState, tab: Tab) { const href = pathForTab(tab, state.basePath); @@ -89,7 +90,7 @@ export function renderChatControls(state: AppViewState) { state.resetToolStream(); void loadChatHistory(state); }} - title="Refresh chat history" + title=${t("chatControls.refreshHistory")} > ${refreshIcon} @@ -105,9 +106,7 @@ export function renderChatControls(state: AppViewState) { }); }} aria-pressed=${showThinking} - title=${disableThinkingToggle - ? "Disabled during onboarding" - : "Toggle assistant thinking/working output"} + title=${disableThinkingToggle ? t("chatControls.disabledOnboarding") : t("chatControls.toggleThinking")} > ${icons.brain} @@ -122,9 +121,7 @@ export function renderChatControls(state: AppViewState) { }); }} aria-pressed=${focusActive} - title=${disableFocusToggle - ? "Disabled during onboarding" - : "Toggle focus mode (hide sidebar + page header)"} + title=${disableFocusToggle ? t("chatControls.disabledOnboarding") : t("chatControls.toggleFocus")} > ${focusIcon} @@ -171,14 +168,14 @@ export function renderThemeToggle(state: AppViewState) { return html`
-
+
@@ -186,8 +183,8 @@ export function renderThemeToggle(state: AppViewState) { class="theme-toggle__button ${state.theme === "light" ? "active" : ""}" @click=${applyTheme("light")} aria-pressed=${state.theme === "light"} - aria-label="Light theme" - title="Light" + aria-label=${t("theme.light")} + title=${t("theme.light")} > ${renderSunIcon()} @@ -195,8 +192,8 @@ export function renderThemeToggle(state: AppViewState) { class="theme-toggle__button ${state.theme === "dark" ? "active" : ""}" @click=${applyTheme("dark")} aria-pressed=${state.theme === "dark"} - aria-label="Dark theme" - title="Dark" + aria-label=${t("theme.dark")} + title=${t("theme.dark")} > ${renderMoonIcon()} diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index fe67c86f1..a4da7b33c 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -15,6 +15,7 @@ import { icons } from "./icons"; import type { UiSettings } from "./storage"; import type { ThemeMode } from "./theme"; import type { ThemeTransitionContext } from "./theme-transition"; +import { t } from "./i18n"; import type { ConfigSnapshot, CronJob, @@ -104,7 +105,7 @@ export function renderApp(state: AppViewState) { const presenceCount = state.presenceEntries.length; const sessionsCount = state.sessionsResult?.count ?? null; const cronNext = state.cronStatus?.nextWakeAtMs ?? null; - const chatDisabledReason = state.connected ? null : "Disconnected from gateway."; + const chatDisabledReason = state.connected ? null : t("chat.disconnectedReason"); const isChat = state.tab === "chat"; const chatFocus = isChat && (state.settings.chatFocusMode || state.onboarding); const showThinking = state.onboarding ? false : state.settings.chatShowThinking; @@ -122,8 +123,8 @@ export function renderApp(state: AppViewState) { ...state.settings, navCollapsed: !state.settings.navCollapsed, })} - title="${state.settings.navCollapsed ? "Expand sidebar" : "Collapse sidebar"}" - aria-label="${state.settings.navCollapsed ? "Expand sidebar" : "Collapse sidebar"}" + title=${state.settings.navCollapsed ? t("topbar.expandSidebar") : t("topbar.collapseSidebar")} + aria-label=${state.settings.navCollapsed ? t("topbar.expandSidebar") : t("topbar.collapseSidebar")} > ${icons.menu} @@ -133,22 +134,37 @@ export function renderApp(state: AppViewState) {
CLAWDBOT
-
Gateway Dashboard
+
${t("topbar.gatewayDashboard")}
- Health - ${state.connected ? "OK" : "Offline"} + ${t("topbar.health")} + ${state.connected ? t("topbar.healthOk") : t("topbar.healthOffline")}
+ ${renderThemeToggle(state)}