React Email 国际化(i18n)完全指南:next-intl、react-intl 与 react-i18next 多语言邮件实战
2026/9/13 3:26:44 网站建设 项目流程

React Email 国际化(i18n)完全指南:next-intl、react-intl 与 react-i18next 多语言邮件实战

【免费下载链接】react-email💌 Build and send emails using React项目地址: https://gitcode.com/GitHub_Trending/re/react-email

本指南基于 React Email 官方 i18n 文档(skills/react-email/references/I18N.md)编写,系统讲解如何在 React Email 中为邮件模板引入多语言支持。React Email 官方支持 next-intl、react-intl(FormatJS)与 react-i18next 三种主流 i18n 库,本文逐一给出安装、配置、模板改造与发送调用的完整实操方案,并覆盖消息文件组织、RTL 语言、日期货币格式化、主题行翻译等最佳实践。读完本文,你将能把自己的英文邮件模板平滑改造成支持任意数量语言环境的可发送多语言邮件。

为什么邮件需要国际化

邮件是与用户沟通最直接、最私密的方式,用错语言会显著影响体验。React Email 将邮件编写为 React 组件,因此可以复用成熟的 i18n 生态:把硬编码文案抽离到按语言划分的消息文件,运行时根据用户的语言环境(locale)动态渲染对应文案。官方文档明确表示,React Email 对 next-intl、react-i18next、react-intl 三种库提供"官方支持"(officially supports),你可以按项目技术栈自由选择:

定位适用场景
next-intlNext.js 生态的一体化方案,API 简洁基于 Next.js 的应用,希望与 App Router 深度集成
react-intl(FormatJS)强大的 ICU 消息格式化能力需要复数、日期、数字、货币等复杂格式化
react-i18nexti18next 生态,灵活可控非 Next.js 应用,或需要更多底层控制

三者的核心思路一致:先创建每种语言的消息文件,再在邮件组件中通过翻译函数读取文案。区别在于翻译函数的获取方式与消息文件组织形态。

通用前提:pixelBasedPreset 与 Tailwind 样式

本文所有示例模板都使用了Tailwind组件并传入config={{ presets: [pixelBasedPreset] }}pixelBasedPresetreact-email内置导出的 Tailwind 预设(packages/react-email/src/components/tailwind/tailwind.tsx 中定义),它把 Tailwind 默认的 rem 单位字号与间距重新映射为像素值——例如text-base对应16px(行高24px)、p-4对应16px内边距。由于大部分邮件客户端不支持rem单位,使用该预设可保证样式在各客户端下渲染一致。实际项目中若使用自定义 Tailwind 配置,只需将预设替换为你的配置即可,i18n 改造方式完全不变。

方案一:next-intl(Next.js 应用首选)

next-intl 是面向 Next.js 的国际化库,API 直观,适合绝大多数 Next.js 应用。

1. 安装

npm install next-intl

2. 创建消息文件

每种语言一个 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." } }
// 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." } }

3. 改造邮件模板

通过createTranslator在服务端创建翻译器,namespace指定消息文件的顶层命名空间:

import { createTranslator } from 'next-intl'; import { Html, Head, Preview, Body, Container, Heading, Text, Button, Hr, Tailwind, pixelBasedPreset } from 'react-email'; 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 /> <Body className="bg-gray-100 font-sans"> <Preview>{t('subject')}</Preview> <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 box-border" > {t('cta')} </Button> <Hr className="border-solid 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;

要点说明:

  • 组件必须声明为async,因为createTranslator需要await import()动态加载消息文件;
  • <Html lang={locale}>将语言标记写入 HTML,帮助邮件客户端正确识别内容语言;
  • 使用createTranslator而非 Next.js App Router 中的useTranslationsHook 等 API,是因为预览服务器(preview server)与渲染环境无法访问你在 Next.js 应用中定义的 i18n 上下文,必须自建翻译器。

4. 发送时传入 locale

await resend.emails.send({ from: 'Acme <onboarding@resend.dev>', to: ['user@example.com'], subject: 'Welcome', react: <WelcomeEmail name="Jean" verificationUrl="..." locale="fr" /> });

locale作为 prop 传入后,组件内部会加载messages/fr.json渲染法语内容。

方案二:react-intl(FormatJS,复杂格式化首选)

