Email Translations

How to internationalize transactional email templates in MakerKit using next-intl for multi-language email support.

MakerKit's email templates support internationalization using the same next-intl patterns as the main application. When users receive emails like verification links or password resets, the content is sent in their preferred language.

How Email i18n Works

Email templates use a separate translation loader that works outside of the React context:

import { createTranslator } from 'next-intl';
export async function initializeEmailI18n(params: {
language: string | undefined;
namespace: string;
}) {
const language = params.language ?? 'en';
// Load translations from the email templates package
const messages = await import(
`../locales/${language}/${params.namespace}.json`
);
// Create a translator function
const translator = createTranslator({
locale: language,
messages,
});
return { t: translator, language };
}

Email Translation Files

Email translations live in the @kit/email-templates package:

packages/email-templates/src/locales/
├── en/
│ ├── email-verification-email.json
│ ├── reset-password-email.json
│ ├── magic-link-email.json
│ ├── invite-email.json
│ ├── account-delete-email.json
│ ├── change-email-confirmation-email.json
│ ├── change-email-verification-email.json
│ ├── delete-account-otp-email.json
│ ├── otp-email.json
│ ├── otp-email-verification-email.json
│ ├── otp-password-reset-email.json
│ └── otp-sign-in-email.json
├── es/ # Add for Spanish support
│ └── ... (same files)
└── fr/ # Add for French support
└── ... (same files)

Using Translations in Email Templates

Each email template initializes its own translator:

import { initializeEmailI18n } from '../lib/i18n';
interface Props {
verificationUrl: string;
productName: string;
language?: string; // Locale passed from the caller
}
export async function renderEmailVerificationEmail(props: Props) {
const namespace = 'email-verification-email';
// Initialize i18n with the user's language
const { t } = await initializeEmailI18n({
language: props.language,
namespace,
});
// Use translations
const subject = t('subject', { productName: props.productName });
const heading = t('heading');
const mainText = t('mainText');
const ctaText = t('ctaText');
const footerText = t('footerText', { productName: props.productName });
// Render the email...
}

The translation file:

{
"subject": "Verify your email for {productName}",
"heading": "Verify your email address",
"mainText": "Thank you for signing up! Please verify your email address by clicking the button below.",
"ctaText": "Verify Email Address",
"footerText": "This link will expire in 24 hours. If you didn't sign up for {productName}, you can safely ignore this email."
}

Adding Email Translations for a New Language

1. Create the Locale Folder

mkdir packages/email-templates/src/locales/es

2. Copy and Translate Files

