diff --git a/packages/email-templates/package.json b/packages/email-templates/package.json index ce8c33385..d68e0612e 100644 --- a/packages/email-templates/package.json +++ b/packages/email-templates/package.json @@ -19,7 +19,8 @@ "@kit/eslint-config": "workspace:*", "@kit/prettier-config": "workspace:*", "@kit/tailwind-config": "workspace:*", - "@kit/tsconfig": "workspace:*" + "@kit/tsconfig": "workspace:*", + "@kit/i18n": "workspace:*" }, "eslintConfig": { "root": true, diff --git a/packages/email-templates/src/account-delete.email.tsx b/packages/email-templates/src/emails/account-delete.email.tsx similarity index 51% rename from packages/email-templates/src/account-delete.email.tsx rename to packages/email-templates/src/emails/account-delete.email.tsx index e57983bfa..eaaa9eac4 100644 --- a/packages/email-templates/src/account-delete.email.tsx +++ b/packages/email-templates/src/emails/account-delete.email.tsx @@ -10,53 +10,75 @@ import { render, } from '@react-email/components'; +import { initializeEmailI18n } from '../lib/i18n'; + interface Props { productName: string; userDisplayName: string; + language?: string; } -export function renderAccountDeleteEmail(props: Props) { - const previewText = `We have deleted your ${props.productName} account`; +export async function renderAccountDeleteEmail(props: Props) { + const namespace = 'account-delete-email'; - return render( + const { t } = await initializeEmailI18n({ + language: props.language, + namespace, + }); + + const previewText = t(`${namespace}:previewText`, { + productName: props.productName, + }); + + const subject = t(`${namespace}:subject`); + + const html = render( {previewText} - + {previewText} - Hello {props.userDisplayName}, + {t(`${namespace}:hello`, { + displayName: props.userDisplayName, + })} - This is to confirm that we've processed your request to - delete your account with {props.productName}. + {t(`${namespace}:paragraph1`, { + productName: props.productName, + })} - We're sorry to see you go. Please note that this action is - irreversible, and we'll make sure to delete all of your data - from our systems. + {t(`${namespace}:paragraph2`)} - We thank you again for using {props.productName}. + {t(`${namespace}:paragraph3`, { + productName: props.productName, + })} - Best, -
- The {props.productName} Team + {t(`${namespace}:paragraph4`, { + productName: props.productName, + })}
, ); + + return { + html, + subject, + }; } diff --git a/packages/email-templates/src/invite.email.tsx b/packages/email-templates/src/emails/invite.email.tsx similarity index 61% rename from packages/email-templates/src/invite.email.tsx rename to packages/email-templates/src/emails/invite.email.tsx index a7bb74fe6..e501ae006 100644 --- a/packages/email-templates/src/invite.email.tsx +++ b/packages/email-templates/src/emails/invite.email.tsx @@ -17,6 +17,8 @@ import { render, } from '@react-email/components'; +import { initializeEmailI18n } from '../lib/i18n'; + interface Props { teamName: string; teamLogo?: string; @@ -24,31 +26,60 @@ interface Props { invitedUserEmail: string; link: string; productName: string; + language?: string; } -export function renderInviteEmail(props: Props) { - const previewText = `Join ${props.invitedUserEmail} on ${props.productName}`; +export async function renderInviteEmail(props: Props) { + const namespace = 'invite-email'; - return render( + const { t } = await initializeEmailI18n({ + language: props.language, + namespace, + }); + + const previewText = `Join ${props.invitedUserEmail} on ${props.productName}`; + const subject = t(`${namespace}:subject`); + + const heading = t(`${namespace}:heading`, { + teamName: props.teamName, + productName: props.productName, + }); + + const hello = t(`${namespace}:hello`, { + invitedUserEmail: props.invitedUserEmail, + }); + + const mainText = t(`${namespace}:mainText`, { + inviter: props.inviter, + teamName: props.teamName, + productName: props.productName, + }); + + const joinTeam = t(`${namespace}:joinTeam`, { + teamName: props.teamName, + }); + + const html = render( {previewText} - + - Join {props.teamName} on{' '} - {props.productName} + {heading} + - Hello {props.invitedUserEmail}, - - - {props.inviter} has invited you to the{' '} - {props.teamName} team on{' '} - {props.productName}. + {hello} + + + {props.teamLogo && (
@@ -63,28 +94,38 @@ export function renderInviteEmail(props: Props) {
)} +
+ - or copy and paste this URL into your browser:{' '} + {t(`${namespace}:copyPasteLink`)}{' '} {props.link} +
+ - This invitation was intended for{' '} - {props.invitedUserEmail}. + {t(`${namespace}:invitationIntendedFor`, { + invitedUserEmail: props.invitedUserEmail, + })}
, ); + + return { + html, + subject, + }; } diff --git a/packages/email-templates/src/index.ts b/packages/email-templates/src/index.ts index 4008d4d7d..193eafa2c 100644 --- a/packages/email-templates/src/index.ts +++ b/packages/email-templates/src/index.ts @@ -1,2 +1,2 @@ -export * from './invite.email'; -export * from './account-delete.email'; +export * from './emails/invite.email'; +export * from './emails/account-delete.email'; diff --git a/packages/email-templates/src/lib/i18n.ts b/packages/email-templates/src/lib/i18n.ts new file mode 100644 index 000000000..6c05019a9 --- /dev/null +++ b/packages/email-templates/src/lib/i18n.ts @@ -0,0 +1,30 @@ +import { initializeServerI18n } from '@kit/i18n/server'; + +export function initializeEmailI18n(params: { + language: string | undefined; + namespace: string; +}) { + const language = params.language ?? 'en'; + + return initializeServerI18n( + { + supportedLngs: [language], + lng: language, + ns: params.namespace, + }, + async (language, namespace) => { + try { + const data = await import(`../locales/${language}/${namespace}.json`); + + return data as Record; + } catch (error) { + console.log( + `Error loading i18n file: locales/${language}/${namespace}.json`, + error, + ); + + return {}; + } + }, + ); +} diff --git a/packages/email-templates/src/locales/en/account-delete-email.json b/packages/email-templates/src/locales/en/account-delete-email.json new file mode 100644 index 000000000..1b71932e5 --- /dev/null +++ b/packages/email-templates/src/locales/en/account-delete-email.json @@ -0,0 +1,9 @@ +{ + "subject": "We have deleted your {{productName}} account", + "previewText": "We have deleted your {{productName}} account", + "hello": "Hello {{displayName}},", + "paragraph1": "This is to confirm that we have processed your request to delete your account with {{productName}}.", + "paragraph2": "We're sorry to see you go. Please note that this action is irreversible, and we'll make sure to delete all of your data from our systems.", + "paragraph3": "We thank you again for using {{productName}}.", + "paragraph4": "The {{productName}} Team" +} \ No newline at end of file diff --git a/packages/email-templates/src/locales/en/invite-email.json b/packages/email-templates/src/locales/en/invite-email.json new file mode 100644 index 000000000..da06d64e9 --- /dev/null +++ b/packages/email-templates/src/locales/en/invite-email.json @@ -0,0 +1,9 @@ +{ + "subject": "You have been invited to join a team", + "heading": "Join {{teamName}} on {{productName}}", + "hello": "Hello {{invitedUserEmail}},", + "mainText": "{{inviter}} has invited you to the {{teamName}} team on {{productName}}.", + "joinTeam": "Join {{teamName}}", + "copyPasteLink": "or copy and paste this URL into your browser:", + "invitationIntendedFor": "This invitation is intended for {{invitedUserEmail}}." +} \ No newline at end of file diff --git a/packages/features/team-accounts/src/server/services/webhooks/account-invitations-webhook.service.ts b/packages/features/team-accounts/src/server/services/webhooks/account-invitations-webhook.service.ts index 882d41090..5042c115b 100644 --- a/packages/features/team-accounts/src/server/services/webhooks/account-invitations-webhook.service.ts +++ b/packages/features/team-accounts/src/server/services/webhooks/account-invitations-webhook.service.ts @@ -103,7 +103,7 @@ class AccountInvitationsWebhookService { const mailer = await getMailer(); - const html = renderInviteEmail({ + const { html, subject } = await renderInviteEmail({ link: this.getInvitationLink(invitation.invite_token), invitedUserEmail: invitation.email, inviter: inviter.data.name ?? inviter.data.email ?? '', @@ -115,7 +115,7 @@ class AccountInvitationsWebhookService { .sendEmail({ from: env.emailSender, to: invitation.email, - subject: 'You have been invited to join a team', + subject, html, }) .then(() => { diff --git a/packages/features/team-accounts/src/server/services/webhooks/account-webhooks.service.ts b/packages/features/team-accounts/src/server/services/webhooks/account-webhooks.service.ts index cc24cd8a2..b3e019d5d 100644 --- a/packages/features/team-accounts/src/server/services/webhooks/account-webhooks.service.ts +++ b/packages/features/team-accounts/src/server/services/webhooks/account-webhooks.service.ts @@ -55,7 +55,7 @@ class AccountWebhooksService { const { getMailer } = await import('@kit/mailers'); const mailer = await getMailer(); - const html = renderAccountDeleteEmail({ + const { html, subject } = await renderAccountDeleteEmail({ userDisplayName: params.userDisplayName, productName: params.productName, }); @@ -63,7 +63,7 @@ class AccountWebhooksService { return mailer.sendEmail({ to: params.userEmail, from: params.fromEmail, - subject: 'Account Deletion Request', + subject, html, }); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c89088f0..0d1539664 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -521,6 +521,9 @@ importers: '@kit/eslint-config': specifier: workspace:* version: link:../../tooling/eslint + '@kit/i18n': + specifier: workspace:* + version: link:../i18n '@kit/prettier-config': specifier: workspace:* version: link:../../tooling/prettier