Back to Novu

Internationalization (i18n) Guide

.agents/skills/react-email/references/I18N.md

3.15.014.0 KB
Original Source

Internationalization (i18n) Guide

Complete guide for implementing multi-language email support with React Email using Tailwind CSS styling.

React Email officially supports three popular i18n libraries: next-intl, react-i18next, and react-intl.

next-intl

Best choice for Next.js applications with straightforward API.

Installation

bash
npm install next-intl

Setup

1. Create message files:

json
// messages/en.json
{
  "welcome-email": {
    "subject": "Welcome to Acme",
    "greeting": "Hi",
    "body": "Thanks for signing up! We're excited to have you on board.",
    "cta": "Get Started",
    "footer": "If you have questions, reply to this email."
  }
}
json
// messages/es.json
{
  "welcome-email": {
    "subject": "Bienvenido a Acme",
    "greeting": "Hola",
    "body": "¡Gracias por registrarte! Estamos emocionados de tenerte en la plataforma.",
    "cta": "Comenzar",
    "footer": "Si tienes preguntas, responde a este correo electrónico."
  }
}
json
// messages/fr.json
{
  "welcome-email": {
    "subject": "Bienvenue chez Acme",
    "greeting": "Bonjour",
    "body": "Merci de vous être inscrit ! Nous sommes ravis de vous accueillir.",
    "cta": "Commencer",
    "footer": "Si vous avez des questions, répondez à cet e-mail."
  }
}

2. Update email template:

tsx
import { createTranslator } from 'next-intl';
import {
  Html,
  Head,
  Preview,
  Body,
  Container,
  Heading,
  Text,
  Button,
  Hr,
  Tailwind,
  pixelBasedPreset
} from '@react-email/components';

interface WelcomeEmailProps {
  name: string;
  verificationUrl: string;
  locale: string;
}

export default async function WelcomeEmail({
  name,
  verificationUrl,
  locale
}: WelcomeEmailProps) {
  const t = createTranslator({
    messages: await import(`../messages/${locale}.json`),
    namespace: 'welcome-email',
    locale
  });

  return (
    <Html lang={locale}>
      <Tailwind config={{ presets: [pixelBasedPreset] }}>
        <Head />
        <Preview>{t('subject')}</Preview>
        <Body className="bg-gray-100 font-sans">
          <Container className="mx-auto py-10 px-5 max-w-xl">
            <Heading className="text-2xl font-bold text-gray-800">
              {t('subject')}
            </Heading>
            <Text className="text-base leading-7 text-gray-800 my-4">
              {t('greeting')} {name},
            </Text>
            <Text className="text-base leading-7 text-gray-800 my-4">
              {t('body')}
            </Text>
            <Button
              href={verificationUrl}
              className="bg-blue-600 text-white px-5 py-3 rounded block text-center no-underline"
            >
              {t('cta')}
            </Button>
            <Hr className="border-gray-200 my-5" />
            <Text className="text-sm text-gray-500">
              {t('footer')}
            </Text>
          </Container>
        </Body>
      </Tailwind>
    </Html>
  );
}

// Preview props
WelcomeEmail.PreviewProps = {
  name: 'John',
  verificationUrl: 'https://example.com/verify',
  locale: 'en'
} as WelcomeEmailProps;

3. Send with locale:

tsx
await resend.emails.send({
  from: 'Acme <[email protected]>',
  to: ['[email protected]'],
  subject: 'Welcome',
  react: <WelcomeEmail name="Jean" verificationUrl="..." locale="fr" />
});

react-intl (FormatJS)

Good choice for complex formatting needs (plurals, dates, numbers).

Installation

bash
npm install react-intl

Setup

1. Create message files:

json
// messages/en/welcome-email.json
{
  "header": "Welcome to Acme",
  "greeting": "Hi",
  "body": "Thanks for signing up!",
  "cta": "Get Started",
  "itemCount": "{count, plural, one {# item} other {# items}}"
}

2. Use in email:

tsx
import { createIntl } from 'react-intl';
import {
  Html,
  Body,
  Container,
  Text,
  Button,
  Tailwind,
  pixelBasedPreset
} from '@react-email/components';

interface WelcomeEmailProps {
  name: string;
  locale: string;
  itemCount?: number;
}

export default async function WelcomeEmail({
  name,
  locale,
  itemCount = 1
}: WelcomeEmailProps) {
  const { formatMessage } = createIntl({
    locale,
    messages: await import(`../messages/${locale}/welcome-email.json`)
  });

  return (
    <Html lang={locale}>
      <Tailwind config={{ presets: [pixelBasedPreset] }}>
        <Body className="bg-gray-100 font-sans">
          <Container className="mx-auto p-5 max-w-xl">
            <Text className="text-base text-gray-800">
              {formatMessage({ id: 'greeting' })} {name},
            </Text>
            <Text className="text-base text-gray-800">
              {formatMessage({ id: 'body' })}
            </Text>
            <Text className="text-base text-gray-800">
              {formatMessage({ id: 'itemCount' }, { count: itemCount })}
            </Text>
            <Button
              href="https://example.com"
              className="bg-blue-600 text-white px-5 py-3 rounded"
            >
              {formatMessage({ id: 'cta' })}
            </Button>
          </Container>
        </Body>
      </Tailwind>
    </Html>
  );
}

react-i18next

Best for non-Next.js applications or when you need more control.

Installation

bash
npm install react-i18next i18next i18next-resources-to-backend

Setup

1. Configure i18next:

js
// i18n.js
import i18next from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import { initReactI18next } from 'react-i18next';

i18next
  .use(initReactI18next)
  .use(resourcesToBackend((language, namespace) =>
    import(`./messages/${language}/${namespace}.json`)
  ))
  .init({
    supportedLngs: ['en', 'es', 'fr', 'de'],
    fallbackLng: 'en',
    lng: undefined,
    preload: ['en', 'es', 'fr', 'de']
  });

export { i18next };

2. Create translation helper:

js
// get-t.js
import { i18next } from './i18n';

export async function getT(namespace, locale) {
  if (locale && i18next.resolvedLanguage !== locale) {
    await i18next.changeLanguage(locale);
  }
  if (namespace && !i18next.hasLoadedNamespace(namespace)) {
    await i18next.loadNamespaces(namespace);
  }
  return {
    t: i18next.getFixedT(
      locale ?? i18next.resolvedLanguage,
      Array.isArray(namespace) ? namespace[0] : namespace
    ),
    i18n: i18next
  };
}

3. Create message files:

json
// messages/en/welcome-email.json
{
  "subject": "Welcome to Acme",
  "greeting": "Hi",
  "body": "Thanks for signing up!",
  "cta": "Get Started"
}
json
// messages/es/welcome-email.json
{
  "subject": "Bienvenido a Acme",
  "greeting": "Hola",
  "body": "¡Gracias por registrarte!",
  "cta": "Comenzar"
}

4. Use in email template:

tsx
import { getT } from '../get-t';
import {
  Html,
  Body,
  Container,
  Heading,
  Text,
  Button,
  Tailwind,
  pixelBasedPreset
} from '@react-email/components';

interface WelcomeEmailProps {
  name: string;
  locale: string;
}

export default async function WelcomeEmail({ name, locale }: WelcomeEmailProps) {
  const { t } = await getT('welcome-email', locale);

  return (
    <Html lang={locale}>
      <Tailwind config={{ presets: [pixelBasedPreset] }}>
        <Body className="bg-gray-100 font-sans">
          <Container className="mx-auto p-5 max-w-xl">
            <Heading className="text-2xl font-bold text-gray-800">
              {t('subject')}
            </Heading>
            <Text className="text-base text-gray-800">
              {t('greeting')} {name},
            </Text>
            <Text className="text-base text-gray-800">
              {t('body')}
            </Text>
            <Button
              href="https://example.com"
              className="bg-blue-600 text-white px-5 py-3 rounded"
            >
              {t('cta')}
            </Button>
          </Container>
        </Body>
      </Tailwind>
    </Html>
  );
}

Message File Organization

Organize translations by email template:

messages/
├── en.json          # All English translations
│   ├── welcome-email
│   ├── password-reset
│   └── order-confirmation
├── es.json          # All Spanish translations
└── fr.json          # All French translations

Or organize by template with separate files:

messages/
├── en/
│   ├── welcome-email.json
│   ├── password-reset.json
│   └── order-confirmation.json
├── es/
│   ├── welcome-email.json
│   ├── password-reset.json
│   └── order-confirmation.json
└── fr/
    ├── welcome-email.json
    ├── password-reset.json
    └── order-confirmation.json

Translation Keys

Use descriptive, hierarchical keys:

json
{
  "welcome-email": {
    "subject": "Welcome!",
    "preview": "Get started with your account",
    "header": {
      "title": "Welcome to Acme",
      "subtitle": "We're glad you're here"
    },
    "body": {
      "greeting": "Hi",
      "intro": "Thanks for signing up!",
      "next-steps": "Here's how to get started:"
    },
    "cta": {
      "primary": "Get Started",
      "secondary": "Learn More"
    },
    "footer": {
      "help": "Need help? Reply to this email",
      "unsubscribe": "Unsubscribe from these emails"
    }
  }
}

Best Practices

1. Always Pass Locale

Make locale a required prop:

tsx
interface EmailProps {
  locale: string;
  // other props...
}

2. Set HTML Lang Attribute

tsx
<Html lang={locale}>

3. Support RTL Languages

For Arabic, Hebrew, etc.:

tsx
const isRTL = ['ar', 'he', 'fa'].includes(locale);

<Html lang={locale} dir={isRTL ? 'rtl' : 'ltr'}>

4. Fallback Values

Provide fallback translations:

tsx
const t = createTranslator({
  messages: await import(`../messages/${locale}.json`).catch(() =>
    import('../messages/en.json')
  ),
  locale,
  namespace: 'welcome-email'
});

5. Test All Locales

Test email rendering for each supported locale:

tsx
WelcomeEmail.PreviewProps = {
  name: 'Test User',
  locale: 'en'  // Change to test different locales
} as WelcomeEmailProps;

6. Keep Keys Consistent

Use the same translation keys across all locale files:

json
// ✅ Good
// en.json: { "cta": "Get Started" }
// es.json: { "cta": "Comenzar" }

// ❌ Bad
// en.json: { "button": "Get Started" }
// es.json: { "cta": "Comenzar" }

7. Handle Missing Translations

Set up fallback behavior:

tsx
// With next-intl
const t = createTranslator({
  messages,
  locale,
  namespace: 'welcome-email',
  onError: (error) => {
    console.warn('Translation missing:', error);
  }
});

8. Subject Line Translation

Don't forget to translate email subjects:

tsx
const t = createTranslator({...});

await resend.emails.send({
  from: 'Acme <[email protected]>',
  to: [user.email],
  subject: t('subject'),  // ✅ Translated subject
  react: <WelcomeEmail {...props} />
});

9. Format Consistency

Maintain consistent formatting across locales:

  • Date formats (MM/DD/YYYY vs DD/MM/YYYY)
  • Time formats (12h vs 24h)
  • Number separators (1,234.56 vs 1.234,56)
  • Currency symbols and placement ($100 vs 100$)

Use Intl APIs for automatic locale-specific formatting.

Example: Complete Multi-locale Email

tsx
import { createTranslator } from 'next-intl';
import {
  Html,
  Head,
  Preview,
  Body,
  Container,
  Section,
  Heading,
  Text,
  Button,
  Hr,
  Tailwind,
  pixelBasedPreset
} from '@react-email/components';

interface OrderConfirmationProps {
  orderNumber: string;
  total: number;
  currency: string;
  locale: string;
  orderDate: Date;
}

export default async function OrderConfirmation({
  orderNumber,
  total,
  currency,
  locale,
  orderDate
}: OrderConfirmationProps) {
  const t = createTranslator({
    messages: await import(`../messages/${locale}.json`),
    namespace: 'order-confirmation',
    locale
  });

  const isRTL = ['ar', 'he'].includes(locale);

  const currencyFormatter = new Intl.NumberFormat(locale, {
    style: 'currency',
    currency
  });

  const dateFormatter = new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });

  return (
    <Html lang={locale} dir={isRTL ? 'rtl' : 'ltr'}>
      <Tailwind config={{ presets: [pixelBasedPreset] }}>
        <Head />
        <Preview>{t('preview')}</Preview>
        <Body className="bg-gray-100 font-sans">
          <Container className="mx-auto py-10 px-5 max-w-xl">
            <Heading className="text-2xl font-bold text-gray-800">
              {t('title')}
            </Heading>
            <Text className="text-base text-gray-800 my-2">
              {t('order-number')}: {orderNumber}
            </Text>
            <Text className="text-base text-gray-800 my-2">
              {t('order-date')}: {dateFormatter.format(orderDate)}
            </Text>
            <Section className="bg-white p-5 rounded my-4">
              <Text className="text-xl font-bold text-gray-800">
                {t('total')}: {currencyFormatter.format(total)}
              </Text>
            </Section>
            <Button
              href={`https://example.com/orders/${orderNumber}`}
              className="bg-blue-600 text-white px-5 py-3 rounded block text-center no-underline my-5"
            >
              {t('view-order')}
            </Button>
            <Hr className="border-gray-200 my-5" />
            <Text className="text-sm text-gray-500">
              {t('footer')}
            </Text>
          </Container>
        </Body>
      </Tailwind>
    </Html>
  );
}

With message files:

json
// messages/en.json
{
  "order-confirmation": {
    "preview": "Your order has been confirmed",
    "title": "Order Confirmed",
    "order-number": "Order number",
    "order-date": "Order date",
    "total": "Total",
    "view-order": "View Order",
    "footer": "Thank you for your purchase!"
  }
}
json
// messages/es.json
{
  "order-confirmation": {
    "preview": "Tu pedido ha sido confirmado",
    "title": "Pedido Confirmado",
    "order-number": "Número de pedido",
    "order-date": "Fecha del pedido",
    "total": "Total",
    "view-order": "Ver Pedido",
    "footer": "¡Gracias por tu compra!"
  }
}