import { createInstance, type i18n } from 'i18next' import { useEffect, useMemo, useRef } from 'react' import { I18nextProvider } from 'react-i18next' import { useLanguages } from '../../hooks/useLanguages.js' import { useSettingsStore } from '../../stores/settings/SettingsStore.js' import { useSettings } from '../../stores/settings/useSettings.js' import { compactNumberFormatter } from '../../utils/compactNumberFormatter.js' import { currencyExtendedFormatter } from '../../utils/currencyExtendedFormatter.js' import { percentFormatter } from '../../utils/percentFormatter.js' import { useWidgetConfig } from '../WidgetProvider/WidgetProvider.js' import { allLanguages } from './constants.js' import { enResource } from './enResource.js' import { mergeWithLanguageResources } from './i18n.js' import type { LanguageKey, LanguageResource, PartialResource } from './types.js' export const I18nProvider: React.FC = ({ children, }) => { const { languages, languageResources } = useWidgetConfig() const { language, languageCache } = useSettings(['language', 'languageCache']) const i18nInstanceRef = useRef(null) const i18nInstance = useMemo(() => { if (i18nInstanceRef.current) { // Update i18n instance with language and language cache updates if (language && languageCache && language !== 'en') { i18nInstanceRef.current.addResourceBundle( language, 'translation', languageCache, true, true ) } return i18nInstanceRef.current } // Custom language resources (of non-default languages) are added statically. // If custom language resources extend existing languages, they are merged with dynamically loaded resources. const customLanguageKeys = languageResources ? Object.keys(languageResources).filter( (key: string) => !allLanguages.includes(key as LanguageKey) ) : [] const initialLanguage = language || languages?.default const i18n = createInstance({ lng: initialLanguage, fallbackLng: 'en', lowerCaseLng: true, interpolation: { escapeValue: false }, resources: { en: { translation: mergeWithLanguageResources( enResource, languageResources?.en ), }, ...(initialLanguage && initialLanguage !== 'en' && languageCache && { [initialLanguage]: { translation: languageCache, }, }), // Add non-existing custom language resources ...customLanguageKeys.reduce( (acc, key) => { const customResource = languageResources?.[key as LanguageKey] if (customResource) { acc[key] = { translation: customResource, } } return acc }, {} as Record< string, { translation: PartialResource } > ), }, detection: { caches: [], }, returnEmptyString: false, }) i18n.init() i18n.services.formatter?.addCached('numberExt', compactNumberFormatter) i18n.services.formatter?.addCached('currencyExt', currencyExtendedFormatter) i18n.services.formatter?.addCached('percent', percentFormatter) i18nInstanceRef.current = i18n return i18n }, [language, languageResources, languages?.default, languageCache]) return ( {children} ) } // Sync language settings internally when config.languages.default changes externally const DefaultLanguageHandler: React.FC = () => { const { languages } = useWidgetConfig() const [lastDefaultLanguage, setValue] = useSettingsStore((state) => [ state.lastDefaultLanguage, state.setValue, ]) const { setLanguageWithCode: setLanguage } = useLanguages() useEffect(() => { const currentDefaultLanguage = languages?.default const defaultLanguageChanged = currentDefaultLanguage && currentDefaultLanguage !== lastDefaultLanguage if (!defaultLanguageChanged) { return } const updateLanguage = async () => { await setLanguage(currentDefaultLanguage) setValue('lastDefaultLanguage', currentDefaultLanguage) } updateLanguage() }, [languages?.default, setValue, lastDefaultLanguage, setLanguage]) return null }