feat(translation): complete DeepL translation workflow

Frontend:
- Add translate button click handler in submission-modal-enhanced.js
- Display loading state during translation ( Translating...)
- Update French textarea with translated content
- Auto-update word counts after translation
- Show success message with DeepL attribution

Backend:
- Add POST /api/submissions/:id/translate endpoint
- Integrate Translation.service (DeepL)
- Save translations to SubmissionTracking.documents
- Mark translations as 'translatedBy: deepl', 'approved: false'
- Return translated text with caching metadata

Complete Translation Flow:
1. User clicks 'Translate EN → FR' button
2. Frontend sends English text to /api/submissions/:id/translate
3. Backend calls DeepL API via Translation.service
4. Translation cached for 24 hours
5. Result saved to submission.documents[docType].versions[]
6. French textarea populated with translation
7. User can review/edit before saving submission

Next: Configure DEEPL_API_KEY in .env to enable translations
This commit is contained in:
TheFlow 2025-10-24 11:22:50 +13:00
parent f5b8f1a85c
commit 8528fd079b
3 changed files with 178 additions and 1 deletions

View file

@ -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
*/

View file

@ -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
};

View file

@ -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