refactor: update email template system with improved type safety and locale handling

- Replace `templateId` with `template` in email sending function
- Rename `translations` to `messages` in email template props
- Consolidate email template and sending logic in `send.ts`
- Remove separate `templates.ts` file and merge functionality
- Update email template components to use new message structure
- Improve type definitions for email-related functions and props
This commit is contained in:
javayhu 2025-03-08 09:41:14 +08:00
parent 620cedb8c3
commit 988a1a6b58
8 changed files with 95 additions and 92 deletions

View File

@ -54,7 +54,7 @@ export const auth = betterAuth({
console.log("sendResetPassword, locale:", locale);
await send({
to: user.email,
templateId: "forgotPassword",
template: "forgotPassword",
context: {
url,
name: user.name,
@ -72,7 +72,7 @@ export const auth = betterAuth({
console.log("sendVerificationEmail, locale:", locale);
await send({
to: user.email,
templateId: "verifyEmail",
template: "verifyEmail",
context: {
url,
name: user.name,

View File

@ -1,30 +1,29 @@
import { Link, Text } from "@react-email/components";
import React from "react";
import { createTranslator } from "use-intl/core";
import { siteConfig } from "@/config/site";
import EmailButton from "@/mail/components/EmailButton";
import EmailLayout from "@/mail/components/EmailLayout";
import { defaultLocale, defaultTranslations } from "@/mail/translations";
import { defaultLocale, defaultMessages } from "@/mail/messages";
import type { BaseMailProps } from "@/mail/types";
import { siteConfig } from "@/config/site";
import { Text } from "@react-email/components";
import { createTranslator } from "use-intl/core";
export function ForgotPassword({
url,
name,
locale,
translations,
messages,
}: {
url: string;
name: string;
} & BaseMailProps) {
const t = createTranslator({
locale,
messages: translations,
messages,
});
return (
<EmailLayout>
<Text>{t("mail.forgotPassword.title", { name })}</Text>
<Text>{t("mail.forgotPassword.body")}</Text>
<EmailButton href={url}>
@ -41,7 +40,7 @@ export function ForgotPassword({
ForgotPassword.PreviewProps = {
locale: defaultLocale,
translations: defaultTranslations,
messages: defaultMessages,
url: "https://mksaas.com",
name: "username",
};

View File

@ -1,14 +1,16 @@
import { Heading, Text } from "@react-email/components";
import React from "react";
import { createTranslator } from "use-intl/core";
import EmailLayout from "@/mail/components/EmailLayout";
import { defaultLocale, defaultTranslations } from "@/mail/translations";
import { defaultLocale, defaultMessages } from "@/mail/messages";
import type { BaseMailProps } from "@/mail/types";
import { Heading, Text } from "@react-email/components";
import { createTranslator } from "use-intl/core";
export function SubscribeNewsletter({ locale, translations }: BaseMailProps) {
export function SubscribeNewsletter({
locale,
messages,
}: BaseMailProps) {
const t = createTranslator({
locale,
messages: translations,
messages,
});
return (
@ -23,7 +25,7 @@ export function SubscribeNewsletter({ locale, translations }: BaseMailProps) {
SubscribeNewsletter.PreviewProps = {
locale: defaultLocale,
translations: defaultTranslations,
messages: defaultMessages,
};
export default SubscribeNewsletter;

View File

@ -1,30 +1,29 @@
import { Link, Text } from "@react-email/components";
import React from "react";
import { createTranslator } from "use-intl/core";
import { siteConfig } from "@/config/site";
import EmailButton from "@/mail/components/EmailButton";
import EmailLayout from "@/mail/components/EmailLayout";
import { defaultLocale, defaultTranslations } from "@/mail/translations";
import { defaultLocale, defaultMessages } from "@/mail/messages";
import type { BaseMailProps } from "@/mail/types";
import { siteConfig } from "@/config/site";
import { Text } from "@react-email/components";
import { createTranslator } from "use-intl/core";
export function VerifyEmail({
url,
name,
locale,
translations,
messages,
}: {
url: string;
name: string;
} & BaseMailProps) {
const t = createTranslator({
locale,
messages: translations,
messages,
});
return (
<EmailLayout>
<Text>{t("mail.verifyEmail.title", { name })}</Text>
<Text>{t("mail.verifyEmail.body")}</Text>
<EmailButton href={url}>
@ -34,14 +33,14 @@ export function VerifyEmail({
<br /><br /><br />
<Text>{t("mail.common.team", { name: siteConfig.name })}</Text>
<Text>{t("mail.common.copyright", { year: new Date().getFullYear()})}</Text>
<Text>{t("mail.common.copyright", { year: new Date().getFullYear() })}</Text>
</EmailLayout>
);
}
VerifyEmail.PreviewProps = {
locale: defaultLocale,
translations: defaultTranslations,
messages: defaultMessages,
url: "https://mksaas.com",
name: "username",
};

View File

@ -2,6 +2,6 @@ import { routing } from "@/i18n/routing";
const { defaultLocale } = routing;
export { default as defaultTranslations } from "../../messages/en.json";
export { default as defaultMessages } from "../../messages/en.json";
export { defaultLocale };

View File

@ -1,30 +1,36 @@
import type { Messages } from "@/i18n/messages";
import { getMessagesForLocale } from "@/i18n/messages";
import { Locale, routing } from "@/i18n/routing";
import type { mailTemplates } from "@/mail/emails";
import { mailTemplates } from "@/mail/emails";
import { sendEmail } from "@/mail/provider/resend";
import type { TemplateId } from "./templates";
import { getTemplate } from "./templates";
import { render } from "@react-email/render";
export type Template = keyof typeof mailTemplates;
/**
* send email with given template, locale, and context
* send email
*
* 1. with given template, and context
* 2. with given subject, text, and html
*/
export async function send<T extends TemplateId>(
export async function send<T extends Template>(
params: {
to: string;
locale?: Locale;
} & (
| {
templateId: T;
| {
template: T;
context: Omit<
Parameters<(typeof mailTemplates)[T]>[0],
"locale" | "translations"
"locale" | "messages"
>;
}
| {
}
| {
subject: string;
text?: string;
html?: string;
}
),
}
),
) {
const { to, locale = routing.defaultLocale } = params;
console.log("send, locale:", locale);
@ -33,16 +39,18 @@ export async function send<T extends TemplateId>(
let text: string;
let subject: string;
if ("templateId" in params) {
const { templateId, context } = params;
const template = await getTemplate({
templateId,
// if template is provided, get the template
// otherwise, use the subject, text, and html
if ("template" in params) {
const { template, context } = params;
const mailTemplate = await getTemplate({
template,
context,
locale,
});
subject = template.subject;
text = template.text;
html = template.html;
subject = mailTemplate.subject;
text = mailTemplate.text;
html = mailTemplate.html;
} else {
subject = params.subject;
text = params.text ?? "";
@ -62,3 +70,38 @@ export async function send<T extends TemplateId>(
return false;
}
}
/**
* get rendered email for given template, context, and locale
*/
export async function getTemplate<T extends Template>({
template,
context,
locale,
}: {
template: T;
context: Omit<
Parameters<(typeof mailTemplates)[T]>[0],
"locale" | "messages"
>;
locale: Locale;
}) {
const mainTemplate = mailTemplates[template];
const messages = await getMessagesForLocale(locale);
const email = mainTemplate({
...(context as any),
locale,
messages,
});
// get the subject from the messages
const subject =
"subject" in messages.mail[template as keyof Messages["mail"]]
? messages.mail[template].subject
: "";
const html = await render(email);
const text = await render(email, { plainText: true });
return { html, text, subject };
}

View File

@ -1,41 +0,0 @@
import type { Messages } from "@/i18n/messages";
import { getMessagesForLocale } from "@/i18n/messages";
import type { Locale } from "@/i18n/routing";
import { mailTemplates } from "@/mail/emails";
import { render } from "@react-email/render";
export type TemplateId = keyof typeof mailTemplates;
/**
* get rendered email for given template id, context, and locale
*/
export async function getTemplate<T extends TemplateId>({
templateId,
context,
locale,
}: {
templateId: T;
context: Omit<
Parameters<(typeof mailTemplates)[T]>[0],
"locale" | "translations"
>;
locale: Locale;
}) {
const template = mailTemplates[templateId];
const translations = await getMessagesForLocale(locale);
const email = template({
...(context as any),
locale,
translations,
});
const subject =
"subject" in translations.mail[templateId as keyof Messages["mail"]]
? translations.mail[templateId].subject
: "";
const html = await render(email);
const text = await render(email, { plainText: true });
return { html, text, subject };
}

View File

@ -1,13 +1,14 @@
import type { Locale } from "@/i18n/routing";
import type { Messages } from "@/i18n/messages";
export interface SendEmailParams {
export interface EmailParams {
to: string;
subject: string;
text: string;
html?: string;
}
export type SendEmailHandler = (params: SendEmailParams) => Promise<void>;
export type SendEmailHandler = (params: EmailParams) => Promise<void>;
export interface MailProvider {
send: SendEmailHandler;
@ -15,5 +16,5 @@ export interface MailProvider {
export type BaseMailProps = {
locale: Locale;
translations: any;
messages: Messages;
};