返回 oh-my-ppt
index.ts
根目录 / src / renderer / src / i18n / index.ts
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
113 lines TYPESCRIPT