From 686df732ea80d41bb4de517591bb43a5f4bc25a6 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 26 Nov 2025 20:43:49 -0300 Subject: [PATCH 01/49] Adicionar docker-compose.dev.yml para ambiente de desenvolvimento --- docker-compose.dev.yml | 70 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docker-compose.dev.yml diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..c40df47 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,70 @@ +# Docker Compose para ambiente de DESENVOLVIMENTO +# Usa banco de dados separado e subdomínio dev.octto.stackbyte.cloud +# Branch: dev + +services: + postgres_dev: + image: postgres:12-alpine + container_name: occto_postgres_dev + environment: + POSTGRES_USER: ${POSTGRES_USER:-admin} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-adminpassword} + POSTGRES_DB: ${POSTGRES_DB:-occto_db_dev} + volumes: + - postgres_data_dev:/var/lib/postgresql/data + networks: + - occto_network_dev + healthcheck: + test: ["CMD-SHELL", "pg_isready -U admin -d occto_db_dev"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + minio_dev: + image: minio/minio:RELEASE.2023-09-04T19-57-37Z + container_name: occto_minio_dev + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-admin} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-adminpassword} + volumes: + - minio_data_dev:/data + networks: + - occto_network_dev + command: server /data --console-address ":9001" + restart: unless-stopped + + frontend_dev: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: occto_frontend_dev + expose: + - "3000" + environment: + - NODE_ENV=${NODE_ENV:-production} + - DATABASE_URL=${DATABASE_URL:-postgresql://admin:adminpassword@postgres_dev:5432/occto_db_dev?schema=public} + - MINIO_ENDPOINT=${MINIO_ENDPOINT:-minio_dev} + - MINIO_PORT=${MINIO_PORT:-9000} + - MINIO_USE_SSL=${MINIO_USE_SSL:-false} + - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY:-admin} + - MINIO_SECRET_KEY=${MINIO_SECRET_KEY:-adminpassword} + - MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME:-occto-images-dev} + - JWT_SECRET=${JWT_SECRET:-dev_jwt_secret_change_in_production_1234567890} + depends_on: + postgres_dev: + condition: service_healthy + networks: + - occto_network_dev + - dokploy-network + restart: unless-stopped + +networks: + occto_network_dev: + driver: bridge + dokploy-network: + external: true + +volumes: + postgres_data_dev: + minio_data_dev: From 5ac57449b787a2bae891a7b0c17b6747f656d61f Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 26 Nov 2025 20:50:58 -0300 Subject: [PATCH 02/49] Simplificar variaveis do docker-compose.dev --- docker-compose.dev.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index c40df47..402d73d 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -42,14 +42,14 @@ services: expose: - "3000" environment: - - NODE_ENV=${NODE_ENV:-production} - - DATABASE_URL=${DATABASE_URL:-postgresql://admin:adminpassword@postgres_dev:5432/occto_db_dev?schema=public} - - MINIO_ENDPOINT=${MINIO_ENDPOINT:-minio_dev} - - MINIO_PORT=${MINIO_PORT:-9000} - - MINIO_USE_SSL=${MINIO_USE_SSL:-false} - - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY:-admin} - - MINIO_SECRET_KEY=${MINIO_SECRET_KEY:-adminpassword} - - MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME:-occto-images-dev} + - NODE_ENV=production + - DATABASE_URL=postgresql://admin:adminpassword@postgres_dev:5432/occto_db_dev?schema=public + - MINIO_ENDPOINT=minio_dev + - MINIO_PORT=9000 + - MINIO_USE_SSL=false + - MINIO_ACCESS_KEY=admin + - MINIO_SECRET_KEY=adminpassword + - MINIO_BUCKET_NAME=occto-images-dev - JWT_SECRET=${JWT_SECRET:-dev_jwt_secret_change_in_production_1234567890} depends_on: postgres_dev: From 0bde8d4a56a4765be41544f75da5dd3c41225f5b Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 26 Nov 2025 20:55:54 -0300 Subject: [PATCH 03/49] Adicionar emoji de cookie no banner de consentimento --- docker-compose.dev.yml => docker-compose-dev.yml | 0 frontend/src/components/CookieConsent.tsx | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename docker-compose.dev.yml => docker-compose-dev.yml (100%) diff --git a/docker-compose.dev.yml b/docker-compose-dev.yml similarity index 100% rename from docker-compose.dev.yml rename to docker-compose-dev.yml diff --git a/frontend/src/components/CookieConsent.tsx b/frontend/src/components/CookieConsent.tsx index b1bf14c..81845b5 100644 --- a/frontend/src/components/CookieConsent.tsx +++ b/frontend/src/components/CookieConsent.tsx @@ -35,8 +35,8 @@ export default function CookieConsent() {
-
- +
+ 🍪

From 6044a437f8318a3776830dbb4ef059f9aa4c2399 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 26 Nov 2025 21:15:17 -0300 Subject: [PATCH 04/49] Integrar LibreTranslate para traducao automatica --- docker-compose-dev.yml | 1 + docker-compose.yml | 1 + frontend/src/app/api/translate/route.ts | 112 +++++++++++++++++ frontend/src/components/TranslatedText.tsx | 134 +++++++++++++++++++++ frontend/src/hooks/useTranslate.ts | 109 +++++++++++++++++ 5 files changed, 357 insertions(+) create mode 100644 frontend/src/app/api/translate/route.ts create mode 100644 frontend/src/components/TranslatedText.tsx create mode 100644 frontend/src/hooks/useTranslate.ts diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 402d73d..d1d6fd6 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -51,6 +51,7 @@ services: - MINIO_SECRET_KEY=adminpassword - MINIO_BUCKET_NAME=occto-images-dev - JWT_SECRET=${JWT_SECRET:-dev_jwt_secret_change_in_production_1234567890} + - LIBRETRANSLATE_URL=https://libretranslate.stackbyte.cloud depends_on: postgres_dev: condition: service_healthy diff --git a/docker-compose.yml b/docker-compose.yml index 35f6038..11dec00 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,6 +47,7 @@ services: - MINIO_SECRET_KEY=${MINIO_SECRET_KEY:-adminpassword} - MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME:-occto-images} - JWT_SECRET=${JWT_SECRET:-b33500bb3dc5504535c34cc5f79f4ca0f60994b093bded14d48f76c0c090f032234693219e60398cab053a9c55c1d426ef7b1768104db9040254ba7db452f708} + - LIBRETRANSLATE_URL=${LIBRETRANSLATE_URL:-https://libretranslate.stackbyte.cloud} depends_on: postgres: condition: service_healthy diff --git a/frontend/src/app/api/translate/route.ts b/frontend/src/app/api/translate/route.ts new file mode 100644 index 0000000..9d5d66a --- /dev/null +++ b/frontend/src/app/api/translate/route.ts @@ -0,0 +1,112 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const LIBRETRANSLATE_URL = process.env.LIBRETRANSLATE_URL || 'https://libretranslate.stackbyte.cloud'; + +// Cache simples em memória para traduções +const translationCache = new Map(); +const CACHE_TTL = 1000 * 60 * 60 * 24; // 24 horas + +export async function POST(request: NextRequest) { + try { + const { text, source = 'pt', target = 'en' } = await request.json(); + + if (!text || typeof text !== 'string') { + return NextResponse.json({ error: 'Texto é obrigatório' }, { status: 400 }); + } + + // Se origem e destino são iguais, retorna o texto original + if (source === target) { + 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 }); + } + + // Chamar LibreTranslate + const response = await fetch(`${LIBRETRANSLATE_URL}/translate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + q: text, + source: source, + target: target, + format: 'text', + }), + }); + + if (!response.ok) { + console.error('LibreTranslate error:', await response.text()); + return NextResponse.json({ translatedText: text }); // Fallback: retorna original + } + + const data = await response.json(); + const translatedText = data.translatedText || text; + + // Salvar no cache + translationCache.set(cacheKey, { text: translatedText, timestamp: Date.now() }); + + return NextResponse.json({ translatedText }); + } catch (error) { + console.error('Translation error:', error); + return NextResponse.json({ error: 'Erro ao traduzir' }, { status: 500 }); + } +} + +// Endpoint para traduzir múltiplos textos de uma vez +export async function PUT(request: NextRequest) { + try { + const { texts, source = 'pt', target = 'en' } = await request.json(); + + if (!texts || !Array.isArray(texts)) { + return NextResponse.json({ error: 'Array de textos é obrigatório' }, { status: 400 }); + } + + if (source === target) { + return NextResponse.json({ translations: texts }); + } + + const translations = await Promise.all( + texts.map(async (text: string) => { + if (!text) return text; + + const cacheKey = `${source}:${target}:${text}`; + const cached = translationCache.get(cacheKey); + + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + return cached.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; + translationCache.set(cacheKey, { text: translatedText, timestamp: Date.now() }); + return translatedText; + } + } catch (e) { + console.error('Translation error for:', text, e); + } + + return text; // Fallback + }) + ); + + return NextResponse.json({ translations }); + } catch (error) { + console.error('Batch translation error:', error); + return NextResponse.json({ error: 'Erro ao traduzir' }, { status: 500 }); + } +} diff --git a/frontend/src/components/TranslatedText.tsx b/frontend/src/components/TranslatedText.tsx new file mode 100644 index 0000000..9a7cd32 --- /dev/null +++ b/frontend/src/components/TranslatedText.tsx @@ -0,0 +1,134 @@ +'use client'; + +import { useEffect, useState, ElementType, ComponentPropsWithoutRef } from 'react'; +import { useTranslate } from '@/hooks/useTranslate'; + +interface TranslatedTextProps { + text: string; + as?: 'span' | 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div' | 'li'; + className?: string; +} + +/** + * Componente que traduz texto automaticamente via LibreTranslate + * quando o idioma não é português + */ +export function TranslatedText({ text, as = 'span', className }: TranslatedTextProps) { + const { translate, language } = useTranslate(); + const [translatedText, setTranslatedText] = useState(text); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (language === 'PT') { + setTranslatedText(text); + return; + } + + let cancelled = false; + setIsLoading(true); + + translate(text).then((result) => { + if (!cancelled) { + setTranslatedText(result); + setIsLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [text, language, translate]); + + const Tag = as; + + return ( + + {translatedText} + + ); +} + +/** + * Hook para traduzir objetos de conteúdo do banco de dados + */ +export function useTranslatedContent>(content: T): { + translatedContent: T; + isTranslating: boolean +} { + const { translateBatch, language } = useTranslate(); + const [translatedContent, setTranslatedContent] = useState(content); + const [isTranslating, setIsTranslating] = useState(false); + + useEffect(() => { + if (language === 'PT') { + setTranslatedContent(content); + return; + } + + let cancelled = false; + + const translateContent = async () => { + setIsTranslating(true); + + // Extrair todos os textos do objeto + const texts: string[] = []; + const paths: string[] = []; + + const extractTexts = (obj: unknown, path: string = '') => { + if (typeof obj === 'string' && obj.length > 0) { + texts.push(obj); + paths.push(path); + } else if (Array.isArray(obj)) { + obj.forEach((item, index) => extractTexts(item, `${path}[${index}]`)); + } else if (obj && typeof obj === 'object') { + Object.entries(obj).forEach(([key, value]) => { + extractTexts(value, path ? `${path}.${key}` : key); + }); + } + }; + + extractTexts(content); + + if (texts.length === 0) { + setIsTranslating(false); + return; + } + + try { + const translations = await translateBatch(texts); + + if (cancelled) return; + + // Reconstruir objeto com traduções + const newContent = JSON.parse(JSON.stringify(content)); + + paths.forEach((path, index) => { + const parts = path.replace(/\[(\d+)\]/g, '.$1').split('.'); + let current: Record = newContent; + + for (let i = 0; i < parts.length - 1; i++) { + current = current[parts[i]] as Record; + } + + current[parts[parts.length - 1]] = translations[index]; + }); + + setTranslatedContent(newContent); + } catch (error) { + console.error('Translation error:', error); + } finally { + if (!cancelled) { + setIsTranslating(false); + } + } + }; + + translateContent(); + + return () => { + cancelled = true; + }; + }, [content, language, translateBatch]); + + return { translatedContent, isTranslating }; +} diff --git a/frontend/src/hooks/useTranslate.ts b/frontend/src/hooks/useTranslate.ts new file mode 100644 index 0000000..4aef232 --- /dev/null +++ b/frontend/src/hooks/useTranslate.ts @@ -0,0 +1,109 @@ +'use client'; + +import { useState, useCallback } from 'react'; +import { useLanguage } from '@/contexts/LanguageContext'; + +// Cache local no cliente +const clientCache = new Map(); + +export function useTranslate() { + const { language } = useLanguage(); + const [isTranslating, setIsTranslating] = useState(false); + + const translate = useCallback(async (text: string): Promise => { + if (!text || language === 'PT') return text; + + const targetLang = language.toLowerCase(); // PT -> pt, EN -> en, ES -> es + const cacheKey = `pt:${targetLang}:${text}`; + + // Verificar cache local + if (clientCache.has(cacheKey)) { + return clientCache.get(cacheKey)!; + } + + try { + setIsTranslating(true); + const response = await fetch('/api/translate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text, source: 'pt', target: targetLang }), + }); + + if (response.ok) { + const data = await response.json(); + clientCache.set(cacheKey, data.translatedText); + return data.translatedText; + } + } catch (error) { + console.error('Translation error:', error); + } finally { + setIsTranslating(false); + } + + return text; + }, [language]); + + const translateBatch = useCallback(async (texts: string[]): Promise => { + if (language === 'PT') return texts; + + const targetLang = language.toLowerCase(); + + // Separar textos em cache e não em cache + const results: string[] = new Array(texts.length); + const toTranslate: { index: number; text: string }[] = []; + + texts.forEach((text, index) => { + if (!text) { + results[index] = text; + return; + } + + const cacheKey = `pt:${targetLang}:${text}`; + if (clientCache.has(cacheKey)) { + results[index] = clientCache.get(cacheKey)!; + } else { + toTranslate.push({ index, text }); + } + }); + + if (toTranslate.length === 0) return results; + + try { + setIsTranslating(true); + 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(); + toTranslate.forEach((item, i) => { + const translated = data.translations[i]; + results[item.index] = translated; + clientCache.set(`pt:${targetLang}:${item.text}`, translated); + }); + } else { + // Fallback: usar texto original + toTranslate.forEach(item => { + results[item.index] = item.text; + }); + } + } catch (error) { + console.error('Batch translation error:', error); + toTranslate.forEach(item => { + results[item.index] = item.text; + }); + } finally { + setIsTranslating(false); + } + + return results; + }, [language]); + + return { translate, translateBatch, isTranslating, language }; +} From ea0c4ac5a61f876b32588588fa89ef050ba561b2 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 26 Nov 2025 21:33:35 -0300 Subject: [PATCH 05/49] feat: Simplificar sistema de traducao com LibreTranslate - Remover traducoes manuais do LanguageContext - Adicionar componente T para auto-traducao - Usar useTranslatedContent para conteudo do banco - Atualizar todas as paginas publicas - Integrar LibreTranslate para traducao automatica --- frontend/src/app/(public)/contato/page.tsx | 62 +-- frontend/src/app/(public)/page.tsx | 104 ++-- frontend/src/app/(public)/projetos/page.tsx | 48 +- frontend/src/app/(public)/servicos/page.tsx | 32 +- frontend/src/app/(public)/sobre/page.tsx | 32 +- frontend/src/components/Footer.tsx | 35 +- frontend/src/components/Header.tsx | 29 +- frontend/src/components/TranslatedText.tsx | 131 +++-- frontend/src/contexts/LanguageContext.tsx | 543 ++------------------ 9 files changed, 313 insertions(+), 703 deletions(-) diff --git a/frontend/src/app/(public)/contato/page.tsx b/frontend/src/app/(public)/contato/page.tsx index 0d56d5b..f2f52ba 100644 --- a/frontend/src/app/(public)/contato/page.tsx +++ b/frontend/src/app/(public)/contato/page.tsx @@ -1,8 +1,8 @@ "use client"; -import { useLanguage } from "@/contexts/LanguageContext"; import { useToast } from "@/contexts/ToastContext"; import { useState, useEffect } from "react"; +import { useTranslatedContent, T } from "@/components/TranslatedText"; interface ContactInfo { icon: string; @@ -27,7 +27,6 @@ interface ContactContent { } export default function ContatoPage() { - const { t } = useLanguage(); const { success, error: showError } = useToast(); const [content, setContent] = useState(null); const [loading, setLoading] = useState(true); @@ -40,6 +39,9 @@ export default function ContatoPage() { message: '' }); + // Traduzir conteúdo do banco automaticamente + const { translatedContent } = useTranslatedContent(content); + useEffect(() => { fetchContent(); }, []); @@ -91,34 +93,34 @@ export default function ContatoPage() { }; // Valores padrão caso não tenha conteúdo salvo - const hero = content?.hero || { - pretitle: t('contact.info.pretitle'), - title: t('contact.hero.title'), - subtitle: t('contact.hero.subtitle') + const hero = translatedContent?.hero || { + pretitle: 'Fale Conosco', + title: 'Entre em Contato', + subtitle: 'Estamos prontos para atender sua empresa com soluções de engenharia de alta qualidade' }; - const info = content?.info || { - title: t('contact.info.title'), - subtitle: t('contact.info.subtitle'), + const info = translatedContent?.info || { + title: 'Informações', + subtitle: 'Como nos encontrar', description: 'Estamos à disposição para atender sua empresa com a excelência técnica que seu projeto exige.', items: [ { icon: 'ri-whatsapp-line', - title: t('contact.info.phone.title'), - description: t('contact.info.whatsapp.desc'), + title: 'Telefone', + description: 'Atendimento de segunda a sexta, das 8h às 18h', link: 'https://wa.me/5527999999999', linkText: '(27) 99999-9999' }, { icon: 'ri-mail-send-line', - title: t('contact.info.email.title'), - description: t('contact.info.email.desc'), + title: 'E-mail', + description: 'Responderemos em até 24 horas úteis', link: 'mailto:contato@octto.com.br', linkText: 'contato@octto.com.br' }, { icon: 'ri-map-pin-line', - title: t('contact.info.address.title'), + title: 'Endereço', description: 'Av. Nossa Senhora da Penha, 1234\nSanta Lúcia, Vitória - ES\nCEP: 29056-000', link: 'https://maps.google.com', linkText: 'Ver no mapa' @@ -187,12 +189,12 @@ export default function ContatoPage() {

