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
This commit is contained in:
@@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-400 mb-6">
|
||||
Soluções em engenharia mecânica e segurança para movimentação de carga.
|
||||
<T>Soluções em engenharia mecânica e segurança para movimentação de carga.</T>
|
||||
</p>
|
||||
|
||||
<div className="inline-flex items-center gap-2 bg-white/5 border border-white/10 rounded-lg px-3 py-2 mb-6">
|
||||
<i className="ri-verified-badge-fill text-primary"></i>
|
||||
<span className="text-xs font-bold text-gray-300 uppercase tracking-wide">Prestador Oficial <span className="text-primary">Coca-Cola</span></span>
|
||||
<span className="text-xs font-bold text-gray-300 uppercase tracking-wide"><T>Prestador Oficial</T> <span className="text-primary">Coca-Cola</span></span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
@@ -43,30 +44,30 @@ export default function Footer() {
|
||||
|
||||
{/* Links */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold font-headline mb-6">Links Rápidos</h3>
|
||||
<h3 className="text-lg font-bold font-headline mb-6"><T>Links Rápidos</T></h3>
|
||||
<ul className="space-y-4">
|
||||
<li><Link href="/" className="text-gray-400 hover:text-primary transition-colors">{t('nav.home')}</Link></li>
|
||||
<li><Link href="/sobre" className="text-gray-400 hover:text-primary transition-colors">{t('nav.about')}</Link></li>
|
||||
<li><Link href="/servicos" className="text-gray-400 hover:text-primary transition-colors">{t('nav.services')}</Link></li>
|
||||
<li><Link href="/projetos" className="text-gray-400 hover:text-primary transition-colors">{t('nav.projects')}</Link></li>
|
||||
<li><Link href="/contato" className="text-gray-400 hover:text-primary transition-colors">{t('nav.contact')}</Link></li>
|
||||
<li><Link href="/" className="text-gray-400 hover:text-primary transition-colors"><T>{t('nav.home')}</T></Link></li>
|
||||
<li><Link href="/sobre" className="text-gray-400 hover:text-primary transition-colors"><T>{t('nav.about')}</T></Link></li>
|
||||
<li><Link href="/servicos" className="text-gray-400 hover:text-primary transition-colors"><T>{t('nav.services')}</T></Link></li>
|
||||
<li><Link href="/projetos" className="text-gray-400 hover:text-primary transition-colors"><T>{t('nav.projects')}</T></Link></li>
|
||||
<li><Link href="/contato" className="text-gray-400 hover:text-primary transition-colors"><T>{t('nav.contact')}</T></Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Services */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold font-headline mb-6">{t('services.title')}</h3>
|
||||
<h3 className="text-lg font-bold font-headline mb-6"><T>{t('services.title')}</T></h3>
|
||||
<ul className="space-y-4">
|
||||
<li className="text-gray-400">Projetos de Dispositivos</li>
|
||||
<li className="text-gray-400">Engenharia de Implementos</li>
|
||||
<li className="text-gray-400">Inspeção de Equipamentos</li>
|
||||
<li className="text-gray-400">Laudos Técnicos (NR-11/12)</li>
|
||||
<li className="text-gray-400"><T>Projetos de Dispositivos</T></li>
|
||||
<li className="text-gray-400"><T>Engenharia de Implementos</T></li>
|
||||
<li className="text-gray-400"><T>Inspeção de Equipamentos</T></li>
|
||||
<li className="text-gray-400"><T>Laudos Técnicos (NR-11/12)</T></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold font-headline mb-6">{t('nav.contact')}</h3>
|
||||
<h3 className="text-lg font-bold font-headline mb-6"><T>{t('nav.contact')}</T></h3>
|
||||
<ul className="space-y-4">
|
||||
<li className="flex items-start gap-3 text-gray-400">
|
||||
<i className="ri-map-pin-line mt-1 text-primary"></i>
|
||||
@@ -86,11 +87,11 @@ export default function Footer() {
|
||||
|
||||
<div className="border-t border-white/10 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<p className="text-gray-500 text-sm">
|
||||
© {new Date().getFullYear()} OCCTO Engenharia. {t('footer.rights')}
|
||||
© {new Date().getFullYear()} OCCTO Engenharia. <T>{t('footer.rights')}</T>
|
||||
</p>
|
||||
<div className="flex gap-6 text-sm text-gray-500">
|
||||
<Link href="/privacidade" className="hover:text-white">Política de Privacidade</Link>
|
||||
<Link href="/termos" className="hover:text-white">Termos de Uso</Link>
|
||||
<Link href="/privacidade" className="hover:text-white"><T>Política de Privacidade</T></Link>
|
||||
<Link href="/termos" className="hover:text-white"><T>Termos de Uso</T></Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
<nav className="flex items-center gap-6 mr-4">
|
||||
<Link href="/" className="flex items-center gap-2 text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary font-medium transition-colors group">
|
||||
<i className="ri-home-4-line text-lg group-hover:scale-110 transition-transform"></i>
|
||||
<span className="hidden lg:inline">{t('nav.home')}</span>
|
||||
<span className="hidden lg:inline"><T>{t('nav.home')}</T></span>
|
||||
</Link>
|
||||
<Link href="/servicos" className="flex items-center gap-2 text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary font-medium transition-colors group">
|
||||
<i className="ri-tools-line text-lg group-hover:scale-110 transition-transform"></i>
|
||||
<span className="hidden lg:inline">{t('nav.services')}</span>
|
||||
<span className="hidden lg:inline"><T>{t('nav.services')}</T></span>
|
||||
</Link>
|
||||
<Link href="/projetos" className="flex items-center gap-2 text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary font-medium transition-colors group">
|
||||
<i className="ri-briefcase-line text-lg group-hover:scale-110 transition-transform"></i>
|
||||
<span className="hidden lg:inline">{t('nav.projects')}</span>
|
||||
<span className="hidden lg:inline"><T>{t('nav.projects')}</T></span>
|
||||
</Link>
|
||||
<Link href="/contato" className="flex items-center gap-2 text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary font-medium transition-colors group">
|
||||
<i className="ri-mail-send-line text-lg group-hover:scale-110 transition-transform"></i>
|
||||
<span className="hidden lg:inline">{t('nav.contact')}</span>
|
||||
<span className="hidden lg:inline"><T>{t('nav.contact')}</T></span>
|
||||
</Link>
|
||||
<Link href="/sobre" className="flex items-center gap-2 text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary font-medium transition-colors group">
|
||||
<i className="ri-user-line text-lg group-hover:scale-110 transition-transform"></i>
|
||||
<span className="hidden lg:inline">{t('nav.about')}</span>
|
||||
<span className="hidden lg:inline"><T>{t('nav.about')}</T></span>
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<i className="ri-whatsapp-line"></i>
|
||||
<span className="hidden xl:inline">{t('nav.contact_us')}</span>
|
||||
<span className="hidden xl:inline"><T>{t('nav.contact_us')}</T></span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -168,23 +169,23 @@ export default function Header() {
|
||||
<nav className="flex flex-col gap-4 text-base font-medium">
|
||||
<Link href="/" onClick={() => setIsMobileMenuOpen(false)} className="flex items-center gap-3 py-2 border-b border-gray-100 dark:border-white/10 text-secondary dark:text-white">
|
||||
<i className="ri-home-4-line text-primary text-lg"></i>
|
||||
{t('nav.home')}
|
||||
<T>{t('nav.home')}</T>
|
||||
</Link>
|
||||
<Link href="/servicos" onClick={() => setIsMobileMenuOpen(false)} className="flex items-center gap-3 py-2 border-b border-gray-100 dark:border-white/10 text-secondary dark:text-white">
|
||||
<i className="ri-tools-line text-primary text-lg"></i>
|
||||
{t('nav.services')}
|
||||
<T>{t('nav.services')}</T>
|
||||
</Link>
|
||||
<Link href="/projetos" onClick={() => setIsMobileMenuOpen(false)} className="flex items-center gap-3 py-2 border-b border-gray-100 dark:border-white/10 text-secondary dark:text-white">
|
||||
<i className="ri-briefcase-line text-primary text-lg"></i>
|
||||
{t('nav.projects')}
|
||||
<T>{t('nav.projects')}</T>
|
||||
</Link>
|
||||
<Link href="/contato" onClick={() => setIsMobileMenuOpen(false)} className="flex items-center gap-3 py-2 border-b border-gray-100 dark:border-white/10 text-secondary dark:text-white">
|
||||
<i className="ri-mail-send-line text-primary text-lg"></i>
|
||||
{t('nav.contact')}
|
||||
<T>{t('nav.contact')}</T>
|
||||
</Link>
|
||||
<Link href="/sobre" onClick={() => setIsMobileMenuOpen(false)} className="flex items-center gap-3 py-2 border-b border-gray-100 dark:border-white/10 text-secondary dark:text-white">
|
||||
<i className="ri-user-line text-primary text-lg"></i>
|
||||
{t('nav.about')}
|
||||
<T>{t('nav.about')}</T>
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<i className="ri-whatsapp-line text-xl"></i>
|
||||
{t('nav.contact_us')}
|
||||
<T>{t('nav.contact_us')}</T>
|
||||
</Link>
|
||||
|
||||
<div
|
||||
className="flex items-center justify-between p-4 bg-gray-50 dark:bg-white/5 rounded-xl cursor-pointer hover:bg-gray-100 dark:hover:bg-white/10 transition-colors"
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
<span className="text-sm font-bold text-gray-500 dark:text-gray-400">{t('nav.theme')}</span>
|
||||
<span className="text-sm font-bold text-gray-500 dark:text-gray-400"><T>{t('nav.theme')}</T></span>
|
||||
<button
|
||||
className="w-10 h-10 rounded-full bg-white dark:bg-white/10 flex items-center justify-center text-gray-600 dark:text-yellow-400 shadow-sm transition-colors"
|
||||
>
|
||||
@@ -215,7 +216,7 @@ export default function Header() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-white/5 rounded-xl">
|
||||
<span className="text-sm font-bold text-gray-500 dark:text-gray-400">{t('nav.language')}</span>
|
||||
<span className="text-sm font-bold text-gray-500 dark:text-gray-400"><T>{t('nav.language')}</T></span>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setLanguage('PT')} className={`w-10 h-10 rounded-lg flex items-center justify-center text-xl cursor-pointer transition-all ${language === 'PT' ? 'bg-white dark:bg-white/10 shadow-sm scale-110' : 'opacity-50 hover:opacity-100'}`}>🇧🇷</button>
|
||||
<button onClick={() => setLanguage('EN')} className={`w-10 h-10 rounded-lg flex items-center justify-center text-xl cursor-pointer transition-all ${language === 'EN' ? 'bg-white dark:bg-white/10 shadow-sm scale-110' : 'opacity-50 hover:opacity-100'}`}>🇺🇸</button>
|
||||
|
||||
@@ -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<string, string>();
|
||||
|
||||
// Função para traduzir texto via API
|
||||
async function translateText(text: string, targetLang: string): Promise<string> {
|
||||
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: <T>Texto em português</T>
|
||||
*/
|
||||
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 <Tag className={className}>{translatedText}</Tag>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag className={className} data-translating={isLoading}>
|
||||
{translatedText}
|
||||
</Tag>
|
||||
);
|
||||
// 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<string> => {
|
||||
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<string[]> => {
|
||||
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<T extends Record<string, unknown>>(content: T): {
|
||||
translatedContent: T;
|
||||
isTranslating: boolean
|
||||
export function useTranslatedContent<T extends Record<string, unknown>>(content: T | null): {
|
||||
translatedContent: T | null;
|
||||
isTranslating: boolean;
|
||||
} {
|
||||
const { translateBatch, language } = useTranslate();
|
||||
const [translatedContent, setTranslatedContent] = useState<T>(content);
|
||||
const { language } = useLanguage();
|
||||
const [translatedContent, setTranslatedContent] = useState<T | null>(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<T extends Record<string, unknown>>(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<T extends Record<string, unknown>>(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<T extends Record<string, unknown>>(content:
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [content, language, translateBatch]);
|
||||
}, [content, language]);
|
||||
|
||||
return { translatedContent, isTranslating };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user