Skip to content

Commit

Permalink
React Email support (#27)
Browse files Browse the repository at this point in the history
* wip

* Working example

* Working example

* Working example

* Add README
  • Loading branch information
pontusab authored Dec 28, 2024
1 parent e00a5ed commit 3397a0a
Show file tree
Hide file tree
Showing 23 changed files with 514 additions and 2 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ yarn-error.log*
# turbo
.turbo

dist
dist

.react-email
Binary file modified bun.lockb
Binary file not shown.
Binary file added examples/email/emails/static/vercel-arrow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/email/emails/static/vercel-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/email/emails/static/vercel-team.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/email/emails/static/vercel-user.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
148 changes: 148 additions & 0 deletions examples/email/emails/vercel-invite-user.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { setupI18n } from "@languine/react-email";
import {
Body,
Button,
Column,
Container,
Head,
Heading,
Hr,
Html,
Img,
Link,
Preview,
Row,
Section,
Tailwind,
Text,
} from "@react-email/components";
import * as React from "react";

interface VercelInviteUserEmailProps {
locale: string;
username?: string;
userImage?: string;
invitedByUsername?: string;
invitedByEmail?: string;
teamName?: string;
teamImage?: string;
inviteLink?: string;
inviteFromIp?: string;
inviteFromLocation?: string;
}

const baseUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "";

export const VercelInviteUserEmail = ({
locale = "es",
username = "alanturing",
userImage = `${baseUrl}/static/vercel-user.png`,
invitedByUsername = "Alan",
invitedByEmail = "[email protected]",
teamName = "Enigma",
teamImage = `${baseUrl}/static/vercel-team.png`,
inviteLink = "https://vercel.com/teams/invite/foo",
inviteFromIp = "204.13.186.218",
inviteFromLocation = "São Paulo, Brazil",
}: VercelInviteUserEmailProps) => {
const i18n = setupI18n(locale);

return (
<Html>
<Head />
<Preview>{i18n.t("previewText", { invitedByUsername })}</Preview>
<Tailwind>
<Body className="bg-white my-auto mx-auto font-sans px-2">
<Container className="border border-solid border-[#eaeaea] rounded my-[40px] mx-auto p-[20px] max-w-[465px]">
<Section className="mt-[32px]">
<Img
src={`${baseUrl}/static/vercel-logo.png`}
width="40"
height="37"
alt={i18n.t("logoAlt")}
className="my-0 mx-auto"
/>
</Section>
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
{i18n.t("joinTeamHeading", {
teamName: teamName,
company: "Vercel",
})}
</Heading>
<Text className="text-black text-[14px] leading-[24px]">
{i18n.t("greeting", { username })}
</Text>
<Text className="text-black text-[14px] leading-[24px]">
{i18n.t("invitationText", {
invitedByUsername: <strong>{invitedByUsername}</strong>,
email: (
<Link href={`mailto:${invitedByEmail}`}>
{invitedByEmail}
</Link>
),
teamName: <strong>{teamName}</strong>,
company: "Vercel",
})}
</Text>
<Section>
<Row>
<Column align="right">
<Img
className="rounded-full"
src={userImage}
width="64"
height="64"
/>
</Column>
<Column align="center">
<Img
src={`${baseUrl}/static/vercel-arrow.png`}
width="12"
height="9"
alt={i18n.t("invitedToAlt")}
/>
</Column>
<Column align="left">
<Img
className="rounded-full"
src={teamImage}
width="64"
height="64"
/>
</Column>
</Row>
</Section>
<Section className="text-center mt-[32px] mb-[32px]">
<Button
className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center px-5 py-3"
href={inviteLink}
>
{i18n.t("joinTeamButton")}
</Button>
</Section>
<Text className="text-black text-[14px] leading-[24px]">
{i18n.t("copyUrlText")}{" "}
<Link href={inviteLink} className="text-blue-600 no-underline">
{inviteLink}
</Link>
</Text>
<Hr className="border border-solid border-[#eaeaea] my-[26px] mx-0 w-full" />
<Text className="text-[#666666] text-[12px] leading-[24px]">
{i18n.t("footerText", {
username: <span className="text-black">{username}</span>,
ip: <span className="text-black">{inviteFromIp}</span>,
location: (
<span className="text-black">{inviteFromLocation}</span>
),
})}
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);
};

export default VercelInviteUserEmail;
18 changes: 18 additions & 0 deletions examples/email/languine.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig } from "languine";

export default defineConfig({
version: "7.0.0",
locale: {
source: "en",
targets: ["es", "sv", "pt"],
},
files: {
json: {
include: ["locales/[locale].json"],
},
},
llm: {
provider: "openai",
model: "gpt-4-turbo",
},
});
15 changes: 15 additions & 0 deletions examples/email/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"previewText": "Join %{invitedByUsername} on %{company}",
"company": "%{company}",
"logoAlt": "Vercel Logo",
"joinTeamHeading": "Join %{teamName} on %{company}",
"greeting": "Hi %{username},",
"invitationText":
"%{invitedByUsername} (%{email}) has invited you to join the %{teamName} team on %{company}.",
"invitedToAlt": "Invited to",
"joinTeamButton": "Join the team",
"copyUrlText": "Or copy and paste this URL into your browser:",
"footerText":
"This invitation was intended for %{username} (%{ip} from %{location}). If you were not expecting this invitation, you can ignore this email. If you are concerned about your account's safety, please reply to this email to get in touch with us."
}

