feat: add Translation Management with approval workflow
All checks were successful
Deploy / deploy (push) Successful in 12s

- New Translations + TranslationTexts NocoDB tables (auto-created on restart)
- Full CRUD: list, create, update, delete, bulk-delete translations
- Translation texts per language (add/edit/delete inline)
- Review flow: submit-review generates public token link
- Public review page: shows source + all translations, approve/reject/revision
- Email notifications to approvers (registered users)
- Sidebar nav under Marketing category
- Bilingual i18n (80+ keys in en.json and ar.json)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-11 14:49:04 +03:00
parent 14751c42e4
commit b17108b321
9 changed files with 1962 additions and 11 deletions

View File

@@ -85,8 +85,10 @@ const t = {
// Types
post: { en: 'post', ar: 'منشور' },
artefact: { en: 'artefact', ar: 'قطعة إبداعية' },
Post: { en: 'Post', ar: 'المنشور' },
Artefact: { en: 'Artefact', ar: 'القطعة الإبداعية' },
Post: { en: 'Post', ar: 'المنشور' },
Artefact: { en: 'Artefact', ar: 'القطعة الإبداعية' },
translation: { en: 'translation', ar: 'ترجمة' },
Translation: { en: 'Translation', ar: 'الترجمة' },
// Generic
view: { en: 'View', ar: 'عرض' },
@@ -192,14 +194,14 @@ function notifyApproved({ type, record, approverName }) {
getUser(creatorId).then(user => {
if (!user) return;
const l = user.lang;
const typeLabel = tr(type === 'post' ? 'Post' : 'Artefact', l);
const typeLabel = tr(type === 'post' ? 'Post' : type === 'translation' ? 'Translation' : 'Artefact', l);
send({
to: user.email, lang: l,
subject: `${tr('approved', l)}: ${title}`,
heading: tr('approvedHeading', l)(typeLabel),
bodyHtml: `<p>${tr('approvedBody', l)(title, approverName || (l === 'ar' ? 'مراجع' : 'a reviewer'))}</p>`,
ctaText: `${tr('view', l)} ${typeLabel}`,
ctaUrl: `${APP_URL}/${type === 'post' ? 'posts' : 'artefacts'}`,
ctaUrl: `${APP_URL}/${type === 'post' ? 'posts' : type === 'translation' ? 'translations' : 'artefacts'}`,
});
});
}
@@ -213,7 +215,7 @@ function notifyRejected({ type, record, approverName, feedback }) {
getUser(creatorId).then(user => {
if (!user) return;
const l = user.lang;
const typeLabel = tr(type === 'post' ? 'Post' : 'Artefact', l);
const typeLabel = tr(type === 'post' ? 'Post' : type === 'translation' ? 'Translation' : 'Artefact', l);
send({
to: user.email, lang: l,
subject: `${tr('needsChanges', l)}: ${title}`,
@@ -222,16 +224,18 @@ function notifyRejected({ type, record, approverName, feedback }) {
<p>${tr('rejectedBody', l)(title, approverName || (l === 'ar' ? 'مراجع' : 'a reviewer'))}</p>
${feedback ? `<blockquote ${BLOCKQUOTE}>${feedback}</blockquote>` : ''}`,
ctaText: `${tr('view', l)} ${typeLabel}`,
ctaUrl: `${APP_URL}/${type === 'post' ? 'posts' : 'artefacts'}`,
ctaUrl: `${APP_URL}/${type === 'post' ? 'posts' : type === 'translation' ? 'translations' : 'artefacts'}`,
});
});
}
// 4. Revision requested (artefact) → notify creator
function notifyRevisionRequested({ record, approverName, feedback }) {
function notifyRevisionRequested({ type, record, approverName, feedback }) {
const creatorId = record.created_by_user_id;
if (!creatorId) return;
const title = record.title || 'Untitled';
const entityType = type === 'translation' ? 'Translation' : 'Artefact';
const entityPath = type === 'translation' ? 'translations' : 'artefacts';
getUser(creatorId).then(user => {
if (!user) return;
@@ -243,8 +247,8 @@ function notifyRevisionRequested({ record, approverName, feedback }) {
bodyHtml: `
<p>${tr('revisionRequestedBody', l)(title, approverName || (l === 'ar' ? 'مراجع' : 'a reviewer'))}</p>
${feedback ? `<blockquote ${BLOCKQUOTE}>${feedback}</blockquote>` : ''}`,
ctaText: `${tr('view', l)} ${tr('Artefact', l)}`,
ctaUrl: `${APP_URL}/artefacts`,
ctaText: `${tr('view', l)} ${tr(entityType, l)}`,
ctaUrl: `${APP_URL}/${entityPath}`,
});
});
}