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:
TheFlow 2025-10-24 11:17:12 +13:00
parent ce991d86a8
commit d0e1275f58
2 changed files with 95 additions and 9 deletions

View file

@ -43,7 +43,7 @@
"last_deliberation": null
},
"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",
"result": "passed"
},
@ -58,25 +58,25 @@
"tokens": 30000
},
"alerts": [],
"last_updated": "2025-10-23T22:16:03.430Z",
"last_updated": "2025-10-23T22:16:58.258Z",
"initialized": true,
"framework_components": {
"CrossReferenceValidator": {
"message": 0,
"tokens": 0,
"timestamp": "2025-10-23T22:16:30.078Z",
"last_validation": "2025-10-23T22:16:30.078Z",
"validations_performed": 789
"timestamp": "2025-10-23T22:17:08.165Z",
"last_validation": "2025-10-23T22:17:08.164Z",
"validations_performed": 791
},
"BashCommandValidator": {
"message": 0,
"tokens": 0,
"timestamp": null,
"last_validation": "2025-10-23T22:16:30.079Z",
"validations_performed": 432,
"last_validation": "2025-10-23T22:17:08.165Z",
"validations_performed": 433,
"blocks_issued": 38
}
},
"action_count": 432,
"action_count": 433,
"auto_compact_events": []
}

View file

@ -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) {
const readonlyAttr = readonly ? 'readonly' : '';