12 changes: 12 additions & 0 deletions examples/email/locales/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"previewText": "Únete a %{invitedByUsername} en %{company}",
"company": "%{company}",
"logoAlt": "Logo de Vercel",
"joinTeamHeading": "Únete al equipo %{teamName} en %{company}",
"greeting": "Hola %{username},",
"invitationText": "%{invitedByUsername} (%{email}) te ha invitado a unirte al equipo %{teamName} en %{company}.",
"invitedToAlt": "Invitado a",
"joinTeamButton": "Únete al equipo",
"copyUrlText": "O copia y pega esta URL en tu navegador:",
"footerText": "Esta invitación fue destinada para %{username} (%{ip} desde %{location}). Si no esperabas esta invitación, puedes ignorar este correo electrónico. Si te preocupa la seguridad de tu cuenta, por favor responde a este correo electrónico para ponerte en contacto con nosotros."
}
12 changes: 12 additions & 0 deletions examples/email/locales/pt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"previewText": "Junte-se a %{invitedByUsername} na %{company}",
"company": "%{company}",
"logoAlt": "Logotipo da Vercel",
"joinTeamHeading": "Junte-se ao time %{teamName} na %{company}",
"greeting": "Oi %{username},",
"invitationText": "%{invitedByUsername} (%{email}) convidou você para se juntar ao time %{teamName} na %{company}.",
"invitedToAlt": "Convidado para",
"joinTeamButton": "Entrar no time",
"copyUrlText": "Ou copie e cole este URL no seu navegador:",
"footerText": "Este convite foi destinado para %{username} (%{ip} de %{location}). Se você não estava esperando este convite, pode ignorar este e-mail. Se estiver preocupado com a segurança da sua conta, por favor responda a este e-mail para entrar em contato conosco."
}
12 changes: 12 additions & 0 deletions examples/email/locales/sv.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"previewText": "Gå med %{invitedByUsername} på %{company}",
"company": "%{company}",
"logoAlt": "Vercel-logotyp",
"joinTeamHeading": "Gå med i %{teamName} på %{company}",
"greeting": "Hej %{username},",
"invitationText": "%{invitedByUsername} (%{email}) har bjudit in dig att gå med i %{teamName}-teamet på %{company}.",
"invitedToAlt": "Inbjuden till",
"joinTeamButton": "Gå med i teamet",
"copyUrlText": "Eller kopiera och klistra in denna URL i din webbläsare:",
"footerText": "Denna inbjudan var avsedd för %{username} (%{ip} från %{location}). Om du inte förväntade dig denna inbjudan kan du ignorera detta e-postmeddelande. Om du är orolig för säkerheten på ditt konto, vänligen svara på detta e-postmeddelande för att komma i kontakt med oss."
}
20 changes: 20 additions & 0 deletions examples/email/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "react-email-starter",
"version": "0.1.6",
"private": true,
"scripts": {
"build": "email build",
"dev": "email dev",
"export": "email export"
},
"dependencies": {
"@react-email/components": "0.0.31",
"react": "19.0.0",
"react-dom": "19.0.0"
},
"devDependencies": {
"@types/react": "19.0.1",
"@types/react-dom": "19.0.1",
"react-email": "3.0.4"
}
}
27 changes: 27 additions & 0 deletions examples/email/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# React Email Starter

A live preview right in your browser so you don't need to keep sending real emails during development.

## Getting Started

First, install the dependencies:

```sh
npm install
# or
yarn
```

Then, run the development server:

```sh
npm run dev
# or
yarn dev
```

Open [localhost:3000](http://localhost:3000) with your browser to see the result.

## License

MIT License
1 change: 0 additions & 1 deletion examples/next-international/languine.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,5 @@ export default defineConfig({
llm: {
provider: "ollama",
model: "mistral:latest",
temperature: 0,
},
});
60 changes: 60 additions & 0 deletions packages/react-email/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<p align="center">
<img src="https://github.com/midday-ai/languine/blob/main/packages/react-email/image.png" />
</p>

<p align="center">
A lightweight i18n library for React email templates.
</p>

---

```bash
$ npm install @languine/react-email
```

## What is this?

This is a lightweight i18n library for React email templates. It is built on top of `i18n-js`.

Automatically included language files are in the `locales` folder.

## How to use

```tsx
import { setupI18n } from "@languine/react-email";

export function WelcomeEmail({ locale, name }) {
const i18n = setupI18n(locale);

return (
<Html>
<Head />
<Preview>{i18n.t("preview")}</Preview>
<Body>
<Text>{i18n.t("welcome", { name })}</Text>
</Body>
</Html>
);
}
```

### Rendering the email
```tsx
import { render } from '@react-email/render';
import { WelcomeEmail } from "./emails/welcome";

const html = await render(<WelcomeEmail locale="en" name="John" />, {
pretty: true,
});

console.log(html);
```


## Works together with Languine CLI

Automatically add and translate your email templates with [Languine CLI](https://languine.ai).

```bash
$ npx languine@latest
```
Binary file added packages/react-email/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions packages/react-email/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@languine/react-email",
"version": "0.1.0",
"files": ["dist", "README.md"],
"main": "dist/index.mjs",
"types": "dist/index.d.ts",
"license": "MIT",
"scripts": {
"clean": "rm -rf .turbo node_modules",
"lint": "biome check .",
"format": "biome format --write .",
"typecheck": "tsc --noEmit",
"build": "tsup --clean src/index.tsx"
},
"devDependencies": {
"tsup": "^8.3.5",
"typescript": "^5.7.2"
},
"dependencies": {
"i18n-js": "^4.5.1",
"react-string-replace": "^1.1.1"
}
}
Loading

0 comments on commit 3397a0a

Please sign in to comment.