type Locale = 'en' | 'fa'; const SITE_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://skills.palebluedot.live'; const COLORS = { primary: '#0284c7', primaryDark: '#0369a1', background: '#f8fafc', surface: '#ffffff', border: '#e2e8f0', text: '#1e293b', textSecondary: '#64748b', textMuted: '#94a3b8', }; function getDir(locale: Locale): 'rtl' | 'ltr' { return locale === 'fa' ? 'rtl' : 'ltr'; } function getAlign(locale: Locale): 'right' | 'left' { return locale === 'fa' ? 'right' : 'left'; } function getFontFamily(locale: Locale): string { return locale === 'fa' ? 'Tahoma, Arial, sans-serif' : 'Arial, Helvetica, sans-serif'; } /** * Shared base layout wrapper for all email templates */ function baseLayout(locale: Locale, bodyContent: string, footerContent: string): string { const dir = getDir(locale); const align = getAlign(locale); const font = getFontFamily(locale); return ` SkillHub
SkillHub
${bodyContent}
${footerContent}

© ${new Date().getFullYear()} SkillHub — ${new URL(SITE_URL).hostname}

`; } function ctaButton(locale: Locale, text: string, href: string): string { const font = getFontFamily(locale); return `${text}`; } // ============================================================================ // Translation strings // ============================================================================ const translations = { en: { welcome: { subject: 'Welcome to SkillHub!', greeting: (username: string) => `Hi ${username},`, intro: 'Welcome to SkillHub — the open-source marketplace where AI agents find the expertise they need.', whatIs: (count: string) => `Browse ${count}+ skills that work with Claude Code, GitHub Copilot, OpenAI Codex, Cursor, and Windsurf.`, getStarted: 'Get started:', browseSkills: 'Browse Skills', installCli: 'Install CLI', docs: 'Documentation', newsletterCta: 'Want to stay updated? Subscribe to our newsletter for new skills, features, and tips.', subscribeLink: 'Subscribe to Newsletter', footer: 'You received this because you signed up on SkillHub with GitHub.', }, newsletter: { subject: 'You\'re in — welcome to SkillHub Newsletter', greeting: 'Thanks for subscribing!', intro: 'You\'ll get concise updates on what matters:', item1: 'New and trending skills in the marketplace', item2: 'Feature releases and platform improvements', item3: 'Practical tips for AI-powered development', visitUs: 'Visit SkillHub', footer: 'You received this because you subscribed to the SkillHub newsletter.', unsubscribe: 'Unsubscribe', }, claimSubmitted: { addSubject: 'Your skill request has been submitted', removeSubject: 'Your removal request has been processed', addGreeting: 'Your request has been submitted!', removeGreeting: 'Your removal request has been processed!', addIntro: (repoUrl: string, count: number) => `We received your request to add ${repoUrl}${count > 0 ? ` (${count} skill${count > 1 ? 's' : ''} found)` : ''}.`, removeIntro: (skillId: string) => `Your request to remove ${skillId} has been verified and processed.`, addPending: 'Your request is now pending review. We\'ll notify you when it\'s been processed.', removeApproved: 'The skill has been blocked from indexing and will no longer appear on SkillHub.', viewRequests: 'View My Requests', footer: 'You received this because you submitted a request on SkillHub.', }, claimStatus: { approvedSubject: 'Your request has been approved', rejectedSubject: 'Update on your request', approvedGreeting: 'Good news!', rejectedGreeting: 'Update on your request', addApproved: (repoUrl: string) => `Your request to add ${repoUrl} has been approved and will be indexed soon.`, addRejected: (repoUrl: string) => `After review, your request to add ${repoUrl} could not be approved at this time.`, removeApproved: (skillId: string) => `Your request to remove ${skillId} has been approved. The skill has been blocked from indexing.`, removeRejected: (skillId: string) => `After review, your request to remove ${skillId} could not be approved at this time.`, viewRequests: 'View My Requests', footer: 'You received this because you submitted a request on SkillHub.', }, }, fa: { welcome: { subject: 'به SkillHub خوش آمدید!', greeting: (username: string) => `سلام ${username}،`, intro: 'به SkillHub خوش آمدید — بازار متن‌باز مهارت‌هایی که عامل‌های هوش مصنوعی به آن نیاز دارند.', whatIs: (count: string) => `بیش از ${count} مهارت را مرور کنید که با Claude Code، GitHub Copilot، OpenAI Codex، Cursor و Windsurf کار می‌کنند.`, getStarted: 'شروع کنید:', browseSkills: 'مرور مهارت‌ها', installCli: 'نصب CLI', docs: 'مستندات', newsletterCta: 'می‌خواهید به‌روز بمانید؟ در خبرنامه ما برای مهارت‌ها، ویژگی‌ها و نکات جدید عضو شوید.', subscribeLink: 'عضویت در خبرنامه', footer: 'این ایمیل به دلیل ثبت‌نام شما در SkillHub با GitHub ارسال شده است.', }, newsletter: { subject: 'عضویت شما فعال شد — خبرنامه SkillHub', greeting: 'ممنون از عضویت شما!', intro: 'به‌روزرسانی‌های مختصر و مفید دریافت خواهید کرد:', item1: 'مهارت‌های جدید و پرطرفدار در بازار', item2: 'ویژگی‌های جدید و بهبودهای پلتفرم', item3: 'نکات عملی برای توسعه با هوش مصنوعی', visitUs: 'مشاهده SkillHub', footer: 'این ایمیل به دلیل عضویت شما در خبرنامه SkillHub ارسال شده است.', unsubscribe: 'لغو عضویت', }, claimSubmitted: { addSubject: 'درخواست افزودن مهارت شما ثبت شد', removeSubject: 'درخواست حذف مهارت شما پردازش شد', addGreeting: 'درخواست شما ثبت شد!', removeGreeting: 'درخواست حذف شما پردازش شد!', addIntro: (repoUrl: string, count: number) => `درخواست افزودن ${repoUrl}${count > 0 ? ` (${count} مهارت یافت شد)` : ''} دریافت شد.`, removeIntro: (skillId: string) => `درخواست حذف ${skillId} تایید و پردازش شد.`, addPending: 'درخواست شما در انتظار بررسی است. پس از پردازش به شما اطلاع خواهیم داد.', removeApproved: 'مهارت از ایندکس شدن مسدود شد و دیگر در SkillHub نمایش داده نخواهد شد.', viewRequests: 'مشاهده درخواست‌ها', footer: 'این ایمیل به دلیل ثبت درخواست شما در SkillHub ارسال شده است.', }, claimStatus: { approvedSubject: 'درخواست شما تایید شد', rejectedSubject: 'به‌روزرسانی درخواست شما', approvedGreeting: 'خبر خوب!', rejectedGreeting: 'به‌روزرسانی درخواست شما', addApproved: (repoUrl: string) => `درخواست افزودن ${repoUrl} تایید شد و به زودی ایندکس خواهد شد.`, addRejected: (repoUrl: string) => `پس از بررسی، درخواست افزودن ${repoUrl} در حال حاضر قابل تایید نیست.`, removeApproved: (skillId: string) => `درخواست حذف ${skillId} تایید شد. مهارت از ایندکس شدن مسدود شد.`, removeRejected: (skillId: string) => `پس از بررسی، درخواست حذف ${skillId} در حال حاضر قابل تایید نیست.`, viewRequests: 'مشاهده درخواست‌ها', footer: 'این ایمیل به دلیل ثبت درخواست شما در SkillHub ارسال شده است.', }, }, } as const; // ============================================================================ // Template Builders // ============================================================================ export interface EmailTemplate { subject: string; html: string; } /** * Welcome email for new users (first GitHub OAuth login) */ export function buildWelcomeEmail(locale: Locale, username: string, email?: string, totalSkillCount?: number): EmailTemplate { const t = translations[locale].welcome; const dir = getDir(locale); const align = getAlign(locale); // Format skill count: dynamic if provided, fallback to "16,000+" const skillCountStr = totalSkillCount ? (locale === 'fa' ? totalSkillCount.toLocaleString('fa-IR') : totalSkillCount.toLocaleString('en-US')) : (locale === 'fa' ? '۱۶,۰۰۰' : '16,000'); const body = `

