# Email Translations

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

*Canonical: https://makerkit.dev/docs/nextjs-prisma/internationalization/email-translations*

---

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:

```typescript title="packages/email-templates/src/lib/i18n.ts"
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
│   ├── 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:

```typescript title="packages/email-templates/src/emails/email-verification.email.tsx"
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:

```json title="packages/email-templates/src/locales/en/email-verification-email.json"
{
  "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

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

### 2. Copy and Translate Files

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

Then translate each file:

```json title="packages/email-templates/src/locales/es/email-verification-email.json"
{
  "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:

```typescript
// 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

```typescript title="packages/better-auth/src/auth.ts"
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

```typescript title="packages/email-templates/src/emails/welcome.email.tsx"
import { render } from 'react-email';
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:

```json title="packages/email-templates/src/locales/en/welcome-email.json"
{
  "subject": "Welcome to {productName}!",
  "greeting": "Hi {name},",
  "body": "Thanks for joining! We're excited to have you on board.",
  "ctaText": "Get Started"
}
```

```json title="packages/email-templates/src/locales/es/welcome-email.json"
{
  "subject": "¡Bienvenido a {productName}!",
  "greeting": "Hola {name},",
  "body": "¡Gracias por unirte! Estamos emocionados de tenerte.",
  "ctaText": "Comenzar"
}
```

### 3. Export the Template

```typescript title="packages/email-templates/src/index.ts"
export { renderWelcomeEmail } from './emails/welcome.email';
```

## Testing Email Translations

### Preview in Browser

Use React Email's preview server to test emails:

```bash
cd packages/email-templates
pnpm dev
```

### Test with Different Languages

Create test props with different language values:

```typescript
// 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

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

## Common Patterns

### Interpolation

```json
{
  "greeting": "Hello, {name}!",
  "expiry": "This link expires in {hours} hours."
}
```

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

### HTML in Translations

For HTML content, use `dangerouslySetInnerHTML`:

```json
{
  "footerHtml": "Need help? <a href=\"{supportUrl}\">Contact support</a>."
}
```

```typescript
const footerText = t('footerHtml', { supportUrl: 'https://example.com/support' });

<Text dangerouslySetInnerHTML={{ __html: footerText }} />
```

### Conditional Content

Handle optional content with separate keys:

```json
{
  "withDiscount": "Your subscription renews at {price} (you saved {discount}!)",
  "withoutDiscount": "Your subscription renews at {price}"
}
```

```typescript
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

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "How do email translations work in MakerKit?", "answer": "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."},
     {"question": "Where are email translation files stored?", "answer": "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."},
     {"question": "How does the system know which language to use for emails?", "answer": "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."},
     {"question": "How do I add translations for a new email template?", "answer": "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."},
     {"question": "Can I include HTML in email translations?", "answer": "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](./managing-translations) | **Up:** [Internationalization Overview](./overview)
