Email Template Translations | Next.js Supabase SaaS Kit
Learn how to translate email templates for team invitations, account deletion, and OTP verification in your multi-language SaaS application.
Email templates in Makerkit have their own translation system, separate from the main application translations. This allows emails to be sent in the recipient's preferred language.
Email Translation Structure
Email translations are stored in the packages/email-templates package:
packages/email-templates/src/├── locales/│ └── en/│ ├── account-delete-email.json│ ├── invite-email.json│ └── otp-email.json└── lib/ └── i18n.tsDefault Email Templates
Makerkit includes three translatable email templates:
Team Invitation Email
Sent when a user is invited to join a team:
{ "subject": "You have been invited to join a team", "heading": "Join {{teamName}} on {{productName}}", "hello": "Hello {{invitedUserEmail}},", "mainText": "<strong>{{inviter}}</strong> has invited you to the <strong>{{teamName}}</strong> team on <strong>{{productName}}</strong>.", "joinTeam": "Join {{teamName}}", "copyPasteLink": "or copy and paste this URL into your browser:", "invitationIntendedFor": "This invitation is intended for {{invitedUserEmail}}."}Account Deletion Email
Sent when a user requests account deletion:
{ "subject": "Account Deletion Confirmation", "heading": "Your account has been deleted", "hello": "Hello,", "mainText": "Your account and all associated data have been permanently deleted from {{productName}}.", "contactSupport": "If you did not request this deletion, please contact support immediately."}OTP Verification Email
Sent for one-time password verification:
{ "subject": "Your verification code", "heading": "Verification Code", "hello": "Hello,", "mainText": "Your verification code is:", "codeExpiry": "This code will expire in {{minutes}} minutes.", "ignoreIfNotRequested": "If you did not request this code, you can safely ignore this email."}Adding Email Translations
1. Create Language Folder
Create a new folder for your language:
mkdir packages/email-templates/src/locales/es2. Copy and Translate Files
Copy the English templates:
cp packages/email-templates/src/locales/en/*.json packages/email-templates/src/locales/es/Translate each file:
{ "subject": "Has sido invitado a unirte a un equipo", "heading": "Unete a {{teamName}} en {{productName}}", "hello": "Hola {{invitedUserEmail}},", "mainText": "<strong>{{inviter}}</strong> te ha invitado al equipo <strong>{{teamName}}</strong> en <strong>{{productName}}</strong>.", "joinTeam": "Unirse a {{teamName}}", "copyPasteLink": "o copia y pega esta URL en tu navegador:", "invitationIntendedFor": "Esta invitacion es para {{invitedUserEmail}}."}How Email Translations Work
The email template system initializes i18n separately from the main application:
import { initializeServerI18n } from '@kit/i18n/server';import { createI18nSettings } from '@kit/i18n';export function initializeEmailI18n(params: { language: string | undefined; namespace: string;}) { const language = params.language ?? process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? 'en'; return initializeServerI18n( createI18nSettings({ language, languages: [language], namespaces: params.namespace, }), async (language, namespace) => { const data = await import(`../locales/${language}/${namespace}.json`); return data as Record<string, string>; }, );}Key points:
- Each email type has its own namespace (e.g.,
invite-email,otp-email) - The language can be passed when sending the email
- Falls back to
NEXT_PUBLIC_DEFAULT_LOCALEif no language specified
Sending Translated Emails
When sending emails, pass the recipient's preferred language:
import { renderInviteEmail } from '@kit/email-templates';export async function sendInviteEmail(params: { invitedUserEmail: string; inviter: string; teamName: string; inviteLink: string; language?: string; // Recipient's preferred language}) { const { html, subject } = await renderInviteEmail({ invitedUserEmail: params.invitedUserEmail, inviter: params.inviter, teamName: params.teamName, link: params.inviteLink, productName: 'Your App', language: params.language, // Pass the language }); await sendEmail({ to: params.invitedUserEmail, subject, html, });}Determining Recipient Language
To send emails in the recipient's language, you need to know their preference. Common approaches:
From User Profile
Store language preference in the user profile:
// When sending an emailconst user = await getUserById(userId);const language = user.preferredLanguage ?? 'en';await sendInviteEmail({ // ...other params language,});From Request Context
Use the current user's language when they trigger an action:
'use server';import { createI18nServerInstance } from '~/lib/i18n/i18n.server';export async function inviteMember(email: string) { const i18n = await createI18nServerInstance(); const currentLanguage = i18n.language; await sendInviteEmail({ invitedUserEmail: email, // Use inviter's language as default for the invited user language: currentLanguage, });}Adding Custom Email Templates
To add a new translatable email template:
1. Create Translation Files
{ "subject": "Welcome to {{productName}}", "heading": "Welcome aboard!", "hello": "Hello {{userName}},", "mainText": "Thank you for joining {{productName}}. We're excited to have you!", "getStarted": "Get Started", "helpText": "If you have any questions, feel free to reach out to our support team."}2. Create the Email Template Component
import { Body, Button, Container, Head, Heading, Html, Preview, Section, Text, render,} from '@react-email/components';import { initializeEmailI18n } from '../lib/i18n';interface WelcomeEmailProps { userName: string; productName: string; dashboardUrl: string; language?: string;}export async function renderWelcomeEmail(props: WelcomeEmailProps) { const namespace = 'welcome-email'; const { t } = await initializeEmailI18n({ language: props.language, namespace, }); const subject = t(`${namespace}:subject`, { productName: props.productName }); // Use render() to convert JSX to HTML string const html = await render( <Html> <Head /> <Preview>{subject}</Preview> <Body> <Container> <Heading>{t(`${namespace}:heading`)}</Heading> <Text>{t(`${namespace}:hello`, { userName: props.userName })}</Text> <Text> {t(`${namespace}:mainText`, { productName: props.productName })} </Text> <Section> <Button href={props.dashboardUrl}> {t(`${namespace}:getStarted`)} </Button> </Section> <Text>{t(`${namespace}:helpText`)}</Text> </Container> </Body> </Html> ); return { html, subject };}3. Export from Package
export * from './emails/welcome.email';Interpolation in Email Translations
Email translations support the same interpolation syntax as the main application:
Simple Variables
{ "hello": "Hello {{userName}},"}HTML Tags
You can use basic HTML for formatting:
{ "mainText": "<strong>{{inviter}}</strong> has invited you to join <strong>{{teamName}}</strong>."}The email template must render this content appropriately:
<Text dangerouslySetInnerHTML={{ __html: t('mainText', { inviter, teamName }) }} />When using dangerouslySetInnerHTML, ensure all interpolated values come from trusted sources (your database, not user input). Never interpolate raw user input into HTML translations without sanitization. For user-provided content, use plain text translations instead.
Testing Email Translations
Preview in Development
Use the email preview feature to test translations:
# Start the email preview servercd packages/email-templatespnpm devThen open http://localhost:3001 to preview email templates.
Test with Inbucket
When running Supabase locally, emails are captured by Inbucket:
- Start Supabase:
pnpm supabase:start - Open Inbucket:
http://localhost:54324 - Trigger an action that sends an email
- Check Inbucket for the translated email
Verify All Languages
Create a test script to verify translations exist for all configured languages:
# Check that all email translation files existfor lang in en es de fr; do for file in invite-email account-delete-email otp-email; do path="packages/email-templates/src/locales/${lang}/${file}.json" if [ -f "$path" ]; then echo "OK: $path" else echo "MISSING: $path" fi donedoneTroubleshooting
Email Shows English Instead of User's Language
Check that you're passing the language parameter when rendering the email:
const { html, subject } = await renderInviteEmail({ // ... language: user.preferredLanguage, // Must be passed explicitly});Translation File Not Found Error
Verify the file exists at the expected path:
packages/email-templates/src/locales/[language]/[namespace].jsonThe namespace must match the email template name (e.g., invite-email, not invite).
HTML Not Rendering in Email
Email clients have limited HTML support. Stick to basic tags (<strong>, <em>, <br>) and avoid complex CSS. Test with multiple email clients (Gmail, Outlook, Apple Mail).
Frequently Asked Questions
How do I preview email translations locally?
Can I use the same translations for app and email?
How do I add a new email template with translations?
Do email translations support pluralization?
How do I comply with email regulations (CAN-SPAM, GDPR)?
Related Documentation
- Using Translations - Translation basics
- Adding Translations - Add new languages
- Language Selector - Let users change language
- Sending Emails - Email sending configuration