Files
Foxel/web/src/i18n/index.tsx
2026-01-06 16:54:49 +08:00

60 lines
1.8 KiB
TypeScript

import { createContext, useCallback, useContext, useMemo, useState, useEffect } from 'react';
import type { PropsWithChildren } from 'react';
import en from './locales/en.json';
import zhOverrides from './locales/zh.json';
type Lang = 'zh' | 'en';
type Dict = Record<string, string>;
const dicts: Record<Lang, Dict> = {
en,
zh: { ...en, ...zhOverrides },
};
export interface I18nContextValue {
lang: Lang;
setLang: (lang: Lang) => void;
t: (key: string, params?: Record<string, string | number>) => string;
}
const I18nContext = createContext<I18nContextValue | null>(null);
function interpolate(template: string, params?: Record<string, string | number>): string {
if (!params) return template;
return template.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? `{${k}}`));
}
export function I18nProvider({ children }: PropsWithChildren) {
const [lang, setLangState] = useState<Lang>(() => (localStorage.getItem('lang') as Lang) || 'zh');
const setLang = useCallback((l: Lang) => {
setLangState(l);
localStorage.setItem('lang', l);
}, []);
useEffect(() => {
document.documentElement.lang = lang;
window.dispatchEvent(new CustomEvent('foxel:langchange', { detail: { lang } }));
}, [lang]);
const t = useCallback((key: string, params?: Record<string, string | number>) => {
const dict = dicts[lang] || {};
const raw = dict[key] ?? key; // fallback to key (English)
return interpolate(raw, params);
}, [lang]);
const value = useMemo<I18nContextValue>(() => ({ lang, setLang, t }), [lang, setLang, 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;
}