148 lines
4.5 KiB
TypeScript
148 lines
4.5 KiB
TypeScript
'use client';
|
|
|
|
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';
|
|
|
|
type Locale = 'en' | 'es' | 'pt-BR';
|
|
|
|
interface I18nContextType {
|
|
locale: Locale;
|
|
setLocale: (locale: Locale) => void;
|
|
t: (key: string, params?: Record<string, string | number>) => string;
|
|
}
|
|
|
|
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';
|
|
|
|
const normalizeLocale = (language: string): Locale => {
|
|
if (language.startsWith('pt')) return 'pt-BR';
|
|
if (language.startsWith('es')) return 'es';
|
|
return 'en';
|
|
};
|
|
|
|
const getInitialLocale = (): 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 '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>('pt-BR');
|
|
|
|
useEffect(() => {
|
|
let initialLocale: Locale = 'pt-BR';
|
|
try {
|
|
const storedLocale = localStorage.getItem(localeStorageKey);
|
|
if (storedLocale && VALID_LOCALES.includes(storedLocale as Locale)) {
|
|
initialLocale = storedLocale as Locale;
|
|
} else if (navigator.language) {
|
|
initialLocale = normalizeLocale(navigator.language);
|
|
}
|
|
} catch {
|
|
// localStorage might be blocked
|
|
}
|
|
setLocaleState(initialLocale);
|
|
}, []);
|
|
|
|
const setLocale = (newLocale: Locale) => {
|
|
setLocaleState(newLocale);
|
|
};
|
|
|
|
const t = useCallback((key: string, params?: Record<string, string | number>) => {
|
|
// Try current locale first, then fallback to pt-BR, then en
|
|
const dict = dictionaries[locale] || dictionaries['pt-BR'];
|
|
let result = resolveKey(dict, 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 (result === undefined) {
|
|
return key;
|
|
}
|
|
|
|
if (params) {
|
|
return Object.entries(params).reduce(
|
|
(str, [paramKey, paramValue]) => str.replace(new RegExp(`\\{${paramKey}\\}`, 'g'), String(paramValue)),
|
|
result
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}, [locale]);
|
|
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') return;
|
|
try {
|
|
localStorage.setItem(localeStorageKey, locale);
|
|
} catch {
|
|
// localStorage might be blocked
|
|
}
|
|
document.documentElement.lang = locale;
|
|
}, [locale]);
|
|
|
|
return (
|
|
<I18nContext.Provider value={{ locale, setLocale, t }}>
|
|
{children}
|
|
</I18nContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useI18n() {
|
|
const context = useContext(I18nContext);
|
|
if (!context) {
|
|
throw new Error('useI18n must be used within an I18nProvider');
|
|
}
|
|
return context;
|
|
}
|
|
|
|
export function useTranslation() {
|
|
const { t, locale, setLocale } = useI18n();
|
|
return { t, locale, setLocale };
|
|
}
|
|
|
|
export const locales: { code: Locale; name: string; flag: string }[] = [
|
|
{ code: 'en', name: 'English', flag: '🇺🇸' },
|
|
{ code: 'es', name: 'Español', flag: '🇪🇸' },
|
|
{ code: 'pt-BR', name: 'Português', flag: '🇧🇷' },
|
|
];
|