diff --git a/src/i18n/LanguageContext.tsx b/src/i18n/LanguageContext.tsx index 863e759..dd9859c 100644 --- a/src/i18n/LanguageContext.tsx +++ b/src/i18n/LanguageContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useState, useCallback, useMemo, useEffect, type ReactNode } from 'react'; +import { createContext, useContext, useState, useCallback, useMemo, useEffect, useRef, type ReactNode } from 'react'; import { useTina } from 'tinacms/dist/react'; import client from '../../tina/__generated__/client'; import type { Lang, TranslationKey, Translations } from './types'; @@ -9,7 +9,6 @@ function flattenObject(obj: Record, prefix = ''): Record { const fullKey = prefix ? `${prefix}.${key}` : key; if (key === '_') { - // Special case: _ maps the value to the parent key path acc[prefix] = String(val); } else if (typeof val === 'object' && val !== null) { Object.assign(acc, flattenObject(val as Record, fullKey)); @@ -43,20 +42,17 @@ function getInitialLang(): Lang { type TinaQueryResult = { query: string; variables: Record; data: Record }; -// Inner component — only rendered when both TinaCloud queries are ready. -// useTina is never called with an empty query. -function TinaConnectedProvider({ +// Headless component — calls useTina (which requires valid queries) and syncs +// live data back to LanguageProvider via a stable callback. Renders nothing, +// so swapping it in/out never remounts the page children. +function TinaLiveSync({ enResult, ukResult, - lang, - setLang, - children, + onSync, }: { enResult: TinaQueryResult; ukResult: TinaQueryResult; - lang: Lang; - setLang: (l: Lang) => void; - children: ReactNode; + onSync: (enData: unknown, ukData: unknown) => void; }) { const { data: enLiveData } = useTina({ query: enResult.query, @@ -70,6 +66,7 @@ function TinaConnectedProvider({ data: ukResult.data, }); + // Apply design tokens from EN translations useEffect(() => { const design = (enLiveData as any).translationsEn?.design; if (!design) return; @@ -81,25 +78,19 @@ function TinaConnectedProvider({ if (design.colorText) s.setProperty('--light-grey-100', design.colorText); }, [enLiveData]); - const liveTranslations = useMemo(() => ({ - en: flattenObject((enLiveData as any).translationsEn ?? enResult.data) as unknown as Translations, - uk: flattenObject((ukLiveData as any).translationsUk ?? ukResult.data) as unknown as Translations, - }), [enLiveData, ukLiveData, enResult.data, ukResult.data]); + // Sync live translation data back to parent + useEffect(() => { + onSync(enLiveData, ukLiveData); + }, [enLiveData, ukLiveData, onSync]); - const t = useCallback( - (key: TranslationKey): string => liveTranslations[lang][key] ?? liveTranslations.en[key] ?? key, - [lang, liveTranslations] - ); - - const value = useMemo(() => ({ lang, setLang, t }), [lang, setLang, t]); - - return {children}; + return null; } export function LanguageProvider({ children }: { children: ReactNode }) { const [lang, setLangState] = useState(getInitialLang); const [enResult, setEnResult] = useState(null); const [ukResult, setUkResult] = useState(null); + const [liveTranslations, setLiveTranslations] = useState | null>(null); useEffect(() => { client.queries.translationsEn({ relativePath: 'en.json' }) @@ -118,22 +109,39 @@ export function LanguageProvider({ children }: { children: ReactNode }) { document.documentElement.lang = lang; - // Before TinaCloud data loads — use static translations (no useTina calls) - const staticT = useCallback( - (key: TranslationKey): string => staticTranslations[lang][key] ?? staticTranslations.en[key] ?? key, - [lang] + // Stable callback — uses ref to avoid re-creating TinaLiveSync's effect deps + const setLiveRef = useRef(setLiveTranslations); + setLiveRef.current = setLiveTranslations; + + const handleSync = useCallback((enData: unknown, ukData: unknown) => { + setLiveRef.current({ + en: flattenObject((enData as any).translationsEn ?? {}) as unknown as Translations, + uk: flattenObject((ukData as any).translationsUk ?? {}) as unknown as Translations, + }); + }, []); + + const translations = liveTranslations ?? staticTranslations; + + const t = useCallback( + (key: TranslationKey): string => translations[lang][key] ?? translations.en[key] ?? key, + [lang, translations] ); - const staticValue = useMemo(() => ({ lang, setLang, t: staticT }), [lang, setLang, staticT]); - if (enResult && ukResult) { - return ( - - {children} - - ); - } + const value = useMemo(() => ({ lang, setLang, t }), [lang, setLang, t]); - return {children}; + return ( + + {/* TinaLiveSync is null-rendering — mounts/unmounts without affecting children */} + {enResult && ukResult && ( + + )} + {children} + + ); } export function useTranslation(): LanguageContextValue {