react-email
react-email copied to clipboard
i18n (Internationalization and localization)
Using the library in multi-language projects would be tremendous, and avoid copying/pasting templates but instead use the localization library. Are there any plans to extend the project documentation to showcase the use of i18n libraries to translate the emails into different languages?
That would lovely! This is definitely something that we want to have, but I think we want to nail down a few things first. But feel free to share any thoughts on this and how could we do it, we can release it sooner.
I'm using the renderer
inside the node.js API, so my initial thought was to use something like prebuilt middleware for i18n (https://github.com/i18next/i18next-http-middleware, for example). But then I realized that the rendering is outside of the API context. So, for now, I've just used plain i18next
package inside the react code by initializing i18next and using the t
method that the library exports.
As emails, in most cases, are the only thing that needs to be translated in the APIs, I assume that the react-email
project could have an opinionated implementation (for example, the same i18next
) and suggest something like: "Put your translations in this folder... Import this method and call it with a translation key".
But still, I would like to hear your thoughts on how your team would suggest implementing it. If there are any good ideas, I'm happy to do a test implementation and submit a PR with it
I am using @formatjs/intl for translation. Adding the language as parameter I can select which language I want to see with the dev server and allows me to switch between language depending on the applications needs. I would be nice to have an integrated i18n library inside react.email, but this works just a fine for us
import { ThemeColor, ThemeImage } from '$supabase/types';
import { defaultTheme } from './components/default.theme';
import Footer, { Socials } from './components/footer';
import Header from './components/header';
import messages from './components/locale.catering.edit';
import { createIntl } from '@formatjs/intl';
import { Container } from '@react-email/container';
import { Head } from '@react-email/head';
import { Html } from '@react-email/html';
import { Link } from '@react-email/link';
import { Preview } from '@react-email/preview';
import { Text } from '@react-email/text';
import React from 'react';
interface EmailData {
caterer: {
emailAddress: string;
fullName: string;
};
organizer: {
emailAddress: string;
fullName: string;
};
meeting: {
id: string;
subject: string;
start: { date: string; time: string };
end: { date: string; time: string };
timeZone: string;
};
costPlace: string;
orders: Array<{ name: string; amount: number; time: string }>;
}
export function getSubject(locale: 'nl' | 'en'): string {
const intl = createIntl({ messages: messages[locale], locale });
return intl.formatMessage({ id: 'subject' });
}
function Email({
theme = defaultTheme,
locale = defaultLanguage,
data = defaultData,
socials = defaultSocials,
}: {
theme: { themes_colors: ThemeColor; themes_images: ThemeImage };
data: EmailData;
locale: 'nl' | 'en';
socials: Socials;
}) {
const intl = createIntl({ messages: messages[locale], locale });
const defaultTextStyle: React.CSSProperties = {
margin: '0px',
color: theme.themes_colors.base_content_color,
fontFamily: 'tahoma, geneva, sans-serif',
};
const tableStyle: React.CSSProperties = {
border: '0px solid white',
borderCollapse: 'collapse',
textAlign: 'center',
color: theme.themes_colors.base_content_color,
fontFamily: 'tahoma, geneva, sans-serif',
};
return (
<Html>
<Head />
<Preview>{intl.formatMessage({ id: 'subject' })}</Preview>
<Container style={{ margin: '0px auto', width: '100%', maxWidth: '700px', background: theme.themes_colors.base_100_color }}>
<Header title={intl.formatMessage({ id: 'headerTitle' })} subtitle={intl.formatMessage({ id: 'title' })} theme={theme} />
<Text style={{ marginTop: '20px', ...defaultTextStyle }}>{intl.formatMessage({ id: 'salutation' }, { name: data.organizer.fullName })}</Text>
<br />
<Text style={defaultTextStyle}>{intl.formatMessage({ id: 'introduction' })}</Text>
<br />
<Text style={defaultTextStyle}>
<b>{intl.formatMessage({ id: 'employee' })}</b> {data.organizer.fullName} (
<Link href={'mailto:' + data.organizer.emailAddress} style={{ ...defaultTextStyle, textDecoration: 'underline' }}>
{data.organizer.emailAddress}
</Link>
)
</Text>
<Text style={defaultTextStyle}>
<b>{intl.formatMessage({ id: 'orderNumber' })}</b> {data.meeting.id}
</Text>
<Text style={defaultTextStyle}>
<b>{intl.formatMessage({ id: 'costPlate' })}</b> {data.costPlace}
</Text>
<br />
<table style={{ ...tableStyle, width: '100%' }}>
<thead>
<tr style={{ backgroundColor: theme.themes_colors.base_300_color }}>
<th style={tableStyle}>{intl.formatMessage({ id: 'description' })}</th>
<th style={tableStyle}>{intl.formatMessage({ id: 'amount' })}</th>
<th style={tableStyle}>{intl.formatMessage({ id: 'time' })}</th>
</tr>
</thead>
<tbody>
{data.orders.map((order, index) => (
<tr key={index} style={{ backgroundColor: index % 2 === 1 ? theme.themes_colors.base_200_color : 'white' }}>
<td style={tableStyle}>{order.name}</td>
<td style={tableStyle}>{order.amount}</td>
<td style={tableStyle}>{order.time}</td>
</tr>
))}
</tbody>
</table>
<br />
<br />
<Text style={defaultTextStyle}>{intl.formatMessage({ id: 'greetings' })}</Text>
<Text style={defaultTextStyle}>{process.env.FROM_NAME || 'DARWIN - Smart Building Software Solutions'}</Text>
<Footer theme={theme} data={socials} />
</Container>
</Html>
);
}
const defaultLanguage = 'nl';
const defaultData: EmailData = {
meeting: {
id: '3aa7c73d-836a-43ae-b66f-dfe7937650b9',
subject: 'test meeting',
start: { date: '13-02-2023', time: '08:00' },
end: { date: '13-02-2023', time: '08:30' },
timeZone: 'Europe/Amsterdam',
},
caterer: {
fullName: 'Hutten Catering',
emailAddress: '[email protected]',
},
organizer: {
fullName: 'Bastiaan Verhaar',
emailAddress: '[email protected]',
},
costPlace: '123456',
orders: [
{ name: 'coffee', amount: 2, time: '08:15' },
{ name: 'Appel gebak', amount: 2, time: '08:15' },
],
};
export default Email;
Going to close this for now, let me know if you want to reopen
I am using @formatjs/intl for translation. Adding the language as parameter I can select which language I want to see with the dev server and allows me to switch between language depending on the applications needs. I would be nice to have an integrated i18n library inside react.email, but this works just a fine for us
import { ThemeColor, ThemeImage } from '$supabase/types'; import { defaultTheme } from './components/default.theme'; import Footer, { Socials } from './components/footer'; import Header from './components/header'; import messages from './components/locale.catering.edit'; import { createIntl } from '@formatjs/intl'; import { Container } from '@react-email/container'; import { Head } from '@react-email/head'; import { Html } from '@react-email/html'; import { Link } from '@react-email/link'; import { Preview } from '@react-email/preview'; import { Text } from '@react-email/text'; import React from 'react'; interface EmailData { caterer: { emailAddress: string; fullName: string; }; organizer: { emailAddress: string; fullName: string; }; meeting: { id: string; subject: string; start: { date: string; time: string }; end: { date: string; time: string }; timeZone: string; }; costPlace: string; orders: Array<{ name: string; amount: number; time: string }>; } export function getSubject(locale: 'nl' | 'en'): string { const intl = createIntl({ messages: messages[locale], locale }); return intl.formatMessage({ id: 'subject' }); } function Email({ theme = defaultTheme, locale = defaultLanguage, data = defaultData, socials = defaultSocials, }: { theme: { themes_colors: ThemeColor; themes_images: ThemeImage }; data: EmailData; locale: 'nl' | 'en'; socials: Socials; }) { const intl = createIntl({ messages: messages[locale], locale }); const defaultTextStyle: React.CSSProperties = { margin: '0px', color: theme.themes_colors.base_content_color, fontFamily: 'tahoma, geneva, sans-serif', }; const tableStyle: React.CSSProperties = { border: '0px solid white', borderCollapse: 'collapse', textAlign: 'center', color: theme.themes_colors.base_content_color, fontFamily: 'tahoma, geneva, sans-serif', }; return ( <Html> <Head /> <Preview>{intl.formatMessage({ id: 'subject' })}</Preview> <Container style={{ margin: '0px auto', width: '100%', maxWidth: '700px', background: theme.themes_colors.base_100_color }}> <Header title={intl.formatMessage({ id: 'headerTitle' })} subtitle={intl.formatMessage({ id: 'title' })} theme={theme} /> <Text style={{ marginTop: '20px', ...defaultTextStyle }}>{intl.formatMessage({ id: 'salutation' }, { name: data.organizer.fullName })}</Text> <br /> <Text style={defaultTextStyle}>{intl.formatMessage({ id: 'introduction' })}</Text> <br /> <Text style={defaultTextStyle}> <b>{intl.formatMessage({ id: 'employee' })}</b> {data.organizer.fullName} ( <Link href={'mailto:' + data.organizer.emailAddress} style={{ ...defaultTextStyle, textDecoration: 'underline' }}> {data.organizer.emailAddress} </Link> ) </Text> <Text style={defaultTextStyle}> <b>{intl.formatMessage({ id: 'orderNumber' })}</b> {data.meeting.id} </Text> <Text style={defaultTextStyle}> <b>{intl.formatMessage({ id: 'costPlate' })}</b> {data.costPlace} </Text> <br /> <table style={{ ...tableStyle, width: '100%' }}> <thead> <tr style={{ backgroundColor: theme.themes_colors.base_300_color }}> <th style={tableStyle}>{intl.formatMessage({ id: 'description' })}</th> <th style={tableStyle}>{intl.formatMessage({ id: 'amount' })}</th> <th style={tableStyle}>{intl.formatMessage({ id: 'time' })}</th> </tr> </thead> <tbody> {data.orders.map((order, index) => ( <tr key={index} style={{ backgroundColor: index % 2 === 1 ? theme.themes_colors.base_200_color : 'white' }}> <td style={tableStyle}>{order.name}</td> <td style={tableStyle}>{order.amount}</td> <td style={tableStyle}>{order.time}</td> </tr> ))} </tbody> </table> <br /> <br /> <Text style={defaultTextStyle}>{intl.formatMessage({ id: 'greetings' })}</Text> <Text style={defaultTextStyle}>{process.env.FROM_NAME || 'DARWIN - Smart Building Software Solutions'}</Text> <Footer theme={theme} data={socials} /> </Container> </Html> ); } const defaultLanguage = 'nl'; const defaultData: EmailData = { meeting: { id: '3aa7c73d-836a-43ae-b66f-dfe7937650b9', subject: 'test meeting', start: { date: '13-02-2023', time: '08:00' }, end: { date: '13-02-2023', time: '08:30' }, timeZone: 'Europe/Amsterdam', }, caterer: { fullName: 'Hutten Catering', emailAddress: '[email protected]', }, organizer: { fullName: 'Bastiaan Verhaar', emailAddress: '[email protected]', }, costPlace: '123456', orders: [ { name: 'coffee', amount: 2, time: '08:15' }, { name: 'Appel gebak', amount: 2, time: '08:15' }, ], }; export default Email;
Can you explain to me how I then implement it, that the respective language is displayed? And where does it get the locale from? I have on my side the local in configContext. And when I want to preview the emails it renders under .react-email and not on my nextjs version. I don't really understand the principle
@bastiaanv this it was insane, saved my day yesterday.
I had a bit of problem loading the messages, I had to change the target
to esnext
and add in nextjs.config.js
:
webpack(config) {
config.experiments = { ...config.experiments, topLevelAwait: true }
return config
},
But I rather use the "next-intl": "^2.13.2",
(the version matters, maybe you could have problem in the latest one) because I could pass all path in the tree of json like: change_email.preview
, and still pass another variables defined as template inside of the message.
It was almost the same approach of formartjs
actually, next-intl
has it as dependency, so it's just more complete.
import {
Body, Container, Head, Html, Preview, Tailwind
} from "@react-email/components";
import { createTranslator } from "next-intl";
import React from "react";
import { i18n } from "../.react-email/i18n-config";
import { messages } from "../.react-email/src/messages";
import { IEmailDefault } from "../.react-email/src/types/IEmailDefault";
import Header from "../components/Header";
import { mainStyles } from "../styles/styles";
import theme from '../styles/themeJson.json';
export interface IEmailTemplate extends IEmailDefault {
verificationCode: string
}
export const EmailTemplate: React.FC<Partial<IEmailTemplate>> = ({
locale = i18n.defaultLocale,
}) => {
const intl = createTranslator({ messages: messages[locale], locale });
return (
<Tailwind
config={{
theme
}}
>
<Head />
<Html lang={locale} dir="ltr">
<Header />
<Preview>
{intl('change_email.preview', { verificationCode: 0 })}
</Preview>
<Body style={mainStyles}>
<Container className="mx-auto mb-16 w-full">
{/* just some email stuff */}
</Container>
</Body>
</Html>
</Tailwind>
)
};
export default EmailTemplate;
@julioflima can you please show what's in your import { i18n } from "../.react-email/i18n-config";
file?
There is absolutely not much about localization emails =( I just want to understand how you pass and detect locale.
and the second question. Don't you have errors using both next-intl
and tailwind
?
Working with react-intl FormattedMessage component does not work either
I have been working on this enhancement to the boilerplate to include internationalization, feel free to use it while i learn how to upgrade the resend's react-email starter:
https://github.com/Pra3t0r5/react-email-intl-starter
It relies on a script to compile email variants based on available internationalization variants. If you don't take into account some standard utils to handle the files, it doesn't add any dependencies.
This code does not includes localization, since we are handling that from our backend data (by knowing the preferred language of each of our users).
As far i know, that is not a goal of react-email
either, since you have to handle locales server-side, before actually sending the email.
Feel free to thinker with it to include that feature. I will do it eventually.