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)

gemini-2.5-flash
Recomenda-se máximo de 5 para evitar erros de API
Selecionar template...
Idiomas de destino
Nenhum projeto

Fila de Traduções

0 jobs em processamento

Traduções Concluídas

0 traduções disponíveis
Todos os projetos
As traduções concluídas aparecerão aqui. Você poderá visualizá-las e baixá-las.
Concluído
// 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