feat: CMS com limites de caracteres, traduções auto e painel de notificações
This commit is contained in:
193
frontend/src/app/api/admin/translate-pages/route.ts
Normal file
193
frontend/src/app/api/admin/translate-pages/route.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
const LIBRETRANSLATE_URL = process.env.LIBRETRANSLATE_URL || 'https://libretranslate.stackbyte.cloud';
|
||||
const SUPPORTED_LOCALES = ['en', 'es'];
|
||||
|
||||
// Autenticação
|
||||
async function authenticate() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('auth_token')?.value;
|
||||
|
||||
if (!token) return null;
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string };
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.userId },
|
||||
select: { id: true, email: true, name: true }
|
||||
});
|
||||
return user;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Traduzir um texto
|
||||
async function translateText(text: string, targetLang: string): Promise<string> {
|
||||
if (!text || text.trim() === '' || targetLang === 'pt') return text;
|
||||
|
||||
// Verificar cache no banco primeiro
|
||||
const cached = await prisma.translation.findUnique({
|
||||
where: {
|
||||
sourceText_sourceLang_targetLang: {
|
||||
sourceText: text,
|
||||
sourceLang: 'pt',
|
||||
targetLang: targetLang,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (cached) {
|
||||
return cached.translatedText;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[i18n] Traduzindo: "${text.substring(0, 30)}..." para ${targetLang}`);
|
||||
|
||||
const response = await fetch(`${LIBRETRANSLATE_URL}/translate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ q: text, source: 'pt', target: targetLang, format: 'text' }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const translatedText = data.translatedText || text;
|
||||
|
||||
// Salvar no cache
|
||||
try {
|
||||
await prisma.translation.create({
|
||||
data: {
|
||||
sourceText: text,
|
||||
sourceLang: 'pt',
|
||||
targetLang: targetLang,
|
||||
translatedText,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// Ignorar se já existe
|
||||
}
|
||||
|
||||
return translatedText;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[i18n] Erro ao traduzir para ${targetLang}:`, error);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// Traduzir objeto recursivamente
|
||||
async function translateContent(content: unknown, targetLang: string): Promise<unknown> {
|
||||
if (typeof content === 'string') {
|
||||
return await translateText(content, targetLang);
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
const results = [];
|
||||
for (const item of content) {
|
||||
results.push(await translateContent(item, targetLang));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
if (content && typeof content === 'object') {
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(content)) {
|
||||
// Não traduzir campos técnicos
|
||||
if (['icon', 'image', 'img', 'url', 'href', 'id', 'slug', 'src', 'link'].includes(key)) {
|
||||
result[key] = value;
|
||||
} else {
|
||||
result[key] = await translateContent(value, targetLang);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// POST /api/admin/translate-pages - Traduzir todas as páginas para EN e ES
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const user = await authenticate();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Não autorizado' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const slugFilter = body.slug; // Opcional: traduzir só uma página específica
|
||||
|
||||
// Buscar todas as páginas em português
|
||||
const ptPages = await prisma.pageContent.findMany({
|
||||
where: slugFilter
|
||||
? { slug: slugFilter, locale: 'pt' }
|
||||
: { locale: 'pt' }
|
||||
});
|
||||
|
||||
if (ptPages.length === 0) {
|
||||
return NextResponse.json({ error: 'Nenhuma página encontrada para traduzir' }, { status: 404 });
|
||||
}
|
||||
|
||||
const results: { slug: string; locale: string; status: string }[] = [];
|
||||
|
||||
for (const page of ptPages) {
|
||||
for (const targetLocale of SUPPORTED_LOCALES) {
|
||||
try {
|
||||
console.log(`[i18n] Traduzindo página "${page.slug}" para ${targetLocale}...`);
|
||||
|
||||
const translatedContent = await translateContent(page.content, targetLocale) as Prisma.InputJsonValue;
|
||||
|
||||
await prisma.pageContent.upsert({
|
||||
where: { slug_locale: { slug: page.slug, locale: targetLocale } },
|
||||
update: { content: translatedContent },
|
||||
create: { slug: page.slug, locale: targetLocale, content: translatedContent }
|
||||
});
|
||||
|
||||
results.push({ slug: page.slug, locale: targetLocale, status: 'success' });
|
||||
console.log(`[i18n] ✓ Página "${page.slug}" traduzida para ${targetLocale}`);
|
||||
} catch (error) {
|
||||
console.error(`[i18n] ✗ Erro ao traduzir "${page.slug}" para ${targetLocale}:`, error);
|
||||
results.push({ slug: page.slug, locale: targetLocale, status: 'error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Tradução concluída para ${ptPages.length} página(s)`,
|
||||
results
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao traduzir páginas:', error);
|
||||
return NextResponse.json({ error: 'Erro ao traduzir páginas' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/admin/translate-pages - Status das traduções
|
||||
export async function GET() {
|
||||
try {
|
||||
const pages = await prisma.pageContent.findMany({
|
||||
select: { slug: true, locale: true, updatedAt: true },
|
||||
orderBy: [{ slug: 'asc' }, { locale: 'asc' }]
|
||||
});
|
||||
|
||||
// Agrupar por slug
|
||||
const grouped: Record<string, { pt?: Date; en?: Date; es?: Date }> = {};
|
||||
|
||||
for (const page of pages) {
|
||||
if (!grouped[page.slug]) {
|
||||
grouped[page.slug] = {};
|
||||
}
|
||||
grouped[page.slug][page.locale as 'pt' | 'en' | 'es'] = page.updatedAt;
|
||||
}
|
||||
|
||||
return NextResponse.json({ pages: grouped });
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar status:', error);
|
||||
return NextResponse.json({ error: 'Erro ao buscar status' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,9 @@ import prisma from '@/lib/prisma';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Config é global, sempre usa 'pt' como locale base
|
||||
const config = await prisma.pageContent.findUnique({
|
||||
where: { slug: 'config' }
|
||||
where: { slug_locale: { slug: 'config', locale: 'pt' } }
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
@@ -23,12 +24,13 @@ export async function PUT(request: NextRequest) {
|
||||
const { primaryColor } = await request.json();
|
||||
|
||||
const config = await prisma.pageContent.upsert({
|
||||
where: { slug: 'config' },
|
||||
where: { slug_locale: { slug: 'config', locale: 'pt' } },
|
||||
update: {
|
||||
content: { primaryColor }
|
||||
},
|
||||
create: {
|
||||
slug: 'config',
|
||||
locale: 'pt',
|
||||
content: { primaryColor }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const LIBRETRANSLATE_URL = process.env.LIBRETRANSLATE_URL || 'https://libretranslate.stackbyte.cloud';
|
||||
const SUPPORTED_LOCALES = ['pt', 'en', 'es'];
|
||||
const TARGET_TRANSLATION_LOCALES: Array<'en' | 'es'> = ['en', 'es'];
|
||||
|
||||
// Middleware de autenticação
|
||||
async function authenticate() {
|
||||
const cookieStore = await cookies();
|
||||
@@ -24,23 +29,149 @@ async function authenticate() {
|
||||
}
|
||||
}
|
||||
|
||||
// Tradução com cache
|
||||
async function translateText(text: string, targetLang: string): Promise<string> {
|
||||
if (!text || text.trim() === '' || targetLang === 'pt') return text;
|
||||
|
||||
try {
|
||||
const cached = await prisma.translation.findUnique({
|
||||
where: {
|
||||
sourceText_sourceLang_targetLang: {
|
||||
sourceText: text,
|
||||
sourceLang: 'pt',
|
||||
targetLang
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (cached) {
|
||||
return cached.translatedText;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[i18n] Erro ao buscar cache de tradução:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${LIBRETRANSLATE_URL}/translate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ q: text, source: 'pt', target: targetLang, format: 'text' }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const translatedText = data.translatedText || text;
|
||||
|
||||
try {
|
||||
await prisma.translation.create({
|
||||
data: {
|
||||
sourceText: text,
|
||||
sourceLang: 'pt',
|
||||
targetLang,
|
||||
translatedText
|
||||
}
|
||||
});
|
||||
} catch (cacheError) {
|
||||
console.warn('[i18n] Falha ao salvar cache de tradução:', cacheError);
|
||||
}
|
||||
|
||||
return translatedText;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[i18n] Erro ao traduzir texto para ${targetLang}:`, error);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
async function translateContent(content: unknown, targetLang: string): Promise<unknown> {
|
||||
if (targetLang === 'pt') return content;
|
||||
|
||||
const skipKeys = ['icon', 'image', 'img', 'url', 'href', 'id', 'slug', 'src', 'email', 'phone', 'whatsapp', 'link', 'linkText'];
|
||||
|
||||
if (typeof content === 'string') {
|
||||
return translateText(content, targetLang);
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
const translated = [] as unknown[];
|
||||
for (const item of content) {
|
||||
translated.push(await translateContent(item, targetLang));
|
||||
}
|
||||
return translated;
|
||||
}
|
||||
|
||||
if (content && typeof content === 'object') {
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(content)) {
|
||||
if (skipKeys.includes(key)) {
|
||||
result[key] = value;
|
||||
} else {
|
||||
result[key] = await translateContent(value, targetLang);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
async function translateInBackground(slug: string, content: unknown) {
|
||||
console.log(`[i18n] Iniciando tradução de "${slug}" para EN/ES em background...`);
|
||||
|
||||
for (const targetLocale of TARGET_TRANSLATION_LOCALES) {
|
||||
try {
|
||||
const translatedContent = await translateContent(content, targetLocale) as Prisma.InputJsonValue;
|
||||
|
||||
await prisma.pageContent.upsert({
|
||||
where: { slug_locale: { slug, locale: targetLocale } },
|
||||
update: { content: translatedContent },
|
||||
create: { slug, locale: targetLocale, content: translatedContent }
|
||||
});
|
||||
|
||||
console.log(`[i18n] ✓ "${slug}" traduzido para ${targetLocale.toUpperCase()}`);
|
||||
} catch (error) {
|
||||
console.error(`[i18n] ✗ Erro ao traduzir "${slug}" para ${targetLocale}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[i18n] Traduções de "${slug}" finalizadas.`);
|
||||
}
|
||||
|
||||
// GET /api/pages/[slug] - Buscar página específica (público)
|
||||
// Suporta ?locale=en para buscar versão traduzida
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
try {
|
||||
const { slug } = await params;
|
||||
const locale = request.nextUrl.searchParams.get('locale') || 'pt';
|
||||
|
||||
// Buscar a versão do idioma solicitado
|
||||
const page = await prisma.pageContent.findUnique({
|
||||
where: { slug }
|
||||
where: {
|
||||
slug_locale: { slug, locale }
|
||||
}
|
||||
});
|
||||
|
||||
if (!page) {
|
||||
return NextResponse.json({ error: 'Página não encontrada' }, { status: 404 });
|
||||
if (page) {
|
||||
return NextResponse.json(page);
|
||||
}
|
||||
|
||||
return NextResponse.json(page);
|
||||
// Se não existe a versão traduzida, buscar PT como fallback
|
||||
if (locale !== 'pt') {
|
||||
const ptPage = await prisma.pageContent.findUnique({
|
||||
where: { slug_locale: { slug, locale: 'pt' } }
|
||||
});
|
||||
|
||||
if (ptPage) {
|
||||
// Retorna versão PT com flag indicando que não está traduzido
|
||||
return NextResponse.json({ ...ptPage, locale: 'pt', fallback: true });
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Página não encontrada' }, { status: 404 });
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar página:', error);
|
||||
return NextResponse.json({ error: 'Erro ao buscar página' }, { status: 500 });
|
||||
@@ -48,6 +179,7 @@ export async function GET(
|
||||
}
|
||||
|
||||
// PUT /api/pages/[slug] - Atualizar página (admin apenas)
|
||||
// Quando salva em PT, automaticamente traduz e salva EN e ES
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
@@ -66,13 +198,23 @@ export async function PUT(
|
||||
return NextResponse.json({ error: 'Conteúdo é obrigatório' }, { status: 400 });
|
||||
}
|
||||
|
||||
const page = await prisma.pageContent.upsert({
|
||||
where: { slug },
|
||||
// 1. Salvar versão em português (principal)
|
||||
const ptPage = await prisma.pageContent.upsert({
|
||||
where: { slug_locale: { slug, locale: 'pt' } },
|
||||
update: { content },
|
||||
create: { slug, content }
|
||||
create: { slug, locale: 'pt', content }
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, page });
|
||||
// 2. Disparar traduções em background para EN/ES
|
||||
translateInBackground(slug, content).catch(error => {
|
||||
console.error(`[i18n] Erro fatal na tradução em background de "${slug}":`, error);
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
page: ptPage,
|
||||
message: 'Conteúdo salvo com sucesso!'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar página:', error);
|
||||
return NextResponse.json({ error: 'Erro ao atualizar página' }, { status: 500 });
|
||||
@@ -80,6 +222,7 @@ export async function PUT(
|
||||
}
|
||||
|
||||
// DELETE /api/pages/[slug] - Deletar página (admin apenas)
|
||||
// Remove todas as versões (PT, EN, ES)
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
@@ -92,11 +235,12 @@ export async function DELETE(
|
||||
|
||||
const { slug } = await params;
|
||||
|
||||
await prisma.pageContent.delete({
|
||||
// Deletar todas as versões de idioma
|
||||
await prisma.pageContent.deleteMany({
|
||||
where: { slug }
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Página deletada com sucesso' });
|
||||
return NextResponse.json({ success: true, message: 'Página deletada com sucesso (todos os idiomas)' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar página:', error);
|
||||
return NextResponse.json({ error: 'Erro ao deletar página' }, { status: 500 });
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const page = await prisma.pageContent.findUnique({
|
||||
where: { slug: 'contact' }
|
||||
});
|
||||
|
||||
if (!page) {
|
||||
return NextResponse.json({ content: null }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ content: page.content });
|
||||
} catch (error) {
|
||||
console.error('Error fetching contact page:', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch page' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const { content } = await request.json();
|
||||
|
||||
const page = await prisma.pageContent.upsert({
|
||||
where: { slug: 'contact' },
|
||||
update: {
|
||||
content
|
||||
},
|
||||
create: {
|
||||
slug: 'contact',
|
||||
content
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, page });
|
||||
} catch (error) {
|
||||
console.error('Error updating contact page:', error);
|
||||
return NextResponse.json({ error: 'Failed to update page' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -31,22 +31,33 @@ export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const slug = searchParams.get('slug');
|
||||
const locale = searchParams.get('locale') || 'pt';
|
||||
|
||||
if (slug) {
|
||||
// Buscar página específica
|
||||
// Buscar página específica com locale
|
||||
const page = await prisma.pageContent.findUnique({
|
||||
where: { slug }
|
||||
where: { slug_locale: { slug, locale } }
|
||||
});
|
||||
|
||||
if (!page) {
|
||||
// Fallback para PT se não encontrar
|
||||
if (locale !== 'pt') {
|
||||
const ptPage = await prisma.pageContent.findUnique({
|
||||
where: { slug_locale: { slug, locale: 'pt' } }
|
||||
});
|
||||
if (ptPage) {
|
||||
return NextResponse.json({ ...ptPage, fallback: true });
|
||||
}
|
||||
}
|
||||
return NextResponse.json({ error: 'Página não encontrada' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(page);
|
||||
}
|
||||
|
||||
// Listar todas as páginas
|
||||
// Listar todas as páginas (só PT para admin)
|
||||
const pages = await prisma.pageContent.findMany({
|
||||
where: { locale: 'pt' },
|
||||
orderBy: { slug: 'asc' }
|
||||
});
|
||||
|
||||
@@ -72,11 +83,11 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Slug e conteúdo são obrigatórios' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Upsert: criar ou atualizar se já existir
|
||||
// Upsert: criar ou atualizar se já existir (versão PT)
|
||||
const page = await prisma.pageContent.upsert({
|
||||
where: { slug },
|
||||
where: { slug_locale: { slug, locale: 'pt' } },
|
||||
update: { content },
|
||||
create: { slug, content }
|
||||
create: { slug, locale: 'pt', content }
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, page });
|
||||
|
||||
@@ -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