import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { getHermesConfigRecord, type HermesConfigRecord, saveHermesConfig } from '@/hermes' import { TRANSLATIONS } from './catalog' import { DEFAULT_LOCALE, localeConfigValue, normalizeLocale } from './languages' import { setRuntimeI18nLocale } from './runtime' import type { Locale, Translations } from './types' export { LOCALE_META } from './languages' export interface I18nConfigClient { getConfig: () => Promise saveConfig: (config: HermesConfigRecord) => Promise<{ ok: boolean }> } const defaultConfigClient: I18nConfigClient = { getConfig: () => { if (typeof window === 'undefined' || !window.hermesDesktop?.api) { return Promise.resolve({}) } return getHermesConfigRecord() }, saveConfig: config => { if (typeof window === 'undefined' || !window.hermesDesktop?.api) { return Promise.resolve({ ok: true }) } return saveHermesConfig(config) } } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } export function getConfigDisplayLanguage(config: HermesConfigRecord): unknown { return isRecord(config.display) ? config.display.language : undefined } export function withConfigDisplayLanguage(config: HermesConfigRecord, locale: Locale): HermesConfigRecord { const display = isRecord(config.display) ? config.display : {} return { ...config, display: { ...display, language: localeConfigValue(locale) } } } function toError(error: unknown): Error { return error instanceof Error ? error : new Error(String(error)) } const RTL_LOCALES = new Set(['ar']) function applyDocumentLocale(locale: Locale) { if (typeof document === 'undefined') { return } document.documentElement.lang = locale document.documentElement.dir = RTL_LOCALES.has(locale) ? 'rtl' : 'ltr' } export interface I18nContextValue { configLoadError: Error | null isLoadingConfig: boolean isSavingLocale: boolean locale: Locale saveError: Error | null setLocale: (next: Locale) => Promise t: Translations } const I18nContext = createContext({ configLoadError: null, isLoadingConfig: false, isSavingLocale: false, locale: DEFAULT_LOCALE, saveError: null, setLocale: async () => {}, t: TRANSLATIONS[DEFAULT_LOCALE] }) export interface I18nProviderProps { children: ReactNode configClient?: I18nConfigClient | null initialLocale?: unknown } export function I18nProvider({ children, configClient = defaultConfigClient, initialLocale }: I18nProviderProps) { const [locale, setLocaleState] = useState(() => normalizeLocale(initialLocale)) const [isLoadingConfig, setIsLoadingConfig] = useState(false) const [isSavingLocale, setIsSavingLocale] = useState(false) const [configLoadError, setConfigLoadError] = useState(null) const [saveError, setSaveError] = useState(null) const localeRef = useRef(locale) // eslint-disable-next-line no-restricted-syntax -- legitimate non-atom ref write (see eslint rule comment) useEffect(() => { localeRef.current = locale setRuntimeI18nLocale(locale) applyDocumentLocale(locale) if (typeof window !== 'undefined') { window.hermesDesktop?.setUiLanguage?.(locale) } }, [locale]) useEffect(() => { if (!configClient) { return } let cancelled = false setIsLoadingConfig(true) setConfigLoadError(null) configClient .getConfig() .then(config => { if (!cancelled) { setLocaleState(normalizeLocale(getConfigDisplayLanguage(config))) } }) .catch(error => { if (!cancelled) { setConfigLoadError(toError(error)) setLocaleState(DEFAULT_LOCALE) } }) .finally(() => { if (!cancelled) { setIsLoadingConfig(false) } }) return () => { cancelled = true } }, [configClient, initialLocale]) const setLocale = useCallback( async (next: Locale) => { const previousLocale = localeRef.current setSaveError(null) setLocaleState(next) if (!configClient) { return } setIsSavingLocale(true) try { const latestConfig = await configClient.getConfig() const result = await configClient.saveConfig(withConfigDisplayLanguage(latestConfig, next)) if (!result.ok) { throw new Error('Failed to save language') } } catch (error) { const nextError = toError(error) setLocaleState(previousLocale) setSaveError(nextError) throw nextError } finally { setIsSavingLocale(false) } }, [configClient] ) const value = useMemo( () => ({ configLoadError, isLoadingConfig, isSavingLocale, locale, saveError, setLocale, t: TRANSLATIONS[locale] }), [configLoadError, isLoadingConfig, isSavingLocale, locale, saveError, setLocale] ) return {children} } export function useI18n(): I18nContextValue { return useContext(I18nContext) }