Files
myeasycms-v2/docs/translations/email-translations.mdoc
Giancarlo Buomprisco 7ebff31475 Next.js Supabase V3 (#463)
Version 3 of the kit:
- Radix UI replaced with Base UI (using the Shadcn UI patterns)
- next-intl replaces react-i18next
- enhanceAction deprecated; usage moved to next-safe-action
- main layout now wrapped with [locale] path segment
- Teams only mode
- Layout updates
- Zod v4
- Next.js 16.2
- Typescript 6
- All other dependencies updated
- Removed deprecated Edge CSRF
- Dynamic Github Action runner
2026-03-24 13:40:38 +08:00

431 lines
12 KiB
Plaintext

---
status: "published"
title: "Email Template Translations | Next.js Supabase SaaS Kit"
label: "Email Translations"
description: "Learn how to translate email templates for team invitations, account deletion, and OTP verification in your multi-language SaaS application."
order: 3
---
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.ts
```
## Default Email Templates
Makerkit includes three translatable email templates:
### Team Invitation Email
Sent when a user is invited to join a team:
```json title="packages/email-templates/src/locales/en/invite-email.json"
{
"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:
```json title="packages/email-templates/src/locales/en/account-delete-email.json"
{
"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"
}
```
### OTP Verification Email
Sent for one-time password verification:
```json title="packages/email-templates/src/locales/en/otp-email.json"
{
"subject": "One-time password for {productName}",
"heading": "One-time password for {productName}",
"otpText": "Your one-time password is: {otp}",
"mainText": "You're receiving this email because you need to verify your identity using a one-time password.",
"footerText": "Please enter the one-time password in the app to continue."
}
```
## Adding Email Translations
### 1. Create Language Folder
Create a new folder for your language:
```bash
mkdir packages/email-templates/src/locales/es
```
### 2. Copy and Translate Files
Copy the English templates:
```bash
cp packages/email-templates/src/locales/en/*.json packages/email-templates/src/locales/es/
```
Translate each file:
```json title="packages/email-templates/src/locales/es/invite-email.json"
{
"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:
```tsx title="packages/email-templates/src/lib/i18n.ts"
import type { AbstractIntlMessages } from 'next-intl';
import { createTranslator } from 'next-intl';
export async function initializeEmailI18n(params: {
language: string | undefined;
namespace: string;
}) {
const language = params.language ?? 'en';
try {
const messages = (await import(
`../locales/${language}/${params.namespace}.json`
)) as AbstractIntlMessages;
const translator = createTranslator({
locale: language,
messages,
});
const t = translator as unknown as (
key: string,
values?: Record<string, unknown>,
) => string;
return { t, language };
} catch (error) {
console.log(
`Error loading i18n file: locales/${language}/${params.namespace}.json`,
error,
);
const t = (key: string) => key;
return { t, language };
}
}
```
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 `'en'` if no language specified
## Sending Translated Emails
When sending emails, pass the recipient's preferred language:
```tsx title="apps/web/lib/server/send-invite-email.ts"
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:
```tsx
// When sending an email
const 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:
```tsx title="apps/web/lib/server/invite-member.ts"
'use server';
import { getLocale } from 'next-intl/server';
export async function inviteMember(email: string) {
const currentLanguage = await getLocale();
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
```json title="packages/email-templates/src/locales/en/welcome-email.json"
{
"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
```tsx title="packages/email-templates/src/emails/welcome.email.tsx"
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('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('heading')}</Heading>
<Text>{t('hello', { userName: props.userName })}</Text>
<Text>
{t('mainText', { productName: props.productName })}
</Text>
<Section>
<Button href={props.dashboardUrl}>
{t('getStarted')}
</Button>
</Section>
<Text>{t('helpText')}</Text>
</Container>
</Body>
</Html>
);
return { html, subject };
}
```
### 3. Export from Package
```tsx title="packages/email-templates/src/index.ts"
export * from './emails/welcome.email';
```
## Interpolation in Email Translations
Email translations support the same interpolation syntax as the main application:
### Simple Variables
```json
{
"hello": "Hello {userName},"
}
```
### HTML Tags
You can use basic HTML for formatting:
```json
{
"mainText": "<strong>{inviter}</strong> has invited you to join <strong>{teamName}</strong>."
}
```
The email template must render this content appropriately:
```tsx
<Text dangerouslySetInnerHTML={{ __html: t('mainText', { inviter, teamName }) }} />
```
{% callout type="warning" title="Security Warning" %}
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.
{% /callout %}
## Testing Email Translations
### Preview in Development
Use the email preview feature to test translations:
```bash
# Start the email preview server
cd packages/email-templates
pnpm dev
```
Then open `http://localhost:3001` to preview email templates.
### Test with Inbucket
When running Supabase locally, emails are captured by Inbucket:
1. Start Supabase: `pnpm supabase:web:start`
2. Open Inbucket: `http://localhost:54324`
3. Trigger an action that sends an email
4. Check Inbucket for the translated email
### Verify All Languages
Create a test script to verify translations exist for all configured languages:
```bash
# Check that all email translation files exist
for 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
done
done
```
## Troubleshooting
### Email Shows English Instead of User's Language
Check that you're passing the language parameter when rendering the email:
```tsx
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].json
```
The 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).
{% faq
title="Frequently Asked Questions"
items=[
{"question": "How do I preview email translations locally?", "answer": "Run 'pnpm dev' in the packages/email-templates directory to start the email preview server at localhost:3001. You can switch languages in the preview to test different translations."},
{"question": "Can I use the same translations for app and email?", "answer": "Email templates use a separate translation system in packages/email-templates/src/locales. This separation allows emails to be rendered without the full app context and keeps email-specific strings isolated."},
{"question": "How do I add a new email template with translations?", "answer": "Create the translation JSON files in packages/email-templates/src/locales/[lang]/[template-name].json, then create the React Email component that calls initializeEmailI18n with the matching namespace."},
{"question": "Do email translations support pluralization?", "answer": "Yes, email translations use next-intl's createTranslator which supports ICU message format for pluralization."},
{"question": "How do I comply with email regulations (CAN-SPAM, GDPR)?", "answer": "Include an unsubscribe link in marketing emails, add your physical address, and honor unsubscribe requests within 10 days. For GDPR, ensure you have consent before sending and document it. These requirements apply regardless of language."}
]
/%}
## Related Documentation
- [Using Translations](/docs/next-supabase-turbo/translations/using-translations) - Translation basics
- [Adding Translations](/docs/next-supabase-turbo/translations/adding-translations) - Add new languages
- [Language Selector](/docs/next-supabase-turbo/translations/language-selector) - Let users change language
- [Sending Emails](/docs/next-supabase-turbo/emails/sending-emails) - Email sending configuration