This commit is contained in:
eric
2026-06-07 01:17:46 -05:00
parent 283d65d1d1
commit cd885f5fcd
110 changed files with 17876 additions and 11 deletions

View File

@@ -0,0 +1,103 @@
"use client";
import {
createContext,
useContext,
useCallback,
type ReactNode,
} from "react";
export type Locale = "zh" | "en";
const LOCALES: Locale[] = ["zh", "en"];
const DEFAULT_LOCALE: Locale = "zh";
type Messages = Record<string, unknown>;
type LocaleContextValue = {
locale: Locale;
messages: Messages;
t: (key: string) => string;
setLocale: (locale: Locale) => void;
};
const LocaleContext = createContext<LocaleContextValue | null>(null);
function getNested(obj: unknown, path: string): string | undefined {
const keys = path.split(".");
let current: unknown = obj;
for (const key of keys) {
if (current == null || typeof current !== "object") return undefined;
current = (current as Record<string, unknown>)[key];
}
return typeof current === "string" ? current : undefined;
}
export function LocaleProvider({
locale,
messages,
children,
}: {
locale: Locale;
messages: Messages;
children: ReactNode;
}) {
const t = useCallback(
(key: string) => {
const value = getNested(messages, key);
return value ?? key;
},
[messages]
);
const setLocale = useCallback((newLocale: Locale) => {
const path = window.location.pathname;
const newPath = path.replace(/^\/(zh|en)/, `/${newLocale}`);
window.location.href = newPath || `/${newLocale}/digital`;
}, []);
const value: LocaleContextValue = {
locale,
messages,
t,
setLocale,
};
return (
<LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>
);
}
export function useLocale() {
const ctx = useContext(LocaleContext);
if (!ctx) throw new Error("useLocale must be used within LocaleProvider");
return ctx.locale;
}
export function useTranslation(namespace?: string) {
const ctx = useContext(LocaleContext);
if (!ctx) throw new Error("useTranslation must be used within LocaleProvider");
return {
t: (key: string) => {
const fullKey = namespace ? `${namespace}.${key}` : key;
return ctx.t(fullKey);
},
};
}
export function useLocaleRouter() {
const locale = useLocale();
return {
replace: (pathname: string, opts?: { locale?: Locale }) => {
const newLocale = opts?.locale ?? locale;
const cleanPath = pathname.replace(/^\/(zh|en)/, "") || "/";
const digitalPath = cleanPath.startsWith("/digital")
? cleanPath
: `/digital${cleanPath === "/" ? "" : cleanPath}`;
const newPath = `/${newLocale}${digitalPath}`;
window.location.href = newPath;
},
};
}
export { LOCALES, DEFAULT_LOCALE };

View File

@@ -0,0 +1,65 @@
"use client";
import {
createContext,
useContext,
useEffect,
useState,
useCallback,
type ReactNode,
} from "react";
const STORAGE_KEY = "theme";
export type Theme = "light" | "dark";
type ThemeContextValue = {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() =>
typeof document !== "undefined" &&
document.documentElement.classList.contains("dark")
? "dark"
: "light"
);
useEffect(() => {
const root = document.documentElement;
if (theme === "dark") {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
localStorage.setItem(STORAGE_KEY, theme);
}, [theme]);
const setTheme = useCallback((newTheme: Theme) => {
setThemeState(newTheme);
}, []);
const toggleTheme = useCallback(() => {
setThemeState((prev) => (prev === "light" ? "dark" : "light"));
}, []);
const value: ThemeContextValue = {
theme,
setTheme,
toggleTheme,
};
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
return ctx;
}