.agents/skills/react-email/references/I18N.md
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.
Best choice for Next.js applications with straightforward API.
npm install next-intl
1. Create message files:
// 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."
}
}
// 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."
}
}
// 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:
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:
await resend.emails.send({
from: 'Acme <[email protected]>',
to: ['[email protected]'],
subject: 'Welcome',
react: <WelcomeEmail name="Jean" verificationUrl="..." locale="fr" />
});
Good choice for complex formatting needs (plurals, dates, numbers).
npm install react-intl
1. Create message files:
// 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:
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>
);
}
Best for non-Next.js applications or when you need more control.
npm install react-i18next i18next i18next-resources-to-backend
1. Configure i18next:
// 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:
// 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:
// messages/en/welcome-email.json
{
"subject": "Welcome to Acme",
"greeting": "Hi",
"body": "Thanks for signing up!",
"cta": "Get Started"
}
// messages/es/welcome-email.json
{
"subject": "Bienvenido a Acme",
"greeting": "Hola",
"body": "¡Gracias por registrarte!",
"cta": "Comenzar"
}
4. Use in email template:
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>
);
}
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
Use descriptive, hierarchical keys:
{
"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"
}
}
}
Make locale a required prop:
interface EmailProps {
locale: string;
// other props...
}
<Html lang={locale}>
For Arabic, Hebrew, etc.:
const isRTL = ['ar', 'he', 'fa'].includes(locale);
<Html lang={locale} dir={isRTL ? 'rtl' : 'ltr'}>
Provide fallback translations:
const t = createTranslator({
messages: await import(`../messages/${locale}.json`).catch(() =>
import('../messages/en.json')
),
locale,
namespace: 'welcome-email'
});
Test email rendering for each supported locale:
WelcomeEmail.PreviewProps = {
name: 'Test User',
locale: 'en' // Change to test different locales
} as WelcomeEmailProps;
Use the same translation keys across all locale files:
// ✅ Good
// en.json: { "cta": "Get Started" }
// es.json: { "cta": "Comenzar" }
// ❌ Bad
// en.json: { "button": "Get Started" }
// es.json: { "cta": "Comenzar" }
Set up fallback behavior:
// With next-intl
const t = createTranslator({
messages,
locale,
namespace: 'welcome-email',
onError: (error) => {
console.warn('Translation missing:', error);
}
});
Don't forget to translate email subjects:
const t = createTranslator({...});
await resend.emails.send({
from: 'Acme <[email protected]>',
to: [user.email],
subject: t('subject'), // ✅ Translated subject
react: <WelcomeEmail {...props} />
});
Maintain consistent formatting across locales:
Use Intl APIs for automatic locale-specific formatting.
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:
// 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!"
}
}
// 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!"
}
}