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/es2. Copy and Translate Files
# Copy all English email templatescp 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 senderconst 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:
- NEXT_LOCALE cookie (set by the i18n middleware when user browses the app)
- User database field if you store language preference
- 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
| Template | Namespace | Purpose |
|---|---|---|
| Email Verification | email-verification-email | Verify new user's email |
| Password Reset | reset-password-email | Reset password link |
| Magic Link | magic-link-email | Passwordless sign-in |
| Team Invite | invite-email | Invite to organization |
| Account Delete | account-delete-email | Confirm account deletion |
| Change Email | change-email-* | Email change flow |
| OTP Codes | otp-* | 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-templatespnpm devTest with Different Languages
Create test props with different language values:
// Test Englishconst enEmail = await renderEmailVerificationEmail({ verificationUrl: 'https://example.com/verify', productName: 'MyApp', language: 'en',});// Test Spanishconst esEmail = await renderEmailVerificationEmail({ verificationUrl: 'https://example.com/verify', productName: 'MyApp', language: 'es',});Verify All Templates Have Translations
# Compare translation files between localesdiff <(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
- Check that the
languageprop is being passed to the email renderer - Verify the
NEXT_LOCALEcookie is being set correctly - 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.jsonCreate the missing file or ensure the locale folder exists.
Translation Key Not Found
Returns the key name as fallback. Check that:
- The key exists in the JSON file
- The namespace matches the filename
- There are no typos in the key
Frequently Asked Questions
How do email translations work in MakerKit?
Where are email translation files stored?
How does the system know which language to use for emails?
How do I add translations for a new email template?
Can I include HTML in email translations?
Previous: Managing Translations | Up: Internationalization Overview