feat: i18n for emails (#1442)

## Description

Support setting a document language that will control the language used
for sending emails to recipients. Additional work has been done to
convert all emails to using our i18n implementation so we can later add
controls for sending other kinds of emails in a users target language.

## Related Issue

N/A

## Changes Made

- Added `<Trans>` and `msg` macros to emails
- Introduced a new `renderEmailWithI18N` utility in the lib package
- Updated all emails to use the `<Tailwind>` component at the top level
due to rendering constraints
- Updated the `i18n.server.tsx` file to not use a top level await

## Testing Performed

- Configured document language and verified emails were sent in the
expected language
- Created a document from a template and verified that the templates
language was transferred to the document
This commit is contained in:
Lucas Smith
2024-11-05 11:52:54 +11:00
committed by GitHub
parent 04b1ce1aab
commit 4dd95016b1
163 changed files with 3549 additions and 1461 deletions

View File

@ -1,3 +1,6 @@
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Button, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
@ -10,17 +13,21 @@ export const TemplateConfirmationEmail = ({
confirmationLink,
assetBaseUrl,
}: TemplateConfirmationEmailProps) => {
const { _ } = useLingui();
return (
<>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Section className="flex-row items-center justify-center">
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
Welcome to Documenso!
<Trans>Welcome to Documenso!</Trans>
</Text>
<Text className="my-1 text-center text-base text-slate-400">
Before you get started, please confirm your email address by clicking the button below:
<Trans>
Before you get started, please confirm your email address by clicking the button below:
</Trans>
</Text>
<Section className="mb-6 mt-8 text-center">
@ -28,11 +35,13 @@ export const TemplateConfirmationEmail = ({
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
href={confirmationLink}
>
Confirm email
<Trans>Confirm email</Trans>
</Button>
<Text className="mt-8 text-center text-sm italic text-slate-400">
You can also copy and paste this link into your browser: {confirmationLink} (link
expires in 1 hour)
<Trans>
You can also copy and paste this link into your browser: {confirmationLink} (link
expires in 1 hour)
</Trans>
</Text>
</Section>
</Section>

View File

@ -1,3 +1,5 @@
import { Trans } from '@lingui/macro';
import { Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
@ -19,16 +21,18 @@ export const TemplateDocumentCancel = ({
<Section>
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
{inviterName} has cancelled the document
<br />"{documentName}"
<Trans>
{inviterName} has cancelled the document
<br />"{documentName}"
</Trans>
</Text>
<Text className="my-1 text-center text-base text-slate-400">
All signatures have been voided.
<Trans>All signatures have been voided.</Trans>
</Text>
<Text className="my-1 text-center text-base text-slate-400">
You don't need to sign it anymore.
<Trans>You don't need to sign it anymore.</Trans>
</Text>
</Section>
</>

View File

@ -1,3 +1,5 @@
import { Trans } from '@lingui/macro';
import { Button, Column, Img, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
@ -30,17 +32,17 @@ export const TemplateDocumentCompleted = ({
src={getAssetUrl('/static/completed.png')}
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
/>
Completed
<Trans>Completed</Trans>
</Text>
</Column>
</Section>
<Text className="text-primary mb-0 text-center text-lg font-semibold">
{customBody ?? `${documentName}” was signed by all signers`}
<Trans>{customBody ?? `${documentName}” was signed by all signers`}</Trans>
</Text>
<Text className="my-1 text-center text-base text-slate-400">
Continue by downloading the document.
<Trans>Continue by downloading the document.</Trans>
</Text>
<Section className="mb-6 mt-8 text-center">
@ -59,7 +61,7 @@ export const TemplateDocumentCompleted = ({
src={getAssetUrl('/static/download.png')}
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
/>
Download
<Trans>Download</Trans>
</Button>
</Section>
</Section>

View File

@ -1,3 +1,6 @@
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { RECIPIENT_ROLES_DESCRIPTION_ENG } from '@documenso/lib/constants/recipient-roles';
import type { RecipientRole } from '@documenso/prisma/client';
@ -26,6 +29,8 @@ export const TemplateDocumentInvite = ({
isTeamInvite,
teamName,
}: TemplateDocumentInviteProps) => {
const { _ } = useLingui();
const { actionVerb, progressiveVerb } = RECIPIENT_ROLES_DESCRIPTION_ENG[role];
return (
@ -35,28 +40,30 @@ export const TemplateDocumentInvite = ({
<Section>
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
{selfSigner ? (
<>
{`Please ${actionVerb.toLowerCase()} your document`}
<Trans>
{`Please ${_(actionVerb).toLowerCase()} your document`}
<br />
{`"${documentName}"`}
</>
</Trans>
) : isTeamInvite ? (
<>
{`${inviterName} on behalf of ${teamName} has invited you to ${actionVerb.toLowerCase()}`}
<Trans>
{`${inviterName} on behalf of ${teamName} has invited you to ${_(
actionVerb,
).toLowerCase()}`}
<br />
{`"${documentName}"`}
</>
</Trans>
) : (
<>
{`${inviterName} has invited you to ${actionVerb.toLowerCase()}`}
<Trans>
{`${inviterName} has invited you to ${_(actionVerb).toLowerCase()}`}
<br />
{`"${documentName}"`}
</>
</Trans>
)}
</Text>
<Text className="my-1 text-center text-base text-slate-400">
Continue by {progressiveVerb.toLowerCase()} the document.
<Trans>Continue by {_(progressiveVerb).toLowerCase()} the document.</Trans>
</Text>
<Section className="mb-6 mt-8 text-center">
@ -64,7 +71,7 @@ export const TemplateDocumentInvite = ({
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
href={signDocumentLink}
>
{actionVerb} Document
<Trans>{_(actionVerb)} Document</Trans>
</Button>
</Section>
</Section>

View File

@ -1,3 +1,5 @@
import { Trans } from '@lingui/macro';
import { Column, Img, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
@ -26,19 +28,21 @@ export const TemplateDocumentPending = ({
src={getAssetUrl('/static/clock.png')}
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
/>
Waiting for others
<Trans>Waiting for others</Trans>
</Text>
</Column>
</Section>
<Text className="text-primary mb-0 text-center text-lg font-semibold">
{documentName} has been signed
<Trans>{documentName} has been signed</Trans>
</Text>
<Text className="mx-auto mb-6 mt-1 max-w-[80%] text-center text-base text-slate-400">
We're still waiting for other signers to sign this document.
<br />
We'll notify you as soon as it's ready.
<Trans>
We're still waiting for other signers to sign this document.
<br />
We'll notify you as soon as it's ready.
</Trans>
</Text>
</Section>
</>

View File

@ -1,3 +1,4 @@
import { Trans } from '@lingui/macro';
import { env } from 'next-runtime-env';
import { Button, Column, Img, Link, Section, Text } from '../components';
@ -32,25 +33,27 @@ export const TemplateDocumentSelfSigned = ({
src={getAssetUrl('/static/completed.png')}
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
/>
Completed
<Trans>Completed</Trans>
</Text>
</Column>
</Section>
<Text className="text-primary mb-0 mt-6 text-center text-lg font-semibold">
You have signed {documentName}
<Trans>You have signed {documentName}</Trans>
</Text>
<Text className="mx-auto mb-6 mt-1 max-w-[80%] text-center text-base text-slate-400">
Create a{' '}
<Link
href={signUpUrl}
target="_blank"
className="text-documenso-700 hover:text-documenso-600 whitespace-nowrap"
>
free account
</Link>{' '}
to access your signed documents at any time.
<Trans>
Create a{' '}
<Link
href={signUpUrl}
target="_blank"
className="text-documenso-700 hover:text-documenso-600 whitespace-nowrap"
>
free account
</Link>{' '}
to access your signed documents at any time.
</Trans>
</Text>
<Section className="mb-6 mt-8 text-center">
@ -62,7 +65,7 @@ export const TemplateDocumentSelfSigned = ({
src={getAssetUrl('/static/user-plus.png')}
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
/>
Create account
<Trans>Create account</Trans>
</Button>
<Button
@ -73,7 +76,7 @@ export const TemplateDocumentSelfSigned = ({
src={getAssetUrl('/static/review.png')}
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
/>
View plans
<Trans>View plans</Trans>
</Button>
</Section>
</Section>

View File

@ -1,3 +1,5 @@
import { Trans } from '@lingui/macro';
import { Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
@ -18,20 +20,22 @@ export const TemplateDocumentDelete = ({
<Section>
<Text className="text-primary mb-0 mt-6 text-left text-lg font-semibold">
Your document has been deleted by an admin!
<Trans>Your document has been deleted by an admin!</Trans>
</Text>
<Text className="mx-auto mb-6 mt-1 text-left text-base text-slate-400">
"{documentName}" has been deleted by an admin.
<Trans>"{documentName}" has been deleted by an admin.</Trans>
</Text>
<Text className="mx-auto mb-6 mt-1 text-left text-base text-slate-400">
This document can not be recovered, if you would like to dispute the reason for future
documents please contact support.
<Trans>
This document can not be recovered, if you would like to dispute the reason for future
documents please contact support.
</Trans>
</Text>
<Text className="mx-auto mt-1 text-left text-base text-slate-400">
The reason provided for deletion is the following:
<Trans>The reason provided for deletion is the following:</Trans>
</Text>
<Text className="mx-auto mb-6 mt-1 text-left text-base italic text-slate-400">

View File

@ -1,3 +1,5 @@
import { Trans } from '@lingui/macro';
import { Link, Section, Text } from '../components';
export type TemplateFooterProps = {
@ -9,10 +11,12 @@ export const TemplateFooter = ({ isDocument = true }: TemplateFooterProps) => {
<Section>
{isDocument && (
<Text className="my-4 text-base text-slate-400">
This document was sent using{' '}
<Link className="text-[#7AC455]" href="https://documen.so/mail-footer">
Documenso.
</Link>
<Trans>
This document was sent using{' '}
<Link className="text-[#7AC455]" href="https://documen.so/mail-footer">
Documenso.
</Link>
</Trans>
</Text>
)}

View File

@ -1,3 +1,5 @@
import { Trans } from '@lingui/macro';
import { Button, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
@ -16,11 +18,11 @@ export const TemplateForgotPassword = ({
<Section className="flex-row items-center justify-center">
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
Forgot your password?
<Trans>Forgot your password?</Trans>
</Text>
<Text className="my-1 text-center text-base text-slate-400">
That's okay, it happens! Click the button below to reset your password.
<Trans>That's okay, it happens! Click the button below to reset your password.</Trans>
</Text>
<Section className="mb-6 mt-8 text-center">
@ -28,7 +30,7 @@ export const TemplateForgotPassword = ({
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
href={resetPasswordLink}
>
Reset Password
<Trans>Reset Password</Trans>
</Button>
</Section>
</Section>

View File

@ -1,3 +1,4 @@
import { Trans } from '@lingui/macro';
import { env } from 'next-runtime-env';
import { Button, Section, Text } from '../components';
@ -18,11 +19,11 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
<Section className="flex-row items-center justify-center">
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
Password updated!
<Trans>Password updated!</Trans>
</Text>
<Text className="my-1 text-center text-base text-slate-400">
Your password has been updated.
<Trans>Your password has been updated.</Trans>
</Text>
<Section className="mb-6 mt-8 text-center">
@ -30,7 +31,7 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
href={`${NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signin`}
>
Sign In
<Trans>Sign In</Trans>
</Button>
</Section>
</Section>