/** * 用户偏好:深色 / 浅色模式与界面语言。 * 两项都持久化到 localStorage,并同步到 供 CSS 变量取色。 */ import { createContext, useContext, useEffect, useMemo, useState } from "react"; import type { ReactNode } from "react"; import { makeT, setGlobalLang } from "./i18n"; import type { Lang, TFn } from "./i18n"; export type ThemeMode = "light" | "dark"; const MODE_KEY = "nex_math_theme"; const LANG_KEY = "nex_math_lang"; function readMode(): ThemeMode { try { return localStorage.getItem(MODE_KEY) === "dark" ? "dark" : "light"; } catch { return "light"; } } function readLang(): Lang { try { return localStorage.getItem(LANG_KEY) === "en" ? "en" : "zh"; } catch { return "zh"; } } interface Prefs { mode: ThemeMode; lang: Lang; t: TFn; toggleMode: () => void; setLang: (lang: Lang) => void; } const PrefsContext = createContext({ mode: "light", lang: "zh", t: makeT("zh"), toggleMode: () => {}, setLang: () => {}, }); export function PrefsProvider({ children }: { children: ReactNode }) { const [mode, setMode] = useState(readMode); const [lang, setLangState] = useState(readLang); useEffect(() => { document.documentElement.dataset.theme = mode; document.documentElement.style.colorScheme = mode; try { localStorage.setItem(MODE_KEY, mode); } catch { // 隐私模式下写入失败时忽略 } }, [mode]); useEffect(() => { setGlobalLang(lang); document.documentElement.lang = lang === "en" ? "en" : "zh-CN"; // 浏览器标签页标题跟随语言,文案同样只来自 i18n document.title = makeT(lang)("app.name"); try { localStorage.setItem(LANG_KEY, lang); } catch { // 同上 } }, [lang]); const value = useMemo( () => ({ mode, lang, t: makeT(lang), toggleMode: () => setMode((current) => (current === "dark" ? "light" : "dark")), setLang: (next: Lang) => setLangState(next), }), [mode, lang], ); return {children}; } export function usePrefs(): Prefs { return useContext(PrefsContext); } export function useT(): TFn { return useContext(PrefsContext).t; }