react-intl 基于 FormatJS,其 ICU MessageFormat 语法擅长处理复数、日期、数字、货币等复杂格式化需求。

1. 安装

npm install react-intl

2. 创建消息文件

采用messages/<locale>/<template>.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}}" }

注意itemCount使用了 ICU 复数语法:count为 1 时输出 "1 item",否则输出 "n items"。这正是 react-intl 相对其他方案的核心差异化能力。

3. 在邮件中使用

通过createIntl获取formatMessage,以消息 id 引用文案:

import { createIntl } from 'react-intl'; import { Html, Body, Container, Text, Button, Tailwind, pixelBasedPreset } from 'react-email'; 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 box-border" > {formatMessage({ id: 'cta' })} </Button> </Container> </Body> </Tailwind> </Html> ); }

formatMessage({ id: 'itemCount' }, { count: itemCount })itemCount变量注入 ICU 消息,formatMessage会自动按当前 locale 的复数规则选择正确形式。此外,react-intl 还可在模板中直接使用<FormattedNumber><FormattedDate><FormattedPlural>等声明式组件,适合在邮件正文中做更精细的格式化。

方案三:react-i18next(非 Next.js 应用或需要更多控制)

react-i18next 是 i18next 的 React 绑定,适合非 Next.js 应用,或当你需要完全掌控翻译加载与缓存逻辑时。

1. 安装

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

三个包各司其职:i18next是核心引擎,react-i18next提供 React 集成,i18next-resources-to-backend允许用动态import()按需加载语言资源。

2. 配置 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 };

配置项说明:

  • supportedLngs:声明支持的语言列表;
  • fallbackLng:缺失翻译时的回退语言(通常设为英文);
  • lng: undefined:不锁定默认语言,交由每次调用时指定;
  • preload:在服务端(Node 环境)预先加载所有语言资源,避免首次渲染时的异步竞态;resourcesToBackend的回调(language, namespace) => import(...)会在运行时按需加载messages/<language>/<namespace>.json

3. 创建服务端翻译辅助函数

// 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 }; }

getT的核心逻辑:当目标 locale 与当前已解析语言不一致时先changeLanguage切换;若命名空间尚未加载则loadNamespaces加载;最终通过getFixedT(locale, namespace)返回绑定好语言与命名空间的翻译函数t。由于 i18next 实例是全局单例,getT确保每次渲染都拿到正确的语言快照。

4. 创建消息文件

messages/<locale>/<template>.json组织:

// 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" }

5. 在邮件模板中使用

import { getT } from '../get-t'; import { Html, Body, Container, Heading, Text, Button, Tailwind, pixelBasedPreset } from 'react-email'; 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 box-border" > {t('cta')} </Button> </Container> </Body> </Tailwind> </Html> ); }

消息文件组织方式

按命名空间组织(推荐)

两种常用形态:

形态 A:每种语言一个聚合文件

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

适合 next-intl 风格(namespace定位),小项目或文案量少时维护成本最低。

形态 B:按模板拆分独立文件

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

适合 react-intl 与 react-i18next 风格,每个邮件模板独立成文件,便于团队分工与按需加载。仓库官方文档另有独立指南:apps/docs/guides/internationalization/next-intl.mdx、apps/docs/guides/internationalization/react-i18next.mdx、apps/docs/guides/internationalization/react-intl.mdx。

翻译键命名规范

使用描述性、层级化的键名,避免扁平、无结构的键:

{ "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" } } }

层级结构天然提供了语义分组与命名空间,也便于在模板中通过t('body.greeting')精确取值。

最佳实践

1. 始终把 locale 设为必传 prop

locale是邮件渲染的语言开关,不应有默认值依赖:

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

2. 设置 HTML lang 属性

React Email 的Html组件默认lang="en"dir="ltr"(见 packages/react-email/src/components/html/html.tsx),显式传入lang={locale}可覆盖默认值:

<Html lang={locale}>

这样邮件客户端、屏幕阅读器与垃圾邮件过滤器都能正确识别内容语言。

3. 支持 RTL 语言

对阿拉伯语、希伯来语等从右向左书写的语言,需要同时设置dir

const isRTL = ['ar', 'he', 'fa'].includes(locale); <Html lang={locale} dir={isRTL ? 'rtl' : 'ltr'}>

