fix(i18n): initialize locale from browser/localStorage and add fallback chain
- Initialize useState with getInitialLocale() instead of hardcoded 'en', so the correct locale is used from the very first render - Default to 'pt-BR' instead of 'en' for SSR and fallback - Add fallback chain in t(): tries current locale -> pt-BR -> en - Extract resolveKey() helper for cleaner key resolution - Cast dictionaries as Record<string, unknown> to avoid type issues - Wrap localStorage access in try-catch for blocked storage scenarios Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8fb358a984
commit
55705a0fbb
1 changed files with 52 additions and 36 deletions
|
|
@ -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, string | number>) => string;
|
||||
}
|
||||
|
||||
const dictionaries: Record<Locale, typeof en> = {
|
||||
en,
|
||||
es,
|
||||
'pt-BR': ptBR,
|
||||
const dictionaries: Record<Locale, Record<string, unknown>> = {
|
||||
en: en as Record<string, unknown>,
|
||||
es: es as Record<string, unknown>,
|
||||
'pt-BR': ptBR as Record<string, unknown>,
|
||||
};
|
||||
|
||||
const VALID_LOCALES: Locale[] = ['en', 'es', 'pt-BR'];
|
||||
|
||||
const I18nContext = createContext<I18nContextType | null>(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<string, unknown>, 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<string, unknown>)[k];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return typeof value === 'string' ? value : undefined;
|
||||
}
|
||||
|
||||
export function I18nProvider({ children }: { children: ReactNode }) {
|
||||
const [locale, setLocaleState] = useState<Locale>('en');
|
||||
const [locale, setLocaleState] = useState<Locale>(getInitialLocale);
|
||||
|
||||
const setLocale = (newLocale: Locale) => {
|
||||
setLocaleState(newLocale);
|
||||
};
|
||||
|
||||
const t = useCallback((key: string, params?: Record<string, string | number>) => {
|
||||
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<string, unknown>)[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]);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue