fix(ui): language dropdown selection not persisting after refresh (#48019)
Merged via squash. Prepared head SHA: 06c82586d96392dfd49a6944971d854f5890dc1c Co-authored-by: git-jxj <65210887+git-jxj@users.noreply.github.com> Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com> Reviewed-by: @altaywtf
This commit is contained in:
parent
2de28379dd
commit
abce640772
@ -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
|
||||
|
||||
|
||||
@ -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> = {}): ChatProps {
|
||||
};
|
||||
}
|
||||
|
||||
function createOverviewProps(overrides: Partial<OverviewProps> = {}): 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<HTMLSelectElement>("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<HTMLSelectElement>("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(
|
||||
|
||||
@ -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`
|
||||
<section class="grid">
|
||||
@ -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`<option value=${loc}>${t(`languages.${key}`)}</option>`;
|
||||
return html`<option value=${loc} ?selected=${currentLocale === loc}>
|
||||
${t(`languages.${key}`)}
|
||||
</option>`;
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user