${t.greeting(username)}

${t.intro}

${t.whatIs(skillCountStr)}

${t.getStarted}

${ctaButton(locale, t.browseSkills, `${SITE_URL}/${locale}/browse`)} ${ctaButton(locale, t.docs, `${SITE_URL}/${locale}/docs`)}

${locale === 'en' ? 'Install CLI:' : 'نصب CLI:'}

npm install -g skillhub

${locale === 'en' ? '💡 Tip' : '💡 نکته'}

${locale === 'en' ? 'Skills work best when explicitly invoked. Try searching and installing on the fly:' : 'مهارت\u200Cها وقتی بهترین عملکرد را دارند که صریحاً فراخوانی شوند. جستجو و نصب لحظه\u200Cای را امتحان کنید:'}

npx skillhub search "react testing" --sort stars
npx skillhub install <skill-id> --project

${t.newsletterCta}

${t.subscribeLink} →
`; const footer = `

${t.footer}

`; return { subject: t.subject, html: baseLayout(locale, body, footer), }; } /** * Newsletter welcome email for actual newsletter subscribers */ export function buildNewsletterWelcomeEmail(locale: Locale, email: string): EmailTemplate { const t = translations[locale].newsletter; const align = getAlign(locale); const body = `

${t.greeting}

${t.intro}

💡 ${locale === 'en' ? `Tip: Skills work best when explicitly invoked. Try searching and installing on the fly: npx skillhub search "your topic". After installing, read the SKILL.md and follow its instructions. See CLI docs →` : `نکته: مهارت\u200Cها وقتی بهترین عملکرد را دارند که صریحاً فراخوانی شوند. جستجو و نصب لحظه\u200Cای را امتحان کنید: npx skillhub search "your topic". پس از نصب، فایل SKILL.md را بخوانید و دستورالعمل\u200Cها را دنبال کنید. مستندات CLI →`}

${ctaButton(locale, t.visitUs, `${SITE_URL}/${locale}`)}
`; const unsubscribeUrl = `${SITE_URL}/api/newsletter/unsubscribe?email=${encodeURIComponent(email)}`; const footer = `

${t.footer}
${t.unsubscribe}

`; return { subject: t.subject, html: baseLayout(locale, body, footer), }; } /** * Claim submission confirmation email */ export function buildClaimSubmittedEmail( locale: Locale, type: 'add' | 'remove', details: { skillId?: string; repositoryUrl?: string; skillCount?: number } ): EmailTemplate { const t = translations[locale].claimSubmitted; const align = getAlign(locale); const subject = type === 'add' ? t.addSubject : t.removeSubject; const greeting = type === 'add' ? t.addGreeting : t.removeGreeting; let introText: string; let statusText: string; if (type === 'add') { introText = t.addIntro(details.repositoryUrl || '', details.skillCount || 0); statusText = t.addPending; } else { introText = t.removeIntro(details.skillId || ''); statusText = t.removeApproved; } const body = `

${greeting}

${introText}

${statusText}

${ctaButton(locale, t.viewRequests, `${SITE_URL}/${locale}/claim`)}
`; const footer = `

${t.footer}

`; return { subject, html: baseLayout(locale, body, footer), }; } /** * Claim status change notification email */ export function buildClaimStatusEmail( locale: Locale, type: 'add' | 'remove', status: 'approved' | 'rejected', details: { skillId?: string; repositoryUrl?: string } ): EmailTemplate { const t = translations[locale].claimStatus; const align = getAlign(locale); const subject = status === 'approved' ? t.approvedSubject : t.rejectedSubject; const greeting = status === 'approved' ? t.approvedGreeting : t.rejectedGreeting; let messageText: string; if (type === 'add' && status === 'approved') { messageText = t.addApproved(details.repositoryUrl || ''); } else if (type === 'add' && status === 'rejected') { messageText = t.addRejected(details.repositoryUrl || ''); } else if (type === 'remove' && status === 'approved') { messageText = t.removeApproved(details.skillId || ''); } else { messageText = t.removeRejected(details.skillId || ''); } const body = `

${greeting}

${messageText}

${ctaButton(locale, t.viewRequests, `${SITE_URL}/${locale}/claim`)}
`; const footer = `

${t.footer}

`; return { subject, html: baseLayout(locale, body, footer), }; } // ============================================================================ // Outreach Email Template (English only — for GitHub repo owners) // ============================================================================