'use client'; import { useEffect, useState, ReactNode, useRef, useMemo } from 'react'; import { useLocale } from '@/contexts/LocaleContext'; // Cache global de traduções const translationCache = new Map(); // Função para traduzir texto via API (requisição individual) async function translateText(text: string, targetLang: string): Promise { if (!text || text.trim() === '') return text; const cacheKey = `pt:${targetLang}:${text}`; // Cache hit: retorna imediatamente if (translationCache.has(cacheKey)) { return translationCache.get(cacheKey)!; } try { const response = await fetch('/api/translate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, source: 'pt', target: targetLang }), }); if (response.ok) { const data = await response.json(); const translated = data.translatedText || text; translationCache.set(cacheKey, translated); return translated; } } catch (error) { console.error('[T] Translation error:', error); } return text; } // Função para traduzir múltiplos textos de uma vez (BATCH - muito mais rápido) async function translateBatchTexts(texts: string[], targetLang: string): Promise { if (!texts.length) return texts; // Verificar quais já estão em cache const results: string[] = new Array(texts.length); const toTranslate: { index: number; text: string }[] = []; texts.forEach((text, i) => { if (!text || text.trim() === '') { results[i] = text || ''; return; } const cacheKey = `pt:${targetLang}:${text}`; if (translationCache.has(cacheKey)) { results[i] = translationCache.get(cacheKey)!; } else { toTranslate.push({ index: i, text }); } }); // Se todos estão em cache, retorna direto if (toTranslate.length === 0) { return results; } // Traduzir os que faltam via batch API try { const response = await fetch('/api/translate', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ texts: toTranslate.map(t => t.text), source: 'pt', target: targetLang }), }); if (response.ok) { const data = await response.json(); const translations = data.translations || []; toTranslate.forEach((item, idx) => { const translated = translations[idx] || item.text; results[item.index] = translated; translationCache.set(`pt:${targetLang}:${item.text}`, translated); }); } else { // Fallback: usar textos originais toTranslate.forEach(item => { results[item.index] = item.text; }); } } catch (error) { console.error('[T] Batch translation error:', error); toTranslate.forEach(item => { results[item.index] = item.text; }); } return results; } interface AutoTranslateProps { children: ReactNode; as?: 'span' | 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div' | 'li' | 'label'; className?: string; } /** * Componente que traduz texto automaticamente via LibreTranslate * Uso: Texto em português */ export function T({ children, as = 'span', className }: AutoTranslateProps) { const { locale } = useLocale(); // Converter children para string de forma estável const originalText = useMemo(() => { return typeof children === 'string' ? children : String(children || ''); }, [children]); const [translatedText, setTranslatedText] = useState(originalText); const lastTranslatedRef = useRef<{ text: string; lang: string } | null>(null); useEffect(() => { console.log('[T] useEffect - locale:', locale, 'text:', originalText.substring(0, 20)); // Se idioma é PT, mostrar texto original if (locale === 'pt') { setTranslatedText(originalText); lastTranslatedRef.current = null; return; } // Evitar tradução duplicada if ( lastTranslatedRef.current?.text === originalText && lastTranslatedRef.current?.lang === locale ) { console.log('[T] Pulando - já traduzido'); return; } // Verificar cache primeiro (síncrono) const cacheKey = `pt:${locale}:${originalText}`; if (translationCache.has(cacheKey)) { console.log('[T] Cache hit:', originalText.substring(0, 20)); setTranslatedText(translationCache.get(cacheKey)!); lastTranslatedRef.current = { text: originalText, lang: locale }; return; } console.log('[T] Chamando API para:', originalText.substring(0, 20)); let cancelled = false; translateText(originalText, locale).then((result) => { console.log('[T] Resultado:', result.substring(0, 20)); if (!cancelled) { setTranslatedText(result); lastTranslatedRef.current = { text: originalText, lang: locale }; } }); return () => { cancelled = true; }; }, [originalText, locale]); const Tag = as; return {translatedText}; } // Alias para uso mais curto export const AutoTranslate = T; /** * Hook para traduzir texto programaticamente */ export function useTranslate() { const { locale } = useLocale(); const [isTranslating, setIsTranslating] = useState(false); const translate = async (text: string): Promise => { if (!text || locale === 'pt') return text; setIsTranslating(true); try { const result = await translateText(text, locale); return result; } finally { setIsTranslating(false); } }; const translateBatch = async (texts: string[]): Promise => { if (locale === 'pt') return texts; setIsTranslating(true); try { const results = await Promise.all( texts.map(text => translateText(text, locale)) ); return results; } finally { setIsTranslating(false); } }; return { translate, translateBatch, isTranslating, locale }; } /** * Hook para traduzir conteúdo do banco de dados */ export function useTranslatedContent>(content: T | null): { translatedContent: T | null; isTranslating: boolean; } { const { locale } = useLocale(); const [translatedContent, setTranslatedContent] = useState(content); const [isTranslating, setIsTranslating] = useState(false); useEffect(() => { if (!content || locale === 'pt') { setTranslatedContent(content); return; } let cancelled = false; const targetLang = locale; const translateContent = async () => { setIsTranslating(true); // Extrair todos os textos do objeto const texts: string[] = []; const paths: string[] = []; const extractTexts = (obj: unknown, path: string = '') => { if (typeof obj === 'string' && obj.length > 0 && obj.length < 5000) { texts.push(obj); paths.push(path); } else if (Array.isArray(obj)) { obj.forEach((item, index) => extractTexts(item, `${path}[${index}]`)); } else if (obj && typeof obj === 'object') { Object.entries(obj).forEach(([key, value]) => { // Ignorar campos que não devem ser traduzidos if (['icon', 'image', 'img', 'url', 'href', 'id', 'slug'].includes(key)) return; extractTexts(value, path ? `${path}.${key}` : key); }); } }; extractTexts(content); if (texts.length === 0) { setIsTranslating(false); return; } try { // Traduzir todos os textos const translations = await Promise.all( texts.map(text => translateText(text, targetLang)) ); if (cancelled) return; // Reconstruir objeto com traduções const newContent = JSON.parse(JSON.stringify(content)); paths.forEach((path, index) => { const parts = path.replace(/\[(\d+)\]/g, '.$1').split('.'); let current: Record = newContent; for (let i = 0; i < parts.length - 1; i++) { current = current[parts[i]] as Record; } current[parts[parts.length - 1]] = translations[index]; }); setTranslatedContent(newContent); } catch (error) { console.error('Translation error:', error); } finally { if (!cancelled) { setIsTranslating(false); } } }; translateContent(); return () => { cancelled = true; }; }, [content, locale]); return { translatedContent, isTranslating }; }