mirror of
https://github.com/hansjone/netx.git
synced 2026-10-11 04:13:21 +08:00
feat(web): NetX workbench, UME-only UI, i18n, and header status
- Replace sidebar app with workbench home and module tabs (openOrFocusModule) - Remove legacy pages; add module registry, shared tab channel, WEB.md - Add zh/en i18n, global ToastProvider, restore oclaw bridge pill in header Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
664775927b
commit
282dbe75af
33 changed files with 1631 additions and 1677 deletions
78
web/src/i18n/context.tsx
Normal file
78
web/src/i18n/context.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import en from "./en";
|
||||
import zh from "./zh";
|
||||
|
||||
export type Locale = "zh" | "en";
|
||||
|
||||
const STORAGE_KEY = "netx-locale";
|
||||
|
||||
const MESSAGES: Record<Locale, typeof zh> = { zh, en };
|
||||
|
||||
function resolveMessage(obj: unknown, path: string): string {
|
||||
const keys = path.split(".");
|
||||
let cur: unknown = obj;
|
||||
for (const k of keys) {
|
||||
if (cur == null || typeof cur !== "object") return path;
|
||||
cur = (cur as Record<string, unknown>)[k];
|
||||
}
|
||||
return typeof cur === "string" ? cur : path;
|
||||
}
|
||||
|
||||
function interpolate(text: string, vars?: Record<string, string | number>): string {
|
||||
if (!vars) return text;
|
||||
let out = text;
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
out = out.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), String(v));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
type I18nContextValue = {
|
||||
locale: Locale;
|
||||
setLocale: (locale: Locale) => void;
|
||||
t: (key: string, vars?: Record<string, string | number>) => string;
|
||||
};
|
||||
|
||||
const I18nContext = createContext<I18nContextValue | null>(null);
|
||||
|
||||
export function I18nProvider({ children }: { children: ReactNode }) {
|
||||
const [locale, setLocaleState] = useState<Locale>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
return saved === "en" ? "en" : "zh";
|
||||
} catch {
|
||||
return "zh";
|
||||
}
|
||||
});
|
||||
|
||||
const setLocale = useCallback((next: Locale) => {
|
||||
setLocaleState(next);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, next);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, []);
|
||||
|
||||
const t = useCallback(
|
||||
(key: string, vars?: Record<string, string | number>) => {
|
||||
const raw = resolveMessage(MESSAGES[locale], key);
|
||||
return interpolate(raw, vars);
|
||||
},
|
||||
[locale],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = locale === "en" ? "en" : "zh-CN";
|
||||
}, [locale]);
|
||||
|
||||
const value = useMemo(() => ({ locale, setLocale, t }), [locale, setLocale, t]);
|
||||
|
||||
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
|
||||
}
|
||||
|
||||
export function useI18n() {
|
||||
const ctx = useContext(I18nContext);
|
||||
if (!ctx) throw new Error("useI18n must be used within I18nProvider");
|
||||
return ctx;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue