openclaw/ui/src/ui/navigation.ts
Mike a4dee5cba8 feat(ui): add i18n internationalization support with Chinese translation
- Add i18n core module with locale management and translation helpers
- Add English (en.json) and Chinese (zh.json) translation files
- Add language-switcher component for the top bar
- Update navigation.ts to use localized strings
- Update app-render.ts to include language switcher and i18n
- Update app.ts to subscribe to locale changes for reactivity

Features:
- Auto-detect browser language preference
- Persist language choice in localStorage
- Support for nested translation keys (e.g., nav.tabs.chat)
- Fallback to English for missing translations

Translations include:
- Navigation menu items and descriptions
- Status indicators (connected, offline, etc.)
- Action buttons (save, cancel, apply, etc.)
- Configuration categories
- Common UI elements
2026-01-28 19:18:32 +08:00

154 lines
3.9 KiB
TypeScript

import type { IconName } from "./icons.js";
import { t } from "../i18n/index.js";
// Tab group labels (used as keys for i18n)
const TAB_GROUP_LABELS = {
Chat: "nav.groups.chat",
Control: "nav.groups.control",
Agent: "nav.groups.agent",
Settings: "nav.groups.settings",
} as const;
export const TAB_GROUPS = [
{ label: "Chat", tabs: ["chat"] },
{
label: "Control",
tabs: ["overview", "channels", "instances", "sessions", "cron"],
},
{ label: "Agent", tabs: ["skills", "nodes"] },
{ label: "Settings", tabs: ["config", "debug", "logs"] },
] as const;
/** Get localized label for a tab group */
export function localizedGroupLabel(label: string): string {
const key = TAB_GROUP_LABELS[label as keyof typeof TAB_GROUP_LABELS];
return key ? t(key) : label;
}
export type Tab =
| "overview"
| "channels"
| "instances"
| "sessions"
| "cron"
| "skills"
| "nodes"
| "chat"
| "config"
| "debug"
| "logs";
const TAB_PATHS: Record<Tab, string> = {
overview: "/overview",
channels: "/channels",
instances: "/instances",
sessions: "/sessions",
cron: "/cron",
skills: "/skills",
nodes: "/nodes",
chat: "/chat",
config: "/config",
debug: "/debug",
logs: "/logs",
};
const PATH_TO_TAB = new Map(
Object.entries(TAB_PATHS).map(([tab, path]) => [path, tab as Tab]),
);
export function normalizeBasePath(basePath: string): string {
if (!basePath) return "";
let base = basePath.trim();
if (!base.startsWith("/")) base = `/${base}`;
if (base === "/") return "";
if (base.endsWith("/")) base = base.slice(0, -1);
return base;
}
export function normalizePath(path: string): string {
if (!path) return "/";
let normalized = path.trim();
if (!normalized.startsWith("/")) normalized = `/${normalized}`;
if (normalized.length > 1 && normalized.endsWith("/")) {
normalized = normalized.slice(0, -1);
}
return normalized;
}
export function pathForTab(tab: Tab, basePath = ""): string {
const base = normalizeBasePath(basePath);
const path = TAB_PATHS[tab];
return base ? `${base}${path}` : path;
}
export function tabFromPath(pathname: string, basePath = ""): Tab | null {
const base = normalizeBasePath(basePath);
let path = pathname || "/";
if (base) {
if (path === base) {
path = "/";
} else if (path.startsWith(`${base}/`)) {
path = path.slice(base.length);
}
}
let normalized = normalizePath(path).toLowerCase();
if (normalized.endsWith("/index.html")) normalized = "/";
if (normalized === "/") return "chat";
return PATH_TO_TAB.get(normalized) ?? null;
}
export function inferBasePathFromPathname(pathname: string): string {
let normalized = normalizePath(pathname);
if (normalized.endsWith("/index.html")) {
normalized = normalizePath(normalized.slice(0, -"/index.html".length));
}
if (normalized === "/") return "";
const segments = normalized.split("/").filter(Boolean);
if (segments.length === 0) return "";
for (let i = 0; i < segments.length; i++) {
const candidate = `/${segments.slice(i).join("/")}`.toLowerCase();
if (PATH_TO_TAB.has(candidate)) {
const prefix = segments.slice(0, i);
return prefix.length ? `/${prefix.join("/")}` : "";
}
}
return `/${segments.join("/")}`;
}
export function iconForTab(tab: Tab): IconName {
switch (tab) {
case "chat":
return "messageSquare";
case "overview":
return "barChart";
case "channels":
return "link";
case "instances":
return "radio";
case "sessions":
return "fileText";
case "cron":
return "loader";
case "skills":
return "zap";
case "nodes":
return "monitor";
case "config":
return "settings";
case "debug":
return "bug";
case "logs":
return "scrollText";
default:
return "folder";
}
}
export function titleForTab(tab: Tab): string {
return t(`nav.tabs.${tab}`);
}
export function subtitleForTab(tab: Tab): string {
return t(`nav.subtitles.${tab}`);
}