Files
assetx/webapp/contexts/AppContext.tsx

124 lines
3.3 KiB
TypeScript
Raw Permalink Normal View History

"use client";
import { createContext, useContext, useState, useEffect, useMemo, useCallback, ReactNode } from "react";
type Language = "zh" | "en";
type Theme = "light" | "dark";
const VALID_LANGUAGES: Language[] = ["zh", "en"];
const VALID_THEMES: Theme[] = ["light", "dark"];
function isValidLanguage(val: unknown): val is Language {
return typeof val === 'string' && VALID_LANGUAGES.includes(val as Language);
}
function isValidTheme(val: unknown): val is Theme {
return typeof val === 'string' && VALID_THEMES.includes(val as Theme);
}
interface AppContextType {
language: Language;
setLanguage: (lang: Language) => void;
theme: Theme;
setTheme: (theme: Theme) => void;
t: (key: string) => string;
}
const AppContext = createContext<AppContextType | undefined>(undefined);
// 模块级翻译缓存
const translationCache: Record<string, Record<string, unknown>> = {};
function loadTranslations(lang: Language): Record<string, unknown> {
if (!translationCache[lang]) {
translationCache[lang] = require(`../locales/${lang}.json`);
}
return translationCache[lang];
}
export function AppProvider({ children }: { children: ReactNode }) {
const [language, setLanguageState] = useState<Language>("en");
const [theme, setThemeState] = useState<Theme>("light");
const [mounted, setMounted] = useState(false);
// SSR 守卫:仅 mounted 后读取 localStorage/matchMedia
useEffect(() => {
setMounted(true);
try {
const savedLanguage = localStorage.getItem("language");
if (isValidLanguage(savedLanguage)) {
setLanguageState(savedLanguage);
}
// 强制 light 主题
localStorage.setItem("theme", "light");
} catch {
// 隐私模式或 localStorage 不可用
}
}, []);
// Apply theme
useEffect(() => {
if (!mounted) return;
try {
localStorage.setItem("theme", theme);
} catch {
// 隐私模式兼容
}
if (theme === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}, [theme, mounted]);
// Save language preference
const setLanguage = useCallback((lang: Language) => {
setLanguageState(lang);
try {
localStorage.setItem("language", lang);
} catch {
// 隐私模式兼容
}
}, []);
const setTheme = useCallback((t: Theme) => {
setThemeState(t);
}, []);
// Translation function — 使用模块级缓存
const t = useCallback((key: string): string => {
const translations = loadTranslations(language);
const keys = key.split(".");
let value: unknown = translations;
for (const k of keys) {
if (typeof value === 'object' && value !== null) {
value = (value as Record<string, unknown>)[k];
} else {
return key;
}
}
return typeof value === 'string' ? value : key;
}, [language]);
const contextValue = useMemo(
() => ({ language, setLanguage, theme, setTheme, t }),
[language, setLanguage, theme, setTheme, t]
);
return (
<AppContext.Provider value={contextValue}>
{children}
</AppContext.Provider>
);
}
export function useApp() {
const context = useContext(AppContext);
if (!context) {
throw new Error("useApp must be used within AppProvider");
}
return context;
}