Html组件的dir属性同样透传到最终的<html>标签(源码默认'ltr')。

4. 提供回退翻译

当某语言文件缺失时回退到默认语言,防止渲染崩溃:

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

5. 逐一测试所有语言环境

利用PreviewProps逐个验证各语言渲染效果——这是 React Email 预览服务器提供的本地调试机制:

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

locale依次改为esfr等,在本地预览服务器中检查文案、换行、按钮宽度是否正常。

6. 保持各语言文件键一致

所有语言文件的翻译键必须完全一致,否则会出现键名漂移导致的缺失翻译:

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

7. 处理缺失翻译

在翻译器上注册onError回调,提前感知缺失键:

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

8. 不要忘记翻译主题行

邮件主题(subject)是收件人最先看到的内容,同样要翻译。主题行的翻译发生在组件之外,可以在发送前单独创建翻译器获取:

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

Preview组件中的{t('subject')}负责邮件预览区文案,而这里的subject是真正的邮件主题行,两者都需要翻译。

9. 保持格式一致性

不同语言对日期、时间、数字、货币的书写习惯差异巨大:

  • 日期格式:MM/DD/YYYY 与 DD/MM/YYYY
  • 时间格式:12 小时制与 24 小时制
  • 数字分隔符:1,234.56 与 1.234,56
  • 货币符号及位置:$100 与 100$

务必使用IntlAPI(Intl.NumberFormatIntl.DateTimeFormat)按 locale 自动格式化,而不是在模板中硬编码。仓库示例邮件中已有此实践,例如 apps/demo/emails/Community/notifications/yelp-recent-login.tsx 与 apps/demo/emails/Community/reset-password/twitch-reset-password.tsx 都使用Intl.DateTimeFormat生成本地化日期。

完整示例:多语言订单确认邮件

将以上实践整合,实现一封支持多语言、RTL、本地化货币与日期的订单确认邮件:

import { createTranslator } from 'next-intl'; import { Html, Head, Preview, Body, Container, Section, Heading, Text, Button, Hr, Tailwind, pixelBasedPreset } from 'react-email'; 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 /> <Body className="bg-gray-100 font-sans"> <Preview>{t('preview')}</Preview> <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 box-border" > {t('view-order')} </Button> <Hr className="border-solid border-gray-200 my-5" /> <Text className="text-sm text-gray-500"> {t('footer')} </Text> </Container> </Body> </Tailwind> </Html> ); }

对应的消息文件(以英文和西班牙语为例):

// 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!" } }

示例中的关键设计:

  • Intl.NumberFormat(locale, { style: 'currency', currency })按 locale 输出正确货币格式(如英语的$1,234.56与西班牙语的1.234,56 US$);
  • Intl.DateTimeFormat(locale, {...})按 locale 输出本地化日期;
  • isRTL数组可扩展其他从右向左书写的语言(如波斯语fa);
  • 阿拉伯语、希伯来语环境下dir="rtl"会让整个布局镜像翻转,配合 Tailwind 的盒模型样式仍能保持排版正确。

三套方案速查与选型建议

维度next-intlreact-intlreact-i18next
安装命令npm install next-intlnpm install react-intlnpm install react-i18next i18next i18next-resources-to-backend
核心 APIcreateTranslatorcreateIntl+formatMessagegetT+t(基于 i18next 实例)
消息组织每语言一个聚合文件 + namespace每语言每模板独立文件每语言每模板独立文件 + 命名空间
复杂格式化借助IntlAPI原生 ICU 语法(复数/日期/数字/货币)借助IntlAPI
最适场景Next.js 应用需要强格式化能力非 Next.js 或需底层控制

选型建议:Next.js 项目直接选用 next-intl;邮件中含大量复数、货币、日期等复杂文案时优先 react-intl;Node 或其他框架下希望深度掌控翻译加载流程时选 react-i18next。三种方案均要求邮件组件为async组件、locale为必传 prop,并配合<Html lang={locale}>标记内容语言。若需深入学习各方案在官方文档中的独立教程,可分别查阅 next-intl 指南、react-i18next 指南 与 react-intl 指南。

【免费下载链接】react-email💌 Build and send emails using React项目地址: https://gitcode.com/GitHub_Trending/re/react-email

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询