Tradutor/Adaptador Gemini (Simulação Corrigida)
/* Estilos isolados com prefixo 'trad-' para evitar conflitos com WordPress */
.trad-container {
max-width: 960px;
margin: 0 auto;
}
.trad-form-container {
background-color: white;
border-radius: 10px;
box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 20px;
}
.trad-language-checkboxes {
max-height: 300px;
overflow-y: auto;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 10px;
}
.trad-job-card {
background-color: #f8f9fa;
border-radius: 8px;
margin-bottom: 10px;
padding: 10px;
box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.05);
}
.trad-job-details {
display: none;
margin-top: 10px;
padding: 10px;
background-color: #e9ecef;
border-radius: 5px;
}
.trad-nav-tabs .nav-link.active {
background-color: #f8f9fa;
border-bottom-color: #f8f9fa;
}
.trad-hidden {
display: none;
}
.trad-api-key-input {
position: relative;
}
.trad-toggle-api-visibility {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
background: none;
border: none;
}
.trad-translation-result {
background-color: #f0f8ff;
border-radius: 8px;
margin-bottom: 10px;
padding: 15px;
border-left: 5px solid #007bff;
}
.trad-translation-text {
max-height: 200px;
overflow-y: auto;
background-color: white;
padding: 10px;
border-radius: 5px;
border: 1px solid #dee2e6;
margin: 10px 0;
font-size: 0.9rem;
white-space: pre-wrap;
}
.trad-results-container {
border-top: 1px solid #dee2e6;
margin-top: 20px;
padding-top: 20px;
}
.trad-badge-status {
font-size: 0.8rem;
padding: 0.25rem 0.5rem;
margin-right: 5px;
}
.trad-loading-indicator {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid rgba(0, 123, 255, 0.25);
border-right-color: #007bff;
border-radius: 50%;
animation: spinner 0.75s linear infinite;
margin-right: 5px;
}
@keyframes spinner {
to {transform: rotate(360deg);}
}
Tradutor/Adaptador Gemini (Versão Web)
-
-
-
// SIMULAÇÃO: Definindo um objeto tradutor_ajax básico para que o script de verificação não falhe imediatamente.
// No seu ambiente WordPress real, isso é criado pelo wp_localize_script.
var tradutor_ajax = {
ajax_url: "/wp-admin/admin-ajax.php", // URL de exemplo
nonce: "abcdef1234", // Nonce de exemplo
user_logged_in: true // Simular usuário logado
};
// CONTEÚDO DO SCRIPT DE VERIFICAÇÃO (antes estava aparecendo como texto)
// Verificar se estamos dentro do WordPress e se o objeto tradutor_ajax está disponível
document.addEventListener("DOMContentLoaded", function() {
// Adicionar uma mensagem de status na interface
const statusContainer = document.createElement("div");
statusContainer.className = "alert alert-info mt-3";
statusContainer.id = "trad-storage-status";
if (typeof tradutor_ajax !== "undefined" && tradutor_ajax.ajax_url) {
statusContainer.innerHTML = "
Modo de armazenamento: Banco de dados WordPress (As traduções serão salvas no servidor e ficarão disponíveis em qualquer dispositivo).";
// Tenta adicionar ao container do formulário principal
const formContainer = document.querySelector(".trad-form-container");
if (formContainer) {
formContainer.appendChild(statusContainer);
}
console.log("WordPress detectado. Utilizando armazenamento no banco de dados.");
} else {
statusContainer.innerHTML = "
Modo de armazenamento: Navegador (localStorage) (As traduções são salvas apenas neste navegador. Para armazenamento permanente, configure o WordPress.)";
// Tenta adicionar ao container do formulário principal
const formContainer = document.querySelector(".trad-form-container");
if (formContainer) {
formContainer.appendChild(statusContainer);
}
console.log("WordPress não detectado. Utilizando localStorage como fallback.");
// Adicionar aviso para navegação anônima
try {
localStorage.setItem("test", "test");
localStorage.removeItem("test");
} catch (e) {
const anonWarning = document.createElement("div");
anonWarning.className = "alert alert-warning mt-2";
anonWarning.innerHTML = "
Atenção: Você está usando modo de navegação anônima/incógnito. O armazenamento local está limitado e as traduções podem não ser salvas permanentemente.";
if (formContainer) {
formContainer.appendChild(anonWarning);
}
}
}
// Se houver mais código JS problemático, adicione-o aqui.
});
// Namespace para evitar conflitos com outros scripts no WordPress
(function(window) {
// Este é o namespace principal do nosso aplicativo
var TradutorApp = window.TradutorApp = window.TradutorApp || {};
// Inicializa quando o documento estiver pronto
document.addEventListener("DOMContentLoaded", function() {
// ----- Constantes -----
TradutorApp.LANG_CULTURE_MAP = {
"Português (Brasil)": {"lang": "Português (Brasil)", "culture": "Brasil"},
"Inglês (Estados Unidos)": {"lang": "Inglês (Estados Unidos)", "culture": "Estados Unidos"},
"Espanhol (México)": {"lang": "Espanhol (México)", "culture": "México"},
"Francês": {"lang": "Francês", "culture": "França"},
"Alemão": {"lang": "Alemão", "culture": "Alemanha"},
"Italiano": {"lang": "Italiano", "culture": "Itália"},
"Coreano": {"lang": "Coreano", "culture": "Coreia do Sul"},
"Árabe": {"lang": "Árabe", "culture": "Países Árabes"},
"Romeno": {"lang": "Romeno", "culture": "Romênia"},
"Sueco": {"lang": "Sueco", "culture": "Suécia"},
"Holandês": {"lang": "Holandês", "culture": "Holanda"},
"Polonês": {"lang": "Polonês", "culture": "Polônia"},
"Grego": {"lang": "Grego", "culture": "Grécia"},
"Russo": {"lang": "Russo", "culture": "Rússia"},
"Turco": {"lang": "Turco", "culture": "Turquia"},
"Tcheco": {"lang": "Tcheco", "culture": "República Tcheca"},
"Húngaro": {"lang": "Húngaro", "culture": "Hungria"}
};
TradutorApp.FOLDER_NAME_MAP = {
"Português (Brasil)": "Portugues-BR",
"Inglês (Estados Unidos)": "Ingles-US",
"Espanhol (México)": "Espanhol-MX",
"Francês": "Frances-FR",
"Alemão": "Alemao-DE",
"Italiano": "Italiano-IT",
"Coreano": "Coreano-KR",
"Árabe": "Arabe-AR",
"Romeno": "Romeno-RO",
"Sueco": "Sueco-SE",
"Holandês": "Holandes-NL",
"Polonês": "Polones-PL",
"Grego": "Grego-GR",
"Russo": "Russo-RU",
"Turco": "Turco-TR",
"Tcheco": "Tcheco-CZ",
"Húngaro": "Hungaro-HU"
};
// Template de tradução
TradutorApp.BASE_PROMPT_TEMPLATE_WITH_TITLE = `Sua função é traduzir e adaptar um texto do Português para o {target_language}, mantendo o tom, estilo e intenção do original, mas adaptando para a cultura da {target_culture}.
==== INSTRUÇÕES DETALHADAS ====
OBJETIVO: Traduza o texto fornecido no final deste prompt para {target_language} e adapte-o culturalmente para {target_culture}.
DIRETRIZES DE ADAPTAÇÃO CULTURAL:
1. Adapte nomes de personagens para equivalentes comuns e naturais na {target_culture}
2. Substitua locais (cidades, ruas, bairros) por equivalentes típicos da {target_culture}
3. Adapte referências a comidas, bebidas e hábitos alimentares para opções equivalentes na cultura-alvo
4. Modifique instituições (escolas, universidades, órgãos governamentais) para equivalentes na {target_culture}
5. Adapte expressões idiomáticas e gírias para equivalentes que transmitam o mesmo significado e impacto emocional
6. Ajuste referências a feriados, celebrações e tradições para equivalentes culturais apropriados
7. Mantenha o mesmo nível de formalidade/informalidade e o mesmo tom emocional do original
PRESERVAÇÃO ESTILÍSTICA:
- Mantenha a estrutura de parágrafos e a divisão de ideias do original
- Preserve o estilo narrativo (primeira/terceira pessoa, tempo verbal predominante)
- Mantenha o mesmo nível de detalhamento e descrições
- Preserve o ritmo e cadência do texto original sempre que possível
OUTPUT FORMATADO OBRIGATÓRIO:
1. Na PRIMEIRA linha, coloque APENAS o título traduzido/adaptado (incluindo número inicial, se houver), precedido EXATAMENTE por \"TITULO_TRADUZIDO: \". Exemplo:
TITULO_TRADUZIDO: 36. [Título adaptado aqui]
2. Deixe UMA linha em branco APÓS a linha do título.
3. Em seguida, coloque o CORPO do texto principal traduzido/adaptado, sem comentários adicionais e sem marcações de textos em negrito ou títulos internos. NÃO repita o título no corpo do texto.
==== TÍTULO ORIGINAL ====
{original_title}
==== TEXTO ORIGINAL PARA TRADUÇÃO ====
{original_text}`;
// URL base da API Gemini
TradutorApp.API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/";
// ----- Variáveis Globais -----
TradutorApp.projects = [];
TradutorApp.templates = {};
TradutorApp.jobs = {};
TradutorApp.completedTranslations = [];
TradutorApp.jobIdCounter = 1;
TradutorApp.activeTranslationCount = 0;
TradutorApp.runningTranslations = new Set();
// ----- Elementos DOM -----
const elements = TradutorApp.elements = {
toggleLanguagesBtn: document.getElementById("tradToggleLanguagesBtn"),
languageCheckboxes: document.querySelectorAll(".trad-language-checkboxes input[type=\"checkbox\"]"),
addToQueueBtn: document.getElementById("tradAddToQueueBtn"),
newProjectBtn: document.getElementById("tradNewProjectBtn"),
createProjectBtn: document.getElementById("tradCreateProjectBtn"),
projectSelect: document.getElementById("tradProjectSelect"),
saveTemplateBtn: document.getElementById("tradSaveTemplateBtn"),
saveTemplateConfirmBtn: document.getElementById("tradSaveTemplateConfirmBtn"),
templateSelect: document.getElementById("tradTemplateSelect"),
manageTemplatesBtn: document.getElementById("tradManageTemplatesBtn"),
apiKeyInput: document.getElementById("tradApiKey"),
toggleApiVisibilityBtn: document.querySelector(".trad-toggle-api-visibility"),
saveApiKeyCheckbox: document.getElementById("tradSaveApiKey"),
modelSelect: document.getElementById("tradModelSelect"),
concurrentTranslationsInput: document.getElementById("tradConcurrentTranslations"),
titleInput: document.getElementById("tradTitleInput"),
originalTextInput: document.getElementById("tradOriginalTextInput"),
jobsContainer: document.getElementById("tradJobsContainer"),
jobCountBadge: document.getElementById("tradJobCount"),
resultsContainer: document.getElementById("tradResultsContainer"),
resultsCountBadge: document.getElementById("tradResultsCount"),
downloadAllBtn: document.getElementById("tradDownloadAllBtn"),
deleteAllBtn: document.getElementById("tradDeleteAllBtn"),
resultsProjectFilter: document.getElementById("tradResultsProjectFilter"),
newProjectModal: new bootstrap.Modal(document.getElementById("tradNewProjectModal")),
newProjectNameInput: document.getElementById("tradNewProjectName"),
saveTemplateModal: new bootstrap.Modal(document.getElementById("tradSaveTemplateModal")),
templateNameInput: document.getElementById("tradTemplateName"),
manageTemplatesModal: new bootstrap.Modal(document.getElementById("tradManageTemplatesModal")),
templatesList: document.getElementById("tradTemplatesList"),
noTemplatesMessage: document.getElementById("tradNoTemplatesMessage"),
notificationModal: new bootstrap.Modal(document.getElementById("tradNotificationModal")),
notificationContent: document.getElementById("tradNotificationContent"),
jobItemTemplate: document.getElementById("tradJobItemTemplate"),
translationResultTemplate: document.getElementById("tradTranslationResultTemplate")
};
// ----- Funções -----
// Função para mostrar notificações
TradutorApp.showNotification = function(message) {
elements.notificationContent.textContent = message;
elements.notificationModal.show();
};
// Função para carregar dados do localStorage
TradutorApp.loadLocalStorage = function() {
// Carregar API Key
const savedApiKey = localStorage.getItem("tradApiKey");
if (savedApiKey) {
elements.apiKeyInput.value = savedApiKey;
elements.saveApiKeyCheckbox.checked = true;
}
// Carregar Projetos
const savedProjects = localStorage.getItem("tradProjects");
if (savedProjects) {
TradutorApp.projects = JSON.parse(savedProjects);
TradutorApp.updateProjectSelects();
}
// Carregar Templates
const savedTemplates = localStorage.getItem("tradTemplates");
if (savedTemplates) {
TradutorApp.templates = JSON.parse(savedTemplates);
TradutorApp.updateTemplateSelect();
TradutorApp.updateManageTemplatesModal();
}
// Carregar Traduções Concluídas (se não estiver usando WordPress)
if (typeof tradutor_ajax === "undefined" || !tradutor_ajax.ajax_url) {
const savedResults = localStorage.getItem("tradCompletedTranslations");
if (savedResults) {
TradutorApp.completedTranslations = JSON.parse(savedResults);
TradutorApp.updateResultsDisplay();
}
} else {
// Carregar traduções do WordPress via AJAX
TradutorApp.loadTranslationsFromWordPress();
}
};
// Função para salvar API Key
TradutorApp.saveApiKey = function() {
if (elements.saveApiKeyCheckbox.checked) {
localStorage.setItem("tradApiKey", elements.apiKeyInput.value);
} else {
localStorage.removeItem("tradApiKey");
}
};
// Função para alternar visibilidade da API Key
TradutorApp.toggleApiVisibility = function() {
const input = elements.apiKeyInput;
const button = elements.toggleApiVisibilityBtn;
if (input.type === "password") {
input.type = "text";
button.textContent = "🙈";
} else {
input.type = "password";
button.textContent = "👁️";
}
};
// Função para atualizar selects de projeto
TradutorApp.updateProjectSelects = function() {
const selects = [elements.projectSelect, elements.resultsProjectFilter];
selects.forEach(select => {
// Limpar opções existentes (exceto a primeira padrão)
while (select.options.length > 1) {
select.remove(1);
}
// Adicionar projetos
TradutorApp.projects.forEach(project => {
const option = document.createElement("option");
option.value = project;
option.textContent = project;
select.appendChild(option);
});
});
};
// Função para criar novo projeto
TradutorApp.createNewProject = function() {
const projectName = elements.newProjectNameInput.value.trim();
if (projectName && !TradutorApp.projects.includes(projectName)) {
TradutorApp.projects.push(projectName);
TradutorApp.projects.sort();
localStorage.setItem("tradProjects", JSON.stringify(TradutorApp.projects));
TradutorApp.updateProjectSelects();
elements.projectSelect.value = projectName; // Seleciona o novo projeto
elements.newProjectNameInput.value = "";
elements.newProjectModal.hide();
} else if (!projectName) {
TradutorApp.showNotification("Por favor, insira um nome para o projeto.");
} else {
TradutorApp.showNotification("Um projeto com este nome já existe.");
}
};
// Função para atualizar select de template
TradutorApp.updateTemplateSelect = function() {
const select = elements.templateSelect;
while (select.options.length > 1) {
select.remove(1);
}
Object.keys(TradutorApp.templates).sort().forEach(name => {
const option = document.createElement("option");
option.value = name;
option.textContent = name;
select.appendChild(option);
});
};
// Função para abrir modal de salvar template
TradutorApp.openSaveTemplateModal = function() {
const selectedTemplate = elements.templateSelect.value;
if (selectedTemplate && selectedTemplate !== "Selecionar template...") {
elements.templateNameInput.value = selectedTemplate;
} else {
elements.templateNameInput.value = "";
}
elements.saveTemplateModal.show();
};
// Função para salvar template
TradutorApp.saveTemplate = function() {
const templateName = elements.templateNameInput.value.trim();
if (!templateName) {
TradutorApp.showNotification("Por favor, insira um nome para o template.");
return;
}
const selectedLanguages = Array.from(elements.languageCheckboxes)
.filter(cb => cb.checked)
.map(cb => cb.value);
TradutorApp.templates[templateName] = {
languages: selectedLanguages,
model: elements.modelSelect.value,
concurrent: elements.concurrentTranslationsInput.value
};
localStorage.setItem("tradTemplates", JSON.stringify(TradutorApp.templates));
TradutorApp.updateTemplateSelect();
TradutorApp.updateManageTemplatesModal();
elements.templateSelect.value = templateName;
elements.saveTemplateModal.hide();
TradutorApp.showNotification(`Template "${templateName}" salvo com sucesso.`);
};
// Função para carregar template selecionado
TradutorApp.loadSelectedTemplate = function() {
const templateName = elements.templateSelect.value;
if (templateName && templateName !== "Selecionar template..." && TradutorApp.templates[templateName]) {
const template = TradutorApp.templates[templateName];
// Marcar idiomas
elements.languageCheckboxes.forEach(cb => {
cb.checked = template.languages.includes(cb.value);
});
// Definir modelo e concorrência
elements.modelSelect.value = template.model;
elements.concurrentTranslationsInput.value = template.concurrent;
// Atualizar botão de selecionar todos
TradutorApp.updateToggleLanguagesButton();
}
};
// Função para atualizar modal de gerenciamento de templates
TradutorApp.updateManageTemplatesModal = function() {
const list = elements.templatesList;
list.innerHTML = ""; // Limpar lista
const templateNames = Object.keys(TradutorApp.templates).sort();
if (templateNames.length === 0) {
elements.noTemplatesMessage.style.display = "block";
list.style.display = "none";
} else {
elements.noTemplatesMessage.style.display = "none";
list.style.display = "block";
templateNames.forEach(name => {
const item = document.createElement("div");
item.className = "list-group-item d-flex justify-content-between align-items-center";
item.textContent = name;
const deleteBtn = document.createElement("button");
deleteBtn.className = "btn btn-sm btn-outline-danger";
deleteBtn.innerHTML = "×";
deleteBtn.title = "Excluir Template";
deleteBtn.onclick = function() {
TradutorApp.deleteTemplate(name);
};
item.appendChild(deleteBtn);
list.appendChild(item);
});
}
};
// Função para deletar template
TradutorApp.deleteTemplate = function(templateName) {
if (confirm(`Tem certeza que deseja excluir o template "${templateName}"?`)) {
delete TradutorApp.templates[templateName];
localStorage.setItem("tradTemplates", JSON.stringify(TradutorApp.templates));
TradutorApp.updateTemplateSelect();
TradutorApp.updateManageTemplatesModal();
TradutorApp.showNotification(`Template "${templateName}" excluído.`);
}
};
// Função para alternar seleção de idiomas
TradutorApp.toggleAllLanguages = function() {
const allChecked = Array.from(elements.languageCheckboxes).every(cb => cb.checked);
elements.languageCheckboxes.forEach(cb => {
cb.checked = !allChecked;
});
TradutorApp.updateToggleLanguagesButton();
};
// Função para atualizar texto do botão de selecionar/desselecionar todos
TradutorApp.updateToggleLanguagesButton = function() {
const allChecked = Array.from(elements.languageCheckboxes).every(cb => cb.checked);
elements.toggleLanguagesBtn.textContent = allChecked ? "Desselecionar Todos" : "Selecionar Todos";
};
// Função para adicionar job à fila
TradutorApp.addJobToQueue = function() {
const apiKey = elements.apiKeyInput.value.trim();
const title = elements.titleInput.value.trim();
const originalText = elements.originalTextInput.value.trim();
const selectedLanguages = Array.from(elements.languageCheckboxes)
.filter(cb => cb.checked)
.map(cb => cb.value);
const projectName = elements.projectSelect.value === "Nenhum projeto" ? "" : elements.projectSelect.value;
if (!apiKey) {
TradutorApp.showNotification("Por favor, insira sua API Key.");
return;
}
if (!title) {
TradutorApp.showNotification("Por favor, insira um título.");
return;
}
if (!originalText) {
TradutorApp.showNotification("Por favor, insira o texto original.");
return;
}
if (selectedLanguages.length === 0) {
TradutorApp.showNotification("Por favor, selecione pelo menos um idioma de destino.");
return;
}
const jobId = `job-${TradutorApp.jobIdCounter++}`;
const jobData = {
id: jobId,
title: title,
originalText: originalText,
languages: selectedLanguages,
projectName: projectName,
model: elements.modelSelect.value,
apiKey: apiKey,
status: "Pendente",
languageStatus: selectedLanguages.reduce((acc, lang) => {
acc[lang] = { status: "Pendente", result: null, error: null };
return acc;
}, {})
};
TradutorApp.jobs[jobId] = jobData;
TradutorApp.addJobToUI(jobData);
TradutorApp.updateJobCountBadge();
TradutorApp.processQueue();
// Limpar campos após adicionar (opcional)
// elements.titleInput.value = "";
// elements.originalTextInput.value = "";
// Mudar para a aba da fila
const queueTab = new bootstrap.Tab(document.getElementById("trad-queue-tab"));
queueTab.show();
};
// Função para adicionar job à interface da fila
TradutorApp.addJobToUI = function(jobData) {
const template = elements.jobItemTemplate.content.cloneNode(true);
const jobCard = template.querySelector(".trad-job-card");
jobCard.dataset.jobId = jobData.id;
jobCard.querySelector(".trad-job-title").textContent = jobData.title;
jobCard.querySelector(".trad-job-status").textContent = jobData.status;
// Botão de detalhes
const detailsBtn = jobCard.querySelector(".trad-toggle-details-btn");
detailsBtn.onclick = () => TradutorApp.toggleJobDetails(jobData.id);
// Botão de cancelar
const cancelBtn = jobCard.querySelector(".trad-cancel-job-btn");
cancelBtn.onclick = () => TradutorApp.cancelJob(jobData.id);
// Adicionar status por idioma
const detailsContainer = jobCard.querySelector(".trad-job-details");
Object.entries(jobData.languageStatus).forEach(([lang, statusInfo]) => {
const langDiv = document.createElement("div");
langDiv.className = "d-flex justify-content-between align-items-center mb-1";
langDiv.dataset.lang = lang;
langDiv.innerHTML = `
${lang}
${statusInfo.status}
`;
detailsContainer.appendChild(langDiv);
});
elements.jobsContainer.appendChild(template);
};
// Função para atualizar status de um job na UI
TradutorApp.updateJobStatusUI = function(jobId, overallStatus, language, langStatus, errorMessage = null) {
const jobCard = elements.jobsContainer.querySelector(`.trad-job-card[data-job-id="${jobId}"]`);
if (!jobCard) return;
// Atualizar status geral
if (overallStatus) {
jobCard.querySelector(".trad-job-status").textContent = overallStatus;
TradutorApp.jobs[jobId].status = overallStatus;
}
// Atualizar status do idioma
if (language && langStatus) {
const langDiv = jobCard.querySelector(`.trad-job-details div[data-lang="${language}"]`);
if (langDiv) {
const badge = langDiv.querySelector(".trad-badge-status");
badge.textContent = langStatus;
badge.className = "badge trad-badge-status "; // Reset classes
switch(langStatus) {
case "Processando": badge.classList.add("bg-info"); break;
case "Concluído": badge.classList.add("bg-success"); break;
case "Erro": badge.classList.add("bg-danger"); break;
case "Cancelado": badge.classList.add("bg-warning", "text-dark"); break;
default: badge.classList.add("bg-secondary");
}
// Adicionar tooltip de erro
if (langStatus === "Erro" && errorMessage) {
badge.title = errorMessage;
new bootstrap.Tooltip(badge); // Inicializar tooltip
}
}
TradutorApp.jobs[jobId].languageStatus[language].status = langStatus;
if (errorMessage) {
TradutorApp.jobs[jobId].languageStatus[language].error = errorMessage;
}
}
// Atualizar estado do botão cancelar
if (overallStatus === "Concluído" || overallStatus === "Erro" || overallStatus === "Cancelado") {
const cancelBtn = jobCard.querySelector(".trad-cancel-job-btn");
if (cancelBtn) cancelBtn.disabled = true;
}
};
// Função para mostrar/ocultar detalhes do job
TradutorApp.toggleJobDetails = function(jobId) {
const jobCard = elements.jobsContainer.querySelector(`.trad-job-card[data-job-id="${jobId}"]`);
if (!jobCard) return;
const detailsDiv = jobCard.querySelector(".trad-job-details");
const button = jobCard.querySelector(".trad-toggle-details-btn");
if (detailsDiv.style.display === "none" || detailsDiv.style.display === "") {
detailsDiv.style.display = "block";
button.textContent = "-";
} else {
detailsDiv.style.display = "none";
button.textContent = "+";
}
};
// Função para cancelar job
TradutorApp.cancelJob = function(jobId) {
if (!TradutorApp.jobs[jobId]) return;
const job = TradutorApp.jobs[jobId];
if (job.status === "Concluído" || job.status === "Erro" || job.status === "Cancelado") {
return; // Não pode cancelar job finalizado
}
// Marcar como cancelado
job.status = "Cancelado";
Object.keys(job.languageStatus).forEach(lang => {
if (job.languageStatus[lang].status === "Pendente" || job.languageStatus[lang].status === "Processando") {
job.languageStatus[lang].status = "Cancelado";
}
});
// Remover das traduções ativas se estiver rodando
if (TradutorApp.runningTranslations.has(jobId)) {
TradutorApp.runningTranslations.delete(jobId);
TradutorApp.activeTranslationCount--;
}
TradutorApp.updateJobStatusUI(jobId, "Cancelado");
Object.keys(job.languageStatus).forEach(lang => {
TradutorApp.updateJobStatusUI(jobId, null, lang, job.languageStatus[lang].status);
});
TradutorApp.updateJobCountBadge();
TradutorApp.processQueue(); // Tentar processar o próximo
};
// Função para atualizar contador de jobs
TradutorApp.updateJobCountBadge = function() {
const pendingOrProcessing = Object.values(TradutorApp.jobs).filter(
job => job.status === "Pendente" || job.status === "Processando"
).length;
elements.jobCountBadge.textContent = `${pendingOrProcessing} job(s) em processamento`;
};
// Função para processar a fila
TradutorApp.processQueue = function() {
const maxConcurrent = parseInt(elements.concurrentTranslationsInput.value) || 1;
while (TradutorApp.activeTranslationCount job.status === "Pendente");
if (!nextJob) {
break; // Sem jobs pendentes
}
TradutorApp.startJob(nextJob.id);
}
};
// Função para iniciar um job
TradutorApp.startJob = function(jobId) {
if (!TradutorApp.jobs[jobId] || TradutorApp.jobs[jobId].status !== "Pendente") {
return;
}
TradutorApp.activeTranslationCount++;
TradutorApp.runningTranslations.add(jobId);
TradutorApp.updateJobStatusUI(jobId, "Processando");
TradutorApp.updateJobCountBadge();
const job = TradutorApp.jobs[jobId];
const languagesToTranslate = job.languages.filter(lang => job.languageStatus[lang].status === "Pendente");
// Usar Promise.all para gerenciar todas as traduções do job
Promise.all(languagesToTranslate.map(lang => TradutorApp.translateLanguage(jobId, lang)))
.then(() => {
// Verificar se todas as traduções foram concluídas ou tiveram erro (e não foram canceladas)
const allDone = Object.values(job.languageStatus).every(
statusInfo => statusInfo.status === "Concluído" || statusInfo.status === "Erro"
);
if (job.status !== "Cancelado") {
if (allDone) {
const hasError = Object.values(job.languageStatus).some(s => s.status === "Erro");
TradutorApp.updateJobStatusUI(jobId, hasError ? "Erro" : "Concluído");
if (!hasError) {
TradutorApp.moveJobToResults(jobId);
}
} else {
// Se chegou aqui, pode ser que algumas foram canceladas no meio
// O status geral já deve ter sido atualizado para "Cancelado" ou "Erro" se for o caso
if (job.status === "Processando") {
// Se ainda está processando mas algo deu errado (ex: cancelamento parcial)
// Marca como erro para indicar que não completou normalmente
TradutorApp.updateJobStatusUI(jobId, "Erro");
}
}
}
})
.catch(error => {
// Erro inesperado no Promise.all (não deveria acontecer com o catch individual)
console.error("Erro inesperado no processamento do job:", error);
if (job.status !== "Cancelado") {
TradutorApp.updateJobStatusUI(jobId, "Erro");
}
})
.finally(() => {
// Só decrementa se o job ainda estiver marcado como rodando
if (TradutorApp.runningTranslations.has(jobId)) {
TradutorApp.runningTranslations.delete(jobId);
TradutorApp.activeTranslationCount--;
}
TradutorApp.updateJobCountBadge();
TradutorApp.processQueue(); // Tentar iniciar o próximo job
});
};
// Função para traduzir um idioma específico de um job
TradutorApp.translateLanguage = async function(jobId, language) {
const job = TradutorApp.jobs[jobId];
if (!job || job.status === "Cancelado" || job.languageStatus[language].status !== "Pendente") {
return; // Job cancelado ou idioma já processado/em processamento
}
TradutorApp.updateJobStatusUI(jobId, null, language, "Processando");
const cultureInfo = TradutorApp.LANG_CULTURE_MAP[language] || {lang: language, culture: language};
const prompt = TradutorApp.BASE_PROMPT_TEMPLATE_WITH_TITLE
.replace(/{target_language}/g, cultureInfo.lang)
.replace(/{target_culture}/g, cultureInfo.culture)
.replace("{original_title}", job.title)
.replace("{original_text}", job.originalText);
const apiUrl = `${TradutorApp.API_BASE_URL}${job.model}:generateContent?key=${job.apiKey}`;
try {
const response = await fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
contents: [{
parts: [{
text: prompt
}]
}]
})
});
// Verificar se o job foi cancelado enquanto esperava a resposta
if (job.status === "Cancelado") {
TradutorApp.updateJobStatusUI(jobId, null, language, "Cancelado");
return;
}
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
} catch(e) {
throw new Error(`Erro HTTP ${response.status}: ${response.statusText}`);
}
const errorMessage = errorData?.error?.message || `Erro HTTP ${response.status}`;
console.error(`Erro na API Gemini para ${language}:`, errorMessage, errorData);
throw new Error(errorMessage);
}
const data = await response.json();
// Verificar se o job foi cancelado após receber a resposta
if (job.status === "Cancelado") {
TradutorApp.updateJobStatusUI(jobId, null, language, "Cancelado");
return;
}
// Processar a resposta
if (data.candidates && data.candidates[0] && data.candidates[0].content && data.candidates[0].content.parts && data.candidates[0].content.parts[0]) {
const fullText = data.candidates[0].content.parts[0].text;
// Extrair título e corpo
const titleMatch = fullText.match(/^TITULO_TRADUZIDO:\s*(.*)/);
const translatedTitle = titleMatch ? titleMatch[1].trim() : `[${language}] ${job.title}`; // Fallback
const bodyText = titleMatch ? fullText.substring(titleMatch[0].length).trim() : fullText.trim();
job.languageStatus[language].status = "Concluído";
job.languageStatus[language].result = {
title: translatedTitle,
text: bodyText
};
TradutorApp.updateJobStatusUI(jobId, null, language, "Concluído");
} else {
// Resposta inesperada da API
console.error(`Resposta inesperada da API Gemini para ${language}:`, data);
throw new Error("Formato de resposta inesperado da API.");
}
} catch (error) {
console.error(`Erro ao traduzir ${language} para o job ${jobId}:`, error);
job.languageStatus[language].status = "Erro";
job.languageStatus[language].error = error.message;
TradutorApp.updateJobStatusUI(jobId, "Erro", language, "Erro", error.message);
// Não rejeita a promessa aqui para que Promise.all possa continuar
}
};
// Função para mover job concluído para a aba de resultados
TradutorApp.moveJobToResults = function(jobId) {
const job = TradutorApp.jobs[jobId];
if (!job || job.status !== "Concluído") return;
const dateCompleted = new Date().toISOString();
Object.entries(job.languageStatus).forEach(([lang, statusInfo]) => {
if (statusInfo.status === "Concluído") {
const resultData = {
id: `${jobId}-${lang}`,
jobId: jobId,
title: statusInfo.result.title,
language: lang,
projectName: job.projectName,
originalText: job.originalText, // Guardar original para referência
translatedText: statusInfo.result.text,
dateCompleted: dateCompleted,
status: "Concluído" // Status individual da tradução
};
// Salvar no WordPress ou localStorage
if (typeof tradutor_ajax !== "undefined" && tradutor_ajax.ajax_url) {
TradutorApp.saveTranslationToWordPress(resultData);
} else {
TradutorApp.completedTranslations.push(resultData);
}
}
});
// Remover da UI da fila e dos dados de jobs ativos
const jobCard = elements.jobsContainer.querySelector(`.trad-job-card[data-job-id="${jobId}"]`);
if (jobCard) {
jobCard.remove();
}
delete TradutorApp.jobs[jobId];
// Atualizar contadores e display de resultados
TradutorApp.updateJobCountBadge();
if (typeof tradutor_ajax === "undefined" || !tradutor_ajax.ajax_url) {
localStorage.setItem("tradCompletedTranslations", JSON.stringify(TradutorApp.completedTranslations));
TradutorApp.updateResultsDisplay();
}
// Se usando WP, o display será atualizado após o save/load AJAX
};
// Função para salvar tradução no WordPress via AJAX
TradutorApp.saveTranslationToWordPress = function(resultData) {
$.ajax({
url: tradutor_ajax.ajax_url,
type: "POST",
data: {
action: "salvar_traducao",
security: tradutor_ajax.nonce,
traducao: JSON.stringify(resultData)
},
success: function(response) {
if (response.success) {
console.log("Tradução salva no WP:", response.data.message);
// Recarregar traduções para atualizar a lista
TradutorApp.loadTranslationsFromWordPress();
} else {
console.error("Erro ao salvar tradução no WP:", response.data);
TradutorApp.showNotification("Erro ao salvar tradução no servidor: " + response.data);
// Adicionar ao localStorage como fallback?
// TradutorApp.completedTranslations.push(resultData);
// localStorage.setItem("tradCompletedTranslations", JSON.stringify(TradutorApp.completedTranslations));
// TradutorApp.updateResultsDisplay();
}
},
error: function(jqXHR, textStatus, errorThrown) {
console.error("Erro AJAX ao salvar tradução:", textStatus, errorThrown);
TradutorApp.showNotification("Erro de comunicação ao salvar tradução no servidor.");
}
});
};
// Função para carregar traduções do WordPress via AJAX
TradutorApp.loadTranslationsFromWordPress = function() {
$.ajax({
url: tradutor_ajax.ajax_url,
type: "POST",
data: {
action: "carregar_traducoes",
security: tradutor_ajax.nonce
},
success: function(response) {
if (response.success) {
TradutorApp.completedTranslations = response.data.traducoes;
TradutorApp.updateResultsDisplay();
} else {
console.error("Erro ao carregar traduções do WP:", response.data);
TradutorApp.showNotification("Erro ao carregar traduções do servidor: " + response.data);
}
},
error: function(jqXHR, textStatus, errorThrown) {
console.error("Erro AJAX ao carregar traduções:", textStatus, errorThrown);
TradutorApp.showNotification("Erro de comunicação ao carregar traduções do servidor.");
}
});
};
// Função para atualizar display de resultados
TradutorApp.updateResultsDisplay = function() {
const container = elements.resultsContainer;
container.innerHTML = ""; // Limpar resultados
const filter = elements.resultsProjectFilter.value;
const filteredResults = TradutorApp.completedTranslations.filter(result =>
filter === "all" || result.projectName === filter
);
if (filteredResults.length === 0) {
container.innerHTML =
`
${filter === "all" ? "Nenhuma tradução concluída ainda." : `Nenhuma tradução concluída para o projeto \"${filter}\".`}
`;
} else {
filteredResults.sort((a, b) => new Date(b.dateCompleted) - new Date(a.dateCompleted)); // Ordenar por data
filteredResults.forEach(result => {
const template = elements.translationResultTemplate.content.cloneNode(true);
const resultCard = template.querySelector(".trad-translation-result");
resultCard.dataset.jobId = result.jobId;
resultCard.dataset.lang = result.language;
resultCard.dataset.resultId = result.id; // Usar ID único (do BD ou gerado)
resultCard.querySelector(".trad-result-title").textContent = result.title;
resultCard.querySelector(".trad-result-meta").textContent =
`${result.language} ${result.projectName ? `(${result.projectName})` : ""} - ${new Date(result.dateCompleted).toLocaleString()}`;
const translationTextDiv = resultCard.querySelector(".trad-translation-text");
translationTextDiv.textContent = result.translatedText;
// Botão Visualizar
const viewBtn = resultCard.querySelector(".trad-view-translation-btn");
viewBtn.onclick = () => {
const collapse = new bootstrap.Collapse(translationTextDiv, { toggle: true });
};
// Botão Baixar
const downloadBtn = resultCard.querySelector(".trad-download-translation-btn");
downloadBtn.onclick = () => TradutorApp.downloadTranslation(result);
container.appendChild(template);
});
}
// Atualizar contador
elements.resultsCountBadge.textContent = `${TradutorApp.completedTranslations.length} traduções disponíveis`;
// Habilitar/desabilitar botões gerais
const hasResults = TradutorApp.completedTranslations.length > 0;
elements.downloadAllBtn.disabled = !hasResults;
elements.deleteAllBtn.disabled = !hasResults;
};
// Função para baixar uma tradução individual
TradutorApp.downloadTranslation = function(result) {
const filename = `${TradutorApp.sanitizeFilename(result.title)} [${TradutorApp.FOLDER_NAME_MAP[result.language] || result.language}].txt`;
const content = `TITULO_TRADUZIDO: ${result.title}\n\n${result.translatedText}`;
TradutorApp.triggerDownload(filename, content);
};
// Função para baixar todas as traduções visíveis (filtradas)
TradutorApp.downloadAllTranslations = function() {
const filter = elements.resultsProjectFilter.value;
const resultsToDownload = TradutorApp.completedTranslations.filter(result =>
filter === "all" || result.projectName === filter
);
if (resultsToDownload.length === 0) {
TradutorApp.showNotification("Não há traduções para baixar.");
return;
}
// Se for apenas uma, baixa direto
if (resultsToDownload.length === 1) {
TradutorApp.downloadTranslation(resultsToDownload[0]);
return;
}
// Se forem várias, perguntar sobre ZIP (requer JSZip)
if (typeof JSZip === "undefined") {
// Baixar arquivos separados sequencialmente (pode ser bloqueado pelo navegador)
if (confirm(`Você está prestes a baixar ${resultsToDownload.length} arquivos separados. Seu navegador pode bloquear múltiplos downloads. Deseja continuar?`)) {
resultsToDownload.forEach((result, index) => {
// Pequeno delay para tentar evitar bloqueio
setTimeout(() => TradutorApp.downloadTranslation(result), index * 500);
});
}
} else {
// Criar ZIP (requer que a biblioteca JSZip esteja carregada)
const zip = new JSZip();
resultsToDownload.forEach(result => {
const filename = `${TradutorApp.sanitizeFilename(result.title)} [${TradutorApp.FOLDER_NAME_MAP[result.language] || result.language}].txt`;
const content = `TITULO_TRADUZIDO: ${result.title}\n\n${result.translatedText}`;
zip.file(filename, content);
});
zip.generateAsync({type:"blob"})
.then(function(content) {
const zipFilename = filter === "all" ? "traducoes_concluidas.zip" : `traducoes_${TradutorApp.sanitizeFilename(filter)}.zip`;
TradutorApp.triggerDownload(zipFilename, content);
});
}
};
// Função auxiliar para disparar download
TradutorApp.triggerDownload = function(filename, content) {
const blob = (content instanceof Blob) ? content : new Blob([content], { type: "text/plain;charset=utf-8" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
};
// Função para sanitizar nomes de arquivo
TradutorApp.sanitizeFilename = function(name) {
// Remove caracteres inválidos e substitui espaços
return name.replace(/[:"/\\|?*\x00-\x1F]/g, ").replace(/\s+/g, "_");
};
// Função para excluir todas as traduções (visíveis/filtradas)
TradutorApp.deleteAllTranslations = function() {
const filter = elements.resultsProjectFilter.value;
const resultsToDelete = TradutorApp.completedTranslations.filter(result =>
filter === "all" || result.projectName === filter
);
const count = resultsToDelete.length;
if (count === 0) {
TradutorApp.showNotification("Não há traduções para excluir.");
return;
}
const message = filter === "all"
? `Tem certeza que deseja excluir TODAS as ${count} traduções concluídas?`
: `Tem certeza que deseja excluir as ${count} traduções concluídas do projeto \"${filter}\"?`;
if (confirm(message)) {
if (typeof tradutor_ajax !== "undefined" && tradutor_ajax.ajax_url) {
// Excluir via AJAX (requer endpoint no backend)
// Implementar chamada AJAX para exclusão em massa ou individual
TradutorApp.showNotification("Função de exclusão em massa no servidor ainda não implementada.");
// Exemplo de exclusão individual sequencial (menos eficiente):
/*
resultsToDelete.forEach(result => {
TradutorApp.deleteSingleTranslation(result.id);
});
*/
} else {
// Excluir do localStorage
if (filter === "all") {
TradutorApp.completedTranslations = [];
} else {
TradutorApp.completedTranslations = TradutorApp.completedTranslations.filter(result =>
result.projectName !== filter
);
}
localStorage.setItem("tradCompletedTranslations", JSON.stringify(TradutorApp.completedTranslations));
TradutorApp.updateResultsDisplay();
TradutorApp.showNotification("Traduções excluídas com sucesso.");
}
}
};
// Função para excluir uma tradução individual (necessário para WP)
TradutorApp.deleteSingleTranslation = function(resultId) {
if (typeof tradutor_ajax !== "undefined" && tradutor_ajax.ajax_url) {
$.ajax({
url: tradutor_ajax.ajax_url,
type: "POST",
data: {
action: "excluir_traducao",
security: tradutor_ajax.nonce,
id: resultId // Passar o ID único da tradução
},
success: function(response) {
if (response.success) {
console.log("Tradução excluída:", resultId);
// Remover da lista local e atualizar UI
TradutorApp.completedTranslations = TradutorApp.completedTranslations.filter(t => t.id !== resultId);
TradutorApp.updateResultsDisplay();
} else {
console.error("Erro ao excluir tradução no WP:", response.data);
TradutorApp.showNotification("Erro ao excluir tradução no servidor: " + response.data);
}
},
error: function(jqXHR, textStatus, errorThrown) {
console.error("Erro AJAX ao excluir tradução:", textStatus, errorThrown);
TradutorApp.showNotification("Erro de comunicação ao excluir tradução.");
}
});
} else {
// Lógica para localStorage (já coberta por deleteAllTranslations)
}
};
// ----- Event Listeners -----
elements.toggleLanguagesBtn.addEventListener("click", TradutorApp.toggleAllLanguages);
elements.languageCheckboxes.forEach(cb => {
cb.addEventListener("change", TradutorApp.updateToggleLanguagesButton);
});
elements.saveApiKeyCheckbox.addEventListener("change", TradutorApp.saveApiKey);
elements.apiKeyInput.addEventListener("blur", TradutorApp.saveApiKey); // Salvar ao perder foco também
elements.toggleApiVisibilityBtn.addEventListener("click", TradutorApp.toggleApiVisibility);
elements.newProjectBtn.addEventListener("click", () => elements.newProjectModal.show());
elements.createProjectBtn.addEventListener("click", TradutorApp.createNewProject);
elements.saveTemplateBtn.addEventListener("click", TradutorApp.openSaveTemplateModal);
elements.saveTemplateConfirmBtn.addEventListener("click", TradutorApp.saveTemplate);
elements.templateSelect.addEventListener("change", TradutorApp.loadSelectedTemplate);
elements.manageTemplatesBtn.addEventListener("click", () => elements.manageTemplatesModal.show());
elements.addToQueueBtn.addEventListener("click", TradutorApp.addJobToQueue);
elements.downloadAllBtn.addEventListener("click", TradutorApp.downloadAllTranslations);
elements.deleteAllBtn.addEventListener("click", TradutorApp.deleteAllTranslations);
elements.resultsProjectFilter.addEventListener("change", TradutorApp.updateResultsDisplay);
// ----- Inicialização -----
TradutorApp.loadLocalStorage();
TradutorApp.updateToggleLanguagesButton(); // Estado inicial do botão
}); // Fim DOMContentLoaded
})(window); // Fim do namespace