Skip to content

Commit abce640

Browse files
git-jxjaltaywtf
andauthored
fix(ui): language dropdown selection not persisting after refresh (#48019)
Merged via squash. Prepared head SHA: 06c8258 Co-authored-by: git-jxj <[email protected]> Co-authored-by: altaywtf <[email protected]> Reviewed-by: @altaywtf
1 parent 2de2837 commit abce640

File tree

3 files changed

+96
-3
lines changed

3 files changed

+96
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Docs: https://docs.openclaw.ai
103103
- 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.
104104
- 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.
105105
- 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.
106+
- 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.
106107

107108
## 2026.3.13
108109

ui/src/ui/views/chat.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import { render } from "lit";
44
import { describe, expect, it, vi } from "vitest";
5+
import { i18n } from "../../i18n/index.ts";
56
import { getSafeLocalStorage } from "../../local-storage.ts";
67
import { renderChatSessionSelect } from "../app-render.helpers.ts";
78
import type { AppViewState } from "../app-view-state.ts";
89
import type { GatewayBrowserClient } from "../gateway.ts";
910
import type { ModelCatalogEntry } from "../types.ts";
1011
import type { SessionsListResult } from "../types.ts";
1112
import { renderChat, type ChatProps } from "./chat.ts";
13+
import { renderOverview, type OverviewProps } from "./overview.ts";
1214

1315
function createSessions(): SessionsListResult {
1416
return {
@@ -195,6 +197,57 @@ function createProps(overrides: Partial<ChatProps> = {}): ChatProps {
195197
};
196198
}
197199

200+
function createOverviewProps(overrides: Partial<OverviewProps> = {}): OverviewProps {
201+
return {
202+
connected: false,
203+
hello: null,
204+
settings: {
205+
gatewayUrl: "",
206+
token: "",
207+
sessionKey: "main",
208+
lastActiveSessionKey: "main",
209+
theme: "claw",
210+
themeMode: "system",
211+
chatFocusMode: false,
212+
chatShowThinking: true,
213+
chatShowToolCalls: true,
214+
splitRatio: 0.6,
215+
navCollapsed: false,
216+
navWidth: 220,
217+
navGroupsCollapsed: {},
218+
locale: "en",
219+
},
220+
password: "",
221+
lastError: null,
222+
lastErrorCode: null,
223+
presenceCount: 0,
224+
sessionsCount: null,
225+
cronEnabled: null,
226+
cronNext: null,
227+
lastChannelsRefresh: null,
228+
usageResult: null,
229+
sessionsResult: null,
230+
skillsReport: null,
231+
cronJobs: [],
232+
cronStatus: null,
233+
attentionItems: [],
234+
eventLog: [],
235+
overviewLogLines: [],
236+
showGatewayToken: false,
237+
showGatewayPassword: false,
238+
onSettingsChange: () => undefined,
239+
onPasswordChange: () => undefined,
240+
onSessionKeyChange: () => undefined,
241+
onToggleGatewayTokenVisibility: () => undefined,
242+
onToggleGatewayPasswordVisibility: () => undefined,
243+
onConnect: () => undefined,
244+
onRefresh: () => undefined,
245+
onNavigate: () => undefined,
246+
onRefreshLogs: () => undefined,
247+
...overrides,
248+
};
249+
}
250+
198251
describe("chat view", () => {
199252
it("uses the assistant avatar URL for the welcome state when the identity avatar is only initials", () => {
200253
const container = document.createElement("div");
@@ -285,6 +338,41 @@ describe("chat view", () => {
285338
expect(groupedLogo?.getAttribute("src")).toBe("/openclaw/favicon.svg");
286339
});
287340

341+
it("keeps the persisted overview locale selected before i18n hydration finishes", async () => {
342+
const container = document.createElement("div");
343+
const props = createOverviewProps({
344+
settings: {
345+
...createOverviewProps().settings,
346+
locale: "zh-CN",
347+
},
348+
});
349+
350+
try {
351+
localStorage.clear();
352+
} catch {
353+
/* noop */
354+
}
355+
await i18n.setLocale("en");
356+
357+
render(renderOverview(props), container);
358+
await Promise.resolve();
359+
360+
let select = container.querySelector<HTMLSelectElement>("select");
361+
expect(i18n.getLocale()).toBe("en");
362+
expect(select?.value).toBe("zh-CN");
363+
expect(select?.selectedOptions[0]?.textContent?.trim()).toBe("简体中文 (Simplified Chinese)");
364+
365+
await i18n.setLocale("zh-CN");
366+
render(renderOverview(props), container);
367+
await Promise.resolve();
368+
369+
select = container.querySelector<HTMLSelectElement>("select");
370+
expect(select?.value).toBe("zh-CN");
371+
expect(select?.selectedOptions[0]?.textContent?.trim()).toBe("简体中文 (简体中文)");
372+
373+
await i18n.setLocale("en");
374+
});
375+
288376
it("renders compacting indicator as a badge", () => {
289377
const container = document.createElement("div");
290378
render(

ui/src/ui/views/overview.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { html, nothing } from "lit";
2-
import { t, i18n, SUPPORTED_LOCALES, type Locale } from "../../i18n/index.ts";
2+
import { t, i18n, SUPPORTED_LOCALES, type Locale, isSupportedLocale } from "../../i18n/index.ts";
33
import type { EventLogEntry } from "../app-events.ts";
44
import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "../external-link.ts";
55
import { formatRelativeTimestamp, formatDurationHuman } from "../format.ts";
@@ -190,7 +190,9 @@ export function renderOverview(props: OverviewProps) {
190190
`;
191191
})();
192192

193-
const currentLocale = i18n.getLocale();
193+
const currentLocale = isSupportedLocale(props.settings.locale)
194+
? props.settings.locale
195+
: i18n.getLocale();
194196

195197
return html`
196198
<section class="grid">
@@ -295,7 +297,9 @@ export function renderOverview(props: OverviewProps) {
295297
>
296298
${SUPPORTED_LOCALES.map((loc) => {
297299
const key = loc.replace(/-([a-zA-Z])/g, (_, c) => c.toUpperCase());
298-
return html`<option value=${loc}>${t(`languages.${key}`)}</option>`;
300+
return html`<option value=${loc} ?selected=${currentLocale === loc}>
301+
${t(`languages.${key}`)}
302+
</option>`;
299303
})}
300304
</select>
301305
</label>

0 commit comments

Comments
 (0)