249 lines
7.3 KiB
TypeScript
249 lines
7.3 KiB
TypeScript
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();
|
|
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 (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 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_locale: { slug, locale }
|
|
}
|
|
});
|
|
|
|
if (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 });
|
|
}
|
|
}
|
|
|
|
// 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 }> }
|
|
) {
|
|
try {
|
|
const user = await authenticate();
|
|
if (!user) {
|
|
return NextResponse.json({ error: 'Não autorizado' }, { status: 401 });
|
|
}
|
|
|
|
const { slug } = await params;
|
|
const body = await request.json();
|
|
const { content } = body;
|
|
|
|
if (!content) {
|
|
return NextResponse.json({ error: 'Conteúdo é obrigatório' }, { status: 400 });
|
|
}
|
|
|
|
// 1. Salvar versão em português (principal)
|
|
const ptPage = await prisma.pageContent.upsert({
|
|
where: { slug_locale: { slug, locale: 'pt' } },
|
|
update: { content },
|
|
create: { slug, locale: 'pt', content }
|
|
});
|
|
|
|
// 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 });
|
|
}
|
|
}
|
|
|
|
// 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 }> }
|
|
) {
|
|
try {
|
|
const user = await authenticate();
|
|
if (!user) {
|
|
return NextResponse.json({ error: 'Não autorizado' }, { status: 401 });
|
|
}
|
|
|
|
const { slug } = await params;
|
|
|
|
// Deletar todas as versões de idioma
|
|
await prisma.pageContent.deleteMany({
|
|
where: { slug }
|
|
});
|
|
|
|
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 });
|
|
}
|
|
}
|