feat(admin): add multilingual document editor with DeepL translation
- Display English and French versions side-by-side for all documents - Add 'Translate EN → FR' button using DeepL - Show word counts for each language version - Display translation metadata (translatedBy, approved status) - Mark primary language for each document - Support readonly mode for blog-linked content Documents tab now shows: - Main Article (EN/FR) - Cover Letter (EN/FR) - Author Bio (EN/FR) - Pitch Email (EN/FR) Next: Add translation button click handler and API endpoint
This commit is contained in:
parent
ce991d86a8
commit
d0e1275f58
2 changed files with 95 additions and 9 deletions
|
|
@ -43,7 +43,7 @@
|
||||||
"last_deliberation": null
|
"last_deliberation": null
|
||||||
},
|
},
|
||||||
"FileEditHook": {
|
"FileEditHook": {
|
||||||
"timestamp": "2025-10-23T22:09:40.745Z",
|
"timestamp": "2025-10-23T22:16:58.258Z",
|
||||||
"file": "/home/theflow/projects/tractatus/public/js/admin/submission-modal-enhanced.js",
|
"file": "/home/theflow/projects/tractatus/public/js/admin/submission-modal-enhanced.js",
|
||||||
"result": "passed"
|
"result": "passed"
|
||||||
},
|
},
|
||||||
|
|
@ -58,25 +58,25 @@
|
||||||
"tokens": 30000
|
"tokens": 30000
|
||||||
},
|
},
|
||||||
"alerts": [],
|
"alerts": [],
|
||||||
"last_updated": "2025-10-23T22:16:03.430Z",
|
"last_updated": "2025-10-23T22:16:58.258Z",
|
||||||
"initialized": true,
|
"initialized": true,
|
||||||
"framework_components": {
|
"framework_components": {
|
||||||
"CrossReferenceValidator": {
|
"CrossReferenceValidator": {
|
||||||
"message": 0,
|
"message": 0,
|
||||||
"tokens": 0,
|
"tokens": 0,
|
||||||
"timestamp": "2025-10-23T22:16:30.078Z",
|
"timestamp": "2025-10-23T22:17:08.165Z",
|
||||||
"last_validation": "2025-10-23T22:16:30.078Z",
|
"last_validation": "2025-10-23T22:17:08.164Z",
|
||||||
"validations_performed": 789
|
"validations_performed": 791
|
||||||
},
|
},
|
||||||
"BashCommandValidator": {
|
"BashCommandValidator": {
|
||||||
"message": 0,
|
"message": 0,
|
||||||
"tokens": 0,
|
"tokens": 0,
|
||||||
"timestamp": null,
|
"timestamp": null,
|
||||||
"last_validation": "2025-10-23T22:16:30.079Z",
|
"last_validation": "2025-10-23T22:17:08.165Z",
|
||||||
"validations_performed": 432,
|
"validations_performed": 433,
|
||||||
"blocks_issued": 38
|
"blocks_issued": 38
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"action_count": 432,
|
"action_count": 433,
|
||||||
"auto_compact_events": []
|
"auto_compact_events": []
|
||||||
}
|
}
|
||||||
|
|
@ -441,7 +441,93 @@ function renderPublicationRequirements(publicationId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper: Render document editor
|
* Helper: Render multilingual document editor
|
||||||
|
* Supports multiple language versions with translation
|
||||||
|
*/
|
||||||
|
function renderMultilingualDocument(docType, title, submission, article, isStandalone) {
|
||||||
|
const doc = submission.documents?.[docType] || {};
|
||||||
|
const versions = doc.versions || [];
|
||||||
|
const primaryLang = doc.primaryLanguage || 'en';
|
||||||
|
|
||||||
|
// Get English and French versions
|
||||||
|
const enVersion = versions.find(v => v.language === 'en');
|
||||||
|
const frVersion = versions.find(v => v.language === 'fr');
|
||||||
|
|
||||||
|
// For main article, use blog content as English version if no submission version exists
|
||||||
|
let enContent = enVersion?.content || '';
|
||||||
|
if (docType === 'mainArticle' && !enContent && article?.content) {
|
||||||
|
enContent = article.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frContent = frVersion?.content || '';
|
||||||
|
|
||||||
|
const enWordCount = enContent ? enContent.split(/\s+/).filter(w => w.length > 0).length : 0;
|
||||||
|
const frWordCount = frContent ? frContent.split(/\s+/).filter(w => w.length > 0).length : 0;
|
||||||
|
|
||||||
|
const readonly = docType === 'mainArticle' && !isStandalone && article;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="border rounded-lg overflow-hidden mb-6">
|
||||||
|
<div class="bg-gray-100 px-4 py-3 flex items-center justify-between">
|
||||||
|
<h4 class="font-semibold text-gray-900">${escapeHtml(title)}</h4>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
data-action="translate-document"
|
||||||
|
data-doc-type="${docType}"
|
||||||
|
data-from-lang="en"
|
||||||
|
data-to-lang="fr"
|
||||||
|
class="px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 ${enContent ? '' : 'opacity-50 cursor-not-allowed'}"
|
||||||
|
${enContent ? '' : 'disabled'}
|
||||||
|
title="Translate English to French using DeepL">
|
||||||
|
🌍 EN → FR
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- English Version -->
|
||||||
|
<div class="border-b bg-white">
|
||||||
|
<div class="px-4 py-2 bg-gray-50 flex items-center justify-between border-b">
|
||||||
|
<span class="text-sm font-medium text-gray-700">🇬🇧 English Version${primaryLang === 'en' ? ' (Primary)' : ''}</span>
|
||||||
|
<span class="text-xs text-gray-600">${enWordCount.toLocaleString()} words</span>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<textarea
|
||||||
|
id="doc-${docType}-en"
|
||||||
|
data-doc-type="${docType}"
|
||||||
|
data-lang="en"
|
||||||
|
rows="8"
|
||||||
|
class="w-full border rounded-md p-3 text-sm font-mono resize-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
${readonly ? 'readonly' : ''}
|
||||||
|
placeholder="${readonly ? '' : 'Enter ' + title.toLowerCase() + ' in English...'}"
|
||||||
|
>${escapeHtml(enContent)}</textarea>
|
||||||
|
${readonly ? '<p class="text-xs text-gray-500 mt-2">🔒 Linked from blog post - edit the blog post to change this content</p>' : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- French Version -->
|
||||||
|
<div class="bg-white">
|
||||||
|
<div class="px-4 py-2 bg-gray-50 flex items-center justify-between border-b">
|
||||||
|
<span class="text-sm font-medium text-gray-700">🇫🇷 French Version${primaryLang === 'fr' ? ' (Primary)' : ''}</span>
|
||||||
|
<span class="text-xs text-gray-600">${frWordCount.toLocaleString()} words</span>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<textarea
|
||||||
|
id="doc-${docType}-fr"
|
||||||
|
data-doc-type="${docType}"
|
||||||
|
data-lang="fr"
|
||||||
|
rows="8"
|
||||||
|
class="w-full border rounded-md p-3 text-sm font-mono resize-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="Enter ${title.toLowerCase()} in French (or translate from English)..."
|
||||||
|
>${escapeHtml(frContent)}</textarea>
|
||||||
|
${frVersion?.translatedBy ? `<p class="text-xs text-gray-500 mt-2">Translated by: ${frVersion.translatedBy}${frVersion.approved ? ' ✓ Approved' : ''}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper: Render document editor (legacy single-language version)
|
||||||
*/
|
*/
|
||||||
function renderDocumentEditor(docType, title, content, wordCount, readonly) {
|
function renderDocumentEditor(docType, title, content, wordCount, readonly) {
|
||||||
const readonlyAttr = readonly ? 'readonly' : '';
|
const readonlyAttr = readonly ? 'readonly' : '';
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue