fix(ui): preserve locale bootstrap and trusted-proxy overview behavior

This commit is contained in:
Peter Steinberger
2026-02-17 02:46:16 +01:00
parent accb673490
commit 742e6543c7
8 changed files with 88 additions and 67 deletions

View File

@@ -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<Locale> = ["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<Locale, TranslationMap> = { en } as Record<Locale, TranslationMap>;
@@ -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;

View File

@@ -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",

View File

@@ -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",

View File

@@ -61,6 +61,7 @@ export const zh_CN: TranslationMap = {
sessionKey: "默认会话密钥",
language: "语言",
connectHint: "点击连接以应用连接更改。",
trustedProxy: "通过受信任代理认证。",
},
snapshot: {
title: "快照",

View File

@@ -61,6 +61,7 @@ export const zh_TW: TranslationMap = {
sessionKey: "默認會話密鑰",
language: "語言",
connectHint: "點擊連接以應用連接更改。",
trustedProxy: "通過受信任代理身份驗證。",
},
snapshot: {
title: "快照",

View File

@@ -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 = "";

View File

@@ -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;

View File

@@ -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"
/>
</label>
<label class="field">
<span>${t("overview.access.token")}</span>
<input
.value=${props.settings.token}
@input=${(e: Event) => {
const v = (e.target as HTMLInputElement).value;
props.onSettingsChange({ ...props.settings, token: v });
}}
placeholder="OPENCLAW_GATEWAY_TOKEN"
/>
</label>
<label class="field">
<span>${t("overview.access.password")}</span>
<input
type="password"
.value=${props.password}
@input=${(e: Event) => {
const v = (e.target as HTMLInputElement).value;
props.onPasswordChange(v);
}}
placeholder="system or shared password"
/>
</label>
${
isTrustedProxy
? ""
: html`
<label class="field">
<span>${t("overview.access.token")}</span>
<input
.value=${props.settings.token}
@input=${(e: Event) => {
const v = (e.target as HTMLInputElement).value;
props.onSettingsChange({ ...props.settings, token: v });
}}
placeholder="OPENCLAW_GATEWAY_TOKEN"
/>
</label>
<label class="field">
<span>${t("overview.access.password")}</span>
<input
type="password"
.value=${props.password}
@input=${(e: Event) => {
const v = (e.target as HTMLInputElement).value;
props.onPasswordChange(v);
}}
placeholder="system or shared password"
/>
</label>
`
}
<label class="field">
<span>${t("overview.access.sessionKey")}</span>
<input
@@ -194,7 +206,9 @@ export function renderOverview(props: OverviewProps) {
<div class="row" style="margin-top: 14px;">
<button class="btn" @click=${() => props.onConnect()}>${t("common.connect")}</button>
<button class="btn" @click=${() => props.onRefresh()}>${t("common.refresh")}</button>
<span class="muted">${t("overview.access.connectHint")}</span>
<span class="muted">${
isTrustedProxy ? t("overview.access.trustedProxy") : t("overview.access.connectHint")
}</span>
</div>
</div>