| 1 | import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react' |
| 2 | import { useSettingsStore } from '../store' |
| 3 | import { en } from './en' |
| 4 | import { zh } from './zh' |
| 5 | |
| 6 | export type Lang = 'zh' | 'en' |
| 7 | |
| 8 | export type DeepStringShape<T> = { |
| 9 | [K in keyof T]: T[K] extends string ? string : DeepStringShape<T[K]> |
| 10 | } |
| 11 | |
| 12 | type DotKeys<T> = { |
| 13 | [K in keyof T & string]: T[K] extends string |
| 14 | ? K |
| 15 | : T[K] extends Record<string, unknown> |
| 16 | ? `${K}.${DotKeys<T[K]>}` |
| 17 | : never |
| 18 | }[keyof T & string] |
| 19 | |
| 20 | export type I18nKey = DotKeys<typeof zh> |
| 21 | export type TranslationParams = Record<string, string | number> |
| 22 | |
| 23 | interface LangContextValue { |
| 24 | lang: Lang |
| 25 | setLang: (lang: Lang) => void |
| 26 | t: (key: I18nKey, params?: TranslationParams) => string |
| 27 | } |
| 28 | |
| 29 | const LANG_STORAGE_KEY = 'oh-my-ppt:lang' |
| 30 | const messages = { zh, en } as const |
| 31 | |
| 32 | const LangContext = createContext<LangContextValue | null>(null) |
| 33 | |
| 34 | function normalizeLang(value: unknown): Lang | null { |
| 35 | return value === 'en' || value === 'zh' ? value : null |
| 36 | } |
| 37 | |
| 38 | function readStoredLang(): Lang | null { |
| 39 | if (typeof window === 'undefined') return null |
| 40 | return normalizeLang(window.localStorage.getItem(LANG_STORAGE_KEY)) |
| 41 | } |
| 42 | |
| 43 | function writeStoredLang(lang: Lang): void { |
| 44 | if (typeof window === 'undefined') return |
| 45 | window.localStorage.setItem(LANG_STORAGE_KEY, lang) |
| 46 | } |
| 47 | |
| 48 | function getByPath(obj: unknown, path: string): string | undefined { |
| 49 | const value = path.split('.').reduce<unknown>((acc, key) => { |
| 50 | if (acc && typeof acc === 'object') { |
| 51 | return (acc as Record<string, unknown>)[key] |
| 52 | } |
| 53 | return undefined |
| 54 | }, obj) |
| 55 | |
| 56 | return typeof value === 'string' ? value : undefined |
| 57 | } |
| 58 | |
| 59 | function interpolate(template: string, params?: TranslationParams): string { |
| 60 | if (!params) return template |
| 61 | return template.replace(/\{(\w+)\}/g, (match, key) => |
| 62 | Object.prototype.hasOwnProperty.call(params, key) ? String(params[key]) : match |
| 63 | ) |
| 64 | } |
| 65 | |
| 66 | export function LangProvider({ children }: { children: ReactNode }): React.JSX.Element { |
| 67 | const fetchSettings = useSettingsStore((state) => state.fetchSettings) |
| 68 | const [lang, setLangState] = useState<Lang>( |
| 69 | () => normalizeLang(useSettingsStore.getState().settings?.locale) || readStoredLang() || 'zh' |
| 70 | ) |
| 71 | |
| 72 | useEffect(() => { |
| 73 | void fetchSettings() |
| 74 | }, [fetchSettings]) |
| 75 | |
| 76 | useEffect(() => { |
| 77 | return useSettingsStore.subscribe((state) => { |
| 78 | const nextLocale = normalizeLang(state.settings?.locale) |
| 79 | if (!nextLocale) return |
| 80 | setLangState(nextLocale) |
| 81 | writeStoredLang(nextLocale) |
| 82 | }) |
| 83 | }, []) |
| 84 | |
| 85 | const setLang = useCallback((nextLang: Lang) => { |
| 86 | setLangState(nextLang) |
| 87 | writeStoredLang(nextLang) |
| 88 | void useSettingsStore.getState().saveSettings({ locale: nextLang }) |
| 89 | }, []) |
| 90 | |
| 91 | const t = useCallback( |
| 92 | (key: I18nKey, params?: TranslationParams) => { |
| 93 | const template = getByPath(messages[lang], key) || getByPath(messages.zh, key) || key |
| 94 | return interpolate(template, params) |
| 95 | }, |
| 96 | [lang] |
| 97 | ) |
| 98 | |
| 99 | const value = useMemo(() => ({ lang, setLang, t }), [lang, setLang, t]) |
| 100 | |
| 101 | return createElement(LangContext.Provider, { value }, children) |
| 102 | } |
| 103 | |
| 104 | export function useLang(): LangContextValue { |
| 105 | const value = useContext(LangContext) |
| 106 | if (!value) throw new Error('useLang must be used within LangProvider') |
| 107 | return value |
| 108 | } |
| 109 | |
| 110 | export function useT(): LangContextValue['t'] { |
| 111 | return useLang().t |
| 112 | } |
| 113 |