Files
pangolin/web/usercenter/lib/theme.tsx
T
wangjia 8c14913275
Deploy Site / deploy-site (push) Successful in 1m45s
Deploy Client / build-windows (push) Successful in 1m58s
Deploy Client / build-android (push) Successful in 2m10s
Deploy Client / build-macos (push) Successful in 4m2s
Deploy Client / build-ios (push) Successful in 3m40s
Deploy Client / release-deploy (push) Successful in 2m2s
feat(usercenter/i18n): 6 语(加 日/韩/俄/西)+ 默认英文
- lib/i18n.ts: Lang/Entry 扩 6 语;STRINGS 130 条补 ja/ko/ru/es;makeT 回退英文
- lib/api/errors.ts: ERROR_TEXT 14 条补 4 语;bilingual 改为 code 本地字典优先
  → 英文字典 → 后端 message → 通用兜底
- lib/theme.tsx: 默认语种 zh→en;localStorage 白名单扩 6 语;setAttribute('lang') 直用 lang
- components/shared.tsx: LangSeg 段控 → 原生 select 6 语下拉
- app/layout.tsx: 默认壳 html lang zh→en;title/desc 改英文
- next build 通过(5 页静态);tsc 干净;无红线词

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-07 13:31:38 +08:00

70 lines
2.1 KiB
TypeScript

'use client';
// theme.tsx — 语言 + 明暗主题上下文(localStorage 持久化,SSR 安全)
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import type { Lang } from './i18n';
type Theme = 'light' | 'dark';
interface Ctx {
lang: Lang;
setLang: (l: Lang) => void;
theme: Theme;
setTheme: (t: Theme) => void;
toggleTheme: () => void;
}
const ThemeCtx = createContext<Ctx | null>(null);
const LANG_KEY = 'pg_uc_lang';
const THEME_KEY = 'pg_uc_theme';
export function UIProvider({ children }: { children: React.ReactNode }) {
const [lang, setLangState] = useState<Lang>('en'); // 默认英文(国际化默认语种)
const [theme, setThemeState] = useState<Theme>('light');
// 挂载后读取持久化(避免 hydration 不一致)
useEffect(() => {
try {
const l = window.localStorage.getItem(LANG_KEY) as Lang | null;
const t = window.localStorage.getItem(THEME_KEY) as Theme | null;
if (l && (['zh', 'en', 'ja', 'ko', 'ru', 'es'] as Lang[]).includes(l)) setLangState(l);
const initial: Theme = t === 'dark' || t === 'light' ? t : window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
setThemeState(initial);
} catch {
/* ignore */
}
}, []);
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.setAttribute('lang', lang);
}, [theme, lang]);
const setLang = useCallback((l: Lang) => {
setLangState(l);
try {
window.localStorage.setItem(LANG_KEY, l);
} catch {
/* ignore */
}
}, []);
const setTheme = useCallback((t: Theme) => {
setThemeState(t);
try {
window.localStorage.setItem(THEME_KEY, t);
} catch {
/* ignore */
}
}, []);
const toggleTheme = useCallback(() => setTheme(theme === 'dark' ? 'light' : 'dark'), [theme, setTheme]);
return <ThemeCtx.Provider value={{ lang, setLang, theme, setTheme, toggleTheme }}>{children}</ThemeCtx.Provider>;
}
export function useUI(): Ctx {
const c = useContext(ThemeCtx);
if (!c) throw new Error('useUI must be used within UIProvider');
return c;
}