-

{t('contact.form.title')}

+

Envie uma Mensagem

- +
setFormData({...formData, name: e.target.value})} className="w-full pl-11 pr-4 py-3.5 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" - placeholder={t('contact.form.name.placeholder')} + placeholder="Seu nome completo" />
- +
- +
setFormData({...formData, email: e.target.value})} className="w-full pl-11 pr-4 py-3.5 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" - placeholder={t('contact.form.email.placeholder')} + placeholder="seu@email.com" />
- +
- +
@@ -281,11 +283,11 @@ export default function ContatoPage() { {submitting ? ( <> - Enviando... + Enviando... ) : ( <> - {t('contact.form.submit')} + Enviar Mensagem )} diff --git a/frontend/src/app/(public)/page.tsx b/frontend/src/app/(public)/page.tsx index 4b87358..dfcf423 100644 --- a/frontend/src/app/(public)/page.tsx +++ b/frontend/src/app/(public)/page.tsx @@ -1,72 +1,74 @@ "use client"; import Link from "next/link"; -import { useLanguage } from "@/contexts/LanguageContext"; import { usePageContent } from "@/hooks/usePageContent"; +import { useTranslatedContent, T } from "@/components/TranslatedText"; export default function Home() { - const { t } = useLanguage(); const { content, loading } = usePageContent('home'); + + // Traduzir conteúdo do banco automaticamente + const { translatedContent } = useTranslatedContent(content); - // Usar conteúdo personalizado do banco ou fallback para traduções - const hero = content?.hero || { - title: t('home.hero.title'), - subtitle: t('home.hero.subtitle'), - buttonText: t('home.hero.cta_primary') + // Usar conteúdo traduzido ou fallback + const hero = translatedContent?.hero || { + title: 'Engenharia de Excelência para Seus Projetos', + subtitle: 'Soluções completas em engenharia veicular, mecânica e segurança do trabalho com mais de 15 anos de experiência.', + buttonText: 'Conheça Nossos Serviços' }; - const features = content?.features || { - pretitle: t('home.features.pretitle'), - title: t('home.features.title'), + const features = translatedContent?.features || { + pretitle: 'Por que nos escolher', + title: 'Nossos Diferenciais', items: [ - { icon: 'ri-shield-star-line', title: t('home.features.1.title'), description: t('home.features.1.desc') }, - { icon: 'ri-settings-4-line', title: t('home.features.2.title'), description: t('home.features.2.desc') }, - { icon: 'ri-truck-line', title: t('home.features.3.title'), description: t('home.features.3.desc') } + { icon: 'ri-shield-star-line', title: 'Qualidade Garantida', description: 'Processos certificados e equipe altamente qualificada.' }, + { icon: 'ri-settings-4-line', title: 'Soluções Personalizadas', description: 'Atendimento sob medida para suas necessidades.' }, + { icon: 'ri-truck-line', title: 'Especialização Veicular', description: 'Expertise em engenharia automotiva.' } ] as Array<{ icon: string; title: string; description: string }> }; - const services = content?.services || { - pretitle: t('home.services.pretitle'), - title: t('home.services.title'), + const services = translatedContent?.services || { + pretitle: 'Nossos Serviços', + title: 'O Que Fazemos', items: [ - { icon: 'ri-draft-line', title: t('home.services.1.title'), description: t('home.services.1.desc') }, - { icon: 'ri-file-paper-2-line', title: t('home.services.2.title'), description: t('home.services.2.desc') }, - { icon: 'ri-alert-line', title: t('home.services.3.title'), description: t('home.services.3.desc') }, - { icon: 'ri-truck-fill', title: t('home.services.4.title'), description: t('home.services.4.desc') } + { icon: 'ri-draft-line', title: 'Projetos Técnicos', description: 'Desenvolvimento de projetos de engenharia.' }, + { icon: 'ri-file-paper-2-line', title: 'Laudos e Perícias', description: 'Emissão de laudos técnicos.' }, + { icon: 'ri-alert-line', title: 'Segurança do Trabalho', description: 'Implementação de normas de segurança.' }, + { icon: 'ri-truck-fill', title: 'Engenharia Veicular', description: 'Modificações e adaptações de veículos.' } ] as Array<{ icon: string; title: string; description: string }> }; - const about = content?.about || { - pretitle: t('home.about.pretitle'), - title: t('home.about.title'), - description: t('home.about.desc'), + const about = translatedContent?.about || { + pretitle: 'Conheça a OCCTO', + title: 'Sobre Nós', + description: 'Com mais de 15 anos de experiência, a OCCTO Engenharia se consolidou como referência em soluções de engenharia.', highlights: [ - t('home.about.list.1'), - t('home.about.list.2'), - t('home.about.list.3') + 'Mais de 500 clientes atendidos', + 'Equipe técnica qualificada', + 'Parceiro oficial de grandes empresas' ] as string[] }; - const testimonials = content?.testimonials || { - pretitle: t('home.testimonials.pretitle'), - title: t('home.testimonials.title'), + const testimonials = translatedContent?.testimonials || { + pretitle: 'Depoimentos', + title: 'O Que Dizem Nossos Clientes', items: [ - { name: 'Ricardo Mendes', role: t('home.testimonials.1.role'), text: t('home.testimonials.1.text') }, - { name: 'Fernanda Costa', role: t('home.testimonials.2.role'), text: t('home.testimonials.2.text') }, - { name: 'Paulo Oliveira', role: t('home.testimonials.3.role'), text: t('home.testimonials.3.text') } + { name: 'Ricardo Mendes', role: 'Gerente de Frota', text: 'Excelente trabalho!' }, + { name: 'Fernanda Costa', role: 'Diretora de Operações', text: 'Parceria de confiança.' }, + { name: 'Paulo Oliveira', role: 'Engenheiro Chefe', text: 'Conhecimento técnico incomparável.' } ] as Array<{ name: string; role: string; text: string }> }; - const stats = content?.stats || { + const stats = translatedContent?.stats || { clients: '500+', projects: '1200+', years: '15' }; - const cta = content?.cta || { - title: t('home.cta.title'), - text: t('home.cta.desc'), - button: t('home.cta.button') + const cta = translatedContent?.cta || { + title: 'Pronto para tirar seu projeto do papel?', + text: 'Entre em contato com nossa equipe de especialistas.', + button: 'Fale Conosco' }; return ( @@ -81,7 +83,7 @@ export default function Home() {
- {t('home.hero.badge')} Coca-Cola + Prestador de Serviço Oficial Coca-Cola

@@ -95,7 +97,7 @@ export default function Home() { {hero.buttonText} - {t('home.hero.cta_secondary')} + Ver Soluções

@@ -143,7 +145,7 @@ export default function Home() {
- {t('home.services.link')} + Ver todos os serviços
@@ -173,7 +175,7 @@ export default function Home() { ))} - {t('home.about.link')} + Conheça nossa expertise
@@ -184,29 +186,29 @@ export default function Home() {
-

{t('home.projects.pretitle')}

-

{t('home.projects.title')}

+

Portfólio

+

Projetos Recentes

- {t('home.projects.link')} + Ver todos os projetos
{[ - { img: "https://images.unsplash.com/photo-1616401784845-180882ba9ba8?q=80&w=2070&auto=format&fit=crop", title: t('home.projects.1.title'), cat: t('home.projects.1.cat') }, - { img: "https://images.unsplash.com/photo-1581092335397-9583eb92d232?q=80&w=2070&auto=format&fit=crop", title: t('home.projects.2.title'), cat: t('home.projects.2.cat') }, - { img: "https://images.unsplash.com/photo-1504917595217-d4dc5ebe6122?q=80&w=2070&auto=format&fit=crop", title: t('home.projects.3.title'), cat: t('home.projects.3.cat') } + { img: "https://images.unsplash.com/photo-1616401784845-180882ba9ba8?q=80&w=2070&auto=format&fit=crop", title: "Projeto de Adequação - Coca-Cola", cat: "Engenharia Veicular" }, + { img: "https://images.unsplash.com/photo-1581092335397-9583eb92d232?q=80&w=2070&auto=format&fit=crop", title: "Laudo de Guindaste Articulado", cat: "Inspeção Técnica" }, + { img: "https://images.unsplash.com/photo-1504917595217-d4dc5ebe6122?q=80&w=2070&auto=format&fit=crop", title: "Dispositivo de Içamento Especial", cat: "Projeto Mecânico" } ].map((project, index) => (
- {project.cat} -

{project.title}

+ {project.cat} +

{project.title}

- {t('home.projects.view_details')} + Ver detalhes
diff --git a/frontend/src/app/(public)/projetos/page.tsx b/frontend/src/app/(public)/projetos/page.tsx index 9f82d40..daeb914 100644 --- a/frontend/src/app/(public)/projetos/page.tsx +++ b/frontend/src/app/(public)/projetos/page.tsx @@ -1,57 +1,55 @@ "use client"; import Link from "next/link"; -import { useLanguage } from "@/contexts/LanguageContext"; +import { T } from "@/components/TranslatedText"; export default function ProjetosPage() { - const { t } = useLanguage(); - // Placeholder data - will be replaced by database content const projects = [ { id: 1, - title: t('home.projects.1.title'), - category: t('home.projects.1.cat'), + title: "Adequação de Frota de Caminhões", + category: "Engenharia Veicular", location: "Vitória, ES", image: "https://images.unsplash.com/photo-1616401784845-180882ba9ba8?q=80&w=2070&auto=format&fit=crop", description: "Projeto de adequação técnica de 50 caminhões para instalação de carrocerias especiais e sistemas de segurança." }, { id: 2, - title: t('home.projects.2.title'), - category: t('home.projects.2.cat'), + title: "Laudo Técnico de Guindaste Industrial", + category: "Laudos e Perícias", location: "Serra, ES", image: "https://images.unsplash.com/photo-1535082623926-b3a33d531740?q=80&w=2052&auto=format&fit=crop", description: "Inspeção completa e emissão de laudo técnico para guindaste de 45 toneladas, com testes de carga e verificação estrutural." }, { id: 3, - title: t('home.projects.3.title'), - category: t('home.projects.3.cat'), + title: "Projeto de Equipamento Portuário", + category: "Projetos Mecânicos", location: "Aracruz, ES", image: "https://images.unsplash.com/photo-1504917595217-d4dc5ebe6122?q=80&w=2070&auto=format&fit=crop", description: "Desenvolvimento e cálculo estrutural de Spreader para movimentação de contêineres em área portuária." }, { id: 4, - title: t('home.projects.4.title'), - category: t('home.projects.4.cat'), + title: "Adequação NR-12 de Linha de Produção", + category: "Segurança do Trabalho", location: "Linhares, ES", image: "https://images.unsplash.com/photo-1581092921461-eab62e97a782?q=80&w=2070&auto=format&fit=crop", description: "Inventário e adequação de segurança de 120 máquinas operatrizes conforme norma regulamentadora NR-12." }, { id: 5, - title: t('home.projects.5.title'), - category: t('home.projects.5.cat'), + title: "Homologação de Veículos Especiais", + category: "Engenharia Veicular", location: "Viana, ES", image: "https://images.unsplash.com/photo-1591768793355-74d04bb6608f?q=80&w=2070&auto=format&fit=crop", description: "Processo completo de homologação e certificação de plataformas elevatórias para distribuição urbana." }, { id: 6, - title: t('home.projects.6.title'), - category: t('home.projects.6.cat'), + title: "Sistema de Proteção Contra Quedas", + category: "Segurança do Trabalho", location: "Cariacica, ES", image: "https://images.unsplash.com/photo-1504328345606-18bbc8c9d7d1?q=80&w=2070&auto=format&fit=crop", description: "Projeto e instalação de sistema de linha de vida para proteção contra quedas em operações de carga e descarga." @@ -65,9 +63,9 @@ export default function ProjetosPage() {
-

{t('projects.hero.title')}

+

Nossos Projetos

- {t('projects.hero.subtitle')} + Conheça alguns dos projetos que já realizamos para nossos clientes

@@ -77,10 +75,10 @@ export default function ProjetosPage() {
{/* Filters (Placeholder) */}
- - - - + + + +
@@ -90,20 +88,20 @@ export default function ProjetosPage() {
- {project.category} + {project.category}
-

{project.title}

+

{project.title}

{project.location}

- {project.description} + {project.description}

- {t('projects.card.details')} + Ver Detalhes
diff --git a/frontend/src/app/(public)/servicos/page.tsx b/frontend/src/app/(public)/servicos/page.tsx index 7ebd1c8..6019714 100644 --- a/frontend/src/app/(public)/servicos/page.tsx +++ b/frontend/src/app/(public)/servicos/page.tsx @@ -1,28 +1,26 @@ "use client"; import Link from "next/link"; -import { useLanguage } from "@/contexts/LanguageContext"; +import { T } from "@/components/TranslatedText"; export default function ServicosPage() { - const { t } = useLanguage(); - const services = [ { icon: "ri-draft-line", - title: t('home.services.1.title'), - description: t('home.services.1.desc'), + title: "Projetos Técnicos", + description: "Desenvolvimento de projetos de engenharia mecânica, estrutural e veicular com alta precisão e conformidade normativa.", features: ["Projeto Mecânico 3D", "Cálculo Estrutural", "Dispositivos Especiais", "Homologação de Equipamentos"] }, { icon: "ri-truck-line", - title: t('home.features.3.title'), - description: t('home.features.3.desc'), + title: "Engenharia Veicular", + description: "Expertise em modificações, adaptações e homologações veiculares com foco em segurança e conformidade.", features: ["Projeto de Instalação", "Estudo de Estabilidade", "Adequação de Carrocerias", "Regularização Veicular"] }, { icon: "ri-file-paper-2-line", - title: t('home.services.2.title'), - description: t('home.services.2.desc'), + title: "Laudos e Perícias", + description: "Emissão de laudos técnicos e pareceres periciais para equipamentos, estruturas e veículos.", features: ["Laudos de Munck/Guindaste", "Inspeção de Segurança", "Teste de Carga", "Certificação de Equipamentos"] }, { @@ -40,9 +38,9 @@ export default function ServicosPage() {
-

{t('services.hero.title')}

+

Nossos Serviços

- {t('services.hero.subtitle')} + Soluções completas em engenharia para atender às necessidades da sua empresa

@@ -65,22 +63,22 @@ export default function ServicosPage() { 0{index + 1}
-

{service.title}

+

{service.title}

- {service.description} + {service.description}

- {t('services.scope')} + Escopo de Atuação

    {service.features.map((feature, idx) => (
  • - {feature} + {feature}
  • ))}
@@ -94,9 +92,9 @@ export default function ServicosPage() { {/* CTA */}
-

{t('services.cta.title')}

+

Precisa de um serviço especializado?

- {t('services.cta.button')} + Solicite um Orçamento
diff --git a/frontend/src/app/(public)/sobre/page.tsx b/frontend/src/app/(public)/sobre/page.tsx index 1ef2045..7e98961 100644 --- a/frontend/src/app/(public)/sobre/page.tsx +++ b/frontend/src/app/(public)/sobre/page.tsx @@ -1,11 +1,9 @@ "use client"; import Image from "next/image"; -import { useLanguage } from "@/contexts/LanguageContext"; +import { T } from "@/components/TranslatedText"; export default function SobrePage() { - const { t } = useLanguage(); - return (
{/* Hero Section */} @@ -13,9 +11,9 @@ export default function SobrePage() {
-

{t('about.hero.title')}

+

Sobre a OCCTO

- {t('about.hero.subtitle')} + Conheça nossa história, missão e valores que nos guiam na entrega de excelência em engenharia

@@ -25,13 +23,13 @@ export default function SobrePage() {
-

{t('about.history.title')}

-

{t('about.history.subtitle')}

+

Nossa História

+

Mais de 15 anos de experiência em engenharia

- {t('about.history.p1')} + A OCCTO Engenharia foi fundada com o objetivo de oferecer soluções completas em engenharia mecânica, veicular e segurança do trabalho. Ao longo de mais de 15 anos, construímos uma trajetória sólida baseada na excelência técnica e no compromisso com a satisfação dos nossos clientes.

- {t('about.history.p2')} + Nossa equipe é formada por engenheiros altamente qualificados e especializados, que trabalham com as mais modernas ferramentas e metodologias para garantir resultados precisos e confiáveis em cada projeto.

@@ -50,30 +48,30 @@ export default function SobrePage() {
-

{t('about.values.title')}

-

{t('about.values.subtitle')}

+

Nossos Valores

+

O que nos move

-

{t('about.values.quality.title')}

-

{t('about.values.quality.desc')}

+

Qualidade

+

Comprometimento com a excelência em cada projeto, garantindo precisão e conformidade em todas as entregas.

-

{t('about.values.transparency.title')}

-

{t('about.values.transparency.desc')}

+

Transparência

+

Relações baseadas na honestidade e comunicação clara, mantendo nossos clientes sempre informados.

-

{t('about.values.sustainability.title')}

-

{t('about.values.sustainability.desc')}

+

Sustentabilidade

+

Compromisso com práticas responsáveis e soluções que minimizam impactos ambientais.

diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx index d82f151..d5cbb0a 100644 --- a/frontend/src/components/Footer.tsx +++ b/frontend/src/components/Footer.tsx @@ -2,6 +2,7 @@ import Link from 'next/link'; import { useLanguage } from '@/contexts/LanguageContext'; +import { T } from '@/components/TranslatedText'; export default function Footer() { const { t } = useLanguage(); @@ -20,12 +21,12 @@ export default function Footer() {

- Soluções em engenharia mecânica e segurança para movimentação de carga. + Soluções em engenharia mecânica e segurança para movimentação de carga.

- Prestador Oficial Coca-Cola + Prestador Oficial Coca-Cola
@@ -43,30 +44,30 @@ export default function Footer() { {/* Links */}
-

Links Rápidos

+

Links Rápidos

    -
  • {t('nav.home')}
  • -
  • {t('nav.about')}
  • -
  • {t('nav.services')}
  • -
  • {t('nav.projects')}
  • -
  • {t('nav.contact')}
  • +
  • {t('nav.home')}
  • +
  • {t('nav.about')}
  • +
  • {t('nav.services')}
  • +
  • {t('nav.projects')}
  • +
  • {t('nav.contact')}
{/* Services */}
-

{t('services.title')}

+

{t('services.title')}

    -
  • Projetos de Dispositivos
  • -
  • Engenharia de Implementos
  • -
  • Inspeção de Equipamentos
  • -
  • Laudos Técnicos (NR-11/12)
  • +
  • Projetos de Dispositivos
  • +
  • Engenharia de Implementos
  • +
  • Inspeção de Equipamentos
  • +
  • Laudos Técnicos (NR-11/12)
{/* Contact */}
-

{t('nav.contact')}

+

{t('nav.contact')}

  • @@ -86,11 +87,11 @@ export default function Footer() {

    - © {new Date().getFullYear()} OCCTO Engenharia. {t('footer.rights')} + © {new Date().getFullYear()} OCCTO Engenharia. {t('footer.rights')}

    - Política de Privacidade - Termos de Uso + Política de Privacidade + Termos de Uso
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index c8fbeb1..0724513 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -4,6 +4,7 @@ import Link from 'next/link'; import { useState, useEffect } from 'react'; import { useTheme } from "next-themes"; import { useLanguage } from '@/contexts/LanguageContext'; +import { T } from '@/components/TranslatedText'; export default function Header() { const [isSearchOpen, setIsSearchOpen] = useState(false); @@ -68,23 +69,23 @@ export default function Header() { @@ -94,7 +95,7 @@ export default function Header() { className="px-6 py-2.5 bg-primary text-white rounded-lg font-bold hover-primary transition-colors flex items-center gap-2" > - {t('nav.contact_us')} + {t('nav.contact_us')}
@@ -168,23 +169,23 @@ export default function Header() { @@ -195,14 +196,14 @@ export default function Header() { className="w-full py-4 bg-primary text-white rounded-xl font-bold text-center flex items-center justify-center gap-2 shadow-lg shadow-primary/20" > - {t('nav.contact_us')} + {t('nav.contact_us')}
- {t('nav.theme')} + {t('nav.theme')}
- {t('nav.language')} + {t('nav.language')}
diff --git a/frontend/src/components/TranslatedText.tsx b/frontend/src/components/TranslatedText.tsx index 9a7cd32..31b4fbb 100644 --- a/frontend/src/components/TranslatedText.tsx +++ b/frontend/src/components/TranslatedText.tsx @@ -1,71 +1,137 @@ 'use client'; -import { useEffect, useState, ElementType, ComponentPropsWithoutRef } from 'react'; -import { useTranslate } from '@/hooks/useTranslate'; +import { useEffect, useState, ReactNode } from 'react'; +import { useLanguage, Language } from '@/contexts/LanguageContext'; -interface TranslatedTextProps { - text: string; - as?: 'span' | 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div' | 'li'; +// Cache global de traduções +const translationCache = new Map(); + +// Função para traduzir texto via API +async function translateText(text: string, targetLang: string): Promise { + if (!text || text.trim() === '') return text; + + const cacheKey = `pt:${targetLang}:${text}`; + + if (translationCache.has(cacheKey)) { + return translationCache.get(cacheKey)!; + } + + try { + const response = await fetch('/api/translate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text, source: 'pt', target: targetLang }), + }); + + if (response.ok) { + const data = await response.json(); + const translated = data.translatedText || text; + translationCache.set(cacheKey, translated); + return translated; + } + } catch (error) { + console.error('Translation error:', error); + } + + return text; +} + +interface AutoTranslateProps { + children: string; + as?: 'span' | 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div' | 'li' | 'label'; className?: string; } /** * Componente que traduz texto automaticamente via LibreTranslate - * quando o idioma não é português + * Uso: Texto em português */ -export function TranslatedText({ text, as = 'span', className }: TranslatedTextProps) { - const { translate, language } = useTranslate(); - const [translatedText, setTranslatedText] = useState(text); - const [isLoading, setIsLoading] = useState(false); +export function T({ children, as = 'span', className }: AutoTranslateProps) { + const { language } = useLanguage(); + const [translatedText, setTranslatedText] = useState(children); useEffect(() => { if (language === 'PT') { - setTranslatedText(text); + setTranslatedText(children); return; } let cancelled = false; - setIsLoading(true); + const targetLang = language.toLowerCase(); - translate(text).then((result) => { + translateText(children, targetLang).then((result) => { if (!cancelled) { setTranslatedText(result); - setIsLoading(false); } }); return () => { cancelled = true; }; - }, [text, language, translate]); + }, [children, language]); const Tag = as; + return {translatedText}; +} - return ( - - {translatedText} - - ); +// Alias para uso mais curto +export const AutoTranslate = T; + +/** + * Hook para traduzir texto programaticamente + */ +export function useTranslate() { + const { language } = useLanguage(); + const [isTranslating, setIsTranslating] = useState(false); + + const translate = async (text: string): Promise => { + if (!text || language === 'PT') return text; + + setIsTranslating(true); + try { + const result = await translateText(text, language.toLowerCase()); + return result; + } finally { + setIsTranslating(false); + } + }; + + const translateBatch = async (texts: string[]): Promise => { + if (language === 'PT') return texts; + + setIsTranslating(true); + try { + const results = await Promise.all( + texts.map(text => translateText(text, language.toLowerCase())) + ); + return results; + } finally { + setIsTranslating(false); + } + }; + + return { translate, translateBatch, isTranslating, language }; } /** - * Hook para traduzir objetos de conteúdo do banco de dados + * Hook para traduzir conteúdo do banco de dados */ -export function useTranslatedContent>(content: T): { - translatedContent: T; - isTranslating: boolean +export function useTranslatedContent>(content: T | null): { + translatedContent: T | null; + isTranslating: boolean; } { - const { translateBatch, language } = useTranslate(); - const [translatedContent, setTranslatedContent] = useState(content); + const { language } = useLanguage(); + const [translatedContent, setTranslatedContent] = useState(content); const [isTranslating, setIsTranslating] = useState(false); useEffect(() => { - if (language === 'PT') { + if (!content || language === 'PT') { setTranslatedContent(content); return; } let cancelled = false; + const targetLang = language.toLowerCase(); const translateContent = async () => { setIsTranslating(true); @@ -75,13 +141,15 @@ export function useTranslatedContent>(content: const paths: string[] = []; const extractTexts = (obj: unknown, path: string = '') => { - if (typeof obj === 'string' && obj.length > 0) { + if (typeof obj === 'string' && obj.length > 0 && obj.length < 5000) { texts.push(obj); paths.push(path); } else if (Array.isArray(obj)) { obj.forEach((item, index) => extractTexts(item, `${path}[${index}]`)); } else if (obj && typeof obj === 'object') { Object.entries(obj).forEach(([key, value]) => { + // Ignorar campos que não devem ser traduzidos + if (['icon', 'image', 'img', 'url', 'href', 'id', 'slug'].includes(key)) return; extractTexts(value, path ? `${path}.${key}` : key); }); } @@ -95,7 +163,10 @@ export function useTranslatedContent>(content: } try { - const translations = await translateBatch(texts); + // Traduzir todos os textos + const translations = await Promise.all( + texts.map(text => translateText(text, targetLang)) + ); if (cancelled) return; @@ -128,7 +199,7 @@ export function useTranslatedContent>(content: return () => { cancelled = true; }; - }, [content, language, translateBatch]); + }, [content, language]); return { translatedContent, isTranslating }; } diff --git a/frontend/src/contexts/LanguageContext.tsx b/frontend/src/contexts/LanguageContext.tsx index 773f606..a4d5863 100644 --- a/frontend/src/contexts/LanguageContext.tsx +++ b/frontend/src/contexts/LanguageContext.tsx @@ -1,526 +1,65 @@ "use client"; -import React, { createContext, useContext, useState, useEffect } from 'react'; +import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; -type Language = 'PT' | 'EN' | 'ES'; +export type Language = 'PT' | 'EN' | 'ES'; interface LanguageContextType { language: Language; setLanguage: (lang: Language) => void; - t: (key: string) => string; - tDynamic: (content: { PT: string, EN?: string, ES?: string }) => string; + t: (text: string) => string; // Retorna texto em PT (será traduzido pelo componente) } const LanguageContext = createContext(undefined); -export const translations = { - PT: { - 'nav.home': 'Início', - 'nav.services': 'Serviços', - 'nav.projects': 'Projetos', - 'nav.contact': 'Contato', - 'nav.about': 'Sobre', - 'nav.search': 'Buscar...', - 'nav.contact_us': 'Fale Conosco', - 'nav.theme': 'Tema', - 'nav.language': 'Idioma', - 'footer.rights': 'Todos os direitos reservados.', +// Textos fixos do sistema em português (serão traduzidos automaticamente pelo componente T) +const systemTexts: Record = { + // Navegação + 'nav.home': 'Início', + 'nav.services': 'Serviços', + 'nav.projects': 'Projetos', + 'nav.contact': 'Contato', + 'nav.about': 'Sobre', + 'nav.search': 'Buscar...', + 'nav.contact_us': 'Fale Conosco', + 'nav.theme': 'Tema', + 'nav.language': 'Idioma', + + // Footer + 'footer.rights': 'Todos os direitos reservados.', + 'services.title': 'Serviços', - // Home - Hero - 'home.hero.badge': 'Prestador de Serviço Oficial', - 'home.hero.title': 'Engenharia de', - 'home.hero.title_highlight': 'Dispositivos de Içamento', - 'home.hero.subtitle': 'Desenvolvemos projetos, laudos e soluções técnicas para equipamentos de movimentação de carga. Segurança e conformidade normativa para sua operação.', - 'home.hero.cta_primary': 'Falar com Engenheiro', - 'home.hero.cta_secondary': 'Ver Soluções', - - // Home - Features - 'home.features.pretitle': 'Diferenciais', - 'home.features.title': 'Segurança e Eficiência', - 'home.features.1.title': 'Normas Técnicas', - 'home.features.1.desc': 'Projetos e adequações rigorosamente alinhados com as normas NR-12, NR-11 e resoluções do CONTRAN.', - 'home.features.2.title': 'Engenharia Mecânica', - 'home.features.2.desc': 'Desenvolvimento de dispositivos de içamento e soluções personalizadas para otimizar sua logística.', - 'home.features.3.title': 'Projetos de Implementos', - 'home.features.3.desc': 'Engenharia especializada para instalação e adequação de Muncks, plataformas e dispositivos em veículos de carga.', - - // Home - Services - 'home.services.pretitle': 'O que fazemos', - 'home.services.title': 'Soluções Especializadas', - 'home.services.1.title': 'Projetos Mecânicos', - 'home.services.1.desc': 'Desenvolvimento de dispositivos de içamento (Spreaders, Balancins).', - 'home.services.2.title': 'Laudos Técnicos', - 'home.services.2.desc': 'Inspeção e certificação de equipamentos de carga conforme normas.', - 'home.services.3.title': 'Adequação NR-12', - 'home.services.3.desc': 'Projetos de segurança para máquinas e equipamentos.', - 'home.services.4.title': 'Engenharia Veicular', - 'home.services.4.desc': 'Projetos para instalação de equipamentos em caminhões.', - 'home.services.link': 'Ver todos os serviços', - - // Home - About - 'home.about.pretitle': 'Sobre Nós', - 'home.about.title': 'Engenharia que garante segurança', - 'home.about.desc': 'A Octto Engenharia é parceira técnica de grandes empresas logísticas. Não operamos frotas, nós garantimos que os equipamentos que movem sua carga sejam seguros, eficientes e estejam dentro das normas.', - 'home.about.list.1': 'Projetos de Dispositivos de Içamento', - 'home.about.list.2': 'Laudos Técnicos para Muncks e Guindastes', - 'home.about.list.3': 'Responsabilidade Técnica (ART) garantida', - 'home.about.link': 'Conheça nossa expertise', - - // Home - Projects - 'home.projects.pretitle': 'Portfólio', - 'home.projects.title': 'Projetos Recentes', - 'home.projects.link': 'Ver todos os projetos', - 'home.projects.1.cat': 'Engenharia Veicular', - 'home.projects.1.title': 'Projeto de Adequação - Coca-Cola', - 'home.projects.2.cat': 'Inspeção Técnica', - 'home.projects.2.title': 'Laudo de Guindaste Articulado', - 'home.projects.3.cat': 'Projeto Mecânico', - 'home.projects.3.title': 'Dispositivo de Içamento Especial', - 'home.projects.4.cat': 'Laudos', - 'home.projects.4.title': 'Certificação NR-12 - Parque Industrial', - 'home.projects.5.cat': 'Engenharia Veicular', - 'home.projects.5.title': 'Homologação de Plataforma Elevatória', - 'home.projects.6.cat': 'Segurança do Trabalho', - 'home.projects.6.title': 'Projeto de Linha de Vida para Caminhões', - 'home.projects.view_details': 'Ver detalhes', - - // Home - Testimonials - 'home.testimonials.pretitle': 'Depoimentos', - 'home.testimonials.title': 'Parceiros que confiam', - 'home.testimonials.1.text': 'A Octto realizou a adequação de toda nossa frota de caminhões com excelência técnica e rapidez.', - 'home.testimonials.1.role': 'Gerente de Frota, Distribuidora Bebidas', - 'home.testimonials.2.text': 'Os laudos técnicos emitidos pela Octto nos deram total segurança jurídica e operacional.', - 'home.testimonials.2.role': 'Diretora Operacional, Logística Express', - 'home.testimonials.3.text': 'O projeto do dispositivo de içamento resolveu um gargalo antigo da nossa produção. Recomendo.', - 'home.testimonials.3.role': 'Engenheiro Chefe, Indústria Metalúrgica', - - // Home - CTA - 'home.cta.title': 'Pronto para iniciar seu projeto?', - 'home.cta.desc': 'Entre em contato conosco hoje mesmo e descubra como podemos ajudar a transformar sua visão em realidade.', - 'home.cta.button': 'Falar com um Especialista', - - // Services Page - 'services.hero.title': 'Nossos Serviços', - 'services.hero.subtitle': 'Soluções completas em engenharia mecânica e movimentação de carga.', - 'services.cta.title': 'Precisa de uma solução personalizada?', - 'services.cta.button': 'Falar com um Engenheiro', - 'services.scope': 'Escopo do Serviço', - 'services.title': 'Serviços', - - // Projects Page - 'projects.hero.title': 'Nossos Projetos', - 'projects.hero.subtitle': 'Explore nosso portfólio de soluções em movimentação de carga e engenharia mecânica.', - 'projects.filter.all': 'Todos', - 'projects.filter.implements': 'Implementos', - 'projects.filter.mechanical': 'Projetos Mecânicos', - 'projects.filter.reports': 'Laudos', - 'projects.card.details': 'Ver detalhes', - - // About Page - 'about.hero.title': 'Sobre a Octto', - 'about.hero.subtitle': 'Conheça nossa trajetória, valores e o compromisso com a excelência na engenharia.', - 'about.history.pretitle': 'Nossa História', - 'about.history.title': 'Nossa História', - 'about.history.subtitle': 'Engenharia que impulsiona a logística', - 'about.history.p1': 'A Octto Engenharia nasceu da necessidade do mercado por soluções técnicas especializadas em movimentação de carga e implementos rodoviários. Identificamos que grandes frotas careciam de engenharia de ponta para garantir segurança e eficiência.', - 'about.history.p2': 'Hoje, somos parceiros estratégicos de grandes empresas de distribuição, como a Coca-Cola, desenvolvendo projetos de adequação, manutenção e certificação de equipamentos que são vitais para a cadeia logística nacional.', - 'about.values.pretitle': 'Nossos Pilares', - 'about.values.title': 'Nossos Pilares', - 'about.values.subtitle': 'Valores que nos guiam', - 'about.values.1.title': 'Excelência Técnica', - 'about.values.1.desc': 'Busca incessante pela perfeição em cada detalhe construtivo e de projeto.', - 'about.values.2.title': 'Transparência', - 'about.values.2.desc': 'Relacionamento claro e honesto com clientes, fornecedores e colaboradores.', - 'about.values.3.title': 'Sustentabilidade', - 'about.values.3.desc': 'Compromisso com práticas que respeitam o meio ambiente e a sociedade.', - 'about.values.quality.title': 'Excelência Técnica', - 'about.values.quality.desc': 'Busca incessante pela perfeição em cada detalhe construtivo e de projeto.', - 'about.values.transparency.title': 'Transparência', - 'about.values.transparency.desc': 'Relacionamento claro e honesto com clientes, fornecedores e colaboradores.', - 'about.values.sustainability.title': 'Sustentabilidade', - 'about.values.sustainability.desc': 'Compromisso com práticas que respeitam o meio ambiente e a sociedade.', - - // Contact Page - 'contact.hero.title': 'Contato', - 'contact.hero.subtitle': 'Estamos prontos para ouvir sobre o seu projeto. Entre em contato conosco.', - 'contact.info.pretitle': 'Fale Conosco', - 'contact.info.title': 'Canais de Atendimento', - 'contact.info.subtitle': 'Entre em contato pelos nossos canais oficiais', - 'contact.info.whatsapp.desc': 'Atendimento rápido e direto.', - 'contact.info.email.desc': 'Para orçamentos e dúvidas técnicas.', - 'contact.info.office.title': 'Escritório', - 'contact.info.phone.title': 'WhatsApp', - 'contact.info.email.title': 'E-mail', - 'contact.info.address.title': 'Escritório', - 'contact.form.title': 'Envie uma mensagem', - 'contact.form.name': 'Nome', - 'contact.form.name.placeholder': 'Seu nome', - 'contact.form.phone': 'Telefone', - 'contact.form.email': 'E-mail', - 'contact.form.email.placeholder': 'seu@email.com', - 'contact.form.subject': 'Assunto', - 'contact.form.message': 'Mensagem', - 'contact.form.message.placeholder': 'Como podemos ajudar?', - 'contact.form.submit': 'Enviar Mensagem', - 'contact.form.subject.select': 'Selecione um assunto', - 'contact.form.subject.quote': 'Solicitar Orçamento', - 'contact.form.subject.doubt': 'Dúvida Técnica', - 'contact.form.subject.partnership': 'Parceria', - 'contact.form.subject.other': 'Trabalhe Conosco', - - // Cookie Consent - 'cookie.text': 'Utilizamos cookies para melhorar sua experiência e analisar o tráfego do site. Ao continuar navegando, você concorda com nossa', - 'cookie.policy': 'Política de Privacidade', - 'cookie.accept': 'Aceitar', - 'cookie.decline': 'Recusar', - - // WhatsApp - 'whatsapp.label': 'Atendimento Rápido', - }, - EN: { - 'nav.home': 'Home', - 'nav.services': 'Services', - 'nav.projects': 'Projects', - 'nav.contact': 'Contact', - 'nav.about': 'About', - 'nav.search': 'Search...', - 'nav.contact_us': 'Contact Us', - 'nav.theme': 'Theme', - 'nav.language': 'Language', - 'footer.rights': 'All rights reserved.', - - // Home - Hero - 'home.hero.badge': 'Official Service Provider', - 'home.hero.title': 'Engineering of', - 'home.hero.title_highlight': 'Lifting Devices', - 'home.hero.subtitle': 'We develop projects, reports and technical solutions for load handling equipment. Safety and regulatory compliance for your operation.', - 'home.hero.cta_primary': 'Talk to an Engineer', - 'home.hero.cta_secondary': 'View Solutions', - - // Home - Features - 'home.features.pretitle': 'Differentials', - 'home.features.title': 'Safety and Efficiency', - 'home.features.1.title': 'Technical Standards', - 'home.features.1.desc': 'Projects and adaptations strictly aligned with NR-12, NR-11 standards and CONTRAN resolutions.', - 'home.features.2.title': 'Mechanical Engineering', - 'home.features.2.desc': 'Development of lifting devices and custom solutions to optimize your logistics.', - 'home.features.3.title': 'Implement Projects', - 'home.features.3.desc': 'Specialized engineering for installation and adaptation of Cranes, platforms and devices on cargo vehicles.', - - // Home - Services - 'home.services.pretitle': 'What we do', - 'home.services.title': 'Specialized Solutions', - 'home.services.1.title': 'Mechanical Projects', - 'home.services.1.desc': 'Development of lifting devices (Spreaders, Beams).', - 'home.services.2.title': 'Technical Reports', - 'home.services.2.desc': 'Inspection and certification of cargo equipment according to standards.', - 'home.services.3.title': 'NR-12 Adaptation', - 'home.services.3.desc': 'Safety projects for machinery and equipment.', - 'home.services.4.title': 'Vehicular Engineering', - 'home.services.4.desc': 'Projects for equipment installation on trucks.', - 'home.services.link': 'View all services', - - // Home - About - 'home.about.pretitle': 'About Us', - 'home.about.title': 'Engineering that ensures safety', - 'home.about.desc': 'Octto Engineering is a technical partner for major logistics companies. We do not operate fleets, we ensure that the equipment moving your cargo is safe, efficient and compliant.', - 'home.about.list.1': 'Lifting Device Projects', - 'home.about.list.2': 'Technical Reports for Cranes', - 'home.about.list.3': 'Technical Responsibility (ART) guaranteed', - 'home.about.link': 'Know our expertise', - - // Home - Projects - 'home.projects.pretitle': 'Portfolio', - 'home.projects.title': 'Recent Projects', - 'home.projects.link': 'View all projects', - 'home.projects.1.cat': 'Vehicular Engineering', - 'home.projects.1.title': 'Adaptation Project - Coca-Cola', - 'home.projects.2.cat': 'Technical Inspection', - 'home.projects.2.title': 'Articulated Crane Report', - 'home.projects.3.cat': 'Mechanical Project', - 'home.projects.3.title': 'Special Lifting Device', - 'home.projects.4.cat': 'Reports', - 'home.projects.4.title': 'NR-12 Certification - Industrial Park', - 'home.projects.5.cat': 'Vehicular Engineering', - 'home.projects.5.title': 'Lifting Platform Homologation', - 'home.projects.6.cat': 'Work Safety', - 'home.projects.6.title': 'Lifeline Project for Trucks', - 'home.projects.view_details': 'View details', - - // Home - Testimonials - 'home.testimonials.pretitle': 'Testimonials', - 'home.testimonials.title': 'Partners who trust', - 'home.testimonials.1.text': 'Octto performed the adaptation of our entire truck fleet with technical excellence and speed.', - 'home.testimonials.1.role': 'Fleet Manager, Beverage Distributor', - 'home.testimonials.2.text': 'The technical reports issued by Octto gave us total legal and operational security.', - 'home.testimonials.2.role': 'Operations Director, Logistics Express', - 'home.testimonials.3.text': 'The lifting device project solved an old bottleneck in our production. Highly recommend.', - 'home.testimonials.3.role': 'Chief Engineer, Metallurgical Industry', - - // Home - CTA - 'home.cta.title': 'Ready to start your project?', - 'home.cta.desc': 'Contact us today and discover how we can help transform your vision into reality.', - 'home.cta.button': 'Talk to a Specialist', - - // Services Page - 'services.hero.title': 'Our Services', - 'services.hero.subtitle': 'Complete solutions in mechanical engineering and load handling.', - 'services.cta.title': 'Need a custom solution?', - 'services.cta.button': 'Talk to an Engineer', - 'services.scope': 'Service Scope', - 'services.title': 'Services', - - // Projects Page - 'projects.hero.title': 'Our Projects', - 'projects.hero.subtitle': 'Explore our portfolio of solutions in load handling and mechanical engineering.', - 'projects.filter.all': 'All', - 'projects.filter.implements': 'Implements', - 'projects.filter.mechanical': 'Mechanical Projects', - 'projects.filter.reports': 'Reports', - 'projects.card.details': 'View details', - - // About Page - 'about.hero.title': 'About Octto', - 'about.hero.subtitle': 'Know our trajectory, values and commitment to engineering excellence.', - 'about.history.pretitle': 'Our History', - 'about.history.title': 'Our History', - 'about.history.subtitle': 'Engineering that drives logistics', - 'about.history.p1': 'Octto Engineering was born from the market need for specialized technical solutions in load handling and road implements. We identified that large fleets lacked cutting-edge engineering to ensure safety and efficiency.', - 'about.history.p2': 'Today, we are strategic partners of major distribution companies, such as Coca-Cola, developing adaptation, maintenance and equipment certification projects that are vital to the national logistics chain.', - 'about.values.pretitle': 'Our Pillars', - 'about.values.title': 'Our Pillars', - 'about.values.subtitle': 'Values that guide us', - 'about.values.1.title': 'Technical Excellence', - 'about.values.1.desc': 'Relentless pursuit of perfection in every constructive and design detail.', - 'about.values.2.title': 'Transparency', - 'about.values.2.desc': 'Clear and honest relationship with customers, suppliers and employees.', - 'about.values.3.title': 'Sustainability', - 'about.values.3.desc': 'Commitment to practices that respect the environment and society.', - 'about.values.quality.title': 'Technical Excellence', - 'about.values.quality.desc': 'Relentless pursuit of perfection in every constructive and design detail.', - 'about.values.transparency.title': 'Transparency', - 'about.values.transparency.desc': 'Clear and honest relationship with customers, suppliers and employees.', - 'about.values.sustainability.title': 'Sustainability', - 'about.values.sustainability.desc': 'Commitment to practices that respect the environment and society.', - - // Contact Page - 'contact.hero.title': 'Contact', - 'contact.hero.subtitle': 'We are ready to hear about your project. Contact us.', - 'contact.info.pretitle': 'Contact Us', - 'contact.info.title': 'Service Channels', - 'contact.info.subtitle': 'Contact us through our official channels', - 'contact.info.whatsapp.desc': 'Fast and direct service.', - 'contact.info.email.desc': 'For quotes and technical questions.', - 'contact.info.office.title': 'Office', - 'contact.info.phone.title': 'WhatsApp', - 'contact.info.email.title': 'E-mail', - 'contact.info.address.title': 'Office', - 'contact.form.title': 'Send a message', - 'contact.form.name': 'Name', - 'contact.form.name.placeholder': 'Your name', - 'contact.form.phone': 'Phone', - 'contact.form.email': 'E-mail', - 'contact.form.email.placeholder': 'your@email.com', - 'contact.form.subject': 'Subject', - 'contact.form.message': 'Message', - 'contact.form.message.placeholder': 'How can we help?', - 'contact.form.submit': 'Send Message', - 'contact.form.subject.select': 'Select a subject', - 'contact.form.subject.quote': 'Request Quote', - 'contact.form.subject.doubt': 'Technical Question', - 'contact.form.subject.partnership': 'Partnership', - 'contact.form.subject.other': 'Work with Us', - - // Cookie Consent - 'cookie.text': 'We use cookies to improve your experience and analyze site traffic. By continuing to browse, you agree to our', - 'cookie.policy': 'Privacy Policy', - 'cookie.accept': 'Accept', - 'cookie.decline': 'Decline', - - // WhatsApp - 'whatsapp.label': 'Quick Service', - }, - ES: { - 'nav.home': 'Inicio', - 'nav.services': 'Servicios', - 'nav.projects': 'Proyectos', - 'nav.contact': 'Contacto', - 'nav.about': 'Sobre', - 'nav.search': 'Buscar...', - 'nav.contact_us': 'Hable con Nosotros', - 'nav.theme': 'Tema', - 'nav.language': 'Idioma', - 'footer.rights': 'Todos los derechos reservados.', - - // Home - Hero - 'home.hero.badge': 'Proveedor de Servicio Oficial', - 'home.hero.title': 'Ingeniería de', - 'home.hero.title_highlight': 'Dispositivos de Elevación', - 'home.hero.subtitle': 'Desarrollamos proyectos, informes y soluciones técnicas para equipos de movimiento de carga. Seguridad y cumplimiento normativo para su operación.', - 'home.hero.cta_primary': 'Hablar con Ingeniero', - 'home.hero.cta_secondary': 'Ver Soluciones', - - // Home - Features - 'home.features.pretitle': 'Diferenciales', - 'home.features.title': 'Seguridad y Eficiencia', - 'home.features.1.title': 'Normas Técnicas', - 'home.features.1.desc': 'Proyectos y adecuaciones rigurosamente alineados con las normas NR-12, NR-11 y resoluciones del CONTRAN.', - 'home.features.2.title': 'Ingeniería Mecánica', - 'home.features.2.desc': 'Desarrollo de dispositivos de elevación y soluciones personalizadas para optimizar su logística.', - 'home.features.3.title': 'Proyectos de Implementos', - 'home.features.3.desc': 'Ingeniería especializada para instalación y adecuación de Grúas, plataformas y dispositivos en vehículos de carga.', - - // Home - Services - 'home.services.pretitle': 'Lo que hacemos', - 'home.services.title': 'Soluciones Especializadas', - 'home.services.1.title': 'Proyectos Mecánicos', - 'home.services.1.desc': 'Desarrollo de dispositivos de elevación (Spreaders, Balancines).', - 'home.services.2.title': 'Informes Técnicos', - 'home.services.2.desc': 'Inspección y certificación de equipos de carga conforme normas.', - 'home.services.3.title': 'Adecuación NR-12', - 'home.services.3.desc': 'Proyectos de seguridad para máquinas y equipos.', - 'home.services.4.title': 'Ingeniería Vehicular', - 'home.services.4.desc': 'Proyectos para instalación de equipos en camiones.', - 'home.services.link': 'Ver todos los servicios', - - // Home - About - 'home.about.pretitle': 'Sobre Nosotros', - 'home.about.title': 'Ingeniería que garantiza seguridad', - 'home.about.desc': 'Octto Ingeniería es socia técnica de grandes empresas logísticas. No operamos flotas, garantizamos que los equipos que mueven su carga sean seguros, eficientes y cumplan con las normas.', - 'home.about.list.1': 'Proyectos de Dispositivos de Elevación', - 'home.about.list.2': 'Informes Técnicos para Grúas', - 'home.about.list.3': 'Responsabilidad Técnica (ART) garantizada', - 'home.about.link': 'Conozca nuestra experiencia', - - // Home - Projects - 'home.projects.pretitle': 'Portafolio', - 'home.projects.title': 'Proyectos Recientes', - 'home.projects.link': 'Ver todos los proyectos', - 'home.projects.1.cat': 'Ingeniería Vehicular', - 'home.projects.1.title': 'Proyecto de Adecuación - Coca-Cola', - 'home.projects.2.cat': 'Inspección Técnica', - 'home.projects.2.title': 'Informe de Grúa Articulada', - 'home.projects.3.cat': 'Proyecto Mecánico', - 'home.projects.3.title': 'Dispositivo de Elevación Especial', - 'home.projects.4.cat': 'Informes', - 'home.projects.4.title': 'Certificación NR-12 - Parque Industrial', - 'home.projects.5.cat': 'Ingeniería Vehicular', - 'home.projects.5.title': 'Homologación de Plataforma Elevadora', - 'home.projects.6.cat': 'Seguridad Laboral', - 'home.projects.6.title': 'Proyecto de Línea de Vida para Camiones', - 'home.projects.view_details': 'Ver detalles', - - // Home - Testimonials - 'home.testimonials.pretitle': 'Testimonios', - 'home.testimonials.title': 'Socios que confían', - 'home.testimonials.1.text': 'Octto realizó la adecuación de toda nuestra flota de camiones con excelencia técnica y rapidez.', - 'home.testimonials.1.role': 'Gerente de Flota, Distribuidora Bebidas', - 'home.testimonials.2.text': 'Los informes técnicos emitidos por Octto nos dieron total seguridad jurídica y operativa.', - 'home.testimonials.2.role': 'Directora Operativa, Logística Express', - 'home.testimonials.3.text': 'El proyecto del dispositivo de elevación resolvió un cuello de botella antiguo de nuestra producción. Recomiendo.', - 'home.testimonials.3.role': 'Ingeniero Jefe, Industria Metalúrgica', - - // Home - CTA - 'home.cta.title': '¿Listo para iniciar su proyecto?', - 'home.cta.desc': 'Contáctenos hoy mismo y descubra cómo podemos ayudar a transformar su visión en realidad.', - 'home.cta.button': 'Hablar con un Especialista', - - // Services Page - 'services.hero.title': 'Nuestros Servicios', - 'services.hero.subtitle': 'Soluciones completas en ingeniería mecánica y movimiento de carga.', - 'services.cta.title': '¿Necesita una solución personalizada?', - 'services.cta.button': 'Hablar con un Ingeniero', - 'services.scope': 'Alcance del Servicio', - 'services.title': 'Servicios', - - // Projects Page - 'projects.hero.title': 'Nuestros Proyectos', - 'projects.hero.subtitle': 'Explore nuestro portafolio de soluciones en movimiento de carga e ingeniería mecánica.', - 'projects.filter.all': 'Todos', - 'projects.filter.implements': 'Implementos', - 'projects.filter.mechanical': 'Proyectos Mecánicos', - 'projects.filter.reports': 'Informes', - 'projects.card.details': 'Ver detalles', - - // About Page - 'about.hero.title': 'Sobre Octto', - 'about.hero.subtitle': 'Conozca nuestra trayectoria, valores y el compromiso con la excelencia en la ingeniería.', - 'about.history.pretitle': 'Nuestra Historia', - 'about.history.title': 'Nuestra Historia', - 'about.history.subtitle': 'Ingeniería que impulsa la logística', - 'about.history.p1': 'Octto Ingeniería nació de la necesidad del mercado por soluciones técnicas especializadas en movimiento de carga e implementos viales. Identificamos que grandes flotas carecían de ingeniería de punta para garantizar seguridad y eficiencia.', - 'about.history.p2': 'Hoy, somos socios estratégicos de grandes empresas de distribución, como Coca-Cola, desarrollando proyectos de adecuación, mantenimiento y certificación de equipos que son vitales para la cadena logística nacional.', - 'about.values.pretitle': 'Nuestros Pilares', - 'about.values.title': 'Nuestros Pilares', - 'about.values.subtitle': 'Valores que nos guían', - 'about.values.1.title': 'Excelencia Técnica', - 'about.values.1.desc': 'Búsqueda incesante de la perfección en cada detalle construtivo e de diseño.', - 'about.values.2.title': 'Transparencia', - 'about.values.2.desc': 'Relación clara y honesta con clientes, proveedores y empleados.', - 'about.values.3.title': 'Sostenibilidad', - 'about.values.3.desc': 'Compromiso con prácticas que respetan el medio ambiente y la sociedad.', - 'about.values.quality.title': 'Excelência Técnica', - 'about.values.quality.desc': 'Búsqueda incesante de la perfección en cada detalhe construtivo e de projeto.', - 'about.values.transparency.title': 'Transparência', - 'about.values.transparency.desc': 'Relacionamento claro e honesto com clientes, fornecedores e colaboradores.', - 'about.values.sustainability.title': 'Sustentabilidade', - 'about.values.sustainability.desc': 'Compromisso com práticas que respeitam o meio ambiente e a sociedade.', - - // Contact Page - 'contact.hero.title': 'Contacto', - 'contact.hero.subtitle': 'Estamos listos para escuchar sobre su proyecto. Contáctenos.', - 'contact.info.pretitle': 'Hable con Nosotros', - 'contact.info.title': 'Canales de Atención', - 'contact.info.subtitle': 'Contáctenos a través de nuestros canales oficiales', - 'contact.info.whatsapp.desc': 'Atención rápida y directa.', - 'contact.info.email.desc': 'Para presupuestos y dudas técnicas.', - 'contact.info.office.title': 'Oficina', - 'contact.info.phone.title': 'WhatsApp', - 'contact.info.email.title': 'E-mail', - 'contact.info.address.title': 'Oficina', - 'contact.form.title': 'Envíe un mensaje', - 'contact.form.name': 'Nombre', - 'contact.form.name.placeholder': 'Su nombre', - 'contact.form.phone': 'Teléfono', - 'contact.form.email': 'E-mail', - 'contact.form.email.placeholder': 'su@email.com', - 'contact.form.subject': 'Asunto', - 'contact.form.message': 'Mensaje', - 'contact.form.message.placeholder': '¿Cómo podemos ayudar?', - 'contact.form.submit': 'Enviar Mensaje', - 'contact.form.subject.select': 'Seleccione un asunto', - 'contact.form.subject.budget': 'Solicitar Presupuesto', - 'contact.form.subject.tech': 'Duda Técnica', - 'contact.form.subject.partnership': 'Asociación', - 'contact.form.subject.work': 'Trabaje con Nosotros', - - // Cookie Consent - 'cookie.text': 'Utilizamos cookies para mejorar su experiencia y analizar el tráfico del sitio. Al continuar navegando, acepta nuestra', - 'cookie.policy': 'Política de Privacidad', - 'cookie.accept': 'Aceptar', - 'cookie.decline': 'Rechazar', - - // WhatsApp - 'whatsapp.label': 'Atención Rápida', - } + // Cookies + 'cookie.text': 'Utilizamos cookies para melhorar sua experiência e analisar o tráfego do site. Ao continuar navegando, você concorda com nossa', + 'cookie.policy': 'Política de Privacidade', + 'cookie.accept': 'Aceitar', + 'cookie.decline': 'Recusar', }; export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [language, setLanguage] = useState('PT'); + const [language, setLanguageState] = useState('PT'); - const t = (key: string): string => { - return translations[language][key as keyof typeof translations[typeof language]] || key; + // Carregar idioma salvo + useEffect(() => { + const saved = localStorage.getItem('language') as Language; + if (saved && ['PT', 'EN', 'ES'].includes(saved)) { + setLanguageState(saved); + } + }, []); + + const setLanguage = (lang: Language) => { + setLanguageState(lang); + localStorage.setItem('language', lang); }; - const tDynamic = (content: { PT: string, EN?: string, ES?: string }): string => { - if (language === 'PT') return content.PT; - if (language === 'EN' && content.EN) return content.EN; - if (language === 'ES' && content.ES) return content.ES; - return content.PT; + // Função t() agora apenas retorna o texto em PT + // A tradução é feita pelo componente AutoTranslate + const t = (key: string): string => { + return systemTexts[key] || key; }; return ( - + {children} ); From 6e32ffdc95ae9121bd15bcc02a8a425d7752105e Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 27 Nov 2025 12:05:23 -0300 Subject: [PATCH 06/49] =?UTF-8?q?feat:=20CMS=20com=20limites=20de=20caract?= =?UTF-8?q?eres,=20tradu=C3=A7=C3=B5es=20auto=20e=20painel=20de=20notifica?= =?UTF-8?q?=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/diario-de-bordo/resumo-25-27.md | 49 +++ docs/tasks.md | 3 +- frontend/middleware.ts | 91 +++++ frontend/next.config.ts | 5 +- frontend/prisma/check-translations.mjs | 23 ++ frontend/prisma/migrate-locale.mjs | 49 +++ frontend/prisma/schema.prisma | 20 +- frontend/prisma/translate-pages.mjs | 147 ++++++++ frontend/scripts/checkTranslations.cjs | 44 +++ frontend/scripts/translateSlug.cjs | 126 +++++++ frontend/src/app/(public)/layout.tsx | 5 +- frontend/src/app/(public)/page.tsx | 43 ++- frontend/src/app/[locale]/contato/page.tsx | 317 ++++++++++++++++++ frontend/src/app/[locale]/layout.tsx | 35 ++ frontend/src/app/[locale]/page.tsx | 252 ++++++++++++++ .../src/app/[locale]/privacidade/page.tsx | 61 ++++ frontend/src/app/[locale]/projetos/page.tsx | 117 +++++++ frontend/src/app/[locale]/servicos/page.tsx | 126 +++++++ frontend/src/app/[locale]/sobre/page.tsx | 82 +++++ frontend/src/app/[locale]/termos/page.tsx | 55 +++ frontend/src/app/admin/layout.tsx | 186 +++++++++- .../src/app/admin/paginas/contato/page.tsx | 105 +++++- frontend/src/app/admin/paginas/home/page.tsx | 247 ++++++++++++-- frontend/src/app/admin/paginas/sobre/page.tsx | 98 +++++- .../app/api/admin/translate-pages/route.ts | 193 +++++++++++ frontend/src/app/api/config/route.ts | 6 +- frontend/src/app/api/pages/[slug]/route.ts | 164 ++++++++- frontend/src/app/api/pages/contact/route.ts | 41 --- frontend/src/app/api/pages/route.ts | 23 +- frontend/src/app/api/translate/route.ts | 169 +++++++--- frontend/src/components/Footer.tsx | 42 +-- frontend/src/components/Header.tsx | 98 +++--- frontend/src/components/TranslatedText.tsx | 142 ++++++-- .../src/components/admin/CharLimitBadge.tsx | 24 ++ frontend/src/contexts/LocaleContext.tsx | 85 +++++ frontend/src/hooks/usePageContent.ts | 14 +- frontend/src/lib/i18n.ts | 44 +++ frontend/src/locales/en.json | 204 +++++++++++ frontend/src/locales/es.json | 204 +++++++++++ frontend/src/locales/pt.json | 204 +++++++++++ 40 files changed, 3665 insertions(+), 278 deletions(-) create mode 100644 docs/diario-de-bordo/resumo-25-27.md create mode 100644 frontend/middleware.ts create mode 100644 frontend/prisma/check-translations.mjs create mode 100644 frontend/prisma/migrate-locale.mjs create mode 100644 frontend/prisma/translate-pages.mjs create mode 100644 frontend/scripts/checkTranslations.cjs create mode 100644 frontend/scripts/translateSlug.cjs create mode 100644 frontend/src/app/[locale]/contato/page.tsx create mode 100644 frontend/src/app/[locale]/layout.tsx create mode 100644 frontend/src/app/[locale]/page.tsx create mode 100644 frontend/src/app/[locale]/privacidade/page.tsx create mode 100644 frontend/src/app/[locale]/projetos/page.tsx create mode 100644 frontend/src/app/[locale]/servicos/page.tsx create mode 100644 frontend/src/app/[locale]/sobre/page.tsx create mode 100644 frontend/src/app/[locale]/termos/page.tsx create mode 100644 frontend/src/app/api/admin/translate-pages/route.ts delete mode 100644 frontend/src/app/api/pages/contact/route.ts create mode 100644 frontend/src/components/admin/CharLimitBadge.tsx create mode 100644 frontend/src/contexts/LocaleContext.tsx create mode 100644 frontend/src/lib/i18n.ts create mode 100644 frontend/src/locales/en.json create mode 100644 frontend/src/locales/es.json create mode 100644 frontend/src/locales/pt.json diff --git a/docs/diario-de-bordo/resumo-25-27.md b/docs/diario-de-bordo/resumo-25-27.md new file mode 100644 index 0000000..40927e1 --- /dev/null +++ b/docs/diario-de-bordo/resumo-25-27.md @@ -0,0 +1,49 @@ +## Resumo Geral do Projeto (Atualizado em 27/11/2025) + +### 1. Visão Geral +- Plataforma Next.js full-stack com duas frentes principais: + - **Site público** dentro de `src/app/(public)` e rotas localizadas em `src/app/[locale]` alimentadas por conteúdo dinâmico vindo do CMS. + - **Painel Admin** em `src/app/admin` para operação interna (gestão de páginas, serviços, projetos, usuários e mensagens). +- Back-end único via rotas App Router + Prisma/PostgreSQL (`prisma/pageContent`, `project`, `service`, etc.). + +### 2. Conteúdo Dinâmico & CMS +- CRUD de páginas no admin acessa `/api/pages` (genérico) e `/api/pages/[slug]` (detalhe com autenticação JWT). +- Páginas gerenciadas até agora: `home`, `sobre`, `contato`, além de `config` (metadados globais) e rotas públicas estruturadas. +- Formular pós-edição acionam `translation:refresh` no front para atualizar badges do sininho. +- Layout administrativo (`admin/layout.tsx`) fornece sidebar, menu, avatar modal, confirmação padrão e o **painel de traduções** com polling + badge. + +### 3. Plataforma de Traduções +- Salvar conteúdo em PT dispara `translateInBackground` (EN/ES) dentro de `src/app/api/pages/[slug]/route.ts` utilizando LibreTranslate + cache em `prisma.translation`. +- API auxiliar `/api/admin/translate-pages` permite rodadas manuais e retorno de status consolidado. +- Front exibe estado por slug (Concluída / Em andamento) e dispara notificações quando pendências são resolvidas. +- Job `prisma/check-translations.mjs` e script `scripts/checkTranslations.cjs` ajudam na auditoria dos timestamps. +- Endpoint redundante `/api/pages/contact` foi removido para evitar inconsistências; tudo passa pelo handler dinâmico. + +### 4. Experiência do Editor +- Campos do CMS agora possuem limites visuais via `CharLimitBadge` (estilo Twitter) com `LabelWithLimit`, aplicados a **Home**, **Sobre** e **Contato**. +- Limites também reforçados com `maxLength` para impedir que textos comprometam o layout público. +- Foram definidos ícones reutilizáveis (selector custom) e componentes de formulário padronizados. + +### 5. Site Público +- Utiliza `useTranslatedContent` + `` para mesclar conteúdo dinâmico com fallback estático. +- Páginas principais refletem exatamente o que foi configurado no admin (banner hero, diferenciais, CTA, depoimentos, etc.). +- Formulário de contato envia para `/api/messages`, com feedback via `ToastContext`. + +### 6. Segurança e Infra +- Autenticação do admin baseada em cookie `auth_token` (JWT) validado nas rotas protegidas. +- Upload/remoção de avatar gerenciado via `/api/auth/avatar` com modal padrão. +- Prisma centraliza o schema (`User`, `Project`, `Service`, `Message`, `PageContent`, `Translation`). + +### 7. Histórico de Entregas Relevantes +1. Estruturação do CMS e rotas dinâmicas para páginas públicas. +2. Implementação de tradução automática assíncrona + cache. +3. Criação do painel de notificações com polling/badges. +4. Inclusão de limites de caracteres visíveis e enforce client-side. +5. Remoção de API duplicada e ajustes para manter EN/ES sincronizados. + +### 8. Pendências & Próximos Passos +- Rodar tradução manual para `contact` e `config` (EN/ES ainda desatualizados segundo `scripts/checkTranslations.cjs`). +- Expandir o CMS para outras páginas (ex.: serviços e projetos públicos) caso necessário. +- Opcional: reforçar validação server-side dos limites e criar testes automatizados para o fluxo de tradução. + +Este resumo deve servir como onboarding rápido para qualquer pessoa ou nova IA que precise continuar o desenvolvimento. diff --git a/docs/tasks.md b/docs/tasks.md index 1953e48..46a77fc 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -3,7 +3,7 @@ ## 🔧 Melhorias a Implementar ### 🌐 Internacionalização -- [ ] **Tradução incompleta** - Revisar e traduzir todos os componentes que ainda estão em inglês +- [ ] **Tradução incompleta** - Revisar e traduzir todos os componentes que ainda estão em inglês - em andamento ### 🎨 Configurações do Admin - [ ] **Upload de Logotipo** - Criar formulário para o cliente poder trocar o logotipo do site nas configurações @@ -22,6 +22,7 @@ - Leads não lidos - Últimas mensagens recebidas + - Adicionar modo noturno no painel --- ## ✅ Concluídas diff --git a/frontend/middleware.ts b/frontend/middleware.ts new file mode 100644 index 0000000..209dad9 --- /dev/null +++ b/frontend/middleware.ts @@ -0,0 +1,91 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +// Definir locales diretamente aqui para evitar problemas com edge runtime +const locales = ['pt', 'en', 'es'] as const; +type Locale = (typeof locales)[number]; +const defaultLocale: Locale = 'pt'; + +// Rotas que NÃO devem ter prefixo de idioma +const publicPaths = ['/api', '/admin', '/acesso', '/_next', '/favicon', '/icon']; + +function getLocaleFromPath(pathname: string): Locale | null { + const segments = pathname.split('/'); + const possibleLocale = segments[1]; + + if (locales.includes(possibleLocale as Locale)) { + return possibleLocale as Locale; + } + + return null; +} + +function getLocaleFromHeader(request: NextRequest): Locale { + const acceptLanguage = request.headers.get('accept-language') || ''; + + // Verificar se o navegador prefere algum dos nossos idiomas + for (const locale of locales) { + if (acceptLanguage.toLowerCase().includes(locale)) { + return locale; + } + } + + return defaultLocale; +} + +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + + // Ignorar rotas públicas (API, admin, etc) + if (publicPaths.some(path => pathname.startsWith(path))) { + return NextResponse.next(); + } + + // Ignorar arquivos estáticos + if (pathname.includes('.')) { + return NextResponse.next(); + } + + // Verificar se já tem locale na URL + const pathnameLocale = getLocaleFromPath(pathname); + + if (pathnameLocale) { + // URL já tem locale, continuar + const response = NextResponse.next(); + response.headers.set('x-locale', pathnameLocale); + return response; + } + + // Verificar cookie de preferência + const cookieLocale = request.cookies.get('locale')?.value as Locale | undefined; + + // Determinar locale: cookie > navegador > padrão + let locale: Locale; + + if (cookieLocale && locales.includes(cookieLocale)) { + locale = cookieLocale; + } else { + locale = getLocaleFromHeader(request); + } + + // Se for o locale padrão (pt), não adiciona prefixo na URL + // Isso mantém as URLs limpas: occto.com.br/ ao invés de occto.com.br/pt/ + if (locale === defaultLocale) { + const response = NextResponse.next(); + response.headers.set('x-locale', locale); + return response; + } + + // Redirecionar para URL com locale + const newUrl = new URL(`/${locale}${pathname}`, request.url); + newUrl.search = request.nextUrl.search; + + return NextResponse.redirect(newUrl); +} + +export const config = { + matcher: [ + // Todas as rotas exceto arquivos estáticos + '/((?!_next/static|_next/image|favicon.ico|icon.svg|.*\\..*).*)', + ], +}; diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 034a997..f4b5e4d 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -3,7 +3,10 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: 'standalone', images: { - domains: ['localhost', 'images.unsplash.com'], + remotePatterns: [ + { protocol: 'http', hostname: 'localhost' }, + { protocol: 'https', hostname: 'images.unsplash.com' }, + ], }, typescript: { ignoreBuildErrors: true, diff --git a/frontend/prisma/check-translations.mjs b/frontend/prisma/check-translations.mjs new file mode 100644 index 0000000..0830563 --- /dev/null +++ b/frontend/prisma/check-translations.mjs @@ -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) diff --git a/frontend/prisma/migrate-locale.mjs b/frontend/prisma/migrate-locale.mjs new file mode 100644 index 0000000..86318fe --- /dev/null +++ b/frontend/prisma/migrate-locale.mjs @@ -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(); + }); diff --git a/frontend/prisma/schema.prisma b/frontend/prisma/schema.prisma index fcf0269..45a5d3d 100644 --- a/frontend/prisma/schema.prisma +++ b/frontend/prisma/schema.prisma @@ -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]) } diff --git a/frontend/prisma/translate-pages.mjs b/frontend/prisma/translate-pages.mjs new file mode 100644 index 0000000..e5e2c52 --- /dev/null +++ b/frontend/prisma/translate-pages.mjs @@ -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(); + }); diff --git a/frontend/scripts/checkTranslations.cjs b/frontend/scripts/checkTranslations.cjs new file mode 100644 index 0000000..005a950 --- /dev/null +++ b/frontend/scripts/checkTranslations.cjs @@ -0,0 +1,44 @@ +const { PrismaClient } = require('@prisma/client'); + +async function main() { + const prisma = new PrismaClient(); + try { + const pages = await prisma.pageContent.findMany({ + orderBy: [{ slug: 'asc' }, { locale: 'asc' }], + }); + + const grouped = pages.reduce((acc, page) => { + acc[page.slug] = acc[page.slug] || []; + acc[page.slug].push({ locale: page.locale, updatedAt: page.updatedAt }); + return acc; + }, {}); + + for (const [slug, entries] of Object.entries(grouped)) { + console.log(`\n=== ${slug.toUpperCase()} ===`); + const pt = entries.find((e) => e.locale === 'pt'); + const ptDate = pt ? new Date(pt.updatedAt) : null; + + for (const locale of ['pt', 'en', 'es']) { + const entry = entries.find((e) => e.locale === locale); + if (!entry) { + console.log(`${locale.toUpperCase()}: missing`); + continue; + } + + const dt = new Date(entry.updatedAt); + let status = 'ok'; + if (ptDate && locale !== 'pt' && dt < ptDate) { + status = 'outdated'; + } + console.log(`${locale.toUpperCase()}: ${dt.toISOString()} (${status})`); + } + } + } finally { + await prisma.$disconnect(); + } +} + +main().catch((err) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/frontend/scripts/translateSlug.cjs b/frontend/scripts/translateSlug.cjs new file mode 100644 index 0000000..d3d4b33 --- /dev/null +++ b/frontend/scripts/translateSlug.cjs @@ -0,0 +1,126 @@ +const { PrismaClient } = require('@prisma/client'); + +const prisma = new PrismaClient(); +const LIBRETRANSLATE_URL = process.env.LIBRETRANSLATE_URL || 'https://libretranslate.stackbyte.cloud'; +const TARGET_LOCALES = ['en', 'es']; +const SKIP_KEYS = ['icon', 'image', 'img', 'url', 'href', 'id', 'slug', 'src', 'email', 'phone', 'whatsapp', 'link', 'linkText']; + +async function translateText(text, targetLang) { + if (!text || typeof text !== 'string' || text.trim() === '' || targetLang === 'pt') { + return text; + } + + const cached = await prisma.translation.findUnique({ + where: { + sourceText_sourceLang_targetLang: { + sourceText: text, + sourceLang: 'pt', + targetLang, + }, + }, + }); + + if (cached) { + return cached.translatedText; + } + + 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) { + console.warn(`Translation API error (${targetLang}): ${response.status}`); + return text; + } + + const data = await response.json(); + const translatedText = data.translatedText || text; + + await prisma.translation.upsert({ + where: { + sourceText_sourceLang_targetLang: { + sourceText: text, + sourceLang: 'pt', + targetLang, + }, + }, + update: { translatedText }, + create: { + sourceText: text, + sourceLang: 'pt', + targetLang, + translatedText, + }, + }); + + return translatedText; +} + +async function translateContent(content, targetLang) { + if (targetLang === 'pt') return content; + + if (typeof content === 'string') { + return 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)) { + if (SKIP_KEYS.includes(key)) { + result[key] = value; + } else { + result[key] = await translateContent(value, targetLang); + } + } + return result; + } + + return content; +} + +async function main() { + const slug = process.argv[2]; + if (!slug) { + console.error('Usage: node scripts/translateSlug.cjs '); + process.exit(1); + } + + const ptPage = await prisma.pageContent.findUnique({ + where: { slug_locale: { slug, locale: 'pt' } }, + }); + + if (!ptPage) { + console.error(`Slug "${slug}" not found in locale PT.`); + process.exit(1); + } + + for (const locale of TARGET_LOCALES) { + console.log(`Translating ${slug} -> ${locale.toUpperCase()}...`); + const translatedContent = await translateContent(ptPage.content, locale); + await prisma.pageContent.upsert({ + where: { slug_locale: { slug, locale } }, + update: { content: translatedContent }, + create: { slug, locale, content: translatedContent }, + }); + console.log(`✓ ${locale.toUpperCase()} done.`); + } +} + +main() + .catch((err) => { + console.error(err); + process.exitCode = 1; + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/frontend/src/app/(public)/layout.tsx b/frontend/src/app/(public)/layout.tsx index 4a56be6..65f1f47 100644 --- a/frontend/src/app/(public)/layout.tsx +++ b/frontend/src/app/(public)/layout.tsx @@ -2,6 +2,7 @@ import Header from "@/components/Header"; import Footer from "@/components/Footer"; import CookieConsent from "@/components/CookieConsent"; import WhatsAppButton from "@/components/WhatsAppButton"; +import { LocaleProvider } from "@/contexts/LocaleContext"; export default function PublicLayout({ children, @@ -9,7 +10,7 @@ export default function PublicLayout({ children: React.ReactNode; }) { return ( - <> +
{children} @@ -17,6 +18,6 @@ export default function PublicLayout({
- + ); } diff --git a/frontend/src/app/(public)/page.tsx b/frontend/src/app/(public)/page.tsx index dfcf423..5fe594b 100644 --- a/frontend/src/app/(public)/page.tsx +++ b/frontend/src/app/(public)/page.tsx @@ -2,22 +2,19 @@ import Link from "next/link"; import { usePageContent } from "@/hooks/usePageContent"; -import { useTranslatedContent, T } from "@/components/TranslatedText"; export default function Home() { - const { content, loading } = usePageContent('home'); - - // Traduzir conteúdo do banco automaticamente - const { translatedContent } = useTranslatedContent(content); + // Português é o idioma padrão - busca diretamente sem tradução + const { content, loading } = usePageContent('home', 'pt'); - // Usar conteúdo traduzido ou fallback - const hero = translatedContent?.hero || { + // Usar conteúdo do banco ou fallback + const hero = content?.hero || { title: 'Engenharia de Excelência para Seus Projetos', subtitle: 'Soluções completas em engenharia veicular, mecânica e segurança do trabalho com mais de 15 anos de experiência.', buttonText: 'Conheça Nossos Serviços' }; - const features = translatedContent?.features || { + const features = content?.features || { pretitle: 'Por que nos escolher', title: 'Nossos Diferenciais', items: [ @@ -27,7 +24,7 @@ export default function Home() { ] as Array<{ icon: string; title: string; description: string }> }; - const services = translatedContent?.services || { + const services = content?.services || { pretitle: 'Nossos Serviços', title: 'O Que Fazemos', items: [ @@ -38,7 +35,7 @@ export default function Home() { ] as Array<{ icon: string; title: string; description: string }> }; - const about = translatedContent?.about || { + const about = content?.about || { pretitle: 'Conheça a OCCTO', title: 'Sobre Nós', description: 'Com mais de 15 anos de experiência, a OCCTO Engenharia se consolidou como referência em soluções de engenharia.', @@ -49,7 +46,7 @@ export default function Home() { ] as string[] }; - const testimonials = translatedContent?.testimonials || { + const testimonials = content?.testimonials || { pretitle: 'Depoimentos', title: 'O Que Dizem Nossos Clientes', items: [ @@ -59,13 +56,13 @@ export default function Home() { ] as Array<{ name: string; role: string; text: string }> }; - const stats = translatedContent?.stats || { + const stats = content?.stats || { clients: '500+', projects: '1200+', years: '15' }; - const cta = translatedContent?.cta || { + const cta = content?.cta || { title: 'Pronto para tirar seu projeto do papel?', text: 'Entre em contato com nossa equipe de especialistas.', button: 'Fale Conosco' @@ -83,7 +80,7 @@ export default function Home() {
- Prestador de Serviço Oficial Coca-Cola + Prestador de Serviço Oficial Coca-Cola

@@ -97,7 +94,7 @@ export default function Home() { {hero.buttonText} - Ver Soluções + Ver Soluções

@@ -145,7 +142,7 @@ export default function Home() {
- Ver todos os serviços + Ver todos os serviços
@@ -175,7 +172,7 @@ export default function Home() { ))} - Conheça nossa expertise + Conheça nossa expertise
@@ -186,11 +183,11 @@ export default function Home() {
-

Portfólio

-

Projetos Recentes

+

Portfólio

+

Projetos Recentes

- Ver todos os projetos + Ver todos os projetos
@@ -204,11 +201,11 @@ export default function Home() {
- {project.cat} -

{project.title}

+ {project.cat} +

{project.title}

- Ver detalhes + Ver detalhes
diff --git a/frontend/src/app/[locale]/contato/page.tsx b/frontend/src/app/[locale]/contato/page.tsx new file mode 100644 index 0000000..40d2a5e --- /dev/null +++ b/frontend/src/app/[locale]/contato/page.tsx @@ -0,0 +1,317 @@ +"use client"; + +import { useToast } from "@/contexts/ToastContext"; +import { useState, useEffect } from "react"; +import { useLocale } from "@/contexts/LocaleContext"; + +interface ContactInfo { + icon: string; + title: string; + description: string; + link: string; + linkText: string; +} + +interface ContactContent { + hero: { + pretitle: string; + title: string; + subtitle: string; + }; + info: { + title: string; + subtitle: string; + description: string; + items: ContactInfo[]; + }; +} + +export default function ContatoPage() { + const { success, error: showError } = useToast(); + const { locale, t } = useLocale(); + const [content, setContent] = useState(null); + const [loading, setLoading] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [formData, setFormData] = useState({ + name: '', + phone: '', + email: '', + subject: '', + message: '' + }); + + useEffect(() => { + fetchContent(); + }, [locale]); + + const fetchContent = async () => { + try { + // Busca conteúdo JÁ TRADUZIDO do banco + const response = await fetch(`/api/pages/contact?locale=${locale}`); + if (response.ok) { + const data = await response.json(); + if (data.content) { + setContent(data.content); + } + } + } catch (error) { + console.error('Erro ao carregar conteúdo:', error); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSubmitting(true); + + try { + const response = await fetch('/api/messages', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData) + }); + + if (!response.ok) throw new Error('Erro ao enviar mensagem'); + + // Limpar formulário + setFormData({ + name: '', + phone: '', + email: '', + subject: '', + message: '' + }); + + success('Mensagem enviada com sucesso! Entraremos em contato em breve.'); + } catch (error) { + showError('Erro ao enviar mensagem. Tente novamente.'); + } finally { + setSubmitting(false); + } + }; + + // Valores padrão caso não tenha conteúdo salvo + const hero = content?.hero || { + pretitle: t('contact.pretitle'), + title: t('contact.title'), + subtitle: t('contact.subtitle') + }; + + const info = content?.info || { + title: t('contact.infoTitle'), + subtitle: t('contact.infoSubtitle'), + description: t('contact.infoDescription'), + items: [ + { + icon: 'ri-whatsapp-line', + title: t('contact.phone'), + description: t('contact.phoneDescription'), + link: 'https://wa.me/5527999999999', + linkText: '(27) 99999-9999' + }, + { + icon: 'ri-mail-send-line', + title: t('contact.email'), + description: t('contact.emailDescription'), + link: 'mailto:contato@octto.com.br', + linkText: 'contato@octto.com.br' + }, + { + icon: 'ri-map-pin-line', + title: t('contact.address'), + description: t('contact.addressDescription'), + link: 'https://maps.google.com', + linkText: t('contact.viewOnMap') + } + ] + }; + + return ( +
+ {/* Hero Section */} +
+
+
+
+
+
+ + {hero.pretitle} +
+

{hero.title}

+

+ {hero.subtitle} +

+
+
+
+ +
+ {/* Decorative Elements */} +
+ +
+
+ {/* Informações de Contato */} +
+
+

{info.title}

+

{info.subtitle}

+

+ {info.description} +

+
+ +
+ {info.items.map((item, index) => ( +
+
+
+ +
+
+

{item.title}

+

{item.description}

+ + {item.linkText} + +
+
+
+ ))} +
+
+ + {/* Formulário */} +
+
+
+ +

{t('contact.sendMessage')}

+ + +
+
+ +
+ + setFormData({...formData, name: e.target.value})} + className="w-full pl-11 pr-4 py-3.5 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" + placeholder={t('contact.form.namePlaceholder')} + /> +
+
+
+ +
+ + setFormData({...formData, phone: e.target.value})} + className="w-full pl-11 pr-4 py-3.5 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" + placeholder="(00) 00000-0000" + /> +
+
+
+ +
+ +
+ + setFormData({...formData, email: e.target.value})} + className="w-full pl-11 pr-4 py-3.5 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" + placeholder={t('contact.form.emailPlaceholder')} + /> +
+
+ +
+ +
+ + + +
+
+ +
+ +
+ + +
+
+ + + +
+
+
+
+
+ + {/* Map Section */} +
+ +
+
+
+ ); +} diff --git a/frontend/src/app/[locale]/layout.tsx b/frontend/src/app/[locale]/layout.tsx new file mode 100644 index 0000000..fcec6c4 --- /dev/null +++ b/frontend/src/app/[locale]/layout.tsx @@ -0,0 +1,35 @@ +import Header from "@/components/Header"; +import Footer from "@/components/Footer"; +import CookieConsent from "@/components/CookieConsent"; +import WhatsAppButton from "@/components/WhatsAppButton"; +import { LocaleProvider } from "@/contexts/LocaleContext"; +import { locales, type Locale, defaultLocale } from "@/lib/i18n"; + +// Gerar rotas estáticas para cada locale +export function generateStaticParams() { + return locales.map((locale) => ({ locale })); +} + +interface Props { + children: React.ReactNode; + params: Promise<{ locale: Locale }>; +} + +export default async function LocaleLayout({ children, params }: Props) { + const { locale } = await params; + + // Validar locale + const validLocale = locales.includes(locale) ? locale : defaultLocale; + + return ( + +
+
+ {children} +
+