feat: CMS com limites de caracteres, traduções auto e painel de notificações
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
const LIBRETRANSLATE_URL = process.env.LIBRETRANSLATE_URL || 'https://libretranslate.stackbyte.cloud';
|
||||
|
||||
// Cache simples em memória para traduções
|
||||
const translationCache = new Map<string, { text: string; timestamp: number }>();
|
||||
const CACHE_TTL = 1000 * 60 * 60 * 24; // 24 horas
|
||||
// Cache em memória para evitar queries repetidas na mesma sessão
|
||||
const memoryCache = new Map<string, string>();
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -19,15 +19,33 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ translatedText: text });
|
||||
}
|
||||
|
||||
// Verificar cache
|
||||
const cacheKey = `${source}:${target}:${text}`;
|
||||
const cached = translationCache.get(cacheKey);
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return NextResponse.json({ translatedText: cached.text, cached: true });
|
||||
|
||||
// 1. Verificar cache em memória (mais rápido)
|
||||
if (memoryCache.has(cacheKey)) {
|
||||
return NextResponse.json({ translatedText: memoryCache.get(cacheKey), cached: 'memory' });
|
||||
}
|
||||
|
||||
// Chamar LibreTranslate
|
||||
// 2. Verificar banco de dados
|
||||
const dbTranslation = await prisma.translation.findUnique({
|
||||
where: {
|
||||
sourceText_sourceLang_targetLang: {
|
||||
sourceText: text,
|
||||
sourceLang: source,
|
||||
targetLang: target,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (dbTranslation) {
|
||||
// Salvar em memória para próximas requisições
|
||||
memoryCache.set(cacheKey, dbTranslation.translatedText);
|
||||
return NextResponse.json({ translatedText: dbTranslation.translatedText, cached: 'database' });
|
||||
}
|
||||
|
||||
// 3. Chamar LibreTranslate (só se não tiver no banco)
|
||||
console.log(`[Translate] Chamando LibreTranslate para: "${text.substring(0, 30)}..."`);
|
||||
|
||||
const response = await fetch(`${LIBRETRANSLATE_URL}/translate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -49,10 +67,26 @@ export async function POST(request: NextRequest) {
|
||||
const data = await response.json();
|
||||
const translatedText = data.translatedText || text;
|
||||
|
||||
// Salvar no cache
|
||||
translationCache.set(cacheKey, { text: translatedText, timestamp: Date.now() });
|
||||
// 4. Salvar no banco de dados (persistente)
|
||||
try {
|
||||
await prisma.translation.create({
|
||||
data: {
|
||||
sourceText: text,
|
||||
sourceLang: source,
|
||||
targetLang: target,
|
||||
translatedText: translatedText,
|
||||
},
|
||||
});
|
||||
console.log(`[Translate] Salvo no banco: "${text.substring(0, 30)}..." -> "${translatedText.substring(0, 30)}..."`);
|
||||
} catch (dbError) {
|
||||
// Pode falhar se já existir (race condition), ignorar
|
||||
console.log('[Translate] Já existe no banco (race condition)');
|
||||
}
|
||||
|
||||
return NextResponse.json({ translatedText });
|
||||
// 5. Salvar em memória
|
||||
memoryCache.set(cacheKey, translatedText);
|
||||
|
||||
return NextResponse.json({ translatedText, cached: false });
|
||||
} catch (error) {
|
||||
console.error('Translation error:', error);
|
||||
return NextResponse.json({ error: 'Erro ao traduzir' }, { status: 500 });
|
||||
@@ -72,39 +106,96 @@ export async function PUT(request: NextRequest) {
|
||||
return NextResponse.json({ translations: texts });
|
||||
}
|
||||
|
||||
const translations = await Promise.all(
|
||||
texts.map(async (text: string) => {
|
||||
if (!text) return text;
|
||||
const results: string[] = [];
|
||||
const toTranslate: { index: number; text: string }[] = [];
|
||||
|
||||
const cacheKey = `${source}:${target}:${text}`;
|
||||
const cached = translationCache.get(cacheKey);
|
||||
// Verificar quais já existem no banco
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
const text = texts[i];
|
||||
if (!text) {
|
||||
results[i] = text || '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return cached.text;
|
||||
}
|
||||
const cacheKey = `${source}:${target}:${text}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${LIBRETRANSLATE_URL}/translate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ q: text, source, target, format: 'text' }),
|
||||
});
|
||||
// Verificar memória
|
||||
if (memoryCache.has(cacheKey)) {
|
||||
results[i] = memoryCache.get(cacheKey)!;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const translatedText = data.translatedText || text;
|
||||
translationCache.set(cacheKey, { text: translatedText, timestamp: Date.now() });
|
||||
return translatedText;
|
||||
// Verificar banco
|
||||
const dbTranslation = await prisma.translation.findUnique({
|
||||
where: {
|
||||
sourceText_sourceLang_targetLang: {
|
||||
sourceText: text,
|
||||
sourceLang: source,
|
||||
targetLang: target,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (dbTranslation) {
|
||||
results[i] = dbTranslation.translatedText;
|
||||
memoryCache.set(cacheKey, dbTranslation.translatedText);
|
||||
} else {
|
||||
toTranslate.push({ index: i, text });
|
||||
}
|
||||
}
|
||||
|
||||
// Se todos estão em cache, retorna direto
|
||||
if (toTranslate.length === 0) {
|
||||
return NextResponse.json({ translations: results, allCached: true });
|
||||
}
|
||||
|
||||
// Traduzir os que faltam (em paralelo, mas com limite)
|
||||
const BATCH_SIZE = 5; // Traduzir 5 por vez para não sobrecarregar
|
||||
|
||||
for (let i = 0; i < toTranslate.length; i += BATCH_SIZE) {
|
||||
const batch = toTranslate.slice(i, i + BATCH_SIZE);
|
||||
|
||||
await Promise.all(
|
||||
batch.map(async ({ index, text }) => {
|
||||
try {
|
||||
const response = await fetch(`${LIBRETRANSLATE_URL}/translate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ q: text, source, target, format: 'text' }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const translatedText = data.translatedText || text;
|
||||
|
||||
results[index] = translatedText;
|
||||
|
||||
// Salvar no banco
|
||||
try {
|
||||
await prisma.translation.create({
|
||||
data: {
|
||||
sourceText: text,
|
||||
sourceLang: source,
|
||||
targetLang: target,
|
||||
translatedText,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignorar se já existe
|
||||
}
|
||||
|
||||
memoryCache.set(`${source}:${target}:${text}`, translatedText);
|
||||
} else {
|
||||
results[index] = text;
|
||||
}
|
||||
} catch (e) {
|
||||
results[index] = text;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Translation error for:', text, e);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return text; // Fallback
|
||||
})
|
||||
);
|
||||
|
||||
return NextResponse.json({ translations });
|
||||
return NextResponse.json({ translations: results });
|
||||
} catch (error) {
|
||||
console.error('Batch translation error:', error);
|
||||
return NextResponse.json({ error: 'Erro ao traduzir' }, { status: 500 });
|
||||
|
||||
Reference in New Issue
Block a user