From 15f6c72c6e1d0d6a28d3f4cf465a4ceaa082e78b Mon Sep 17 00:00:00 2001 From: giancarlo Date: Wed, 1 May 2024 17:35:48 +0700 Subject: [PATCH] Add i18n support to email-templates The email-templates package now uses the i18n (internationalization) library to translate email content based on user language preferences. This update involves changes to the email structure, the addition of translation JSON files, and modifications to the files referencing the emails. Email file locations were also restructured for better organization. --- packages/email-templates/package.json | 3 +- .../src/{ => emails}/account-delete.email.tsx | 50 +++++++++---- .../src/{ => emails}/invite.email.tsx | 73 +++++++++++++++---- packages/email-templates/src/index.ts | 4 +- packages/email-templates/src/lib/i18n.ts | 30 ++++++++ .../src/locales/en/account-delete-email.json | 9 +++ .../src/locales/en/invite-email.json | 9 +++ .../account-invitations-webhook.service.ts | 4 +- .../webhooks/account-webhooks.service.ts | 4 +- pnpm-lock.yaml | 3 + 10 files changed, 152 insertions(+), 37 deletions(-) rename packages/email-templates/src/{ => emails}/account-delete.email.tsx (51%) rename packages/email-templates/src/{ => emails}/invite.email.tsx (61%) create mode 100644 packages/email-templates/src/lib/i18n.ts create mode 100644 packages/email-templates/src/locales/en/account-delete-email.json create mode 100644 packages/email-templates/src/locales/en/invite-email.json 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