;
}
diff --git a/lingui.config.ts b/lingui.config.ts
index 855aaf7f5..8026d461f 100644
--- a/lingui.config.ts
+++ b/lingui.config.ts
@@ -19,7 +19,7 @@ const config: LinguiConfig = {
},
{
path: '/packages/lib/translations/{locale}/common',
- include: ['packages/ui', 'packages/lib'],
+ include: ['packages/ui', 'packages/lib', 'packages/email'],
exclude: ['**/node_modules/**'],
},
],
diff --git a/package-lock.json b/package-lock.json
index 27f0b5f26..956db402e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -36774,6 +36774,9 @@
"@documenso/email": "*",
"@documenso/prisma": "*",
"@documenso/signing": "*",
+ "@lingui/core": "^4.11.3",
+ "@lingui/macro": "^4.11.3",
+ "@lingui/react": "^4.11.3",
"@next-auth/prisma-adapter": "1.0.7",
"@noble/ciphers": "0.4.0",
"@noble/hashes": "1.3.2",
diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts
index fc1aae04c..fda6ad1a4 100644
--- a/packages/api/v1/implementation.ts
+++ b/packages/api/v1/implementation.ts
@@ -301,6 +301,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
dateFormat: dateFormat?.value,
redirectUrl: body.meta.redirectUrl,
signingOrder: body.meta.signingOrder,
+ language: body.meta.language,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts
index a6d4618ad..a4fab1089 100644
--- a/packages/api/v1/schema.ts
+++ b/packages/api/v1/schema.ts
@@ -2,6 +2,7 @@ import { extendZodWithOpenApi } from '@anatine/zod-openapi';
import { z } from 'zod';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
+import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import '@documenso/lib/constants/time-zones';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { ZUrlSchema } from '@documenso/lib/schemas/common';
@@ -127,6 +128,7 @@ export const ZCreateDocumentMutationSchema = z.object({
}),
redirectUrl: z.string(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
+ language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
})
.partial(),
authOptions: z
@@ -181,6 +183,7 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
dateFormat: z.string(),
redirectUrl: z.string(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
+ language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
})
.partial()
.optional(),
@@ -247,6 +250,7 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
dateFormat: z.string(),
redirectUrl: ZUrlSchema,
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
+ language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
})
.partial()
.optional(),
diff --git a/packages/email/render.ts b/packages/email/render.ts
deleted file mode 100644
index 46f0d62a5..000000000
--- a/packages/email/render.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { render, renderAsync } from '@react-email/render';
diff --git a/packages/email/render.tsx b/packages/email/render.tsx
new file mode 100644
index 000000000..2a79c8252
--- /dev/null
+++ b/packages/email/render.tsx
@@ -0,0 +1,39 @@
+import * as reactEmail from '@react-email/render';
+
+import config from '@documenso/tailwind-config';
+
+import { Tailwind } from './components';
+
+export const render: typeof reactEmail.render = (element, options) => {
+ return reactEmail.render(
+
+ {element}
+ ,
+ options,
+ );
+};
+
+export const renderAsync: typeof reactEmail.renderAsync = async (element, options) => {
+ return reactEmail.renderAsync(
+
+ {element}
+ ,
+ options,
+ );
+};
diff --git a/packages/email/template-components/template-confirmation-email.tsx b/packages/email/template-components/template-confirmation-email.tsx
index 1036faa3e..0c3c687ec 100644
--- a/packages/email/template-components/template-confirmation-email.tsx
+++ b/packages/email/template-components/template-confirmation-email.tsx
@@ -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 (
<>
- Welcome to Documenso!
+ Welcome to Documenso!
- Before you get started, please confirm your email address by clicking the button below:
+
+ Before you get started, please confirm your email address by clicking the button below:
+
@@ -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
+ Confirm email
- You can also copy and paste this link into your browser: {confirmationLink} (link
- expires in 1 hour)
+
+ You can also copy and paste this link into your browser: {confirmationLink} (link
+ expires in 1 hour)
+
diff --git a/packages/email/template-components/template-document-cancel.tsx b/packages/email/template-components/template-document-cancel.tsx
index dff275de2..d6e1b4221 100644
--- a/packages/email/template-components/template-document-cancel.tsx
+++ b/packages/email/template-components/template-document-cancel.tsx
@@ -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 = ({
- {inviterName} has cancelled the document
- "{documentName}"
+
+ {inviterName} has cancelled the document
+ "{documentName}"
+
- All signatures have been voided.
+ All signatures have been voided.
- You don't need to sign it anymore.
+ You don't need to sign it anymore.
>
diff --git a/packages/email/template-components/template-document-completed.tsx b/packages/email/template-components/template-document-completed.tsx
index 8829c8d06..ed1dfc25e 100644
--- a/packages/email/template-components/template-document-completed.tsx
+++ b/packages/email/template-components/template-document-completed.tsx
@@ -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
+ Completed
- {customBody ?? `“${documentName}” was signed by all signers`}
+ {customBody ?? `“${documentName}” was signed by all signers`}
- Continue by downloading the document.
+ Continue by downloading the document.
@@ -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
+ Download
diff --git a/packages/email/template-components/template-document-invite.tsx b/packages/email/template-components/template-document-invite.tsx
index 62d049e13..6f59bab2c 100644
--- a/packages/email/template-components/template-document-invite.tsx
+++ b/packages/email/template-components/template-document-invite.tsx
@@ -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 = ({
{selfSigner ? (
- <>
- {`Please ${actionVerb.toLowerCase()} your document`}
+
+ {`Please ${_(actionVerb).toLowerCase()} your document`}
{`"${documentName}"`}
- >
+
) : isTeamInvite ? (
- <>
- {`${inviterName} on behalf of ${teamName} has invited you to ${actionVerb.toLowerCase()}`}
+
+ {`${inviterName} on behalf of ${teamName} has invited you to ${_(
+ actionVerb,
+ ).toLowerCase()}`}
{`"${documentName}"`}
- >
+
) : (
- <>
- {`${inviterName} has invited you to ${actionVerb.toLowerCase()}`}
+
+ {`${inviterName} has invited you to ${_(actionVerb).toLowerCase()}`}
{`"${documentName}"`}
- >
+
)}
- Continue by {progressiveVerb.toLowerCase()} the document.
+ Continue by {_(progressiveVerb).toLowerCase()} the document.
@@ -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
+ {_(actionVerb)} Document
diff --git a/packages/email/template-components/template-document-pending.tsx b/packages/email/template-components/template-document-pending.tsx
index f03d7bdbb..161f519dc 100644
--- a/packages/email/template-components/template-document-pending.tsx
+++ b/packages/email/template-components/template-document-pending.tsx
@@ -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
+ Waiting for others
- “{documentName}” has been signed
+ “{documentName}” has been signed
- We're still waiting for other signers to sign this document.
-
- We'll notify you as soon as it's ready.
+
+ We're still waiting for other signers to sign this document.
+
+ We'll notify you as soon as it's ready.
+
>
diff --git a/packages/email/template-components/template-document-self-signed.tsx b/packages/email/template-components/template-document-self-signed.tsx
index db16fb000..f62b3e650 100644
--- a/packages/email/template-components/template-document-self-signed.tsx
+++ b/packages/email/template-components/template-document-self-signed.tsx
@@ -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
+ Completed
- You have signed “{documentName}”
+ You have signed “{documentName}”
- Create a{' '}
-
- free account
- {' '}
- to access your signed documents at any time.
+
+ Create a{' '}
+
+ free account
+ {' '}
+ to access your signed documents at any time.
+
@@ -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
+ Create account
- View plans
+ View plans
diff --git a/packages/email/template-components/template-document-super-delete.tsx b/packages/email/template-components/template-document-super-delete.tsx
index 9cb0a9e71..b26a6f105 100644
--- a/packages/email/template-components/template-document-super-delete.tsx
+++ b/packages/email/template-components/template-document-super-delete.tsx
@@ -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 = ({
- Your document has been deleted by an admin!
+ Your document has been deleted by an admin!
- "{documentName}" has been deleted by an admin.
+ "{documentName}" has been deleted by an admin.
- This document can not be recovered, if you would like to dispute the reason for future
- documents please contact support.
+
+ This document can not be recovered, if you would like to dispute the reason for future
+ documents please contact support.
+
- The reason provided for deletion is the following:
+ The reason provided for deletion is the following:
diff --git a/packages/email/template-components/template-footer.tsx b/packages/email/template-components/template-footer.tsx
index 34cd4047e..36ecbb950 100644
--- a/packages/email/template-components/template-footer.tsx
+++ b/packages/email/template-components/template-footer.tsx
@@ -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) => {
{isDocument && (
- This document was sent using{' '}
-
- Documenso.
-
+
+ This document was sent using{' '}
+
+ Documenso.
+
+
)}
diff --git a/packages/email/template-components/template-forgot-password.tsx b/packages/email/template-components/template-forgot-password.tsx
index c8227b2bd..1ab319a43 100644
--- a/packages/email/template-components/template-forgot-password.tsx
+++ b/packages/email/template-components/template-forgot-password.tsx
@@ -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 = ({
- Forgot your password?
+ Forgot your password?
- That's okay, it happens! Click the button below to reset your password.
+ That's okay, it happens! Click the button below to reset your password.
@@ -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
+ Reset Password
diff --git a/packages/email/template-components/template-reset-password.tsx b/packages/email/template-components/template-reset-password.tsx
index d05393c83..d264385e4 100644
--- a/packages/email/template-components/template-reset-password.tsx
+++ b/packages/email/template-components/template-reset-password.tsx
@@ -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
- Password updated!
+ Password updated!
- Your password has been updated.
+ Your password has been updated.
@@ -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
+ Sign In
diff --git a/packages/email/templates/confirm-email.tsx b/packages/email/templates/confirm-email.tsx
index 59c7add10..4d39f12d6 100644
--- a/packages/email/templates/confirm-email.tsx
+++ b/packages/email/templates/confirm-email.tsx
@@ -1,6 +1,7 @@
-import config from '@documenso/tailwind-config';
+import { msg } from '@lingui/macro';
+import { useLingui } from '@lingui/react';
-import { Body, Container, Head, Html, Img, Preview, Section, Tailwind } from '../components';
+import { Body, Container, Head, Html, Img, Preview, Section } from '../components';
import type { TemplateConfirmationEmailProps } from '../template-components/template-confirmation-email';
import { TemplateConfirmationEmail } from '../template-components/template-confirmation-email';
import { TemplateFooter } from '../template-components/template-footer';
@@ -9,7 +10,9 @@ export const ConfirmEmailTemplate = ({
confirmationLink,
assetBaseUrl = 'http://localhost:3002',
}: TemplateConfirmationEmailProps) => {
- const previewText = `Please confirm your email address`;
+ const { _ } = useLingui();
+
+ const previewText = msg`Please confirm your email address`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
@@ -18,40 +21,30 @@ export const ConfirmEmailTemplate = ({
return (
- {previewText}
-
-
-
-
-
-
+ {_(previewText)}
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
);
};
diff --git a/packages/email/templates/confirm-team-email.tsx b/packages/email/templates/confirm-team-email.tsx
index 552a079f8..8fa3afd3d 100644
--- a/packages/email/templates/confirm-team-email.tsx
+++ b/packages/email/templates/confirm-team-email.tsx
@@ -1,5 +1,7 @@
+import { msg } from '@lingui/macro';
+import { useLingui } from '@lingui/react';
+
import { formatTeamUrl } from '@documenso/lib/utils/teams';
-import config from '@documenso/tailwind-config';
import {
Body,
@@ -11,7 +13,6 @@ import {
Link,
Preview,
Section,
- Tailwind,
Text,
} from '../components';
import { TemplateFooter } from '../template-components/template-footer';
@@ -32,97 +33,90 @@ export const ConfirmTeamEmailTemplate = ({
teamUrl = 'demo',
token = '',
}: ConfirmTeamEmailProps) => {
- const previewText = `Accept team email request for ${teamName} on Documenso`;
+ const { _ } = useLingui();
+
+ const previewText = msg`Accept team email request for ${teamName} on Documenso`;
return (
- {previewText}
-
-
-
-
+ {_(previewText)}
+
+
+
+
+
+
+
+
-
-
-
+
+
+ Verify your team email address
+
-
-
- Verify your team email address
+
+ {teamName} has requested to use your email
+ address for their team on Documenso.
+
+
+
+ {formatTeamUrl(teamUrl, baseUrl)}
+
+
+
+
+ By accepting this request, you will be granting {teamName} access
+ to:
-
- {teamName} has requested to use your email
- address for their team on Documenso.
+
+
+ View all documents sent to and from this email address
+
+
+ Allow document recipients to reply directly to this email address
+
+
+ Send documents on behalf of the team using the email address
+
+
+
+
+ You can revoke access at any time in your team settings on Documenso{' '}
+ here.
-
-
- {formatTeamUrl(teamUrl, baseUrl)}
-
-
-
-
- By accepting this request, you will be granting {teamName}{' '}
- access to:
-
-
-
-
- View all documents sent to and from this email address
-
-
- Allow document recipients to reply directly to this email address
-
-
- Send documents on behalf of the team using the email address
-
-
-
-
- You can revoke access at any time in your team settings on Documenso{' '}
- here.
-
-
-
-
-
-
- Link expires in 1 hour.
-
+
+
+
+
-
+ Link expires in 1 hour.
+
-
-
-
-
-
-
+
+
+
+
+
+
+
);
};
diff --git a/packages/email/templates/document-cancel.tsx b/packages/email/templates/document-cancel.tsx
index 66892bccc..edaec133b 100644
--- a/packages/email/templates/document-cancel.tsx
+++ b/packages/email/templates/document-cancel.tsx
@@ -1,6 +1,7 @@
-import config from '@documenso/tailwind-config';
+import { msg } from '@lingui/macro';
+import { useLingui } from '@lingui/react';
-import { Body, Container, Head, Hr, Html, Img, Preview, Section, Tailwind } from '../components';
+import { Body, Container, Head, Hr, Html, Img, Preview, Section } from '../components';
import type { TemplateDocumentCancelProps } from '../template-components/template-document-cancel';
import { TemplateDocumentCancel } from '../template-components/template-document-cancel';
import { TemplateFooter } from '../template-components/template-footer';
@@ -13,7 +14,9 @@ export const DocumentCancelTemplate = ({
documentName = 'Open Source Pledge.pdf',
assetBaseUrl = 'http://localhost:3002',
}: DocumentCancelEmailTemplateProps) => {
- const previewText = `${inviterName} has cancelled the document ${documentName}, you don't need to sign it anymore.`;
+ const { _ } = useLingui();
+
+ const previewText = msg`${inviterName} has cancelled the document ${documentName}, you don't need to sign it anymore.`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
@@ -22,43 +25,34 @@ export const DocumentCancelTemplate = ({
return (
- {previewText}
-
-
-
-
-
-
+ {_(previewText)}
-
-
-
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
Hi, {userName}{' '}
({userEmail})
-
+
+
-
+
+
We've changed your password as you asked. You can now sign in with your new
password.
-
-
+
+
+
+
Didn't request a password change? We are here to help you secure your account,
just{' '}
contact us.
-
-
-
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
);
};
diff --git a/packages/email/templates/team-delete.tsx b/packages/email/templates/team-delete.tsx
index 240757338..9f44f6724 100644
--- a/packages/email/templates/team-delete.tsx
+++ b/packages/email/templates/team-delete.tsx
@@ -1,7 +1,9 @@
-import { formatTeamUrl } from '@documenso/lib/utils/teams';
-import config from '@documenso/tailwind-config';
+import { msg } from '@lingui/macro';
+import { useLingui } from '@lingui/react';
-import { Body, Container, Head, Hr, Html, Preview, Section, Tailwind, Text } from '../components';
+import { formatTeamUrl } from '@documenso/lib/utils/teams';
+
+import { Body, Container, Head, Hr, Html, Preview, Section, Text } from '../components';
import { TemplateFooter } from '../template-components/template-footer';
import TemplateImage from '../template-components/template-image';
@@ -18,67 +20,60 @@ export const TeamDeleteEmailTemplate = ({
teamUrl = 'demo',
isOwner = false,
}: TeamDeleteEmailProps) => {
+ const { _ } = useLingui();
+
const previewText = isOwner
- ? 'Your team has been deleted'
- : 'A team you were a part of has been deleted';
+ ? msg`Your team has been deleted`
+ : msg`A team you were a part of has been deleted`;
const title = isOwner
- ? 'Your team has been deleted'
- : 'A team you were a part of has been deleted';
+ ? msg`Your team has been deleted`
+ : msg`A team you were a part of has been deleted`;
const description = isOwner
- ? 'The following team has been deleted by you'
- : 'The following team has been deleted by its owner. You will no longer be able to access this team and its documents';
+ ? msg`The following team has been deleted by you`
+ : msg`The following team has been deleted by its owner. You will no longer be able to access this team and its documents`;
return (