diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 726262a..09301f2 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect, startTransition } from 'react'; +import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react'; import en from '@/i18n/en.json'; import es from '@/i18n/es.json'; import ptBR from '@/i18n/pt-BR.json'; @@ -13,12 +13,14 @@ interface I18nContextType { t: (key: string, params?: Record) => string; } -const dictionaries: Record = { - en, - es, - 'pt-BR': ptBR, +const dictionaries: Record> = { + en: en as Record, + es: es as Record, + 'pt-BR': ptBR as Record, }; +const VALID_LOCALES: Locale[] = ['en', 'es', 'pt-BR']; + const I18nContext = createContext(null); const localeStorageKey = 'locale'; @@ -30,63 +32,77 @@ const normalizeLocale = (language: string): Locale => { }; const getInitialLocale = (): Locale => { - if (typeof window === 'undefined') return 'en'; - const storedLocale = localStorage.getItem(localeStorageKey); - if (storedLocale && Object.keys(dictionaries).includes(storedLocale)) { - return storedLocale as Locale; + if (typeof window === 'undefined') return 'pt-BR'; + try { + const storedLocale = localStorage.getItem(localeStorageKey); + if (storedLocale && VALID_LOCALES.includes(storedLocale as Locale)) { + return storedLocale as Locale; + } + } catch { + // localStorage might be blocked } if (typeof navigator !== 'undefined' && navigator.language) { return normalizeLocale(navigator.language); } - return 'en'; + return 'pt-BR'; }; +function resolveKey(dict: Record, key: string): string | undefined { + const keys = key.split('.'); + let value: unknown = dict; + + for (const k of keys) { + if (value && typeof value === 'object' && value !== null && k in value) { + value = (value as Record)[k]; + } else { + return undefined; + } + } + + return typeof value === 'string' ? value : undefined; +} + export function I18nProvider({ children }: { children: ReactNode }) { - const [locale, setLocaleState] = useState('en'); + const [locale, setLocaleState] = useState(getInitialLocale); const setLocale = (newLocale: Locale) => { setLocaleState(newLocale); }; const t = useCallback((key: string, params?: Record) => { - const keys = key.split('.'); - let value: unknown = dictionaries[locale]; + // Try current locale first, then fallback to pt-BR, then en + const dict = dictionaries[locale] || dictionaries['pt-BR']; + let result = resolveKey(dict, key); - for (const k of keys) { - if (value && typeof value === 'object' && value !== null && k in value) { - value = (value as Record)[k]; - } else { - return key; - } + // Fallback chain: try pt-BR if not found, then en + if (result === undefined && locale !== 'pt-BR') { + result = resolveKey(dictionaries['pt-BR'], key); + } + if (result === undefined && locale !== 'en') { + result = resolveKey(dictionaries['en'], key); } - if (typeof value !== 'string') return key; + if (result === undefined) { + return key; + } if (params) { return Object.entries(params).reduce( - (str, [paramKey, paramValue]) => str.replace(`{${paramKey}}`, String(paramValue)), - value + (str, [paramKey, paramValue]) => str.replace(new RegExp(`\\{${paramKey}\\}`, 'g'), String(paramValue)), + result ); } - return value; + return result; }, [locale]); - // Restore locale from localStorage after hydration completes. - // Using startTransition to mark this as non-urgent so React - // finishes hydration before re-rendering with the stored locale. - useEffect(() => { - const stored = getInitialLocale(); - if (stored !== 'en') { - startTransition(() => { - setLocaleState(stored); - }); - } - }, []); - useEffect(() => { if (typeof window === 'undefined') return; - localStorage.setItem(localeStorageKey, locale); + try { + localStorage.setItem(localeStorageKey, locale); + } catch { + // localStorage might be blocked + } document.documentElement.lang = locale; }, [locale]);