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:
parent
f5b8f1a85c
commit
8528fd079b
3 changed files with 178 additions and 1 deletions
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue