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
`;
}
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 "172,000+"
const skillCountStr = totalSkillCount
? (locale === 'fa'
? totalSkillCount.toLocaleString('fa-IR')
: totalSkillCount.toLocaleString('en-US'))
: (locale === 'fa' ? '۱۷۲,۰۰۰' : '172,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
`;
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}
- ${t.item1}
- ${t.item2}
- ${t.item3}
💡 ${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)
// ============================================================================