feat: CMS com limites de caracteres, traduções auto e painel de notificações
This commit is contained in:
23
frontend/prisma/check-translations.mjs
Normal file
23
frontend/prisma/check-translations.mjs
Normal file
@@ -0,0 +1,23 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function check() {
|
||||
const pages = await prisma.pageContent.findMany({
|
||||
where: { slug: 'home' },
|
||||
orderBy: { locale: 'asc' }
|
||||
})
|
||||
|
||||
console.log('\n=== VERSÕES DA PÁGINA HOME ===\n')
|
||||
|
||||
for (const page of pages) {
|
||||
const content = typeof page.content === 'string' ? JSON.parse(page.content) : page.content
|
||||
console.log(`[${page.locale.toUpperCase()}] Hero title: ${content.hero?.title?.substring(0, 70)}...`)
|
||||
console.log(` Atualizado: ${page.updatedAt}`)
|
||||
console.log('')
|
||||
}
|
||||
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
|
||||
check().catch(console.error)
|
||||
49
frontend/prisma/migrate-locale.mjs
Normal file
49
frontend/prisma/migrate-locale.mjs
Normal file
@@ -0,0 +1,49 @@
|
||||
// Script para migrar dados existentes de PageContent para o novo formato com locale
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🔄 Migrando dados para incluir locale...\n');
|
||||
|
||||
// Buscar todos os registros que não têm locale definido ou têm locale null
|
||||
const pages = await prisma.pageContent.findMany();
|
||||
|
||||
console.log(`📄 Encontrados ${pages.length} registros de PageContent\n`);
|
||||
|
||||
for (const page of pages) {
|
||||
// Se o registro já tem locale 'pt' e está no formato correto, pular
|
||||
if (page.locale === 'pt') {
|
||||
console.log(`✓ "${page.slug}" (${page.locale}) - já migrado`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Se tem locale diferente de pt, pular também (já foi migrado)
|
||||
if (page.locale && ['en', 'es'].includes(page.locale)) {
|
||||
console.log(`✓ "${page.slug}" (${page.locale}) - já é tradução`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Atualizar para ter locale 'pt'
|
||||
try {
|
||||
await prisma.pageContent.update({
|
||||
where: { id: page.id },
|
||||
data: { locale: 'pt' }
|
||||
});
|
||||
console.log(`✅ "${page.slug}" - atualizado para locale 'pt'`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Erro ao atualizar "${page.slug}":`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✨ Migração concluída!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('Erro na migração:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -63,9 +63,27 @@ model Message {
|
||||
}
|
||||
|
||||
// Modelo de Conteúdo de Página (para textos editáveis)
|
||||
// Cada página tem uma versão para cada idioma
|
||||
model PageContent {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique // "home", "sobre", "contato"
|
||||
slug String // "home", "sobre", "contato"
|
||||
locale String @default("pt") // "pt", "en", "es"
|
||||
content Json
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([slug, locale]) // Uma entrada por página+idioma
|
||||
@@index([slug])
|
||||
}
|
||||
|
||||
// Modelo de Tradução (cache persistente)
|
||||
model Translation {
|
||||
id String @id @default(cuid())
|
||||
sourceText String @db.Text
|
||||
sourceLang String @default("pt")
|
||||
targetLang String
|
||||
translatedText String @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([sourceText, sourceLang, targetLang])
|
||||
@@index([sourceLang, targetLang])
|
||||
}
|
||||
|
||||
147
frontend/prisma/translate-pages.mjs
Normal file
147
frontend/prisma/translate-pages.mjs
Normal file
@@ -0,0 +1,147 @@
|
||||
// Script para traduzir todas as páginas PT para EN e ES
|
||||
// Executar: node prisma/translate-pages.mjs
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const LIBRETRANSLATE_URL = process.env.LIBRETRANSLATE_URL || 'https://libretranslate.stackbyte.cloud';
|
||||
const SUPPORTED_LOCALES = ['en', 'es'];
|
||||
|
||||
// Traduzir um texto
|
||||
async function translateText(text, targetLang) {
|
||||
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) {
|
||||
console.log(` [cache] "${text.substring(0, 25)}..."`);
|
||||
return cached.translatedText;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(` [traduzindo] "${text.substring(0, 25)}..." -> ${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(` [erro] ${error.message}`);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// Traduzir objeto recursivamente
|
||||
async function translateContent(content, targetLang) {
|
||||
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 = {};
|
||||
for (const [key, value] of Object.entries(content)) {
|
||||
// Não traduzir campos técnicos
|
||||
if (['icon', 'image', 'img', 'url', 'href', 'id', 'slug', 'src', 'link', 'linkText'].includes(key)) {
|
||||
result[key] = value;
|
||||
} else {
|
||||
result[key] = await translateContent(value, targetLang);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🌐 Iniciando tradução de páginas...\n');
|
||||
console.log(`📡 LibreTranslate: ${LIBRETRANSLATE_URL}\n`);
|
||||
|
||||
// Buscar todas as páginas em português
|
||||
const ptPages = await prisma.pageContent.findMany({
|
||||
where: { locale: 'pt' }
|
||||
});
|
||||
|
||||
if (ptPages.length === 0) {
|
||||
console.log('❌ Nenhuma página encontrada em português');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📄 Encontradas ${ptPages.length} páginas em PT\n`);
|
||||
|
||||
for (const page of ptPages) {
|
||||
console.log(`\n📝 Página: ${page.slug}`);
|
||||
console.log('─'.repeat(40));
|
||||
|
||||
for (const targetLocale of SUPPORTED_LOCALES) {
|
||||
console.log(`\n 🔄 Traduzindo para ${targetLocale.toUpperCase()}...`);
|
||||
|
||||
try {
|
||||
const translatedContent = await translateContent(page.content, targetLocale);
|
||||
|
||||
await prisma.pageContent.upsert({
|
||||
where: { slug_locale: { slug: page.slug, locale: targetLocale } },
|
||||
update: { content: translatedContent },
|
||||
create: { slug: page.slug, locale: targetLocale, content: translatedContent }
|
||||
});
|
||||
|
||||
console.log(` ✅ ${page.slug} -> ${targetLocale.toUpperCase()} concluído!`);
|
||||
} catch (error) {
|
||||
console.error(` ❌ Erro: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n' + '═'.repeat(40));
|
||||
console.log('✨ Tradução concluída!');
|
||||
console.log('═'.repeat(40));
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('Erro:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user