diff --git a/apps/cms/src/lib/email-template-defaults.ts b/apps/cms/src/lib/email-template-defaults.ts index 1b41449..af7b955 100644 --- a/apps/cms/src/lib/email-template-defaults.ts +++ b/apps/cms/src/lib/email-template-defaults.ts @@ -21,6 +21,9 @@ export const EMAIL_TEMPLATE_KEYS = [ 'waitlist', 'event-reminder', 'application-acknowledgement', + 'partner-review-submitted', + 'partner-review-decision', + 'partner-review-published', ] as const; export type EmailTemplateKey = (typeof EMAIL_TEMPLATE_KEYS)[number]; @@ -95,4 +98,34 @@ export const DEFAULT_EMAIL_TEMPLATES: Readonly< body: ACKNOWLEDGEMENT_WITH_CALL.en, }, }, + 'partner-review-submitted': { + ar: { + subject: 'مسودة الشريك جاهزة للمراجعة', + body: 'قدّم أحد المساهمين من الشركاء {label}.\n\nيمكنك مراجعته في مدير المحتوى في Strapi. إنها مسودة ولن تكون متاحة للجمهور حتى ينشرها محرر.', + }, + en: { + subject: '[SHARP-ER] Partner draft ready for review', + body: 'A partner contributor submitted {label}.\n\nReview it in Strapi Content Manager. It is a draft and is not public until an Editor publishes it.', + }, + }, + 'partner-review-decision': { + ar: { + subject: 'تحديث بشأن مراجعة {label}', + body: 'تمت مراجعة {label}، والقرار هو: {decision}.\n\nملاحظات المراجع:\n{feedback}\n\nيرجى تحديث مسودتك في مدير المحتوى في Strapi وحفظها لإعادة تقديمها.', + }, + en: { + subject: '[SHARP-ER] Review update: {label}', + body: 'Your {label} has been marked {decision}.\n\nReviewer feedback:\n{feedback}\n\nPlease update your draft in Strapi Content Manager and save it to resubmit.', + }, + }, + 'partner-review-published': { + ar: { + subject: 'تم نشر محتواك', + body: 'تمت الموافقة على {label} ونشره على موقع SHARP-ER.', + }, + en: { + subject: '[SHARP-ER] Your content has been published', + body: 'Your {label} has been approved and published on the SHARP-ER website.', + }, + }, }; \ No newline at end of file diff --git a/apps/cms/src/lib/partner-review-notifications.ts b/apps/cms/src/lib/partner-review-notifications.ts index 08004a7..422bc85 100644 --- a/apps/cms/src/lib/partner-review-notifications.ts +++ b/apps/cms/src/lib/partner-review-notifications.ts @@ -1,6 +1,8 @@ import type { Core } from '@strapi/strapi'; -import { trySendMail } from './event-mail'; +import { DEFAULT_EMAIL_TEMPLATES, type EmailTemplateKey } from './email-template-defaults'; +import { buildBilingualMail, trySendMail } from './event-mail'; +import type { PlaceholderValues } from './email-templates'; const PARTNER_CONTENT = new Set([ 'api::resource.resource', @@ -12,6 +14,10 @@ const PARTNER_CONTENT = new Set([ const NOTIFIABLE_ACTIONS = new Set(['create', 'update', 'publish']); const REVIEW_RETURN_STATUSES = new Set(['changes-requested', 'rejected']); const LOG_PREFIX = '[partner-review-mail]'; +type PartnerReviewTemplateKey = Extract< + EmailTemplateKey, + 'partner-review-submitted' | 'partner-review-decision' | 'partner-review-published' +>; type ReviewDocument = { documentId?: string; @@ -83,27 +89,50 @@ const getSubmitter = async (strapi: Core.Strapi, id: number): Promise { +const ENTRY_TYPE_LABELS: Record = { + resource: { ar: 'مورد', en: 'resource' }, + 'news-article': { ar: 'مقال إخباري', en: 'news article' }, + activity: { ar: 'نشاط', en: 'activity' }, + event: { ar: 'فعالية', en: 'event' }, + opportunity: { ar: 'فرصة', en: 'opportunity' }, +}; + +const entryLabel = (uid: string, document: ReviewDocument | null | undefined, locale: 'ar' | 'en'): string => { const title = document?.title?.trim() || document?.slug?.trim(); - const type = uid.split('.')[1]?.replace(/-/g, ' ') ?? 'content'; - return title ? `${type}: ${title}` : `a ${type} entry`; + const typeSlug = uid.split('.')[1] ?? 'content'; + const type = ENTRY_TYPE_LABELS[typeSlug]?.[locale] ?? (locale === 'ar' ? 'محتوى' : typeSlug.replace(/-/g, ' ')); + if (title) return `${type}: ${title}`; + return locale === 'ar' ? `عنصر من نوع ${type}` : `a ${type} entry`; +}; + +const buildReviewMail = ( + key: PartnerReviewTemplateKey, + vars: { ar: PlaceholderValues; en: PlaceholderValues } +): ReturnType => { + const template = DEFAULT_EMAIL_TEMPLATES[key]; + return buildBilingualMail({ + subjectAr: template.ar.subject, + subjectEn: template.en.subject, + bodyAr: template.ar.body, + bodyEn: template.en.body, + vars, + }); }; const sendReviewNotification = async ( strapi: Core.Strapi, to: string[], - subject: string, - body: string + mail: ReturnType ): Promise => { if (to.length === 0) { - strapi.log.warn(`${LOG_PREFIX} no recipient configured for: ${subject}`); + strapi.log.warn(`${LOG_PREFIX} no recipient configured for: ${mail.subject}`); return; } await Promise.all( to.map((address) => - trySendMail(strapi, { to: address, subject, text: body }).then((sent) => { - if (sent) strapi.log.info(`${LOG_PREFIX} sent "${subject}" to ${address}.`); + trySendMail(strapi, { to: address, ...mail }).then((sent) => { + if (sent) strapi.log.info(`${LOG_PREFIX} sent "${mail.subject}" to ${address}.`); }) ) ); @@ -130,7 +159,8 @@ const notifyAfterSave = async ({ const submitterId = after.submittedByAdminUserId; if (typeof submitterId !== 'number') return; - const label = entryLabel(uid, after); + const labelAr = entryLabel(uid, after, 'ar'); + const labelEn = entryLabel(uid, after, 'en'); if ( (action === 'create' && after.reviewStatus === 'pending') || @@ -140,8 +170,10 @@ const notifyAfterSave = async ({ await sendReviewNotification( strapi, recipients, - `[SHARP-ER] Partner draft ready for review`, - `A partner contributor submitted ${label}.\n\nReview it in Strapi Content Manager. It is a draft and is not public until an Editor publishes it.` + buildReviewMail('partner-review-submitted', { + ar: { label: labelAr }, + en: { label: labelEn }, + }) ); return; } @@ -158,13 +190,24 @@ const notifyAfterSave = async ({ return; } - const decision = after.reviewStatus === 'rejected' ? 'rejected' : 'changes requested'; - const feedback = after.reviewFeedback?.trim() || '(No review feedback was provided.)'; + const decisionEn = after.reviewStatus === 'rejected' ? 'rejected' : 'changes requested'; + const decisionAr = after.reviewStatus === 'rejected' ? 'الرفض' : 'طلب إجراء تغييرات'; + const feedback = after.reviewFeedback?.trim(); await sendReviewNotification( strapi, [submitter.email], - `[SHARP-ER] Review update: ${label}`, - `Your ${label} has been marked ${decision}.\n\nReviewer feedback:\n${feedback}\n\nPlease update your draft in Strapi Content Manager and save it to resubmit.` + buildReviewMail('partner-review-decision', { + ar: { + label: labelAr, + decision: decisionAr, + feedback: feedback || '(لم تُقدَّم ملاحظات للمراجعة.)', + }, + en: { + label: labelEn, + decision: decisionEn, + feedback: feedback || '(No review feedback was provided.)', + }, + }) ); return; } @@ -179,8 +222,10 @@ const notifyAfterSave = async ({ await sendReviewNotification( strapi, [submitter.email], - `[SHARP-ER] Your content has been published`, - `Your ${label} has been approved and published on the SHARP-ER website.` + buildReviewMail('partner-review-published', { + ar: { label: labelAr }, + en: { label: labelEn }, + }) ); } };