feat: CMS com limites de caracteres, traduções auto e painel de notificações
This commit is contained in:
@@ -1,17 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, ReactNode } from 'react';
|
||||
import { useLanguage, Language } from '@/contexts/LanguageContext';
|
||||
import { useEffect, useState, ReactNode, useRef, useMemo } from 'react';
|
||||
import { useLocale } from '@/contexts/LocaleContext';
|
||||
|
||||
// Cache global de traduções
|
||||
const translationCache = new Map<string, string>();
|
||||
|
||||
// Função para traduzir texto via API
|
||||
// Função para traduzir texto via API (requisição individual)
|
||||
async function translateText(text: string, targetLang: string): Promise<string> {
|
||||
if (!text || text.trim() === '') return text;
|
||||
|
||||
const cacheKey = `pt:${targetLang}:${text}`;
|
||||
|
||||
// Cache hit: retorna imediatamente
|
||||
if (translationCache.has(cacheKey)) {
|
||||
return translationCache.get(cacheKey)!;
|
||||
}
|
||||
@@ -30,14 +31,78 @@ async function translateText(text: string, targetLang: string): Promise<string>
|
||||
return translated;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Translation error:', 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<string[]> {
|
||||
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: string;
|
||||
children: ReactNode;
|
||||
as?: 'span' | 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div' | 'li' | 'label';
|
||||
className?: string;
|
||||
}
|
||||
@@ -47,28 +112,59 @@ interface AutoTranslateProps {
|
||||
* Uso: <T>Texto em português</T>
|
||||
*/
|
||||
export function T({ children, as = 'span', className }: AutoTranslateProps) {
|
||||
const { language } = useLanguage();
|
||||
const [translatedText, setTranslatedText] = useState(children);
|
||||
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(() => {
|
||||
if (language === 'PT') {
|
||||
setTranslatedText(children);
|
||||
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;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const targetLang = language.toLowerCase();
|
||||
// Evitar tradução duplicada
|
||||
if (
|
||||
lastTranslatedRef.current?.text === originalText &&
|
||||
lastTranslatedRef.current?.lang === locale
|
||||
) {
|
||||
console.log('[T] Pulando - já traduzido');
|
||||
return;
|
||||
}
|
||||
|
||||
translateText(children, targetLang).then((result) => {
|
||||
// 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;
|
||||
};
|
||||
}, [children, language]);
|
||||
}, [originalText, locale]);
|
||||
|
||||
const Tag = as;
|
||||
return <Tag className={className}>{translatedText}</Tag>;
|
||||
@@ -81,15 +177,15 @@ export const AutoTranslate = T;
|
||||
* Hook para traduzir texto programaticamente
|
||||
*/
|
||||
export function useTranslate() {
|
||||
const { language } = useLanguage();
|
||||
const { locale } = useLocale();
|
||||
const [isTranslating, setIsTranslating] = useState(false);
|
||||
|
||||
const translate = async (text: string): Promise<string> => {
|
||||
if (!text || language === 'PT') return text;
|
||||
if (!text || locale === 'pt') return text;
|
||||
|
||||
setIsTranslating(true);
|
||||
try {
|
||||
const result = await translateText(text, language.toLowerCase());
|
||||
const result = await translateText(text, locale);
|
||||
return result;
|
||||
} finally {
|
||||
setIsTranslating(false);
|
||||
@@ -97,12 +193,12 @@ export function useTranslate() {
|
||||
};
|
||||
|
||||
const translateBatch = async (texts: string[]): Promise<string[]> => {
|
||||
if (language === 'PT') return texts;
|
||||
if (locale === 'pt') return texts;
|
||||
|
||||
setIsTranslating(true);
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
texts.map(text => translateText(text, language.toLowerCase()))
|
||||
texts.map(text => translateText(text, locale))
|
||||
);
|
||||
return results;
|
||||
} finally {
|
||||
@@ -110,7 +206,7 @@ export function useTranslate() {
|
||||
}
|
||||
};
|
||||
|
||||
return { translate, translateBatch, isTranslating, language };
|
||||
return { translate, translateBatch, isTranslating, locale };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,18 +216,18 @@ export function useTranslatedContent<T extends Record<string, unknown>>(content:
|
||||
translatedContent: T | null;
|
||||
isTranslating: boolean;
|
||||
} {
|
||||
const { language } = useLanguage();
|
||||
const { locale } = useLocale();
|
||||
const [translatedContent, setTranslatedContent] = useState<T | null>(content);
|
||||
const [isTranslating, setIsTranslating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!content || language === 'PT') {
|
||||
if (!content || locale === 'pt') {
|
||||
setTranslatedContent(content);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const targetLang = language.toLowerCase();
|
||||
const targetLang = locale;
|
||||
|
||||
const translateContent = async () => {
|
||||
setIsTranslating(true);
|
||||
@@ -199,7 +295,7 @@ export function useTranslatedContent<T extends Record<string, unknown>>(content:
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [content, language]);
|
||||
}, [content, locale]);
|
||||
|
||||
return { translatedContent, isTranslating };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user