# Copy all English email templates
cp packages/email-templates/src/locales/en/*.json \
packages/email-templates/src/locales/es/

Then translate each file:

{
"subject": "Verifica tu correo electrónico para {productName}",
"heading": "Verifica tu dirección de correo",
"mainText": "¡Gracias por registrarte! Por favor verifica tu dirección de correo haciendo clic en el botón de abajo.",
"ctaText": "Verificar Correo",
"footerText": "Este enlace expirará en 24 horas. Si no te registraste en {productName}, puedes ignorar este correo."
}

How Language is Determined

When sending an email, the user's language preference is passed to the email renderer:

// In your auth callback or email sender
const language = await getLanguageFromRequest();
await sendEmail({
to: user.email,
...await renderEmailVerificationEmail({
verificationUrl,
productName: 'Your App',
language, // User's preferred language
}),
});

The language is typically retrieved from:

  1. NEXT_LOCALE cookie (set by the i18n middleware when user browses the app)
  2. User database field if you store language preference
  3. Fallback to 'en' if no preference is found

Getting Language from Request

import { cookies } from 'next/headers';
async function getLanguageFromRequest() {
const cookiesStore = await cookies();
return cookiesStore.get('NEXT_LOCALE')?.value || 'en';
}

Available Email Templates

TemplateNamespacePurpose
Email Verificationemail-verification-emailVerify new user's email
Password Resetreset-password-emailReset password link
Magic Linkmagic-link-emailPasswordless sign-in
Team Inviteinvite-emailInvite to organization
Account Deleteaccount-delete-emailConfirm account deletion
Change Emailchange-email-*Email change flow
OTP Codesotp-*One-time password emails

Creating a New Email Template

1. Create the Template Component

import { render } from '@react-email/components';
import { initializeEmailI18n } from '../lib/i18n';
interface Props {
userName: string;
productName: string;
language?: string;
}
export async function renderWelcomeEmail(props: Props) {
const namespace = 'welcome-email';
const { t } = await initializeEmailI18n({
language: props.language,
namespace,
});
const subject = t('subject', { productName: props.productName });
const greeting = t('greeting', { name: props.userName });
const body = t('body');
const ctaText = t('ctaText');
const html = await render(
// Your email JSX here...
);
return { html, subject };
}

2. Create Translation Files

For each supported locale:

{
"subject": "Welcome to {productName}!",
"greeting": "Hi {name},",
"body": "Thanks for joining! We're excited to have you on board.",
"ctaText": "Get Started"
}
{
"subject": "¡Bienvenido a {productName}!",
"greeting": "Hola {name},",
"body": "¡Gracias por unirte! Estamos emocionados de tenerte.",
"ctaText": "Comenzar"
}

3. Export the Template

export { renderWelcomeEmail } from './emails/welcome.email';

Testing Email Translations

Preview in Browser

Use React Email's preview server to test emails:

cd packages/email-templates
pnpm dev

Test with Different Languages

Create test props with different language values:

// Test English
const enEmail = await renderEmailVerificationEmail({
verificationUrl: 'https://example.com/verify',
productName: 'MyApp',
language: 'en',
});
// Test Spanish
const esEmail = await renderEmailVerificationEmail({
verificationUrl: 'https://example.com/verify',
productName: 'MyApp',
language: 'es',
});

Verify All Templates Have Translations

# Compare translation files between locales
diff <(ls packages/email-templates/src/locales/en/) \
<(ls packages/email-templates/src/locales/es/)

Common Patterns

Interpolation

{
"greeting": "Hello, {name}!",
"expiry": "This link expires in {hours} hours."
}
t('greeting', { name: 'John' }); // "Hello, John!"
t('expiry', { hours: 24 }); // "This link expires in 24 hours."

HTML in Translations

For HTML content, use dangerouslySetInnerHTML:

{
"footerHtml": "Need help? <a href=\"{supportUrl}\">Contact support</a>."
}
const footerText = t('footerHtml', { supportUrl: 'https://example.com/support' });
<Text dangerouslySetInnerHTML={{ __html: footerText }} />

Conditional Content

Handle optional content with separate keys:

{
"withDiscount": "Your subscription renews at {price} (you saved {discount}!)",
"withoutDiscount": "Your subscription renews at {price}"
}
const renewalText = discount
? t('withDiscount', { price, discount })
: t('withoutDiscount', { price });

Troubleshooting

Email Sent in Wrong Language

  1. Check that the language prop is being passed to the email renderer
  2. Verify the NEXT_LOCALE cookie is being set correctly
  3. Ensure the translation file exists for the requested locale

Missing Translation File

If a translation file doesn't exist, the system logs a warning and falls back to the key:

Error loading i18n file: locales/de/email-verification-email.json

Create the missing file or ensure the locale folder exists.

Translation Key Not Found

Returns the key name as fallback. Check that:

  1. The key exists in the JSON file
  2. The namespace matches the filename
  3. There are no typos in the key

Frequently Asked Questions

How do email translations work in MakerKit?
Email templates use initializeEmailI18n() from packages/email-templates/src/lib/i18n.ts. This function loads translation JSON files from packages/email-templates/src/locales/{locale}/ and returns a translator function. Pass the user's language when rendering emails.
Where are email translation files stored?
Email translations are stored separately from app translations in packages/email-templates/src/locales/{locale}/. Each email template has its own JSON file, like email-verification-email.json or invite-email.json.
How does the system know which language to use for emails?
The user's language preference is typically read from the NEXT_LOCALE cookie, which is set by the i18n middleware when users browse the app. You can also store language preference in your database and pass it when sending emails.
How do I add translations for a new email template?
1) Create the email component in packages/email-templates/src/emails/, 2) Create JSON files for each locale in packages/email-templates/src/locales/{locale}/, 3) Use initializeEmailI18n() with the namespace matching your JSON filename, 4) Export the render function from the package index.
Can I include HTML in email translations?
Yes, use HTML in translation values and render with dangerouslySetInnerHTML. For example: "footerHtml": "Need help? <a href=\"{supportUrl}\">Contact support</a>." Then use <Text dangerouslySetInnerHTML={{ __html: t('footerHtml', { supportUrl }) }} />.

Previous: Managing Translations | Up: Internationalization Overview