From 742e6543c7ab342d2771cd8e6e7349846b805fea Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 17 Feb 2026 02:46:16 +0100 Subject: [PATCH] fix(ui): preserve locale bootstrap and trusted-proxy overview behavior --- ui/src/i18n/lib/translate.ts | 12 +++++-- ui/src/i18n/locales/en.ts | 1 + ui/src/i18n/locales/pt-BR.ts | 1 + ui/src/i18n/locales/zh-CN.ts | 1 + ui/src/i18n/locales/zh-TW.ts | 1 + ui/src/ui/app.ts | 67 +++++++++++++++++------------------ ui/src/ui/storage.ts | 4 +-- ui/src/ui/views/overview.ts | 68 ++++++++++++++++++++++-------------- 8 files changed, 88 insertions(+), 67 deletions(-) diff --git a/ui/src/i18n/lib/translate.ts b/ui/src/i18n/lib/translate.ts index 4218206d53..8ba9557b72 100644 --- a/ui/src/i18n/lib/translate.ts +++ b/ui/src/i18n/lib/translate.ts @@ -1,8 +1,14 @@ -import { en } from "../locales/en.ts"; import type { Locale, TranslationMap } from "./types.ts"; +import { en } from "../locales/en.ts"; type Subscriber = (locale: Locale) => void; +export const SUPPORTED_LOCALES: ReadonlyArray = ["en", "zh-CN", "zh-TW", "pt-BR"]; + +export function isSupportedLocale(value: string | null | undefined): value is Locale { + return value !== null && value !== undefined && SUPPORTED_LOCALES.includes(value as Locale); +} + class I18nManager { private locale: Locale = "en"; private translations: Record = { en } as Record; @@ -13,8 +19,8 @@ class I18nManager { } private loadLocale() { - const saved = localStorage.getItem("openclaw.i18n.locale") as Locale; - if (saved && ["en", "zh-CN", "zh-TW", "pt-BR"].includes(saved)) { + const saved = localStorage.getItem("openclaw.i18n.locale"); + if (isSupportedLocale(saved)) { this.locale = saved; } else { const navLang = navigator.language; diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index 6bddd2e22a..db973ec2b7 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -61,6 +61,7 @@ export const en: TranslationMap = { sessionKey: "Default Session Key", language: "Language", connectHint: "Click Connect to apply connection changes.", + trustedProxy: "Authenticated via trusted proxy.", }, snapshot: { title: "Snapshot", diff --git a/ui/src/i18n/locales/pt-BR.ts b/ui/src/i18n/locales/pt-BR.ts index 138ee1224e..77123f0691 100644 --- a/ui/src/i18n/locales/pt-BR.ts +++ b/ui/src/i18n/locales/pt-BR.ts @@ -61,6 +61,7 @@ export const pt_BR: TranslationMap = { sessionKey: "Chave de Sessão Padrão", language: "Idioma", connectHint: "Clique em Conectar para aplicar as alterações de conexão.", + trustedProxy: "Autenticado por proxy confiável.", }, snapshot: { title: "Snapshot", diff --git a/ui/src/i18n/locales/zh-CN.ts b/ui/src/i18n/locales/zh-CN.ts index 376a439921..6addadb11f 100644 --- a/ui/src/i18n/locales/zh-CN.ts +++ b/ui/src/i18n/locales/zh-CN.ts @@ -61,6 +61,7 @@ export const zh_CN: TranslationMap = { sessionKey: "默认会话密钥", language: "语言", connectHint: "点击连接以应用连接更改。", + trustedProxy: "通过受信任代理认证。", }, snapshot: { title: "快照", diff --git a/ui/src/i18n/locales/zh-TW.ts b/ui/src/i18n/locales/zh-TW.ts index 1d956dbb88..9187776eb7 100644 --- a/ui/src/i18n/locales/zh-TW.ts +++ b/ui/src/i18n/locales/zh-TW.ts @@ -61,6 +61,7 @@ export const zh_TW: TranslationMap = { sessionKey: "默認會話密鑰", language: "語言", connectHint: "點擊連接以應用連接更改。", + trustedProxy: "通過受信任代理身份驗證。", }, snapshot: { title: "快照", diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 74ad07fec1..eb66b8ffb0 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -1,6 +1,35 @@ import { LitElement } from "lit"; import { customElement, state } from "lit/decorators.js"; -import { i18n, I18nController, type Locale } from "../i18n/index.ts"; +import type { EventLogEntry } from "./app-events.ts"; +import type { AppViewState } from "./app-view-state.ts"; +import type { DevicePairingList } from "./controllers/devices.ts"; +import type { ExecApprovalRequest } from "./controllers/exec-approval.ts"; +import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "./controllers/exec-approvals.ts"; +import type { SkillMessage } from "./controllers/skills.ts"; +import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway.ts"; +import type { Tab } from "./navigation.ts"; +import type { ResolvedTheme, ThemeMode } from "./theme.ts"; +import type { + AgentsListResult, + AgentsFilesListResult, + AgentIdentityResult, + ConfigSnapshot, + ConfigUiHints, + CronJob, + CronRunLogEntry, + CronStatus, + HealthSnapshot, + LogEntry, + LogLevel, + PresenceEntry, + ChannelsStatusSnapshot, + SessionsListResult, + SkillStatusReport, + StatusSummary, + NostrProfile, +} from "./types.ts"; +import type { NostrProfileFormState } from "./views/channels.nostr-profile-form.ts"; +import { i18n, I18nController, isSupportedLocale } from "../i18n/index.ts"; import { handleChannelConfigReload as handleChannelConfigReloadInternal, handleChannelConfigSave as handleChannelConfigSaveInternal, @@ -20,7 +49,6 @@ import { removeQueuedMessage as removeQueuedMessageInternal, } from "./app-chat.ts"; import { DEFAULT_CRON_FORM, DEFAULT_LOG_LEVEL_FILTERS } from "./app-defaults.ts"; -import type { EventLogEntry } from "./app-events.ts"; import { connectGateway as connectGatewayInternal } from "./app-gateway.ts"; import { handleConnected, @@ -49,38 +77,10 @@ import { type ToolStreamEntry, type CompactionStatus, } from "./app-tool-stream.ts"; -import type { AppViewState } from "./app-view-state.ts"; import { normalizeAssistantIdentity } from "./assistant-identity.ts"; import { loadAssistantIdentity as loadAssistantIdentityInternal } from "./controllers/assistant-identity.ts"; -import type { DevicePairingList } from "./controllers/devices.ts"; -import type { ExecApprovalRequest } from "./controllers/exec-approval.ts"; -import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "./controllers/exec-approvals.ts"; -import type { SkillMessage } from "./controllers/skills.ts"; -import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway.ts"; -import type { Tab } from "./navigation.ts"; import { loadSettings, type UiSettings } from "./storage.ts"; -import type { ResolvedTheme, ThemeMode } from "./theme.ts"; -import type { - AgentsListResult, - AgentsFilesListResult, - AgentIdentityResult, - ConfigSnapshot, - ConfigUiHints, - CronJob, - CronRunLogEntry, - CronStatus, - HealthSnapshot, - LogEntry, - LogLevel, - PresenceEntry, - ChannelsStatusSnapshot, - SessionsListResult, - SkillStatusReport, - StatusSummary, - NostrProfile, -} from "./types.ts"; import { type ChatAttachment, type ChatQueueItem, type CronFormState } from "./ui-types.ts"; -import type { NostrProfileFormState } from "./views/channels.nostr-profile-form.ts"; declare global { interface Window { @@ -109,11 +109,8 @@ export class OpenClawApp extends LitElement { @state() settings: UiSettings = loadSettings(); constructor() { super(); - if (this.settings.locale) { - const supportedLocales: Locale[] = ["en", "zh-CN", "zh-TW", "pt-BR"]; - if (supportedLocales.includes(this.settings.locale as Locale)) { - void i18n.setLocale(this.settings.locale as Locale); - } + if (isSupportedLocale(this.settings.locale)) { + void i18n.setLocale(this.settings.locale); } } @state() password = ""; diff --git a/ui/src/ui/storage.ts b/ui/src/ui/storage.ts index a892b0c6ae..b2eecc3295 100644 --- a/ui/src/ui/storage.ts +++ b/ui/src/ui/storage.ts @@ -1,6 +1,7 @@ const KEY = "openclaw.control.settings.v1"; import type { ThemeMode } from "./theme.ts"; +import { isSupportedLocale } from "../i18n/index.ts"; export type UiSettings = { gatewayUrl: string; @@ -33,7 +34,6 @@ export function loadSettings(): UiSettings { splitRatio: 0.6, navCollapsed: false, navGroupsCollapsed: {}, - locale: "en", }; try { @@ -79,7 +79,7 @@ export function loadSettings(): UiSettings { typeof parsed.navGroupsCollapsed === "object" && parsed.navGroupsCollapsed !== null ? parsed.navGroupsCollapsed : defaults.navGroupsCollapsed, - locale: typeof parsed.locale === "string" ? parsed.locale : defaults.locale, + locale: isSupportedLocale(parsed.locale) ? parsed.locale : undefined, }; } catch { return defaults; diff --git a/ui/src/ui/views/overview.ts b/ui/src/ui/views/overview.ts index 2d361ee95d..caa2a6fd15 100644 --- a/ui/src/ui/views/overview.ts +++ b/ui/src/ui/views/overview.ts @@ -1,9 +1,9 @@ import { html } from "lit"; +import type { GatewayHelloOk } from "../gateway.ts"; +import type { UiSettings } from "../storage.ts"; import { t, i18n, type Locale } from "../../i18n/index.ts"; import { formatRelativeTimestamp, formatDurationHuman } from "../format.ts"; -import type { GatewayHelloOk } from "../gateway.ts"; import { formatNextRun } from "../presenter.ts"; -import type { UiSettings } from "../storage.ts"; export type OverviewProps = { connected: boolean; @@ -25,12 +25,18 @@ export type OverviewProps = { export function renderOverview(props: OverviewProps) { const snapshot = props.hello?.snapshot as - | { uptimeMs?: number; policy?: { tickIntervalMs?: number } } + | { + uptimeMs?: number; + policy?: { tickIntervalMs?: number }; + authMode?: "none" | "token" | "password" | "trusted-proxy"; + } | undefined; const uptime = snapshot?.uptimeMs ? formatDurationHuman(snapshot.uptimeMs) : t("common.na"); const tick = snapshot?.policy?.tickIntervalMs ? `${snapshot.policy.tickIntervalMs}ms` : t("common.na"); + const authMode = snapshot?.authMode; + const isTrustedProxy = authMode === "trusted-proxy"; const authHint = (() => { if (props.connected || !props.lastError) { @@ -141,29 +147,35 @@ export function renderOverview(props: OverviewProps) { placeholder="ws://100.x.y.z:18789" /> - - + ${ + isTrustedProxy + ? "" + : html` + + + ` + }