diff --git a/public/js/admin/submission-modal-enhanced.js b/public/js/admin/submission-modal-enhanced.js index 7d3dbbd6..03b61199 100644 --- a/public/js/admin/submission-modal-enhanced.js +++ b/public/js/admin/submission-modal-enhanced.js @@ -118,6 +118,15 @@ function setupEventListeners() { copyToClipboard(); return; } + + // Translate document + if (target.hasAttribute('data-action') && target.getAttribute('data-action') === 'translate-document') { + const docType = target.getAttribute('data-doc-type'); + const fromLang = target.getAttribute('data-from-lang'); + const toLang = target.getAttribute('data-to-lang'); + translateDocument(docType, fromLang, toLang, target); + return; + } }); // Handle text input changes for word count @@ -850,6 +859,87 @@ async function copyToClipboard() { } } +/** + * Translate document using DeepL + */ +async function translateDocument(docType, fromLang, toLang, buttonEl) { + const sourceTextarea = document.getElementById(`doc-${docType}-${fromLang}`); + const targetTextarea = document.getElementById(`doc-${docType}-${toLang}`); + + if (!sourceTextarea || !targetTextarea) { + alert('Translation error: Cannot find document fields'); + return; + } + + const sourceText = sourceTextarea.value.trim(); + + if (!sourceText) { + alert('No text to translate. Please enter content in the source language first.'); + return; + } + + // Disable button and show loading state + const originalText = buttonEl.textContent; + buttonEl.disabled = true; + buttonEl.textContent = '⏳ Translating...'; + buttonEl.classList.add('opacity-50', 'cursor-not-allowed'); + + try { + const token = localStorage.getItem('admin_token'); + const submissionId = currentSubmission._id; + + const response = await fetch(`/api/submissions/${submissionId}/translate`, { + method: 'POST', + cache: 'no-store', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + docType, + fromLang, + toLang, + text: sourceText + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Translation failed'); + } + + const data = await response.json(); + + // Update target textarea with translation + targetTextarea.value = data.translatedText; + + // Update word count + const wordCount = data.translatedText.split(/\s+/).filter(w => w.length > 0).length; + const wordCountEl = targetTextarea.closest('.bg-white').querySelector('.text-xs.text-gray-600'); + if (wordCountEl) { + wordCountEl.textContent = `${wordCount.toLocaleString()} words`; + } + + // Show success message + const statusEl = document.getElementById('modal-status'); + statusEl.textContent = `✓ Translated to ${toLang.toUpperCase()} using DeepL`; + statusEl.className = 'text-sm text-green-600'; + + setTimeout(() => { + statusEl.textContent = ''; + }, 5000); + + } catch (error) { + console.error('Translation error:', error); + alert(`Translation failed: ${error.message}`); + } finally { + // Re-enable button + buttonEl.disabled = false; + buttonEl.textContent = originalText; + buttonEl.classList.remove('opacity-50', 'cursor-not-allowed'); + } +} + /** * Utility: Escape HTML */ diff --git a/src/controllers/submissions.controller.js b/src/controllers/submissions.controller.js index aef09ba0..996ce964 100644 --- a/src/controllers/submissions.controller.js +++ b/src/controllers/submissions.controller.js @@ -501,6 +501,86 @@ async function deleteSubmission(req, res) { } } +/** + * POST /api/submissions/:id/translate + * Translate document using DeepL + */ +async function translateDocument(req, res) { + try { + const { id } = req.params; + const { docType, fromLang, toLang, text } = req.body; + + // Validate inputs + if (!docType || !fromLang || !toLang || !text) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: docType, fromLang, toLang, text' + }); + } + + // Load translation service + const translationService = require('../services/Translation.service').getInstance(); + + // Check if service is available + const status = translationService.getStatus(); + if (!status.available) { + return res.status(503).json({ + success: false, + error: 'Translation service not available. Please configure DEEPL_API_KEY in .env file.' + }); + } + + console.log(`[Translation] ${fromLang} → ${toLang} for ${docType} (${text.length} chars)`); + + // Perform translation + const result = await translationService.translate(text, fromLang, toLang); + + if (result.error) { + return res.status(500).json({ + success: false, + error: result.errorMessage || 'Translation failed' + }); + } + + // Save translation to submission + const submission = await SubmissionTracking.findById(id); + if (!submission) { + return res.status(404).json({ + success: false, + error: 'Submission not found' + }); + } + + // Add/update the translated version + await submission.setDocumentVersion( + docType, + toLang, + result.translatedText, + { + translatedBy: 'deepl', + approved: false + } + ); + + console.log(`[Translation] Saved ${toLang} version of ${docType} to submission ${id}`); + + res.json({ + success: true, + translatedText: result.translatedText, + cached: result.cached, + detectedLang: result.detectedLang, + service: 'DeepL' + }); + + } catch (error) { + console.error('[Translation] Error:', error); + res.status(500).json({ + success: false, + error: error.message || 'Translation failed' + }); + } +} + module.exports = { createSubmission, getSubmissions, @@ -512,5 +592,6 @@ module.exports = { getSubmissionStatistics, getSubmissionsByPublication, exportSubmission, - deleteSubmission + deleteSubmission, + translateDocument }; diff --git a/src/routes/submissions.routes.js b/src/routes/submissions.routes.js index cf7a8ef6..219d5d3b 100644 --- a/src/routes/submissions.routes.js +++ b/src/routes/submissions.routes.js @@ -43,6 +43,12 @@ router.get('/publication/:publicationId', submissionsController.getSubmissionsBy */ router.get('/by-blog-post/:blogPostId', submissionsController.getSubmissionByBlogPost); +/** + * POST /api/submissions/:id/translate + * Translate document using DeepL + */ +router.post('/:id/translate', submissionsController.translateDocument); + /** * GET /api/submissions/:id/export * Export submission package