- 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
154 lines
3.9 KiB
TypeScript
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}`);
|
|
}
|