diff --git a/CHANGELOG.md b/CHANGELOG.md index 89d6b8e1597..68bd421888c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,7 @@ Docs: https://docs.openclaw.ai - Control UI/model switching: preserve the selected provider prefix when switching models from the chat dropdown, so multi-provider setups no longer send `anthropic/gpt-5.2`-style mismatches when the user picked `openai/gpt-5.2`. (#47581) Thanks @chrishham. - Control UI/storage: scope persisted settings keys by gateway base path, with migration from the legacy shared key, so multiple gateways under one domain stop overwriting each other's dashboard preferences. (#47932) Thanks @bobBot-claw. - Agents/usage tracking: stop forcing `supportsUsageInStreaming: false` on non-native OpenAI-completions providers so compatible backends report token usage and cost again instead of showing all zeros. (#46500) Fixes #46142. Thanks @ademczuk. +- Control UI/overview: keep the language dropdown aligned with the persisted locale during dashboard startup so refreshing the page does not fall back to English before locale hydration completes. (#48019) Thanks @git-jxj. ## 2026.3.13 diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index 68e4a3afe01..5e02b2649e2 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -2,6 +2,7 @@ import { render } from "lit"; import { describe, expect, it, vi } from "vitest"; +import { i18n } from "../../i18n/index.ts"; import { getSafeLocalStorage } from "../../local-storage.ts"; import { renderChatSessionSelect } from "../app-render.helpers.ts"; import type { AppViewState } from "../app-view-state.ts"; @@ -9,6 +10,7 @@ import type { GatewayBrowserClient } from "../gateway.ts"; import type { ModelCatalogEntry } from "../types.ts"; import type { SessionsListResult } from "../types.ts"; import { renderChat, type ChatProps } from "./chat.ts"; +import { renderOverview, type OverviewProps } from "./overview.ts"; function createSessions(): SessionsListResult { return { @@ -195,6 +197,57 @@ function createProps(overrides: Partial = {}): ChatProps { }; } +function createOverviewProps(overrides: Partial = {}): OverviewProps { + return { + connected: false, + hello: null, + settings: { + gatewayUrl: "", + token: "", + sessionKey: "main", + lastActiveSessionKey: "main", + theme: "claw", + themeMode: "system", + chatFocusMode: false, + chatShowThinking: true, + chatShowToolCalls: true, + splitRatio: 0.6, + navCollapsed: false, + navWidth: 220, + navGroupsCollapsed: {}, + locale: "en", + }, + password: "", + lastError: null, + lastErrorCode: null, + presenceCount: 0, + sessionsCount: null, + cronEnabled: null, + cronNext: null, + lastChannelsRefresh: null, + usageResult: null, + sessionsResult: null, + skillsReport: null, + cronJobs: [], + cronStatus: null, + attentionItems: [], + eventLog: [], + overviewLogLines: [], + showGatewayToken: false, + showGatewayPassword: false, + onSettingsChange: () => undefined, + onPasswordChange: () => undefined, + onSessionKeyChange: () => undefined, + onToggleGatewayTokenVisibility: () => undefined, + onToggleGatewayPasswordVisibility: () => undefined, + onConnect: () => undefined, + onRefresh: () => undefined, + onNavigate: () => undefined, + onRefreshLogs: () => undefined, + ...overrides, + }; +} + describe("chat view", () => { it("uses the assistant avatar URL for the welcome state when the identity avatar is only initials", () => { const container = document.createElement("div"); @@ -285,6 +338,41 @@ describe("chat view", () => { expect(groupedLogo?.getAttribute("src")).toBe("/openclaw/favicon.svg"); }); + it("keeps the persisted overview locale selected before i18n hydration finishes", async () => { + const container = document.createElement("div"); + const props = createOverviewProps({ + settings: { + ...createOverviewProps().settings, + locale: "zh-CN", + }, + }); + + try { + localStorage.clear(); + } catch { + /* noop */ + } + await i18n.setLocale("en"); + + render(renderOverview(props), container); + await Promise.resolve(); + + let select = container.querySelector("select"); + expect(i18n.getLocale()).toBe("en"); + expect(select?.value).toBe("zh-CN"); + expect(select?.selectedOptions[0]?.textContent?.trim()).toBe("简体中文 (Simplified Chinese)"); + + await i18n.setLocale("zh-CN"); + render(renderOverview(props), container); + await Promise.resolve(); + + select = container.querySelector("select"); + expect(select?.value).toBe("zh-CN"); + expect(select?.selectedOptions[0]?.textContent?.trim()).toBe("简体中文 (简体中文)"); + + await i18n.setLocale("en"); + }); + it("renders compacting indicator as a badge", () => { const container = document.createElement("div"); render( diff --git a/ui/src/ui/views/overview.ts b/ui/src/ui/views/overview.ts index d24aa92ce9d..bb57874103e 100644 --- a/ui/src/ui/views/overview.ts +++ b/ui/src/ui/views/overview.ts @@ -1,5 +1,5 @@ import { html, nothing } from "lit"; -import { t, i18n, SUPPORTED_LOCALES, type Locale } from "../../i18n/index.ts"; +import { t, i18n, SUPPORTED_LOCALES, type Locale, isSupportedLocale } from "../../i18n/index.ts"; import type { EventLogEntry } from "../app-events.ts"; import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "../external-link.ts"; import { formatRelativeTimestamp, formatDurationHuman } from "../format.ts"; @@ -190,7 +190,9 @@ export function renderOverview(props: OverviewProps) { `; })(); - const currentLocale = i18n.getLocale(); + const currentLocale = isSupportedLocale(props.settings.locale) + ? props.settings.locale + : i18n.getLocale(); return html`
@@ -295,7 +297,9 @@ export function renderOverview(props: OverviewProps) { > ${SUPPORTED_LOCALES.map((loc) => { const key = loc.replace(/-([a-zA-Z])/g, (_, c) => c.toUpperCase()); - return html``; + return html``; })}