Merge branch 'main' into staging

This commit is contained in:
Mythie
2024-11-05 12:07:32 +11:00
200 changed files with 5510 additions and 2529 deletions

View File

@ -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),
});

View File

@ -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(),

View File

@ -1 +0,0 @@
export { render, renderAsync } from '@react-email/render';

39
packages/email/render.tsx Normal file
View File

@ -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(
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
{element}
</Tailwind>,
options,
);
};
export const renderAsync: typeof reactEmail.renderAsync = async (element, options) => {
return reactEmail.renderAsync(
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
{element}
</Tailwind>,
options,
);
};

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>

View File

@ -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 (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Preview>{_(previewText)}</Preview>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<TemplateConfirmationEmail
confirmationLink={confirmationLink}
assetBaseUrl={assetBaseUrl}
/>
</Section>
</Container>
<div className="mx-auto mt-12 max-w-xl" />
<TemplateConfirmationEmail
confirmationLink={confirmationLink}
assetBaseUrl={assetBaseUrl}
/>
</Section>
</Container>
<div className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Tailwind>
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -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 (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 px-2 pt-2 backdrop-blur-sm">
<Preview>{_(previewText)}</Preview>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 px-2 pt-2 backdrop-blur-sm">
<TemplateImage
assetBaseUrl={assetBaseUrl}
className="mb-4 h-6 p-2"
staticAsset="logo.png"
/>
<Section>
<TemplateImage
className="mx-auto"
assetBaseUrl={assetBaseUrl}
className="mb-4 h-6 p-2"
staticAsset="logo.png"
staticAsset="mail-open.png"
/>
</Section>
<Section>
<TemplateImage
className="mx-auto"
assetBaseUrl={assetBaseUrl}
staticAsset="mail-open.png"
/>
</Section>
<Section className="p-2 text-slate-500">
<Text className="text-center text-lg font-medium text-black">
Verify your team email address
</Text>
<Section className="p-2 text-slate-500">
<Text className="text-center text-lg font-medium text-black">
Verify your team email address
<Text className="text-center text-base">
<span className="font-bold">{teamName}</span> has requested to use your email
address for their team on Documenso.
</Text>
<div className="mx-auto mt-6 w-fit rounded-lg bg-gray-50 px-4 py-2 text-base font-medium text-slate-600">
{formatTeamUrl(teamUrl, baseUrl)}
</div>
<Section className="mt-6">
<Text className="my-0 text-sm">
By accepting this request, you will be granting <strong>{teamName}</strong> access
to:
</Text>
<Text className="text-center text-base">
<span className="font-bold">{teamName}</span> has requested to use your email
address for their team on Documenso.
<ul className="mb-0 mt-2">
<li className="text-sm">
View all documents sent to and from this email address
</li>
<li className="mt-1 text-sm">
Allow document recipients to reply directly to this email address
</li>
<li className="mt-1 text-sm">
Send documents on behalf of the team using the email address
</li>
</ul>
<Text className="mt-2 text-sm">
You can revoke access at any time in your team settings on Documenso{' '}
<Link href={`${baseUrl}/settings/teams`}>here.</Link>
</Text>
<div className="mx-auto mt-6 w-fit rounded-lg bg-gray-50 px-4 py-2 text-base font-medium text-slate-600">
{formatTeamUrl(teamUrl, baseUrl)}
</div>
<Section className="mt-6">
<Text className="my-0 text-sm">
By accepting this request, you will be granting <strong>{teamName}</strong>{' '}
access to:
</Text>
<ul className="mb-0 mt-2">
<li className="text-sm">
View all documents sent to and from this email address
</li>
<li className="mt-1 text-sm">
Allow document recipients to reply directly to this email address
</li>
<li className="mt-1 text-sm">
Send documents on behalf of the team using the email address
</li>
</ul>
<Text className="mt-2 text-sm">
You can revoke access at any time in your team settings on Documenso{' '}
<Link href={`${baseUrl}/settings/teams`}>here.</Link>
</Text>
</Section>
<Section className="mb-6 mt-8 text-center">
<Button
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={`${baseUrl}/team/verify/email/${token}`}
>
Accept
</Button>
</Section>
</Section>
<Text className="text-center text-xs text-slate-500">Link expires in 1 hour.</Text>
</Container>
<Section className="mb-6 mt-8 text-center">
<Button
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={`${baseUrl}/team/verify/email/${token}`}
>
Accept
</Button>
</Section>
</Section>
<Hr className="mx-auto mt-12 max-w-xl" />
<Text className="text-center text-xs text-slate-500">Link expires in 1 hour.</Text>
</Container>
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Tailwind>
<Hr className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -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 (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Preview>{_(previewText)}</Preview>
<TemplateDocumentCancel
inviterName={inviterName}
inviterEmail={inviterEmail}
documentName={documentName}
assetBaseUrl={assetBaseUrl}
/>
</Section>
</Container>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Hr className="mx-auto mt-12 max-w-xl" />
<TemplateDocumentCancel
inviterName={inviterName}
inviterEmail={inviterEmail}
documentName={documentName}
assetBaseUrl={assetBaseUrl}
/>
</Section>
</Container>
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Tailwind>
<Hr className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -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 { TemplateDocumentCompletedProps } from '../template-components/template-document-completed';
import { TemplateDocumentCompleted } from '../template-components/template-document-completed';
import { TemplateFooter } from '../template-components/template-footer';
@ -15,7 +16,9 @@ export const DocumentCompletedEmailTemplate = ({
assetBaseUrl = 'http://localhost:3002',
customBody,
}: DocumentCompletedEmailTemplateProps) => {
const previewText = `Completed Document`;
const { _ } = useLingui();
const previewText = msg`Completed Document`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
@ -24,41 +27,32 @@ export const DocumentCompletedEmailTemplate = ({
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
<Section className="p-2">
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Preview>{_(previewText)}</Preview>
<TemplateDocumentCompleted
downloadLink={downloadLink}
documentName={documentName}
assetBaseUrl={assetBaseUrl}
customBody={customBody}
/>
</Section>
</Container>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
<Section className="p-2">
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Tailwind>
<TemplateDocumentCompleted
downloadLink={downloadLink}
documentName={documentName}
assetBaseUrl={assetBaseUrl}
customBody={customBody}
/>
</Section>
</Container>
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -1,18 +1,9 @@
import { RECIPIENT_ROLES_DESCRIPTION_ENG } from '@documenso/lib/constants/recipient-roles';
import config from '@documenso/tailwind-config';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import {
Body,
Button,
Container,
Head,
Html,
Img,
Preview,
Section,
Tailwind,
Text,
} from '../components';
import { RECIPIENT_ROLES_DESCRIPTION_ENG } from '@documenso/lib/constants/recipient-roles';
import { Body, Button, Container, Head, Html, Img, Preview, Section, Text } from '../components';
import TemplateDocumentImage from '../template-components/template-document-image';
import { TemplateFooter } from '../template-components/template-footer';
import { RecipientRole } from '.prisma/client';
@ -32,9 +23,11 @@ export const DocumentCreatedFromDirectTemplateEmailTemplate = ({
documentName = 'Open Source Pledge.pdf',
assetBaseUrl = 'http://localhost:3002',
}: DocumentCompletedEmailTemplateProps) => {
const action = RECIPIENT_ROLES_DESCRIPTION_ENG[recipientRole].actioned.toLowerCase();
const { _ } = useLingui();
const previewText = `Document created from direct template`;
const action = _(RECIPIENT_ROLES_DESCRIPTION_ENG[recipientRole].actioned).toLowerCase();
const previewText = msg`Document created from direct template`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
@ -43,55 +36,48 @@ export const DocumentCreatedFromDirectTemplateEmailTemplate = ({
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
<Section className="p-2">
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Preview>{_(previewText)}</Preview>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
<Section className="p-2">
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Section>
<Text className="text-primary mb-0 text-center text-lg font-semibold">
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Section>
<Text className="text-primary mb-0 text-center text-lg font-semibold">
<Trans>
{recipientName} {action} a document by using one of your direct links
</Text>
</Trans>
</Text>
<div className="mx-auto my-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-sm text-slate-600">
{documentName}
</div>
<div className="mx-auto my-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-sm text-slate-600">
{documentName}
</div>
<Section className="my-6 text-center">
<Button
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={documentLink}
>
View document
</Button>
</Section>
<Section className="my-6 text-center">
<Button
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={documentLink}
>
<Trans>View document</Trans>
</Button>
</Section>
</Section>
</Container>
</Section>
</Container>
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Tailwind>
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -1,20 +1,10 @@
import { Trans, msg } 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';
import config from '@documenso/tailwind-config';
import {
Body,
Container,
Head,
Hr,
Html,
Img,
Link,
Preview,
Section,
Tailwind,
Text,
} from '../components';
import { Body, Container, Head, Hr, Html, Img, Link, Preview, Section, Text } from '../components';
import type { TemplateDocumentInviteProps } from '../template-components/template-document-invite';
import { TemplateDocumentInvite } from '../template-components/template-document-invite';
import { TemplateFooter } from '../template-components/template-footer';
@ -40,13 +30,15 @@ export const DocumentInviteEmailTemplate = ({
isTeamInvite = false,
teamName,
}: DocumentInviteEmailTemplateProps) => {
const action = RECIPIENT_ROLES_DESCRIPTION_ENG[role].actionVerb.toLowerCase();
const { _ } = useLingui();
const action = _(RECIPIENT_ROLES_DESCRIPTION_ENG[role].actionVerb).toLowerCase();
const previewText = selfSigner
? `Please ${action} your document ${documentName}`
? msg`Please ${action} your document ${documentName}`
: isTeamInvite
? `${inviterName} on behalf of ${teamName} has invited you to ${action} ${documentName}`
: `${inviterName} has invited you to ${action} ${documentName}`;
? msg`${inviterName} on behalf of ${teamName} has invited you to ${action} ${documentName}`
: msg`${inviterName} has invited you to ${action} ${documentName}`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
@ -55,67 +47,62 @@ export const DocumentInviteEmailTemplate = ({
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Preview>{_(previewText)}</Preview>
<TemplateDocumentInvite
inviterName={inviterName}
inviterEmail={inviterEmail}
documentName={documentName}
signDocumentLink={signDocumentLink}
assetBaseUrl={assetBaseUrl}
role={role}
selfSigner={selfSigner}
isTeamInvite={isTeamInvite}
teamName={teamName}
/>
</Section>
</Container>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Container className="mx-auto mt-12 max-w-xl">
<Section>
<Text className="my-4 text-base font-semibold">
<TemplateDocumentInvite
inviterName={inviterName}
inviterEmail={inviterEmail}
documentName={documentName}
signDocumentLink={signDocumentLink}
assetBaseUrl={assetBaseUrl}
role={role}
selfSigner={selfSigner}
isTeamInvite={isTeamInvite}
teamName={teamName}
/>
</Section>
</Container>
<Container className="mx-auto mt-12 max-w-xl">
<Section>
<Text className="my-4 text-base font-semibold">
<Trans>
{inviterName}{' '}
<Link className="font-normal text-slate-400" href="mailto:{inviterEmail}">
({inviterEmail})
</Link>
</Text>
</Trans>
</Text>
<Text className="mt-2 text-base text-slate-400">
{customBody ? (
<pre className="font-sans text-base text-slate-400">{customBody}</pre>
) : (
<Text className="mt-2 text-base text-slate-400">
{customBody ? (
<pre className="font-sans text-base text-slate-400">{customBody}</pre>
) : (
<Trans>
`${inviterName} has invited you to ${action} the document "${documentName}".`
)}
</Text>
</Section>
</Container>
</Trans>
)}
</Text>
</Section>
</Container>
<Hr className="mx-auto mt-12 max-w-xl" />
<Hr className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Tailwind>
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -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 { TemplateDocumentPendingProps } from '../template-components/template-document-pending';
import { TemplateDocumentPending } from '../template-components/template-document-pending';
import { TemplateFooter } from '../template-components/template-footer';
@ -11,7 +12,9 @@ export const DocumentPendingEmailTemplate = ({
documentName = 'Open Source Pledge.pdf',
assetBaseUrl = 'http://localhost:3002',
}: DocumentPendingEmailTemplateProps) => {
const previewText = `Pending Document`;
const { _ } = useLingui();
const previewText = msg`Pending Document`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
@ -20,36 +23,27 @@ export const DocumentPendingEmailTemplate = ({
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Preview>{_(previewText)}</Preview>
<TemplateDocumentPending documentName={documentName} assetBaseUrl={assetBaseUrl} />
</Section>
</Container>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Tailwind>
<TemplateDocumentPending documentName={documentName} assetBaseUrl={assetBaseUrl} />
</Section>
</Container>
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -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 { TemplateDocumentSelfSignedProps } from '../template-components/template-document-self-signed';
import { TemplateDocumentSelfSigned } from '../template-components/template-document-self-signed';
import { TemplateFooter } from '../template-components/template-footer';
@ -11,7 +12,9 @@ export const DocumentSelfSignedEmailTemplate = ({
documentName = 'Open Source Pledge.pdf',
assetBaseUrl = 'http://localhost:3002',
}: DocumentSelfSignedTemplateProps) => {
const previewText = `Completed Document`;
const { _ } = useLingui();
const previewText = msg`Completed Document`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
@ -20,39 +23,27 @@ export const DocumentSelfSignedEmailTemplate = ({
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
<Section className="p-2">
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Preview>{_(previewText)}</Preview>
<TemplateDocumentSelfSigned
documentName={documentName}
assetBaseUrl={assetBaseUrl}
/>
</Section>
</Container>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
<Section className="p-2">
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Tailwind>
<TemplateDocumentSelfSigned documentName={documentName} assetBaseUrl={assetBaseUrl} />
</Section>
</Container>
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -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 {
TemplateDocumentDelete,
type TemplateDocumentDeleteProps,
@ -14,7 +15,9 @@ export const DocumentSuperDeleteEmailTemplate = ({
assetBaseUrl = 'http://localhost:3002',
reason = 'Unknown',
}: DocumentDeleteEmailTemplateProps) => {
const previewText = `An admin has deleted your document "${documentName}".`;
const { _ } = useLingui();
const previewText = msg`An admin has deleted your document "${documentName}".`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
@ -23,42 +26,33 @@ export const DocumentSuperDeleteEmailTemplate = ({
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Preview>{_(previewText)}</Preview>
<TemplateDocumentDelete
reason={reason}
documentName={documentName}
assetBaseUrl={assetBaseUrl}
/>
</Section>
</Container>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Hr className="mx-auto mt-12 max-w-xl" />
<TemplateDocumentDelete
reason={reason}
documentName={documentName}
assetBaseUrl={assetBaseUrl}
/>
</Section>
</Container>
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Tailwind>
<Hr className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -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 { TemplateFooter } from '../template-components/template-footer';
import type { TemplateForgotPasswordProps } from '../template-components/template-forgot-password';
import { TemplateForgotPassword } from '../template-components/template-forgot-password';
@ -11,7 +12,9 @@ export const ForgotPasswordTemplate = ({
resetPasswordLink = 'https://documenso.com',
assetBaseUrl = 'http://localhost:3002',
}: ForgotPasswordTemplateProps) => {
const previewText = `Password Reset Requested`;
const { _ } = useLingui();
const previewText = msg`Password Reset Requested`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
@ -20,41 +23,32 @@ export const ForgotPasswordTemplate = ({
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Preview>{_(previewText)}</Preview>
<TemplateForgotPassword
resetPasswordLink={resetPasswordLink}
assetBaseUrl={assetBaseUrl}
/>
</Section>
</Container>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<div className="mx-auto mt-12 max-w-xl" />
<TemplateForgotPassword
resetPasswordLink={resetPasswordLink}
assetBaseUrl={assetBaseUrl}
/>
</Section>
</Container>
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Tailwind>
<div className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -1,17 +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,
Text,
} from '../components';
import { Body, Container, Head, Hr, Html, Img, Preview, Section, Text } from '../components';
import type { TemplateDocumentCancelProps } from '../template-components/template-document-cancel';
import TemplateDocumentImage from '../template-components/template-document-image';
import { TemplateFooter } from '../template-components/template-footer';
@ -23,7 +13,9 @@ export const RecipientRemovedFromDocumentTemplate = ({
documentName = 'Open Source Pledge.pdf',
assetBaseUrl = 'http://localhost:3002',
}: DocumentCancelEmailTemplateProps) => {
const previewText = `${inviterName} has removed you from the document ${documentName}.`;
const { _ } = useLingui();
const previewText = msg`${inviterName} has removed you from the document ${documentName}.`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
@ -32,45 +24,36 @@ export const RecipientRemovedFromDocumentTemplate = ({
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Preview>{_(previewText)}</Preview>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Section>
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
{inviterName} has removed you from the document
<br />"{documentName}"
</Text>
</Section>
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
{inviterName} has removed you from the document
<br />"{documentName}"
</Text>
</Section>
</Container>
</Section>
</Container>
<Hr className="mx-auto mt-12 max-w-xl" />
<Hr className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Tailwind>
<Container className="mx-auto max-w-xl">
<TemplateFooter />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -1,18 +1,7 @@
import config from '@documenso/tailwind-config';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import {
Body,
Container,
Head,
Hr,
Html,
Img,
Link,
Preview,
Section,
Tailwind,
Text,
} from '../components';
import { Body, Container, Head, Hr, Html, Img, Link, Preview, Section, Text } from '../components';
import { TemplateFooter } from '../template-components/template-footer';
import type { TemplateResetPasswordProps } from '../template-components/template-reset-password';
import { TemplateResetPassword } from '../template-components/template-reset-password';
@ -24,7 +13,9 @@ export const ResetPasswordTemplate = ({
userEmail = 'lucas@documenso.com',
assetBaseUrl = 'http://localhost:3002',
}: ResetPasswordTemplateProps) => {
const previewText = `Password Reset Successful`;
const { _ } = useLingui();
const previewText = msg`Password Reset Successful`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
@ -33,65 +24,62 @@ export const ResetPasswordTemplate = ({
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Preview>{_(previewText)}</Preview>
<TemplateResetPassword
userName={userName}
userEmail={userEmail}
assetBaseUrl={assetBaseUrl}
/>
</Section>
</Container>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
<Section>
<Img
src={getAssetUrl('/static/logo.png')}
alt="Documenso Logo"
className="mb-4 h-6"
/>
<Container className="mx-auto mt-12 max-w-xl">
<Section>
<Text className="my-4 text-base font-semibold">
<TemplateResetPassword
userName={userName}
userEmail={userEmail}
assetBaseUrl={assetBaseUrl}
/>
</Section>
</Container>
<Container className="mx-auto mt-12 max-w-xl">
<Section>
<Text className="my-4 text-base font-semibold">
<Trans>
Hi, {userName}{' '}
<Link className="font-normal text-slate-400" href={`mailto:${userEmail}`}>
({userEmail})
</Link>
</Text>
</Trans>
</Text>
<Text className="mt-2 text-base text-slate-400">
<Text className="mt-2 text-base text-slate-400">
<Trans>
We've changed your password as you asked. You can now sign in with your new
password.
</Text>
<Text className="mt-2 text-base text-slate-400">
</Trans>
</Text>
<Text className="mt-2 text-base text-slate-400">
<Trans>
Didn't request a password change? We are here to help you secure your account,
just{' '}
<Link className="text-documenso-700 font-normal" href="mailto:hi@documenso.com">
contact us.
</Link>
</Text>
</Section>
</Container>
</Trans>
</Text>
</Section>
</Container>
<Hr className="mx-auto mt-12 max-w-xl" />
<Hr className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Tailwind>
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -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 (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white text-slate-500">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
<Preview>{_(previewText)}</Preview>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white text-slate-500">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
<TemplateImage
assetBaseUrl={assetBaseUrl}
className="mb-4 h-6 p-2"
staticAsset="logo.png"
/>
<Section>
<TemplateImage
className="mx-auto"
assetBaseUrl={assetBaseUrl}
className="mb-4 h-6 p-2"
staticAsset="logo.png"
staticAsset="delete-team.png"
/>
</Section>
<Section>
<TemplateImage
className="mx-auto"
assetBaseUrl={assetBaseUrl}
staticAsset="delete-team.png"
/>
</Section>
<Section className="p-2 text-slate-500">
<Text className="text-center text-lg font-medium text-black">{_(title)}</Text>
<Section className="p-2 text-slate-500">
<Text className="text-center text-lg font-medium text-black">{title}</Text>
<Text className="my-1 text-center text-base">{_(description)}</Text>
<Text className="my-1 text-center text-base">{description}</Text>
<div className="mx-auto my-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-base font-medium text-slate-600">
{formatTeamUrl(teamUrl, baseUrl)}
</div>
</Section>
</Container>
<div className="mx-auto my-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-base font-medium text-slate-600">
{formatTeamUrl(teamUrl, baseUrl)}
</div>
</Section>
</Container>
<Hr className="mx-auto mt-12 max-w-xl" />
<Hr className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Tailwind>
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -1,7 +1,9 @@
import { formatTeamUrl } from '@documenso/lib/utils/teams';
import config from '@documenso/tailwind-config';
import { Trans, 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';
@ -20,62 +22,57 @@ export const TeamEmailRemovedTemplate = ({
teamName = 'Team Name',
teamUrl = 'demo',
}: TeamEmailRemovedTemplateProps) => {
const previewText = `Team email removed for ${teamName} on Documenso`;
const { _ } = useLingui();
const previewText = msg`Team email removed for ${teamName} on Documenso`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white text-slate-500">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 px-2 pt-2 backdrop-blur-sm">
<Preview>{_(previewText)}</Preview>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white text-slate-500">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 px-2 pt-2 backdrop-blur-sm">
<TemplateImage
assetBaseUrl={assetBaseUrl}
className="mb-4 h-6 p-2"
staticAsset="logo.png"
/>
<Section>
<TemplateImage
className="mx-auto"
assetBaseUrl={assetBaseUrl}
className="mb-4 h-6 p-2"
staticAsset="logo.png"
staticAsset="mail-open-alert.png"
/>
</Section>
<Section>
<TemplateImage
className="mx-auto"
assetBaseUrl={assetBaseUrl}
staticAsset="mail-open-alert.png"
/>
</Section>
<Section className="p-2 text-slate-500">
<Text className="text-center text-lg font-medium text-black">
<Trans>Team email removed</Trans>
</Text>
<Section className="p-2 text-slate-500">
<Text className="text-center text-lg font-medium text-black">
Team email removed
</Text>
<Text className="my-1 text-center text-base">
<Text className="my-1 text-center text-base">
<Trans>
The team email <span className="font-bold">{teamEmail}</span> has been removed
from the following team
</Text>
</Trans>
</Text>
<div className="mx-auto mb-6 mt-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-base font-medium text-slate-600">
{formatTeamUrl(teamUrl, baseUrl)}
</div>
</Section>
</Container>
<div className="mx-auto mb-6 mt-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-base font-medium text-slate-600">
{formatTeamUrl(teamUrl, baseUrl)}
</div>
</Section>
</Container>
<Hr className="mx-auto mt-12 max-w-xl" />
<Hr className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Tailwind>
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -1,18 +1,9 @@
import { formatTeamUrl } from '@documenso/lib/utils/teams';
import config from '@documenso/tailwind-config';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import {
Body,
Button,
Container,
Head,
Hr,
Html,
Preview,
Section,
Tailwind,
Text,
} from '../components';
import { formatTeamUrl } from '@documenso/lib/utils/teams';
import { Body, Button, Container, Head, Hr, Html, Preview, Section, Text } from '../components';
import { TemplateFooter } from '../template-components/template-footer';
import TemplateImage from '../template-components/template-image';
@ -33,80 +24,75 @@ export const TeamInviteEmailTemplate = ({
teamUrl = 'demo',
token = '',
}: TeamInviteEmailProps) => {
const previewText = `Accept invitation to join a team on Documenso`;
const { _ } = useLingui();
const previewText = msg`Accept invitation to join a team on Documenso`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white text-slate-500">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
<Preview>{_(previewText)}</Preview>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white text-slate-500">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
<TemplateImage
assetBaseUrl={assetBaseUrl}
className="mb-4 h-6 p-2"
staticAsset="logo.png"
/>
<Section>
<TemplateImage
className="mx-auto"
assetBaseUrl={assetBaseUrl}
className="mb-4 h-6 p-2"
staticAsset="logo.png"
staticAsset="add-user.png"
/>
</Section>
<Section>
<TemplateImage
className="mx-auto"
assetBaseUrl={assetBaseUrl}
staticAsset="add-user.png"
/>
</Section>
<Section className="p-2 text-slate-500">
<Text className="text-center text-lg font-medium text-black">
<Trans>Join {teamName} on Documenso</Trans>
</Text>
<Section className="p-2 text-slate-500">
<Text className="text-center text-lg font-medium text-black">
Join {teamName} on Documenso
</Text>
<Text className="my-1 text-center text-base">
<Trans>You have been invited to join the following team</Trans>
</Text>
<Text className="my-1 text-center text-base">
You have been invited to join the following team
</Text>
<div className="mx-auto my-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-base font-medium text-slate-600">
{formatTeamUrl(teamUrl, baseUrl)}
</div>
<div className="mx-auto my-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-base font-medium text-slate-600">
{formatTeamUrl(teamUrl, baseUrl)}
</div>
<Text className="my-1 text-center text-base">
<Text className="my-1 text-center text-base">
<Trans>
by <span className="text-slate-900">{senderName}</span>
</Text>
</Trans>
</Text>
<Section className="mb-6 mt-6 text-center">
<Button
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={`${baseUrl}/team/invite/${token}`}
>
Accept
</Button>
<Button
className="ml-4 inline-flex items-center justify-center rounded-lg bg-gray-50 px-6 py-3 text-center text-sm font-medium text-slate-600 no-underline"
href={`${baseUrl}/team/decline/${token}`}
>
Decline
</Button>
</Section>
<Section className="mb-6 mt-6 text-center">
<Button
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={`${baseUrl}/team/invite/${token}`}
>
<Trans>Accept</Trans>
</Button>
<Button
className="ml-4 inline-flex items-center justify-center rounded-lg bg-gray-50 px-6 py-3 text-center text-sm font-medium text-slate-600 no-underline"
href={`${baseUrl}/team/decline/${token}`}
>
<Trans>Decline</Trans>
</Button>
</Section>
</Container>
</Section>
</Container>
<Hr className="mx-auto mt-12 max-w-xl" />
<Hr className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Tailwind>
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -1,7 +1,9 @@
import { formatTeamUrl } from '@documenso/lib/utils/teams';
import config from '@documenso/tailwind-config';
import { Trans, 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';
@ -22,61 +24,56 @@ export const TeamJoinEmailTemplate = ({
teamName = 'Team Name',
teamUrl = 'demo',
}: TeamJoinEmailProps) => {
const previewText = 'A team member has joined a team on Documenso';
const { _ } = useLingui();
const previewText = msg`A team member has joined a team on Documenso`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white text-slate-500">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
<Preview>{_(previewText)}</Preview>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white text-slate-500">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
<TemplateImage
assetBaseUrl={assetBaseUrl}
className="mb-4 h-6 p-2"
staticAsset="logo.png"
/>
<Section>
<TemplateImage
className="mx-auto"
assetBaseUrl={assetBaseUrl}
className="mb-4 h-6 p-2"
staticAsset="logo.png"
staticAsset="add-user.png"
/>
</Section>
<Section>
<TemplateImage
className="mx-auto"
assetBaseUrl={assetBaseUrl}
staticAsset="add-user.png"
/>
</Section>
<Section className="p-2 text-slate-500">
<Text className="text-center text-lg font-medium text-black">
<Section className="p-2 text-slate-500">
<Text className="text-center text-lg font-medium text-black">
<Trans>
{memberName || memberEmail} joined the team {teamName} on Documenso
</Text>
</Trans>
</Text>
<Text className="my-1 text-center text-base">
{memberEmail} joined the following team
</Text>
<Text className="my-1 text-center text-base">
<Trans>{memberEmail} joined the following team</Trans>
</Text>
<div className="mx-auto my-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-base font-medium text-slate-600">
{formatTeamUrl(teamUrl, baseUrl)}
</div>
</Section>
</Container>
<div className="mx-auto my-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-base font-medium text-slate-600">
{formatTeamUrl(teamUrl, baseUrl)}
</div>
</Section>
</Container>
<Hr className="mx-auto mt-12 max-w-xl" />
<Hr className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Tailwind>
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -1,7 +1,9 @@
import { formatTeamUrl } from '@documenso/lib/utils/teams';
import config from '@documenso/tailwind-config';
import { Trans, 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';
@ -22,61 +24,56 @@ export const TeamLeaveEmailTemplate = ({
teamName = 'Team Name',
teamUrl = 'demo',
}: TeamLeaveEmailProps) => {
const previewText = 'A team member has left a team on Documenso';
const { _ } = useLingui();
const previewText = msg`A team member has left a team on Documenso`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white text-slate-500">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
<Preview>{_(previewText)}</Preview>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white text-slate-500">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-2 backdrop-blur-sm">
<TemplateImage
assetBaseUrl={assetBaseUrl}
className="mb-4 h-6 p-2"
staticAsset="logo.png"
/>
<Section>
<TemplateImage
className="mx-auto"
assetBaseUrl={assetBaseUrl}
className="mb-4 h-6 p-2"
staticAsset="logo.png"
staticAsset="delete-user.png"
/>
</Section>
<Section>
<TemplateImage
className="mx-auto"
assetBaseUrl={assetBaseUrl}
staticAsset="delete-user.png"
/>
</Section>
<Section className="p-2 text-slate-500">
<Text className="text-center text-lg font-medium text-black">
<Section className="p-2 text-slate-500">
<Text className="text-center text-lg font-medium text-black">
<Trans>
{memberName || memberEmail} left the team {teamName} on Documenso
</Text>
</Trans>
</Text>
<Text className="my-1 text-center text-base">
{memberEmail} left the following team
</Text>
<Text className="my-1 text-center text-base">
<Trans>{memberEmail} left the following team</Trans>
</Text>
<div className="mx-auto my-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-base font-medium text-slate-600">
{formatTeamUrl(teamUrl, baseUrl)}
</div>
</Section>
</Container>
<div className="mx-auto my-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-base font-medium text-slate-600">
{formatTeamUrl(teamUrl, baseUrl)}
</div>
</Section>
</Container>
<Hr className="mx-auto mt-12 max-w-xl" />
<Hr className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Tailwind>
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -1,18 +1,9 @@
import { formatTeamUrl } from '@documenso/lib/utils/teams';
import config from '@documenso/tailwind-config';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import {
Body,
Button,
Container,
Head,
Hr,
Html,
Preview,
Section,
Tailwind,
Text,
} from '../components';
import { formatTeamUrl } from '@documenso/lib/utils/teams';
import { Body, Button, Container, Head, Hr, Html, Preview, Section, Text } from '../components';
import { TemplateFooter } from '../template-components/template-footer';
import TemplateImage from '../template-components/template-image';
@ -33,78 +24,77 @@ export const TeamTransferRequestTemplate = ({
teamUrl = 'demo',
token = '',
}: TeamTransferRequestTemplateProps) => {
const previewText = 'Accept team transfer request on Documenso';
const { _ } = useLingui();
const previewText = msg`Accept team transfer request on Documenso`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: config.theme.extend.colors,
},
},
}}
>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white text-slate-500">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 px-2 pt-2 backdrop-blur-sm">
<Preview>{_(previewText)}</Preview>
<Body className="mx-auto my-auto font-sans">
<Section className="bg-white text-slate-500">
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 px-2 pt-2 backdrop-blur-sm">
<TemplateImage
assetBaseUrl={assetBaseUrl}
className="mb-4 h-6 p-2"
staticAsset="logo.png"
/>
<Section>
<TemplateImage
className="mx-auto"
assetBaseUrl={assetBaseUrl}
className="mb-4 h-6 p-2"
staticAsset="logo.png"
staticAsset="add-user.png"
/>
</Section>
<Section>
<TemplateImage
className="mx-auto"
assetBaseUrl={assetBaseUrl}
staticAsset="add-user.png"
/>
</Section>
<Section className="p-2 text-slate-500">
<Text className="text-center text-lg font-medium text-black">
<Trans>{teamName} ownership transfer request</Trans>
</Text>
<Section className="p-2 text-slate-500">
<Text className="text-center text-lg font-medium text-black">
{teamName} ownership transfer request
</Text>
<Text className="my-1 text-center text-base">
<Text className="my-1 text-center text-base">
<Trans>
<span className="font-bold">{senderName}</span> has requested that you take
ownership of the following team
</Text>
</Trans>
</Text>
<div className="mx-auto my-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-base font-medium text-slate-600">
{formatTeamUrl(teamUrl, baseUrl)}
</div>
<div className="mx-auto my-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-base font-medium text-slate-600">
{formatTeamUrl(teamUrl, baseUrl)}
</div>
<Text className="text-center text-sm">
<Text className="text-center text-sm">
<Trans>
By accepting this request, you will take responsibility for any billing items
associated with this team.
</Text>
</Trans>
</Text>
<Section className="mb-6 mt-6 text-center">
<Button
className="bg-documenso-500 ml-2 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
href={`${baseUrl}/team/verify/transfer/${token}`}
>
Accept
</Button>
</Section>
<Section className="mb-6 mt-6 text-center">
<Button
className="bg-documenso-500 ml-2 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
href={`${baseUrl}/team/verify/transfer/${token}`}
>
<Trans>Accept</Trans>
</Button>
</Section>
</Section>
<Text className="text-center text-xs">Link expires in 1 hour.</Text>
</Container>
<Text className="text-center text-xs">
<Trans>Link expires in 1 hour.</Trans>
</Text>
</Container>
<Hr className="mx-auto mt-12 max-w-xl" />
<Hr className="mx-auto mt-12 max-w-xl" />
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Tailwind>
<Container className="mx-auto max-w-xl">
<TemplateFooter isDocument={false} />
</Container>
</Section>
</Body>
</Html>
);
};

View File

@ -7,58 +7,94 @@ import { setupI18n } from '@lingui/core';
import { setI18n } from '@lingui/react/server';
import { IS_APP_WEB } from '../../constants/app';
import { SUPPORTED_LANGUAGE_CODES } from '../../constants/i18n';
import {
APP_I18N_OPTIONS,
SUPPORTED_LANGUAGE_CODES,
isValidLanguageCode,
} from '../../constants/i18n';
import { extractLocaleData } from '../../utils/i18n';
import { remember } from '../../utils/remember';
type SupportedLanguages = (typeof SUPPORTED_LANGUAGE_CODES)[number];
async function loadCatalog(lang: SupportedLanguages): Promise<{
export async function loadCatalog(lang: SupportedLanguages): Promise<{
[k: string]: Messages;
}> {
const extension = process.env.NODE_ENV === 'development' ? 'po' : 'js';
const context = IS_APP_WEB ? 'web' : 'marketing';
const { messages } = await import(`../../translations/${lang}/${context}.${extension}`);
let { messages } = await import(`../../translations/${lang}/${context}.${extension}`);
if (extension === 'po') {
const { messages: commonMessages } = await import(
`../../translations/${lang}/common.${extension}`
);
messages = { ...messages, ...commonMessages };
}
return {
[lang]: messages,
};
}
const catalogs = await Promise.all(SUPPORTED_LANGUAGE_CODES.map(loadCatalog));
const catalogs = Promise.all(SUPPORTED_LANGUAGE_CODES.map(loadCatalog));
// transform array of catalogs into a single object
export const allMessages = catalogs.reduce((acc, oneCatalog) => {
return { ...acc, ...oneCatalog };
}, {});
const allMessages = async () => {
return await catalogs.then((catalogs) =>
catalogs.reduce((acc, oneCatalog) => {
return {
...acc,
...oneCatalog,
};
}, {}),
);
};
type AllI18nInstances = { [K in SupportedLanguages]: I18n };
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
export const allI18nInstances = SUPPORTED_LANGUAGE_CODES.reduce((acc, lang) => {
const messages = allMessages[lang] ?? {};
export const allI18nInstances = remember('i18n.allI18nInstances', async () => {
const loadedMessages = await allMessages();
const i18n = setupI18n({
locale: lang,
messages: { [lang]: messages },
});
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return SUPPORTED_LANGUAGE_CODES.reduce((acc, lang) => {
const messages = loadedMessages[lang] ?? {};
return { ...acc, [lang]: i18n };
}, {}) as AllI18nInstances;
const i18n = setupI18n({
locale: lang,
messages: { [lang]: messages },
});
return { ...acc, [lang]: i18n };
}, {}) as AllI18nInstances;
});
// eslint-disable-next-line @typescript-eslint/ban-types
export const getI18nInstance = async (lang?: SupportedLanguages | (string & {})) => {
const instances = await allI18nInstances;
if (!isValidLanguageCode(lang)) {
return instances[APP_I18N_OPTIONS.sourceLang];
}
return instances[lang] ?? instances[APP_I18N_OPTIONS.sourceLang];
};
/**
* This needs to be run in all layouts and page server components that require i18n.
*
* https://lingui.dev/tutorials/react-rsc#pages-layouts-and-lingui
*/
export const setupI18nSSR = () => {
export const setupI18nSSR = async () => {
const { lang, locales } = extractLocaleData({
cookies: cookies(),
headers: headers(),
});
// Get and set a ready-made i18n instance for the given language.
const i18n = allI18nInstances[lang];
const i18n = await getI18nInstance(lang);
// Reactivate the i18n instance with the locale for date and number formatting.
i18n.activate(lang, locales);

View File

@ -47,3 +47,6 @@ export const SUPPORTED_LANGUAGES: Record<string, SupportedLanguage> = {
short: 'es',
},
} satisfies Record<SupportedLanguageCodes, SupportedLanguage>;
export const isValidLanguageCode = (code: unknown): code is SupportedLanguageCodes =>
SUPPORTED_LANGUAGE_CODES.includes(code as SupportedLanguageCodes);

View File

@ -41,31 +41,35 @@ export const RECIPIENT_ROLES_DESCRIPTION_ENG = {
actioned: `Approved`,
progressiveVerb: `Approving`,
roleName: `Approver`,
roleNamePlural: msg`Approvers`,
},
[RecipientRole.CC]: {
actionVerb: `CC`,
actioned: `CC'd`,
progressiveVerb: `CC`,
roleName: `Cc`,
roleNamePlural: msg`Ccers`,
},
[RecipientRole.SIGNER]: {
actionVerb: `Sign`,
actioned: `Signed`,
progressiveVerb: `Signing`,
roleName: `Signer`,
roleNamePlural: msg`Signers`,
},
[RecipientRole.VIEWER]: {
actionVerb: `View`,
actioned: `Viewed`,
progressiveVerb: `Viewing`,
roleName: `Viewer`,
roleNamePlural: msg`Viewers`,
},
} satisfies Record<keyof typeof RecipientRole, unknown>;
export const RECIPIENT_ROLE_TO_EMAIL_TYPE = {
[RecipientRole.SIGNER]: 'SIGNING_REQUEST',
[RecipientRole.VIEWER]: 'VIEW_REQUEST',
[RecipientRole.APPROVER]: 'APPROVE_REQUEST',
[RecipientRole.SIGNER]: `SIGNING_REQUEST`,
[RecipientRole.VIEWER]: `VIEW_REQUEST`,
[RecipientRole.APPROVER]: `APPROVE_REQUEST`,
} as const;
export const RECIPIENT_ROLE_SIGNING_REASONS = {

View File

@ -43,18 +43,10 @@ export class LocalJobProvider extends BaseJobProvider {
}
public async triggerJob(options: SimpleTriggerJobOptions) {
console.log({ jobDefinitions: this._jobDefinitions });
const eligibleJobs = Object.values(this._jobDefinitions).filter(
(job) => job.trigger.name === options.name,
);
console.log({ options });
console.log(
'Eligible jobs:',
eligibleJobs.map((job) => job.name),
);
await Promise.all(
eligibleJobs.map(async (job) => {
// Ideally we will change this to a createMany with returning later once we upgrade Prisma

View File

@ -1,9 +1,9 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
import { prisma } from '@documenso/prisma';
import {
@ -13,6 +13,7 @@ import {
SendStatus,
} from '@documenso/prisma/client';
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import {
@ -23,6 +24,7 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
import { renderCustomEmailTemplate } from '../../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { type JobDefinition } from '../../client/_internal/job';
const SEND_SIGNING_EMAIL_JOB_DEFINITION_ID = 'send.signing.requested.email';
@ -90,22 +92,32 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
const recipientActionVerb =
RECIPIENT_ROLES_DESCRIPTION_ENG[recipient.role].actionVerb.toLowerCase();
const i18n = await getI18nInstance(documentMeta?.language);
let emailMessage = customEmail?.message || '';
let emailSubject = `Please ${recipientActionVerb} this document`;
let emailSubject = i18n._(msg`Please ${recipientActionVerb} this document`);
if (selfSigner) {
emailMessage = `You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`;
emailSubject = `Please ${recipientActionVerb} your document`;
emailMessage = i18n._(
msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`,
);
emailSubject = i18n._(msg`Please ${recipientActionVerb} your document`);
}
if (isDirectTemplate) {
emailMessage = `A document was created by your direct template that requires you to ${recipientActionVerb} it.`;
emailSubject = `Please ${recipientActionVerb} this document created by your direct template`;
emailMessage = i18n._(
msg`A document was created by your direct template that requires you to ${recipientActionVerb} it.`,
);
emailSubject = i18n._(
msg`Please ${recipientActionVerb} this document created by your direct template`,
);
}
if (isTeamDocument && team) {
emailSubject = `${team.name} invited you to ${recipientActionVerb} a document`;
emailMessage = `${user.name} on behalf of ${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`;
emailSubject = i18n._(msg`${team.name} invited you to ${recipientActionVerb} a document`);
emailMessage = i18n._(
msg`${user.name} on behalf of ${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`,
);
}
const customEmailTemplate = {
@ -132,6 +144,14 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
});
await io.runTask('send-signing-email', async () => {
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: documentMeta?.language }),
renderEmailWithI18N(template, {
lang: documentMeta?.language,
plainText: true,
}),
]);
await mailer.sendMail({
to: {
name: recipient.name,
@ -145,8 +165,8 @@ export const SEND_SIGNING_EMAIL_JOB_DEFINITION = {
documentMeta?.subject || emailSubject,
customEmailTemplate,
),
html: render(template),
text: render(template, { plainText: true }),
html,
text,
});
});

View File

@ -1,13 +1,15 @@
import { msg } from '@lingui/macro';
import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
import { WEBAPP_BASE_URL } from '../../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID = 'send.team-member-joined.email';
@ -71,15 +73,23 @@ export const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION = {
teamUrl: team.url,
});
// !: Replace with the actual language of the recipient later
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent),
renderEmailWithI18N(emailContent, { plainText: true }),
]);
const i18n = await getI18nInstance();
await mailer.sendMail({
to: member.user.email,
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: 'A new member has joined your team',
html: render(emailContent),
text: render(emailContent, { plainText: true }),
subject: i18n._(msg`A new member has joined your team`),
html,
text,
});
},
);

View File

@ -1,13 +1,15 @@
import { msg } from '@lingui/macro';
import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client';
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
import { WEBAPP_BASE_URL } from '../../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID = 'send.team-member-left.email';
@ -61,15 +63,22 @@ export const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION = {
teamUrl: team.url,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent),
renderEmailWithI18N(emailContent, { plainText: true }),
]);
const i18n = await getI18nInstance();
await mailer.sendMail({
to: member.user.email,
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: `A team member has left ${team.name}`,
html: render(emailContent),
text: render(emailContent, { plainText: true }),
subject: i18n._(msg`A team member has left ${team.name}`),
html,
text,
});
});
}

View File

@ -25,6 +25,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",

View File

@ -1,11 +1,14 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { ConfirmEmailTemplate } from '@documenso/email/templates/confirm-email';
import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendConfirmationEmailProps {
userId: number;
@ -45,6 +48,13 @@ export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailPro
confirmationLink,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(confirmationTemplate),
renderEmailWithI18N(confirmationTemplate, { plainText: true }),
]);
const i18n = await getI18nInstance();
return mailer.sendMail({
to: {
address: user.email,
@ -54,8 +64,8 @@ export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailPro
name: senderName,
address: senderAddress,
},
subject: 'Please confirm your email',
html: render(confirmationTemplate),
text: render(confirmationTemplate, { plainText: true }),
subject: i18n._(msg`Please confirm your email`),
html,
text,
});
};

View File

@ -1,11 +1,14 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { ForgotPasswordTemplate } from '@documenso/email/templates/forgot-password';
import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendForgotPasswordOptions {
userId: number;
@ -39,6 +42,13 @@ export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions)
resetPasswordLink,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template),
renderEmailWithI18N(template, { plainText: true }),
]);
const i18n = await getI18nInstance();
return await mailer.sendMail({
to: {
address: user.email,
@ -48,8 +58,8 @@ export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions)
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Forgot Password?',
html: render(template),
text: render(template, { plainText: true }),
subject: i18n._(msg`Forgot Password?`),
html,
text,
});
};

View File

@ -1,11 +1,11 @@
import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { ResetPasswordTemplate } from '@documenso/email/templates/reset-password';
import { prisma } from '@documenso/prisma';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendResetPasswordOptions {
userId: number;
@ -26,6 +26,11 @@ export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) =>
userName: user.name || '',
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template),
renderEmailWithI18N(template, { plainText: true }),
]);
return await mailer.sendMail({
to: {
address: user.email,
@ -36,7 +41,7 @@ export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) =>
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Password Reset Success!',
html: render(template),
text: render(template, { plainText: true }),
html,
text,
});
};

View File

@ -9,6 +9,8 @@ import {
import { prisma } from '@documenso/prisma';
import type { DocumentSigningOrder } from '@documenso/prisma/client';
import type { SupportedLanguageCodes } from '../../constants/i18n';
export type CreateDocumentMetaOptions = {
documentId: number;
subject?: string;
@ -19,6 +21,7 @@ export type CreateDocumentMetaOptions = {
redirectUrl?: string;
signingOrder?: DocumentSigningOrder;
typedSignatureEnabled?: boolean;
language?: SupportedLanguageCodes;
userId: number;
requestMetadata: RequestMetadata;
};
@ -34,6 +37,7 @@ export const upsertDocumentMeta = async ({
redirectUrl,
signingOrder,
typedSignatureEnabled,
language,
requestMetadata,
}: CreateDocumentMetaOptions) => {
const user = await prisma.user.findFirstOrThrow({
@ -85,6 +89,7 @@ export const upsertDocumentMeta = async ({
redirectUrl,
signingOrder,
typedSignatureEnabled,
language,
},
update: {
subject,
@ -95,6 +100,7 @@ export const upsertDocumentMeta = async ({
redirectUrl,
signingOrder,
typedSignatureEnabled,
language,
},
});

View File

@ -3,7 +3,6 @@
import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
import { prisma } from '@documenso/prisma';
import type { Document, DocumentMeta, Recipient, User } from '@documenso/prisma/client';
@ -14,6 +13,7 @@ import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export type DeleteDocumentOptions = {
id: number;
@ -191,6 +191,11 @@ const handleDocumentOwnerDelete = async ({
assetBaseUrl,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template),
renderEmailWithI18N(template, { plainText: true }),
]);
await mailer.sendMail({
to: {
address: recipient.email,
@ -201,8 +206,8 @@ const handleDocumentOwnerDelete = async ({
address: FROM_ADDRESS,
},
subject: 'Document Cancelled',
html: render(template),
text: render(template, { plainText: true }),
html,
text,
});
}),
);

View File

@ -1,11 +1,12 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import {
RECIPIENT_ROLES_DESCRIPTION_ENG,
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '@documenso/lib/constants/recipient-roles';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
@ -16,7 +17,9 @@ import { prisma } from '@documenso/prisma';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import type { Prisma } from '@documenso/prisma/client';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { getDocumentWhereInput } from './get-document-by-id';
export type ResendDocumentOptions = {
@ -92,25 +95,28 @@ export const resendDocument = async ({
return;
}
const i18n = await getI18nInstance(document.documentMeta?.language);
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
const { email, name } = recipient;
const selfSigner = email === user.email;
const recipientActionVerb =
RECIPIENT_ROLES_DESCRIPTION_ENG[recipient.role].actionVerb.toLowerCase();
const recipientActionVerb = i18n
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
.toLowerCase();
let emailMessage = customEmail?.message || '';
let emailSubject = `Reminder: Please ${recipientActionVerb} this document`;
let emailMessage = msg`${customEmail?.message || ''}`;
let emailSubject = msg`Reminder: Please ${recipientActionVerb} this document`;
if (selfSigner) {
emailMessage = `You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`;
emailSubject = `Reminder: Please ${recipientActionVerb} your document`;
emailMessage = msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`;
emailSubject = msg`Reminder: Please ${recipientActionVerb} your document`;
}
if (isTeamDocument && document.team) {
emailSubject = `Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`;
emailMessage = `${user.name} on behalf of ${document.team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`;
emailSubject = msg`Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`;
emailMessage = msg`${user.name} on behalf of ${document.team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`;
}
const customEmailTemplate = {
@ -128,7 +134,7 @@ export const resendDocument = async ({
inviterEmail: isTeamDocument ? document.team?.teamEmail?.email || user.email : user.email,
assetBaseUrl,
signDocumentLink,
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
customBody: renderCustomEmailTemplate(i18n._(emailMessage), customEmailTemplate),
role: recipient.role,
selfSigner,
isTeamInvite: isTeamDocument,
@ -137,6 +143,14 @@ export const resendDocument = async ({
await prisma.$transaction(
async (tx) => {
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language }),
renderEmailWithI18N(template, {
lang: document.documentMeta?.language,
plainText: true,
}),
]);
await mailer.sendMail({
to: {
address: email,
@ -147,10 +161,13 @@ export const resendDocument = async ({
address: FROM_ADDRESS,
},
subject: customEmail?.subject
? renderCustomEmailTemplate(`Reminder: ${customEmail.subject}`, customEmailTemplate)
: emailSubject,
html: render(template),
text: render(template, { plainText: true }),
? renderCustomEmailTemplate(
i18n._(msg`Reminder: ${customEmail.subject}`),
customEmailTemplate,
)
: i18n._(emailSubject),
html,
text,
});
await tx.documentAuditLog.create({

View File

@ -1,17 +1,20 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
import { prisma } from '@documenso/prisma';
import { DocumentSource } from '@documenso/prisma/client';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFile } from '../../universal/upload/get-file';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendDocumentOptions {
documentId: number;
@ -61,6 +64,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
}`;
}
const i18n = await getI18nInstance(document.documentMeta?.language);
// If the document owner is not a recipient then send the email to them separately
if (!document.Recipient.find((recipient) => recipient.email === owner.email)) {
const template = createElement(DocumentCompletedEmailTemplate, {
@ -69,6 +74,11 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
downloadLink: documentOwnerDownloadLink,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language }),
renderEmailWithI18N(template, { lang: document.documentMeta?.language, plainText: true }),
]);
await mailer.sendMail({
to: [
{
@ -80,9 +90,9 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Signing Complete!',
html: render(template),
text: render(template, { plainText: true }),
subject: i18n._(msg`Signing Complete!`),
html,
text,
attachments: [
{
filename: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',
@ -129,6 +139,11 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
: undefined,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language }),
renderEmailWithI18N(template, { lang: document.documentMeta?.language, plainText: true }),
]);
await mailer.sendMail({
to: [
{
@ -143,9 +158,9 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
subject:
isDirectTemplate && document.documentMeta?.subject
? renderCustomEmailTemplate(document.documentMeta.subject, customEmailTemplate)
: 'Signing Complete!',
html: render(template),
text: render(template, { plainText: true }),
: i18n._(msg`Signing Complete!`),
html,
text,
attachments: [
{
filename: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',

View File

@ -1,11 +1,14 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { DocumentSuperDeleteEmailTemplate } from '@documenso/email/templates/document-super-delete';
import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendDeleteEmailOptions {
documentId: number;
@ -36,6 +39,13 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
assetBaseUrl,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template),
renderEmailWithI18N(template, { plainText: true }),
]);
const i18n = await getI18nInstance();
await mailer.sendMail({
to: {
address: email,
@ -45,8 +55,8 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Document Deleted!',
html: render(template),
text: render(template, { plainText: true }),
subject: i18n._(msg`Document Deleted!`),
html,
text,
});
};

View File

@ -1,11 +1,14 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending';
import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendPendingEmailOptions {
documentId: number;
@ -28,6 +31,7 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
id: recipientId,
},
},
documentMeta: true,
},
});
@ -50,6 +54,13 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
assetBaseUrl,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language }),
renderEmailWithI18N(template, { lang: document.documentMeta?.language, plainText: true }),
]);
const i18n = await getI18nInstance();
await mailer.sendMail({
to: {
address: email,
@ -59,8 +70,8 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Waiting for others to complete signing.',
html: render(template),
text: render(template, { plainText: true }),
subject: i18n._(msg`Waiting for others to complete signing.`),
html,
text,
});
};

View File

@ -2,17 +2,20 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export type SuperDeleteDocumentOptions = {
id: number;
@ -53,6 +56,13 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
assetBaseUrl,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language }),
renderEmailWithI18N(template, { lang: document.documentMeta?.language, plainText: true }),
]);
const i18n = await getI18nInstance(document.documentMeta?.language);
await mailer.sendMail({
to: {
address: recipient.email,
@ -62,9 +72,9 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: 'Document Cancelled',
html: render(template),
text: render(template, { plainText: true }),
subject: i18n._(msg`Document Cancelled`),
html,
text,
});
}),
);

View File

@ -1,8 +1,9 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import {
@ -21,10 +22,12 @@ import type { Recipient } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { canRecipientBeModified } from '../../utils/recipients';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SetRecipientsForDocumentOptions {
userId: number;
@ -62,6 +65,7 @@ export const setRecipientsForDocument = async ({
},
include: {
Field: true,
documentMeta: true,
},
});
@ -291,6 +295,13 @@ export const setRecipientsForDocument = async ({
assetBaseUrl,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language }),
renderEmailWithI18N(template, { lang: document.documentMeta?.language, plainText: true }),
]);
const i18n = await getI18nInstance(document.documentMeta?.language);
await mailer.sendMail({
to: {
address: recipient.email,
@ -300,9 +311,9 @@ export const setRecipientsForDocument = async ({
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: 'You have been removed from a document',
html: render(template),
text: render(template, { plainText: true }),
subject: i18n._(msg`You have been removed from a document`),
html,
text,
});
}),
);

View File

@ -81,7 +81,7 @@ export const acceptTeamInvitation = async ({ userId, teamId }: AcceptTeamInvitat
await jobs.triggerJob({
name: 'send.team-member-joined.email',
payload: {
teamId: team.id,
teamId: teamMember.teamId,
memberId: teamMember.id,
},
});

View File

@ -1,9 +1,9 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { ConfirmTeamEmailTemplate } from '@documenso/email/templates/confirm-team-email';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
@ -13,6 +13,9 @@ import { createTokenVerification } from '@documenso/lib/utils/token-verification
import { prisma } from '@documenso/prisma';
import { Prisma } from '@documenso/prisma/client';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export type CreateTeamEmailVerificationOptions = {
userId: number;
teamId: number;
@ -122,14 +125,23 @@ export const sendTeamEmailVerificationEmail = async (
token,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template),
renderEmailWithI18N(template, { plainText: true }),
]);
const i18n = await getI18nInstance();
await mailer.sendMail({
to: email,
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: `A request to use your email has been initiated by ${teamName} on Documenso`,
html: render(template),
text: render(template, { plainText: true }),
subject: i18n._(
msg`A request to use your email has been initiated by ${teamName} on Documenso`,
),
html,
text,
});
};

View File

@ -1,9 +1,9 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { nanoid } from 'nanoid';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import type { TeamInviteEmailProps } from '@documenso/email/templates/team-invite';
import { TeamInviteEmailTemplate } from '@documenso/email/templates/team-invite';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
@ -15,6 +15,9 @@ import { prisma } from '@documenso/prisma';
import { TeamMemberInviteStatus } from '@documenso/prisma/client';
import type { TCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export type CreateTeamMemberInvitesOptions = {
userId: number;
userName: string;
@ -148,14 +151,23 @@ export const sendTeamMemberInviteEmail = async ({
...emailTemplateOptions,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template),
renderEmailWithI18N(template, { plainText: true }),
]);
const i18n = await getI18nInstance();
await mailer.sendMail({
to: email,
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: `You have been invited to join ${emailTemplateOptions.teamName} on Documenso`,
html: render(template),
text: render(template, { plainText: true }),
subject: i18n._(
msg`You have been invited to join ${emailTemplateOptions.teamName} on Documenso`,
),
html,
text,
});
};

View File

@ -1,13 +1,17 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { TeamEmailRemovedTemplate } from '@documenso/email/templates/team-email-removed';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export type DeleteTeamEmailOptions = {
userId: number;
userEmail: string;
@ -73,6 +77,13 @@ export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamE
teamUrl: team.url,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template),
renderEmailWithI18N(template, { plainText: true }),
]);
const i18n = await getI18nInstance();
await mailer.sendMail({
to: {
address: team.owner.email,
@ -82,9 +93,9 @@ export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamE
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: `Team email has been revoked for ${team.name}`,
html: render(template),
text: render(template, { plainText: true }),
subject: i18n._(msg`Team email has been revoked for ${team.name}`),
html,
text,
});
} catch (e) {
// Todo: Teams - Alert us.

View File

@ -1,7 +1,6 @@
import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import type { TeamDeleteEmailProps } from '@documenso/email/templates/team-delete';
import { TeamDeleteEmailTemplate } from '@documenso/email/templates/team-delete';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
@ -11,6 +10,7 @@ import { stripe } from '@documenso/lib/server-only/stripe';
import { prisma } from '@documenso/prisma';
import { jobs } from '../../jobs/client';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export type DeleteTeamOptions = {
userId: number;
@ -95,6 +95,11 @@ export const sendTeamDeleteEmail = async ({
...emailTemplateOptions,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template),
renderEmailWithI18N(template, { plainText: true }),
]);
await mailer.sendMail({
to: email,
from: {
@ -102,7 +107,7 @@ export const sendTeamDeleteEmail = async ({
address: FROM_ADDRESS,
},
subject: `Team "${emailTemplateOptions.teamName}" has been deleted on Documenso`,
html: render(template),
text: render(template, { plainText: true }),
html,
text,
});
};

View File

@ -1,13 +1,17 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { TeamTransferRequestTemplate } from '@documenso/email/templates/team-transfer-request';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import { createTokenVerification } from '@documenso/lib/utils/token-verification';
import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export type RequestTeamOwnershipTransferOptions = {
/**
* The ID of the user initiating the transfer.
@ -93,15 +97,24 @@ export const requestTeamOwnershipTransfer = async ({
token,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template),
renderEmailWithI18N(template, { plainText: true }),
]);
const i18n = await getI18nInstance();
await mailer.sendMail({
to: newOwnerUser.email,
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: `You have been requested to take ownership of team ${team.name} on Documenso`,
html: render(template),
text: render(template, { plainText: true }),
subject: i18n._(
msg`You have been requested to take ownership of team ${team.name} on Documenso`,
),
html,
text,
});
},
{ timeout: 30_000 },

View File

@ -1,15 +1,16 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { DocumentCreatedFromDirectTemplateEmailTemplate } from '@documenso/email/templates/document-created-from-direct-template';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import type { Field, Signature } from '@documenso/prisma/client';
import {
DocumentSigningOrder,
DocumentSource,
DocumentStatus,
FieldType,
@ -21,6 +22,7 @@ import {
} from '@documenso/prisma/client';
import type { TSignFieldWithTokenMutationSchema } from '@documenso/trpc/server/field-router/schema';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../../constants/time-zones';
@ -37,6 +39,7 @@ import {
createRecipientAuthOptions,
extractDocumentAuthMethods,
} from '../../utils/document-auth';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { formatDocumentsPath } from '../../utils/teams';
import { sendDocument } from '../document/send-document';
import { validateFieldAuth } from '../document/validate-field-auth';
@ -142,6 +145,8 @@ export const createDocumentFromDirectTemplate = async ({
const metaDateFormat = template.templateMeta?.dateFormat || DEFAULT_DOCUMENT_DATE_FORMAT;
const metaEmailMessage = template.templateMeta?.message || '';
const metaEmailSubject = template.templateMeta?.subject || '';
const metaLanguage = template.templateMeta?.language;
const metaSigningOrder = template.templateMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
// Associate, validate and map to a query every direct template recipient field with the provided fields.
const createDirectRecipientFieldArgs = await Promise.all(
@ -256,6 +261,7 @@ export const createDocumentFromDirectTemplate = async ({
recipient.role === RecipientRole.CC
? SigningStatus.SIGNED
: SigningStatus.NOT_SIGNED,
signingOrder: recipient.signingOrder,
token: nanoid(),
};
}),
@ -267,6 +273,8 @@ export const createDocumentFromDirectTemplate = async ({
dateFormat: metaDateFormat,
message: metaEmailMessage,
subject: metaEmailSubject,
language: metaLanguage,
signingOrder: metaSigningOrder,
},
},
},
@ -330,6 +338,7 @@ export const createDocumentFromDirectTemplate = async ({
signingStatus: SigningStatus.SIGNED,
sendStatus: SendStatus.SENT,
signedAt: initialRequestTime,
signingOrder: directTemplateRecipient.signingOrder,
Field: {
createMany: {
data: directTemplateNonSignatureFields.map(({ templateField, customText }) => ({
@ -524,6 +533,13 @@ export const createDocumentFromDirectTemplate = async ({
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
});
const [html, text] = await Promise.all([
renderEmailWithI18N(emailTemplate, { lang: metaLanguage }),
renderEmailWithI18N(emailTemplate, { lang: metaLanguage, plainText: true }),
]);
const i18n = await getI18nInstance(metaLanguage);
await mailer.sendMail({
to: [
{
@ -535,9 +551,9 @@ export const createDocumentFromDirectTemplate = async ({
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Document created from direct template',
html: render(emailTemplate),
text: render(emailTemplate, { plainText: true }),
subject: i18n._(msg`Document created from direct template`),
html,
text,
});
return {

View File

@ -46,6 +46,7 @@ export const createDocumentFromTemplateLegacy = async ({
Recipient: true,
Field: true,
templateDocumentData: true,
templateMeta: true,
},
});
@ -78,6 +79,17 @@ export const createDocumentFromTemplateLegacy = async ({
token: nanoid(),
})),
},
documentMeta: {
create: {
subject: template.templateMeta?.subject,
message: template.templateMeta?.message,
timezone: template.templateMeta?.timezone,
dateFormat: template.templateMeta?.dateFormat,
redirectUrl: template.templateMeta?.redirectUrl,
signingOrder: template.templateMeta?.signingOrder ?? undefined,
language: template.templateMeta?.language,
},
},
},
include: {

View File

@ -11,6 +11,7 @@ import {
WebhookTriggerEvents,
} from '@documenso/prisma/client';
import type { SupportedLanguageCodes } from '../../constants/i18n';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { ZRecipientAuthOptionsSchema } from '../../types/document-auth';
@ -24,7 +25,10 @@ import {
} from '../../utils/document-auth';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
type FinalRecipient = Pick<Recipient, 'name' | 'email' | 'role' | 'authOptions'> & {
type FinalRecipient = Pick<
Recipient,
'name' | 'email' | 'role' | 'authOptions' | 'signingOrder'
> & {
templateRecipientId: number;
fields: Field[];
};
@ -57,6 +61,7 @@ export type CreateDocumentFromTemplateOptions = {
dateFormat?: string;
redirectUrl?: string;
signingOrder?: DocumentSigningOrder;
language?: SupportedLanguageCodes;
};
requestMetadata?: RequestMetadata;
};
@ -176,6 +181,7 @@ export const createDocumentFromTemplate = async ({
override?.signingOrder ||
template.templateMeta?.signingOrder ||
DocumentSigningOrder.PARALLEL,
language: override?.language || template.templateMeta?.language,
},
},
Recipient: {
@ -197,6 +203,7 @@ export const createDocumentFromTemplate = async ({
recipient.role === RecipientRole.CC
? SigningStatus.SIGNED
: SigningStatus.NOT_SIGNED,
signingOrder: recipient.signingOrder,
token: nanoid(),
};
}),

View File

@ -1,3 +1,5 @@
import { omit } from 'remeda';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import type { Prisma } from '@documenso/prisma/client';
@ -38,6 +40,7 @@ export const duplicateTemplate = async ({
Recipient: true,
Field: true,
templateDocumentData: true,
templateMeta: true,
},
});
@ -53,6 +56,14 @@ export const duplicateTemplate = async ({
},
});
let templateMeta: Prisma.TemplateCreateArgs['data']['templateMeta'] | undefined = undefined;
if (template.templateMeta) {
templateMeta = {
create: omit(template.templateMeta, ['id', 'templateId']),
};
}
const duplicatedTemplate = await prisma.template.create({
data: {
userId,
@ -66,8 +77,8 @@ export const duplicateTemplate = async ({
token: nanoid(),
})),
},
templateMeta,
},
include: {
Recipient: true,
},

View File

@ -39,7 +39,7 @@ export const sendConfirmationToken = async ({
mostRecentToken?.createdAt &&
DateTime.fromJSDate(mostRecentToken.createdAt).diffNow('minutes').minutes > -5
) {
return;
// return;
}
const createdToken = await prisma.verificationToken.create({
@ -64,6 +64,7 @@ export const sendConfirmationToken = async ({
return { success: true };
} catch (err) {
console.log(err);
throw new Error(`Failed to send the confirmation email`);
}
};

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-10-18 04:04\n"
"PO-Revision-Date: 2024-11-01 02:29\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -42,7 +42,7 @@ msgstr "Dokument hinzufügen"
msgid "Add More Users for {0}"
msgstr "Mehr Benutzer hinzufügen für {0}"
#: apps/marketing/src/app/(marketing)/open/page.tsx:165
#: apps/marketing/src/app/(marketing)/open/page.tsx:164
msgid "All our metrics, finances, and learnings are public. We believe in transparency and want to share our journey with you. You can read more about why here: <0>Announcing Open Metrics</0>"
msgstr "Alle unsere Kennzahlen, Finanzen und Erkenntnisse sind öffentlich. Wir glauben an Transparenz und möchten unsere Reise mit Ihnen teilen. Mehr erfahren Sie hier: <0>Ankündigung Offene Kennzahlen</0>"
@ -90,7 +90,7 @@ msgstr "Änderungsprotokoll"
msgid "Choose a template from the community app store. Or submit your own template for others to use."
msgstr "Wählen Sie eine Vorlage aus dem Community-App-Store. Oder reichen Sie Ihre eigene Vorlage ein, damit andere sie benutzen können."
#: apps/marketing/src/app/(marketing)/open/page.tsx:219
#: apps/marketing/src/app/(marketing)/open/page.tsx:218
msgid "Community"
msgstr "Gemeinschaft"
@ -160,10 +160,6 @@ msgstr "Dokumentation"
msgid "Easily embed Documenso into your product. Simply copy and paste our react widget into your application."
msgstr "Betten Sie Documenso ganz einfach in Ihr Produkt ein. Kopieren und fügen Sie einfach unser React-Widget in Ihre Anwendung ein."
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:42
#~ msgid "Easy Sharing (Soon)."
#~ msgstr "Easy Sharing (Soon)."
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:46
msgid "Easy Sharing."
msgstr "Einfaches Teilen."
@ -197,7 +193,7 @@ msgstr "Schnell."
msgid "Faster, smarter and more beautiful."
msgstr "Schneller, intelligenter und schöner."
#: apps/marketing/src/app/(marketing)/open/page.tsx:210
#: apps/marketing/src/app/(marketing)/open/page.tsx:209
msgid "Finances"
msgstr "Finanzen"
@ -250,15 +246,15 @@ msgstr "Fangen Sie heute an."
msgid "Get the latest news from Documenso, including product updates, team announcements and more!"
msgstr "Erhalten Sie die neuesten Nachrichten von Documenso, einschließlich Produkt-Updates, Team-Ankündigungen und mehr!"
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
#: apps/marketing/src/app/(marketing)/open/page.tsx:232
msgid "GitHub: Total Merged PRs"
msgstr "GitHub: Gesamte PRs zusammengeführt"
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
#: apps/marketing/src/app/(marketing)/open/page.tsx:250
msgid "GitHub: Total Open Issues"
msgstr "GitHub: Gesamte offene Issues"
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
#: apps/marketing/src/app/(marketing)/open/page.tsx:224
msgid "GitHub: Total Stars"
msgstr "GitHub: Gesamtanzahl Sterne"
@ -266,7 +262,7 @@ msgstr "GitHub: Gesamtanzahl Sterne"
msgid "Global Salary Bands"
msgstr "Globale Gehaltsbänder"
#: apps/marketing/src/app/(marketing)/open/page.tsx:261
#: apps/marketing/src/app/(marketing)/open/page.tsx:260
msgid "Growth"
msgstr "Wachstum"
@ -290,7 +286,7 @@ msgstr "Integrierte Zahlungen mit Stripe, sodass Sie sich keine Sorgen ums Bezah
msgid "Integrates with all your favourite tools."
msgstr "Integriert sich mit all Ihren Lieblingstools."
#: apps/marketing/src/app/(marketing)/open/page.tsx:289
#: apps/marketing/src/app/(marketing)/open/page.tsx:288
msgid "Is there more?"
msgstr "Gibt es mehr?"
@ -314,11 +310,11 @@ msgstr "Standort"
msgid "Make it your own through advanced customization and adjustability."
msgstr "Machen Sie es zu Ihrem eigenen durch erweiterte Anpassung und Einstellbarkeit."
#: apps/marketing/src/app/(marketing)/open/page.tsx:199
#: apps/marketing/src/app/(marketing)/open/page.tsx:198
msgid "Merged PR's"
msgstr "Zusammengeführte PRs"
#: apps/marketing/src/app/(marketing)/open/page.tsx:234
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
msgid "Merged PRs"
msgstr "Zusammengeführte PRs"
@ -349,8 +345,8 @@ msgstr "Keine Kreditkarte erforderlich"
msgid "None of these work for you? Try self-hosting!"
msgstr "Keines dieser Angebote passt zu Ihnen? Versuchen Sie das Selbst-Hosting!"
#: apps/marketing/src/app/(marketing)/open/page.tsx:194
#: apps/marketing/src/app/(marketing)/open/page.tsx:252
#: apps/marketing/src/app/(marketing)/open/page.tsx:193
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
msgid "Open Issues"
msgstr "Offene Issues"
@ -358,7 +354,7 @@ msgstr "Offene Issues"
msgid "Open Source or Hosted."
msgstr "Open Source oder Hosted."
#: apps/marketing/src/app/(marketing)/open/page.tsx:161
#: apps/marketing/src/app/(marketing)/open/page.tsx:160
#: apps/marketing/src/components/(marketing)/footer.tsx:37
#: apps/marketing/src/components/(marketing)/header.tsx:64
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:40
@ -377,18 +373,10 @@ msgstr "Unsere benutzerdefinierten Vorlagen verfügen über intelligente Regeln,
msgid "Our Enterprise License is great for large organizations looking to switch to Documenso for all their signing needs. It's available for our cloud offering as well as self-hosted setups and offers a wide range of compliance and Adminstration Features."
msgstr "Unsere Enterprise-Lizenz ist ideal für große Organisationen, die auf Documenso für all ihre Signaturanforderungen umsteigen möchten. Sie ist sowohl für unser Cloud-Angebot als auch für selbstgehostete Setups verfügbar und bietet eine breite Palette an Compliance- und Verwaltungsfunktionen."
#: apps/marketing/src/components/(marketing)/enterprise.tsx:20
#~ msgid "Our Enterprise License is great large organizations looking to switch to Documenso for all their signing needs. It's availible for our cloud offering as well as self-hosted setups and offer a wide range of compliance and Adminstration Features."
#~ msgstr "Our Enterprise License is great large organizations looking to switch to Documenso for all their signing needs. It's availible for our cloud offering as well as self-hosted setups and offer a wide range of compliance and Adminstration Features."
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:65
msgid "Our self-hosted option is great for small teams and individuals who need a simple solution. You can use our docker based setup to get started in minutes. Take control with full customizability and data ownership."
msgstr "Unsere selbstgehostete Option ist ideal für kleine Teams und Einzelpersonen, die eine einfache Lösung benötigen. Sie können unser docker-basiertes Setup verwenden, um in wenigen Minuten loszulegen. Übernehmen Sie die Kontrolle mit vollständiger Anpassbarkeit und Datenhoheit."
#: apps/marketing/src/app/(marketing)/open/data.ts:25
#~ msgid "Part-Time"
#~ msgstr "Part-Time"
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:151
msgid "Premium Profile Name"
msgstr "Premium Profilname"
@ -430,10 +418,6 @@ msgstr "Gehalt"
msgid "Save $60 or $120"
msgstr "Sparen Sie $60 oder $120"
#: apps/marketing/src/components/(marketing)/i18n-switcher.tsx:47
#~ msgid "Search languages..."
#~ msgstr "Search languages..."
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:109
msgid "Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us."
msgstr "Sicher. Unsere Rechenzentren befinden sich in Frankfurt (Deutschland) und bieten uns die besten lokalen Datenschutzgesetze. Uns ist die sensible Natur unserer Daten sehr bewusst und wir folgen bewährten Praktiken, um die Sicherheit und Integrität der uns anvertrauten Daten zu gewährleisten."
@ -482,7 +466,7 @@ msgstr "Intelligent."
msgid "Star on GitHub"
msgstr "Auf GitHub favorisieren"
#: apps/marketing/src/app/(marketing)/open/page.tsx:226
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
msgid "Stars"
msgstr "Favoriten"
@ -517,7 +501,7 @@ msgstr "Vorlagen-Shop (Demnächst)."
msgid "That's awesome. You can take a look at the current <0>Issues</0> and join our <1>Discord Community</1> to keep up to date, on what the current priorities are. In any case, we are an open community and welcome all input, technical and non-technical ❤️"
msgstr "Das ist großartig. Sie können sich die aktuellen <0>Issues</0> ansehen und unserer <1>Discord-Community</1> beitreten, um auf dem neuesten Stand zu bleiben, was die aktuellen Prioritäten sind. In jedem Fall sind wir eine offene Gemeinschaft und begrüßen jegliche Beiträge, technische und nicht-technische ❤️"
#: apps/marketing/src/app/(marketing)/open/page.tsx:293
#: apps/marketing/src/app/(marketing)/open/page.tsx:292
msgid "This page is evolving as we learn what makes a great signing company. We'll update it when we have more to share."
msgstr "Diese Seite entwickelt sich weiter, während wir lernen, was ein großartiges Signing-Unternehmen ausmacht. Wir werden sie aktualisieren, wenn wir mehr zu teilen haben."
@ -530,8 +514,8 @@ msgstr "Titel"
msgid "Total Completed Documents"
msgstr "Insgesamt Abgeschlossene Dokumente"
#: apps/marketing/src/app/(marketing)/open/page.tsx:266
#: apps/marketing/src/app/(marketing)/open/page.tsx:267
#: apps/marketing/src/app/(marketing)/open/page.tsx:268
msgid "Total Customers"
msgstr "Insgesamt Kunden"
@ -618,4 +602,3 @@ msgstr "Sie können Documenso kostenlos selbst hosten oder unsere sofort einsatz
#: apps/marketing/src/components/(marketing)/carousel.tsx:272
msgid "Your browser does not support the video tag."
msgstr "Ihr Browser unterstützt das Video-Tag nicht."

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,7 @@ msgstr "Add document"
msgid "Add More Users for {0}"
msgstr "Add More Users for {0}"
#: apps/marketing/src/app/(marketing)/open/page.tsx:165
#: apps/marketing/src/app/(marketing)/open/page.tsx:164
msgid "All our metrics, finances, and learnings are public. We believe in transparency and want to share our journey with you. You can read more about why here: <0>Announcing Open Metrics</0>"
msgstr "All our metrics, finances, and learnings are public. We believe in transparency and want to share our journey with you. You can read more about why here: <0>Announcing Open Metrics</0>"
@ -85,7 +85,7 @@ msgstr "Changelog"
msgid "Choose a template from the community app store. Or submit your own template for others to use."
msgstr "Choose a template from the community app store. Or submit your own template for others to use."
#: apps/marketing/src/app/(marketing)/open/page.tsx:219
#: apps/marketing/src/app/(marketing)/open/page.tsx:218
msgid "Community"
msgstr "Community"
@ -155,10 +155,6 @@ msgstr "Documentation"
msgid "Easily embed Documenso into your product. Simply copy and paste our react widget into your application."
msgstr "Easily embed Documenso into your product. Simply copy and paste our react widget into your application."
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:42
#~ msgid "Easy Sharing (Soon)."
#~ msgstr "Easy Sharing (Soon)."
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:46
msgid "Easy Sharing."
msgstr "Easy Sharing."
@ -192,7 +188,7 @@ msgstr "Fast."
msgid "Faster, smarter and more beautiful."
msgstr "Faster, smarter and more beautiful."
#: apps/marketing/src/app/(marketing)/open/page.tsx:210
#: apps/marketing/src/app/(marketing)/open/page.tsx:209
msgid "Finances"
msgstr "Finances"
@ -245,15 +241,15 @@ msgstr "Get started today."
msgid "Get the latest news from Documenso, including product updates, team announcements and more!"
msgstr "Get the latest news from Documenso, including product updates, team announcements and more!"
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
#: apps/marketing/src/app/(marketing)/open/page.tsx:232
msgid "GitHub: Total Merged PRs"
msgstr "GitHub: Total Merged PRs"
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
#: apps/marketing/src/app/(marketing)/open/page.tsx:250
msgid "GitHub: Total Open Issues"
msgstr "GitHub: Total Open Issues"
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
#: apps/marketing/src/app/(marketing)/open/page.tsx:224
msgid "GitHub: Total Stars"
msgstr "GitHub: Total Stars"
@ -261,7 +257,7 @@ msgstr "GitHub: Total Stars"
msgid "Global Salary Bands"
msgstr "Global Salary Bands"
#: apps/marketing/src/app/(marketing)/open/page.tsx:261
#: apps/marketing/src/app/(marketing)/open/page.tsx:260
msgid "Growth"
msgstr "Growth"
@ -285,7 +281,7 @@ msgstr "Integrated payments with Stripe so you dont have to worry about getti
msgid "Integrates with all your favourite tools."
msgstr "Integrates with all your favourite tools."
#: apps/marketing/src/app/(marketing)/open/page.tsx:289
#: apps/marketing/src/app/(marketing)/open/page.tsx:288
msgid "Is there more?"
msgstr "Is there more?"
@ -309,11 +305,11 @@ msgstr "Location"
msgid "Make it your own through advanced customization and adjustability."
msgstr "Make it your own through advanced customization and adjustability."
#: apps/marketing/src/app/(marketing)/open/page.tsx:199
#: apps/marketing/src/app/(marketing)/open/page.tsx:198
msgid "Merged PR's"
msgstr "Merged PR's"
#: apps/marketing/src/app/(marketing)/open/page.tsx:234
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
msgid "Merged PRs"
msgstr "Merged PRs"
@ -344,8 +340,8 @@ msgstr "No Credit Card required"
msgid "None of these work for you? Try self-hosting!"
msgstr "None of these work for you? Try self-hosting!"
#: apps/marketing/src/app/(marketing)/open/page.tsx:194
#: apps/marketing/src/app/(marketing)/open/page.tsx:252
#: apps/marketing/src/app/(marketing)/open/page.tsx:193
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
msgid "Open Issues"
msgstr "Open Issues"
@ -353,7 +349,7 @@ msgstr "Open Issues"
msgid "Open Source or Hosted."
msgstr "Open Source or Hosted."
#: apps/marketing/src/app/(marketing)/open/page.tsx:161
#: apps/marketing/src/app/(marketing)/open/page.tsx:160
#: apps/marketing/src/components/(marketing)/footer.tsx:37
#: apps/marketing/src/components/(marketing)/header.tsx:64
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:40
@ -372,18 +368,10 @@ msgstr "Our custom templates come with smart rules that can help you save time a
msgid "Our Enterprise License is great for large organizations looking to switch to Documenso for all their signing needs. It's available for our cloud offering as well as self-hosted setups and offers a wide range of compliance and Adminstration Features."
msgstr "Our Enterprise License is great for large organizations looking to switch to Documenso for all their signing needs. It's available for our cloud offering as well as self-hosted setups and offers a wide range of compliance and Adminstration Features."
#: apps/marketing/src/components/(marketing)/enterprise.tsx:20
#~ msgid "Our Enterprise License is great large organizations looking to switch to Documenso for all their signing needs. It's availible for our cloud offering as well as self-hosted setups and offer a wide range of compliance and Adminstration Features."
#~ msgstr "Our Enterprise License is great large organizations looking to switch to Documenso for all their signing needs. It's availible for our cloud offering as well as self-hosted setups and offer a wide range of compliance and Adminstration Features."
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:65
msgid "Our self-hosted option is great for small teams and individuals who need a simple solution. You can use our docker based setup to get started in minutes. Take control with full customizability and data ownership."
msgstr "Our self-hosted option is great for small teams and individuals who need a simple solution. You can use our docker based setup to get started in minutes. Take control with full customizability and data ownership."
#: apps/marketing/src/app/(marketing)/open/data.ts:25
#~ msgid "Part-Time"
#~ msgstr "Part-Time"
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:151
msgid "Premium Profile Name"
msgstr "Premium Profile Name"
@ -425,10 +413,6 @@ msgstr "Salary"
msgid "Save $60 or $120"
msgstr "Save $60 or $120"
#: apps/marketing/src/components/(marketing)/i18n-switcher.tsx:47
#~ msgid "Search languages..."
#~ msgstr "Search languages..."
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:109
msgid "Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us."
msgstr "Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us."
@ -477,7 +461,7 @@ msgstr "Smart."
msgid "Star on GitHub"
msgstr "Star on GitHub"
#: apps/marketing/src/app/(marketing)/open/page.tsx:226
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
msgid "Stars"
msgstr "Stars"
@ -512,7 +496,7 @@ msgstr "Template Store (Soon)."
msgid "That's awesome. You can take a look at the current <0>Issues</0> and join our <1>Discord Community</1> to keep up to date, on what the current priorities are. In any case, we are an open community and welcome all input, technical and non-technical ❤️"
msgstr "That's awesome. You can take a look at the current <0>Issues</0> and join our <1>Discord Community</1> to keep up to date, on what the current priorities are. In any case, we are an open community and welcome all input, technical and non-technical ❤️"
#: apps/marketing/src/app/(marketing)/open/page.tsx:293
#: apps/marketing/src/app/(marketing)/open/page.tsx:292
msgid "This page is evolving as we learn what makes a great signing company. We'll update it when we have more to share."
msgstr "This page is evolving as we learn what makes a great signing company. We'll update it when we have more to share."
@ -525,8 +509,8 @@ msgstr "Title"
msgid "Total Completed Documents"
msgstr "Total Completed Documents"
#: apps/marketing/src/app/(marketing)/open/page.tsx:266
#: apps/marketing/src/app/(marketing)/open/page.tsx:267
#: apps/marketing/src/app/(marketing)/open/page.tsx:268
msgid "Total Customers"
msgstr "Total Customers"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-10-22 02:25\n"
"PO-Revision-Date: 2024-11-01 02:29\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -42,7 +42,7 @@ msgstr "Agregar documento"
msgid "Add More Users for {0}"
msgstr "Agregar más usuarios por {0}"
#: apps/marketing/src/app/(marketing)/open/page.tsx:165
#: apps/marketing/src/app/(marketing)/open/page.tsx:164
msgid "All our metrics, finances, and learnings are public. We believe in transparency and want to share our journey with you. You can read more about why here: <0>Announcing Open Metrics</0>"
msgstr "Todos nuestros métricas, finanzas y aprendizajes son públicos. Creemos en la transparencia y queremos compartir nuestro viaje contigo. Puedes leer más sobre por qué aquí: <0>Anunciando Métricas Abiertas</0>"
@ -90,7 +90,7 @@ msgstr "Registro de cambios"
msgid "Choose a template from the community app store. Or submit your own template for others to use."
msgstr "Elige una plantilla de la tienda de aplicaciones de la comunidad. O envía tu propia plantilla para que otros la usen."
#: apps/marketing/src/app/(marketing)/open/page.tsx:219
#: apps/marketing/src/app/(marketing)/open/page.tsx:218
msgid "Community"
msgstr "Comunidad"
@ -160,10 +160,6 @@ msgstr "Documentación"
msgid "Easily embed Documenso into your product. Simply copy and paste our react widget into your application."
msgstr "Incrusta fácilmente Documenso en tu producto. Simplemente copia y pega nuestro widget de react en tu aplicación."
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:42
#~ msgid "Easy Sharing (Soon)."
#~ msgstr "Easy Sharing (Soon)."
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:46
msgid "Easy Sharing."
msgstr "Compartición fácil."
@ -197,7 +193,7 @@ msgstr "Rápido."
msgid "Faster, smarter and more beautiful."
msgstr "Más rápido, más inteligente y más hermoso."
#: apps/marketing/src/app/(marketing)/open/page.tsx:210
#: apps/marketing/src/app/(marketing)/open/page.tsx:209
msgid "Finances"
msgstr "Finanzas"
@ -250,15 +246,15 @@ msgstr "Comienza hoy."
msgid "Get the latest news from Documenso, including product updates, team announcements and more!"
msgstr "¡Obtén las últimas noticias de Documenso, incluidas actualizaciones de productos, anuncios del equipo y más!"
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
#: apps/marketing/src/app/(marketing)/open/page.tsx:232
msgid "GitHub: Total Merged PRs"
msgstr "GitHub: Total de PRs fusionados"
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
#: apps/marketing/src/app/(marketing)/open/page.tsx:250
msgid "GitHub: Total Open Issues"
msgstr "GitHub: Total de problemas abiertos"
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
#: apps/marketing/src/app/(marketing)/open/page.tsx:224
msgid "GitHub: Total Stars"
msgstr "GitHub: Total de estrellas"
@ -266,7 +262,7 @@ msgstr "GitHub: Total de estrellas"
msgid "Global Salary Bands"
msgstr "Bandas salariales globales"
#: apps/marketing/src/app/(marketing)/open/page.tsx:261
#: apps/marketing/src/app/(marketing)/open/page.tsx:260
msgid "Growth"
msgstr "Crecimiento"
@ -290,7 +286,7 @@ msgstr "Pagos integrados con Stripe para que no tengas que preocuparte por cobra
msgid "Integrates with all your favourite tools."
msgstr "Se integra con todas tus herramientas favoritas."
#: apps/marketing/src/app/(marketing)/open/page.tsx:289
#: apps/marketing/src/app/(marketing)/open/page.tsx:288
msgid "Is there more?"
msgstr "¿Hay más?"
@ -314,11 +310,11 @@ msgstr "Ubicación"
msgid "Make it your own through advanced customization and adjustability."
msgstr "Hazlo tuyo a través de personalización y ajustabilidad avanzadas."
#: apps/marketing/src/app/(marketing)/open/page.tsx:199
#: apps/marketing/src/app/(marketing)/open/page.tsx:198
msgid "Merged PR's"
msgstr "PRs fusionados"
#: apps/marketing/src/app/(marketing)/open/page.tsx:234
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
msgid "Merged PRs"
msgstr "PRs fusionados"
@ -349,8 +345,8 @@ msgstr "No se requiere tarjeta de crédito"
msgid "None of these work for you? Try self-hosting!"
msgstr "¿Ninguna de estas opciones funciona para ti? ¡Prueba el autoalojamiento!"
#: apps/marketing/src/app/(marketing)/open/page.tsx:194
#: apps/marketing/src/app/(marketing)/open/page.tsx:252
#: apps/marketing/src/app/(marketing)/open/page.tsx:193
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
msgid "Open Issues"
msgstr "Problemas Abiertos"
@ -358,7 +354,7 @@ msgstr "Problemas Abiertos"
msgid "Open Source or Hosted."
msgstr "Código Abierto o Alojado."
#: apps/marketing/src/app/(marketing)/open/page.tsx:161
#: apps/marketing/src/app/(marketing)/open/page.tsx:160
#: apps/marketing/src/components/(marketing)/footer.tsx:37
#: apps/marketing/src/components/(marketing)/header.tsx:64
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:40
@ -377,18 +373,10 @@ msgstr "Nuestras plantillas personalizadas vienen con reglas inteligentes que pu
msgid "Our Enterprise License is great for large organizations looking to switch to Documenso for all their signing needs. It's available for our cloud offering as well as self-hosted setups and offers a wide range of compliance and Adminstration Features."
msgstr "Nuestra Licencia Empresarial es excelente para grandes organizaciones que buscan cambiar a Documenso para todas sus necesidades de firma. Está disponible para nuestra oferta en la nube, así como para configuraciones autoalojadas y ofrece una amplia gama de funciones de cumplimiento y administración."
#: apps/marketing/src/components/(marketing)/enterprise.tsx:20
#~ msgid "Our Enterprise License is great large organizations looking to switch to Documenso for all their signing needs. It's availible for our cloud offering as well as self-hosted setups and offer a wide range of compliance and Adminstration Features."
#~ msgstr "Our Enterprise License is great large organizations looking to switch to Documenso for all their signing needs. It's availible for our cloud offering as well as self-hosted setups and offer a wide range of compliance and Adminstration Features."
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:65
msgid "Our self-hosted option is great for small teams and individuals who need a simple solution. You can use our docker based setup to get started in minutes. Take control with full customizability and data ownership."
msgstr "Nuestra opción de autoalojamiento es excelente para pequeños equipos e individuos que necesitan una solución simple. Puedes usar nuestra configuración basada en docker para empezar en minutos. Toma el control con total personalización y propiedad de los datos."
#: apps/marketing/src/app/(marketing)/open/data.ts:25
#~ msgid "Part-Time"
#~ msgstr "Part-Time"
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:151
msgid "Premium Profile Name"
msgstr "Nombre de Perfil Premium"
@ -430,10 +418,6 @@ msgstr "Salario"
msgid "Save $60 or $120"
msgstr "Ahorra $60 o $120"
#: apps/marketing/src/components/(marketing)/i18n-switcher.tsx:47
#~ msgid "Search languages..."
#~ msgstr "Search languages..."
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:109
msgid "Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us."
msgstr "De manera segura. Nuestros centros de datos están ubicados en Frankfurt (Alemania), dándonos las mejores leyes de privacidad locales. Somos muy conscientes de la naturaleza sensible de nuestros datos y seguimos las mejores prácticas para garantizar la seguridad y la integridad de los datos que se nos confían."
@ -482,7 +466,7 @@ msgstr "Inteligente."
msgid "Star on GitHub"
msgstr "Estrella en GitHub"
#: apps/marketing/src/app/(marketing)/open/page.tsx:226
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
msgid "Stars"
msgstr "Estrellas"
@ -517,7 +501,7 @@ msgstr "Tienda de Plantillas (Pronto)."
msgid "That's awesome. You can take a look at the current <0>Issues</0> and join our <1>Discord Community</1> to keep up to date, on what the current priorities are. In any case, we are an open community and welcome all input, technical and non-technical ❤️"
msgstr "Eso es increíble. Puedes echar un vistazo a los <0>Problemas</0> actuales y unirte a nuestra <1>Comunidad de Discord</1> para mantenerte al día sobre cuáles son las prioridades actuales. En cualquier caso, somos una comunidad abierta y agradecemos todas las aportaciones, técnicas y no técnicas ❤️"
#: apps/marketing/src/app/(marketing)/open/page.tsx:293
#: apps/marketing/src/app/(marketing)/open/page.tsx:292
msgid "This page is evolving as we learn what makes a great signing company. We'll update it when we have more to share."
msgstr "Esta página está evolucionando a medida que aprendemos lo que hace una gran empresa de firmas. La actualizaremos cuando tengamos más para compartir."
@ -530,8 +514,8 @@ msgstr "Título"
msgid "Total Completed Documents"
msgstr "Total de Documentos Completados"
#: apps/marketing/src/app/(marketing)/open/page.tsx:266
#: apps/marketing/src/app/(marketing)/open/page.tsx:267
#: apps/marketing/src/app/(marketing)/open/page.tsx:268
msgid "Total Customers"
msgstr "Total de Clientes"
@ -618,4 +602,3 @@ msgstr "Puedes autoalojar Documenso de forma gratuita o usar nuestra versión al
#: apps/marketing/src/components/(marketing)/carousel.tsx:272
msgid "Your browser does not support the video tag."
msgstr "Tu navegador no soporta la etiqueta de video."

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-10-18 04:04\n"
"PO-Revision-Date: 2024-11-01 02:29\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@ -42,7 +42,7 @@ msgstr "Ajouter un document"
msgid "Add More Users for {0}"
msgstr "Ajouter plus d'utilisateurs pour {0}"
#: apps/marketing/src/app/(marketing)/open/page.tsx:165
#: apps/marketing/src/app/(marketing)/open/page.tsx:164
msgid "All our metrics, finances, and learnings are public. We believe in transparency and want to share our journey with you. You can read more about why here: <0>Announcing Open Metrics</0>"
msgstr "Tous nos indicateurs, finances et apprentissages sont publics. Nous croyons en la transparence et souhaitons partager notre parcours avec vous. Vous pouvez en lire plus sur pourquoi ici : <0>Annonce de Open Metrics</0>"
@ -90,7 +90,7 @@ msgstr "Changelog"
msgid "Choose a template from the community app store. Or submit your own template for others to use."
msgstr "Choisissez un modèle dans la boutique d'applications communautaires. Ou soumettez votre propre modèle pour que d'autres puissent l'utiliser."
#: apps/marketing/src/app/(marketing)/open/page.tsx:219
#: apps/marketing/src/app/(marketing)/open/page.tsx:218
msgid "Community"
msgstr "Communauté"
@ -160,10 +160,6 @@ msgstr "Documentation"
msgid "Easily embed Documenso into your product. Simply copy and paste our react widget into your application."
msgstr "Intégrez facilement Documenso dans votre produit. Il vous suffit de copier et coller notre widget React dans votre application."
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:42
#~ msgid "Easy Sharing (Soon)."
#~ msgstr "Easy Sharing (Soon)."
#: apps/marketing/src/components/(marketing)/share-connect-paid-widget-bento.tsx:46
msgid "Easy Sharing."
msgstr "Partage facile."
@ -197,7 +193,7 @@ msgstr "Rapide."
msgid "Faster, smarter and more beautiful."
msgstr "Plus rapide, plus intelligent et plus beau."
#: apps/marketing/src/app/(marketing)/open/page.tsx:210
#: apps/marketing/src/app/(marketing)/open/page.tsx:209
msgid "Finances"
msgstr "Finances"
@ -250,15 +246,15 @@ msgstr "Commencez aujourd'hui."
msgid "Get the latest news from Documenso, including product updates, team announcements and more!"
msgstr "Obtenez les dernières nouvelles de Documenso, y compris les mises à jour de produits, les annonces d'équipe et plus encore !"
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
#: apps/marketing/src/app/(marketing)/open/page.tsx:232
msgid "GitHub: Total Merged PRs"
msgstr "GitHub : Total des PRs fusionnées"
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
#: apps/marketing/src/app/(marketing)/open/page.tsx:250
msgid "GitHub: Total Open Issues"
msgstr "GitHub : Total des problèmes ouverts"
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
#: apps/marketing/src/app/(marketing)/open/page.tsx:224
msgid "GitHub: Total Stars"
msgstr "GitHub : Nombre total d'étoiles"
@ -266,7 +262,7 @@ msgstr "GitHub : Nombre total d'étoiles"
msgid "Global Salary Bands"
msgstr "Bandes de salaire globales"
#: apps/marketing/src/app/(marketing)/open/page.tsx:261
#: apps/marketing/src/app/(marketing)/open/page.tsx:260
msgid "Growth"
msgstr "Croissance"
@ -290,7 +286,7 @@ msgstr "Paiements intégrés avec Stripe afin que vous n'ayez pas à vous soucie
msgid "Integrates with all your favourite tools."
msgstr "S'intègre à tous vos outils préférés."
#: apps/marketing/src/app/(marketing)/open/page.tsx:289
#: apps/marketing/src/app/(marketing)/open/page.tsx:288
msgid "Is there more?"
msgstr "Y a-t-il plus ?"
@ -314,11 +310,11 @@ msgstr "Emplacement"
msgid "Make it your own through advanced customization and adjustability."
msgstr "Faites-en votre propre grâce à une personnalisation avancée et un ajustement."
#: apps/marketing/src/app/(marketing)/open/page.tsx:199
#: apps/marketing/src/app/(marketing)/open/page.tsx:198
msgid "Merged PR's"
msgstr "PRs fusionnées"
#: apps/marketing/src/app/(marketing)/open/page.tsx:234
#: apps/marketing/src/app/(marketing)/open/page.tsx:233
msgid "Merged PRs"
msgstr "PRs fusionnées"
@ -349,8 +345,8 @@ msgstr "Aucune carte de crédit requise"
msgid "None of these work for you? Try self-hosting!"
msgstr "Aucune de ces options ne fonctionne pour vous ? Essayez l'hébergement autonome !"
#: apps/marketing/src/app/(marketing)/open/page.tsx:194
#: apps/marketing/src/app/(marketing)/open/page.tsx:252
#: apps/marketing/src/app/(marketing)/open/page.tsx:193
#: apps/marketing/src/app/(marketing)/open/page.tsx:251
msgid "Open Issues"
msgstr "Problèmes ouverts"
@ -358,7 +354,7 @@ msgstr "Problèmes ouverts"
msgid "Open Source or Hosted."
msgstr "Open Source ou hébergé."
#: apps/marketing/src/app/(marketing)/open/page.tsx:161
#: apps/marketing/src/app/(marketing)/open/page.tsx:160
#: apps/marketing/src/components/(marketing)/footer.tsx:37
#: apps/marketing/src/components/(marketing)/header.tsx:64
#: apps/marketing/src/components/(marketing)/mobile-navigation.tsx:40
@ -377,18 +373,10 @@ msgstr "Nos modèles personnalisés sont dotés de règles intelligentes qui peu
msgid "Our Enterprise License is great for large organizations looking to switch to Documenso for all their signing needs. It's available for our cloud offering as well as self-hosted setups and offers a wide range of compliance and Adminstration Features."
msgstr "Notre licence entreprise est idéale pour les grandes organisations cherchant à passer à Documenso pour tous leurs besoins de signature. Elle est disponible pour notre offre cloud ainsi que pour des configurations auto-hébergées et propose un large éventail de fonctionnalités de conformité et d'administration."
#: apps/marketing/src/components/(marketing)/enterprise.tsx:20
#~ msgid "Our Enterprise License is great large organizations looking to switch to Documenso for all their signing needs. It's availible for our cloud offering as well as self-hosted setups and offer a wide range of compliance and Adminstration Features."
#~ msgstr "Our Enterprise License is great large organizations looking to switch to Documenso for all their signing needs. It's availible for our cloud offering as well as self-hosted setups and offer a wide range of compliance and Adminstration Features."
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:65
msgid "Our self-hosted option is great for small teams and individuals who need a simple solution. You can use our docker based setup to get started in minutes. Take control with full customizability and data ownership."
msgstr "Notre option auto-hébergée est idéale pour les petites équipes et les individus qui ont besoin d'une solution simple. Vous pouvez utiliser notre configuration basée sur Docker pour commencer en quelques minutes. Prenez le contrôle avec une personnalisation complète et une propriété des données."
#: apps/marketing/src/app/(marketing)/open/data.ts:25
#~ msgid "Part-Time"
#~ msgstr "Part-Time"
#: apps/marketing/src/components/(marketing)/pricing-table.tsx:151
msgid "Premium Profile Name"
msgstr "Nom de profil premium"
@ -430,10 +418,6 @@ msgstr "Salaire"
msgid "Save $60 or $120"
msgstr "Économisez 60 $ ou 120 $"
#: apps/marketing/src/components/(marketing)/i18n-switcher.tsx:47
#~ msgid "Search languages..."
#~ msgstr "Search languages..."
#: apps/marketing/src/app/(marketing)/pricing/page.tsx:109
msgid "Securely. Our data centers are located in Frankfurt (Germany), giving us the best local privacy laws. We are very aware of the sensitive nature of our data and follow best practices to ensure the security and integrity of the data entrusted to us."
msgstr "De manière sécurisée. Nos centres de données sont situés à Francfort (Allemagne), ce qui nous permet de bénéficier des meilleures lois locales sur la confidentialité. Nous sommes très conscients de la nature sensible de nos données et suivons les meilleures pratiques pour garantir la sécurité et l'intégrité des données qui nous sont confiées."
@ -482,7 +466,7 @@ msgstr "Intelligent."
msgid "Star on GitHub"
msgstr "Étoile sur GitHub"
#: apps/marketing/src/app/(marketing)/open/page.tsx:226
#: apps/marketing/src/app/(marketing)/open/page.tsx:225
msgid "Stars"
msgstr "Étoiles"
@ -517,7 +501,7 @@ msgstr "Boutique de modèles (Bientôt)."
msgid "That's awesome. You can take a look at the current <0>Issues</0> and join our <1>Discord Community</1> to keep up to date, on what the current priorities are. In any case, we are an open community and welcome all input, technical and non-technical ❤️"
msgstr "C'est génial. Vous pouvez consulter les <0>Problèmes</0> actuels et rejoindre notre <1>Communauté Discord</1> pour rester à jour sur ce qui est actuellement prioritaire. Dans tous les cas, nous sommes une communauté ouverte et accueillons toutes les contributions, techniques et non techniques ❤️"
#: apps/marketing/src/app/(marketing)/open/page.tsx:293
#: apps/marketing/src/app/(marketing)/open/page.tsx:292
msgid "This page is evolving as we learn what makes a great signing company. We'll update it when we have more to share."
msgstr "Cette page évolue à mesure que nous apprenons ce qui fait une grande entreprise de signature. Nous la mettrons à jour lorsque nous aurons plus à partager."
@ -530,8 +514,8 @@ msgstr "Titre"
msgid "Total Completed Documents"
msgstr "Documents totalisés complétés"
#: apps/marketing/src/app/(marketing)/open/page.tsx:266
#: apps/marketing/src/app/(marketing)/open/page.tsx:267
#: apps/marketing/src/app/(marketing)/open/page.tsx:268
msgid "Total Customers"
msgstr "Total des clients"
@ -618,4 +602,3 @@ msgstr "Vous pouvez auto-héberger Documenso gratuitement ou utiliser notre vers
#: apps/marketing/src/components/(marketing)/carousel.tsx:272
msgid "Your browser does not support the video tag."
msgstr "Votre navigateur ne prend pas en charge la balise vidéo."

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies';
import type { I18n } from '@lingui/core';
import type { I18n, MessageDescriptor } from '@lingui/core';
import { IS_APP_WEB, IS_APP_WEB_I18N_ENABLED } from '../constants/app';
import type { I18nLocaleData, SupportedLanguageCodes } from '../constants/i18n';
@ -10,7 +10,17 @@ export async function dynamicActivate(i18nInstance: I18n, locale: string) {
const extension = process.env.NODE_ENV === 'development' ? 'po' : 'js';
const context = IS_APP_WEB ? 'web' : 'marketing';
const { messages } = await import(`../translations/${locale}/${context}.${extension}`);
let { messages } = await import(`../translations/${locale}/${context}.${extension}`);
// Dirty way to load common messages for development since it's not compiled.
if (process.env.NODE_ENV === 'development') {
const commonMessages = await import(`../translations/${locale}/common.${extension}`);
messages = {
...messages,
...commonMessages.messages,
};
}
i18nInstance.loadAndActivate({ locale, messages });
}
@ -106,3 +116,7 @@ export const extractLocaleData = ({
locales,
};
};
export const parseMessageDescriptor = (_: I18n['_'], value: string | MessageDescriptor) => {
return typeof value === 'string' ? value : _(value);
};

View File

@ -4,7 +4,6 @@ export const isValidRedirectUrl = (value: string) => {
try {
const url = new URL(value);
console.log({ protocol: url.protocol });
if (!ALLOWED_PROTOCOLS.includes(url.protocol.slice(0, -1).toLowerCase())) {
return false;
}

View File

@ -0,0 +1,18 @@
declare global {
// eslint-disable-next-line no-var, @typescript-eslint/no-explicit-any
var __documenso_util_remember: Map<string, any>;
}
export function remember<T>(name: string, getValue: () => T): T {
const thusly = globalThis;
if (!thusly.__documenso_util_remember) {
thusly.__documenso_util_remember = new Map();
}
if (!thusly.__documenso_util_remember.has(name)) {
thusly.__documenso_util_remember.set(name, getValue());
}
return thusly.__documenso_util_remember.get(name);
}

View File

@ -0,0 +1,36 @@
import { I18nProvider } from '@lingui/react';
import { render } from '@documenso/email/render';
import { getI18nInstance } from '../client-only/providers/i18n.server';
import {
APP_I18N_OPTIONS,
type SupportedLanguageCodes,
isValidLanguageCode,
} from '../constants/i18n';
export const renderEmailWithI18N = async (
component: React.ReactElement,
options?: {
plainText?: boolean;
// eslint-disable-next-line @typescript-eslint/ban-types
lang?: SupportedLanguageCodes | (string & {});
},
) => {
try {
const providedLang = options?.lang;
const lang = isValidLanguageCode(providedLang) ? providedLang : APP_I18N_OPTIONS.sourceLang;
const i18n = await getI18nInstance(lang);
i18n.activate(lang);
return render(<I18nProvider i18n={i18n}>{component}</I18nProvider>, {
plainText: options?.plainText,
});
} catch (err) {
console.error(err);
throw new Error('Failed to render email');
}
};

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "DocumentMeta" ADD COLUMN "language" TEXT NOT NULL DEFAULT 'en';
-- AlterTable
ALTER TABLE "TemplateMeta" ADD COLUMN "language" TEXT NOT NULL DEFAULT 'en';

View File

@ -370,6 +370,7 @@ model DocumentMeta {
redirectUrl String?
signingOrder DocumentSigningOrder @default(PARALLEL)
typedSignatureEnabled Boolean @default(false)
language String @default("en")
}
enum ReadStatus {
@ -612,6 +613,7 @@ model TemplateMeta {
templateId Int @unique
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
redirectUrl String?
language String @default("en")
}
model Template {

View File

@ -232,6 +232,7 @@ export const documentRouter = router({
dateFormat: meta.dateFormat,
timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
language: meta.language,
userId: ctx.user.id,
requestMetadata,
});

View File

@ -1,5 +1,6 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
@ -70,6 +71,7 @@ export const ZSetSettingsForDocumentMutationSchema = z.object({
message:
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
}),
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
}),
});

View File

@ -3,7 +3,6 @@ import { createElement } from 'react';
import { PDFDocument } from 'pdf-lib';
import { mailer } from '@documenso/email/mailer';
import { renderAsync } from '@documenso/email/render';
import { DocumentSelfSignedEmailTemplate } from '@documenso/email/templates/document-self-signed';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/constants/email';
@ -11,6 +10,7 @@ import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in
import { alphaid } from '@documenso/lib/universal/id';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { renderEmailWithI18N } from '@documenso/lib/utils/render-email-with-i18n';
import { prisma } from '@documenso/prisma';
import {
DocumentSource,
@ -158,8 +158,8 @@ export const singleplayerRouter = router({
});
const [html, text] = await Promise.all([
renderAsync(template),
renderAsync(template, { plainText: true }),
renderEmailWithI18N(template),
renderEmailWithI18N(template, { plainText: true }),
]);
// Send email to signer.

View File

@ -1,6 +1,7 @@
import { TRPCError } from '@trpc/server';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { createDocumentFromDirectTemplate } from '@documenso/lib/server-only/template/create-document-from-direct-template';
@ -214,7 +215,10 @@ export const templateRouter = router({
teamId,
templateId,
data,
meta,
meta: {
...meta,
language: isValidLanguageCode(meta?.language) ? meta?.language : undefined,
},
requestMetadata,
});
} catch (err) {

View File

@ -1,5 +1,6 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
@ -87,6 +88,10 @@ export const ZUpdateTemplateSettingsMutationSchema = z.object({
.max(MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH)
.optional(),
type: z.nativeEnum(TemplateType).optional(),
language: z
.union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)])
.optional()
.default('en'),
}),
meta: z
.object({
@ -101,6 +106,7 @@ export const ZUpdateTemplateSettingsMutationSchema = z.object({
message:
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
}),
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
})
.optional(),
});

View File

@ -139,12 +139,16 @@ export const DocumentShareButton = ({
<DialogContent position="end">
<DialogHeader>
<DialogTitle>Share your signing experience!</DialogTitle>
<DialogTitle>
<Trans>Share your signing experience!</Trans>
</DialogTitle>
<DialogDescription className="mt-4">
Rest assured, your document is strictly confidential and will never be shared. Only your
signing experience will be highlighted. Share your personalized signing card to showcase
your signature!
<Trans>
Rest assured, your document is strictly confidential and will never be shared. Only
your signing experience will be highlighted. Share your personalized signing card to
showcase your signature!
</Trans>
</DialogDescription>
</DialogHeader>
@ -187,7 +191,7 @@ export const DocumentShareButton = ({
<Button variant="outline" className="flex-1" onClick={onCopyClick}>
<Copy className="mr-2 h-4 w-4" />
Copy Link
<Trans>Copy Link</Trans>
</Button>
</div>
</div>

View File

@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Prisma } from '@prisma/client';
import {
CalendarDays,
@ -34,6 +35,7 @@ import {
} from '@documenso/lib/types/field-meta';
import { nanoid } from '@documenso/lib/universal/id';
import { validateFieldsUninserted } from '@documenso/lib/utils/fields';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import {
canRecipientBeModified,
canRecipientFieldsBeModified,
@ -114,6 +116,7 @@ export const AddFieldsFormPartial = ({
teamId,
}: AddFieldsFormProps) => {
const { toast } = useToast();
const { _ } = useLingui();
const [isMissingSignatureDialogVisible, setIsMissingSignatureDialogVisible] = useState(false);
@ -568,7 +571,10 @@ export const AddFieldsFormPartial = ({
{showAdvancedSettings && currentField ? (
<FieldAdvancedSettings
title={msg`Advanced settings`}
description={msg`Configure the ${FRIENDLY_FIELD_TYPE[currentField.type]} field`}
description={msg`Configure the ${parseMessageDescriptor(
_,
FRIENDLY_FIELD_TYPE[currentField.type],
)} field`}
field={currentField}
fields={localFields}
onAdvancedSettings={handleAdvancedSettings}
@ -603,7 +609,7 @@ export const AddFieldsFormPartial = ({
width: fieldBounds.current.width,
}}
>
{FRIENDLY_FIELD_TYPE[selectedField]}
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[selectedField])}
</div>
)}
@ -684,8 +690,7 @@ export const AddFieldsFormPartial = ({
{recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
<CommandGroup key={roleIndex}>
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
{/* Todo: Translations - Add plural translations. */}
{`${RECIPIENT_ROLES_DESCRIPTION_ENG[role].roleName}s`}
{_(RECIPIENT_ROLES_DESCRIPTION_ENG[role].roleNamePlural)}
</div>
{roleRecipients.length === 0 && (
@ -997,7 +1002,7 @@ export const AddFieldsFormPartial = ({
)}
>
<Disc className="h-4 w-4" />
<Trans>Radio</Trans>
Radio
</p>
</CardContent>
</Card>
@ -1023,7 +1028,8 @@ export const AddFieldsFormPartial = ({
)}
>
<CheckSquare className="h-4 w-4" />
<Trans>Checkbox</Trans>
{/* Not translated on purpose. */}
Checkbox
</p>
</CardContent>
</Card>

View File

@ -8,6 +8,7 @@ import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import type { TeamMemberRole } from '@documenso/prisma/client';
@ -98,6 +99,7 @@ export const AddSettingsFormPartial = ({
DATE_FORMATS.find((format) => format.value === document.documentMeta?.dateFormat)
?.value ?? DEFAULT_DOCUMENT_DATE_FORMAT,
redirectUrl: document.documentMeta?.redirectUrl ?? '',
language: document.documentMeta?.language ?? 'en',
},
},
});
@ -165,6 +167,46 @@ export const AddSettingsFormPartial = ({
)}
/>
<FormField
control={form.control}
name="meta.language"
render={({ field }) => (
<FormItem>
<FormLabel className="inline-flex items-center">
<Trans>Language</Trans>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
Controls the language for the document, including the language to be used
for email notifications, and the final certificate that is generated and
attached to the document.
</TooltipContent>
</Tooltip>
</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger className="bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(SUPPORTED_LANGUAGES).map(([code, language]) => (
<SelectItem key={code} value={code}>
{language.full}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="globalAccessAuth"

View File

@ -1,6 +1,7 @@
import { z } from 'zod';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import {
ZDocumentAccessAuthTypesSchema,
@ -39,6 +40,10 @@ export const ZAddSettingsFormSchema = z.object({
message:
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
}),
language: z
.union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)])
.optional()
.default('en'),
}),
});

View File

@ -1,10 +1,11 @@
import { msg } from '@lingui/macro';
import { z } from 'zod';
export const ZAddSignatureFormSchema = z.object({
email: z
.string()
.min(1, { message: 'Email is required' })
.email({ message: 'Invalid email address' }),
.min(1, { message: msg`Email is required`.id })
.email({ message: msg`Invalid email address`.id }),
name: z.string(),
customText: z.string(),
number: z.number().optional(),

View File

@ -504,7 +504,7 @@ export const AddSignersFormPartial = ({
<FormControl>
<Input
type="email"
placeholder="Email"
placeholder={_(msg`Email`)}
{...field}
disabled={
snapshot.isDragging ||

View File

@ -1,3 +1,4 @@
import { msg } from '@lingui/macro';
import { z } from 'zod';
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
@ -11,7 +12,10 @@ export const ZAddSignersFormSchema = z
z.object({
formId: z.string().min(1),
nativeId: z.number().optional(),
email: z.string().email().min(1),
email: z
.string()
.email({ message: msg`Invalid email`.id })
.min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
@ -29,7 +33,7 @@ export const ZAddSignersFormSchema = z
return new Set(emails).size === emails.length;
},
// Dirty hack to handle errors when .root is populated for an array type
{ message: 'Signers must have unique emails', path: ['signers__root'] },
{ message: msg`Signers must have unique emails`.id, path: ['signers__root'] },
);
export type TAddSignersFormSchema = z.infer<typeof ZAddSignersFormSchema>;

View File

@ -2,10 +2,12 @@
import { Caveat } from 'next/font/google';
import { useLingui } from '@lingui/react';
import type { Prisma } from '@prisma/client';
import { createPortal } from 'react-dom';
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import { FieldType } from '@documenso/prisma/client';
import { cn } from '../../lib/utils';
@ -25,6 +27,8 @@ export type ShowFieldItemProps = {
};
export const ShowFieldItem = ({ field, recipients }: ShowFieldItemProps) => {
const { _ } = useLingui();
const coords = useFieldPageCoords(field);
const signerEmail =
@ -47,7 +51,7 @@ export const ShowFieldItem = ({ field, recipients }: ShowFieldItemProps) => {
field.type === FieldType.SIGNATURE && fontCaveat.className,
)}
>
{FRIENDLY_FIELD_TYPE[field.type]}
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])}
<p className="text-muted-foreground/50 w-full truncate text-center text-xs">
{signerEmail}

View File

@ -1,4 +1,5 @@
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/macro';
import { z } from 'zod';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
@ -44,18 +45,18 @@ export const ZDocumentFlowFormSchema = z.object({
export type TDocumentFlowFormSchema = z.infer<typeof ZDocumentFlowFormSchema>;
export const FRIENDLY_FIELD_TYPE: Record<FieldType, string> = {
[FieldType.SIGNATURE]: 'Signature',
[FieldType.FREE_SIGNATURE]: 'Free Signature',
[FieldType.INITIALS]: 'Initials',
[FieldType.TEXT]: 'Text',
[FieldType.DATE]: 'Date',
[FieldType.EMAIL]: 'Email',
[FieldType.NAME]: 'Name',
[FieldType.NUMBER]: 'Number',
[FieldType.RADIO]: 'Radio',
[FieldType.CHECKBOX]: 'Checkbox',
[FieldType.DROPDOWN]: 'Select',
export const FRIENDLY_FIELD_TYPE: Record<FieldType, MessageDescriptor | string> = {
[FieldType.SIGNATURE]: msg`Signature`,
[FieldType.FREE_SIGNATURE]: msg`Free Signature`,
[FieldType.INITIALS]: msg`Initials`,
[FieldType.TEXT]: msg`Text`,
[FieldType.DATE]: msg`Date`,
[FieldType.EMAIL]: msg`Email`,
[FieldType.NAME]: msg`Name`,
[FieldType.NUMBER]: msg`Number`,
[FieldType.RADIO]: `Radio`,
[FieldType.CHECKBOX]: `Checkbox`,
[FieldType.DROPDOWN]: `Select`,
};
export interface DocumentFlowStep {

View File

@ -1,3 +1,4 @@
import { useLingui } from '@lingui/react';
import { AnimatePresence, motion } from 'framer-motion';
import { cn } from '../../lib/utils';
@ -12,6 +13,15 @@ const isErrorWithMessage = (error: unknown): error is { message?: string } => {
};
export const FormErrorMessage = ({ error, className }: FormErrorMessageProps) => {
const { i18n } = useLingui();
let errorMessage = isErrorWithMessage(error) ? error.message : '';
// Checks to see if there's a translation for the string, since we're passing IDs for Zod errors.
if (typeof errorMessage === 'string' && i18n.t(errorMessage)) {
errorMessage = i18n.t(errorMessage);
}
return (
<AnimatePresence>
{isErrorWithMessage(error) && (
@ -30,7 +40,7 @@ export const FormErrorMessage = ({ error, className }: FormErrorMessageProps) =>
}}
className={cn('text-xs text-red-500', className)}
>
{error.message}
{errorMessage}
</motion.p>
)}
</AnimatePresence>

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import { useLingui } from '@lingui/react';
import type * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import { AnimatePresence, motion } from 'framer-motion';
@ -136,13 +137,21 @@ const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { i18n } = useLingui();
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
let body = error ? String(error?.message) : children;
if (!body) {
return null;
}
// Checks to see if there's a translation for the string, since we're passing IDs for Zod errors.
if (typeof body === 'string' && i18n.t(body)) {
body = i18n.t(body);
}
return (
<AnimatePresence>
<motion.div

View File

@ -2,6 +2,7 @@
import dynamic from 'next/dynamic';
import { Trans } from '@lingui/macro';
import { Loader } from 'lucide-react';
export const LazyPDFViewer = dynamic(async () => import('./pdf-viewer'), {
@ -10,7 +11,9 @@ export const LazyPDFViewer = dynamic(async () => import('./pdf-viewer'), {
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
<Loader className="text-documenso h-12 w-12 animate-spin" />
<p className="text-muted-foreground mt-4">Loading document...</p>
<p className="text-muted-foreground mt-4">
<Trans>Loading document...</Trans>
</p>
</div>
),
});

View File

@ -2,6 +2,8 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
import { type PDFDocumentProxy, PasswordResponses } from 'pdfjs-dist';
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
@ -38,7 +40,9 @@ const PDFLoader = () => (
<>
<Loader className="text-documenso h-12 w-12 animate-spin" />
<p className="text-muted-foreground mt-4">Loading document...</p>
<p className="text-muted-foreground mt-4">
<Trans>Loading document...</Trans>
</p>
</>
);
@ -61,6 +65,7 @@ export const PDFViewer = ({
onPageClick,
...props
}: PDFViewerProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const $el = useRef<HTMLDivElement>(null);
@ -158,8 +163,8 @@ export const PDFViewer = ({
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while loading the document.',
title: _(msg`Error`),
description: _(msg`An error occurred while loading the document.`),
variant: 'destructive',
});
}
@ -211,8 +216,12 @@ export const PDFViewer = ({
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
{pdfError ? (
<div className="text-muted-foreground text-center">
<p>Something went wrong while loading the document.</p>
<p className="mt-1 text-sm">Please try again or contact our support.</p>
<p>
<Trans>Something went wrong while loading the document.</Trans>
</p>
<p className="mt-1 text-sm">
<Trans>Please try again or contact our support.</Trans>
</p>
</div>
) : (
<PDFLoader />
@ -222,8 +231,12 @@ export const PDFViewer = ({
error={
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
<div className="text-muted-foreground text-center">
<p>Something went wrong while loading the document.</p>
<p className="mt-1 text-sm">Please try again or contact our support.</p>
<p>
<Trans>Something went wrong while loading the document.</Trans>
</p>
<p className="mt-1 text-sm">
<Trans>Please try again or contact our support.</Trans>
</p>
</div>
</div>
}
@ -243,7 +256,9 @@ export const PDFViewer = ({
/>
</div>
<p className="text-muted-foreground/80 my-2 text-center text-[11px]">
Page {i + 1} of {numPages}
<Trans>
Page {i + 1} of {numPages}
</Trans>
</p>
</div>
))}

View File

@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import {
CalendarDays,
CheckSquare,
@ -28,6 +29,7 @@ import {
ZFieldMetaSchema,
} from '@documenso/lib/types/field-meta';
import { nanoid } from '@documenso/lib/universal/id';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import type { Field, Recipient } from '@documenso/prisma/client';
import { FieldType, RecipientRole } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
@ -85,6 +87,8 @@ export const AddTemplateFieldsFormPartial = ({
onSubmit,
teamId,
}: AddTemplateFieldsFormProps) => {
const { _ } = useLingui();
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
const { currentStep, totalSteps, previousStep } = useStep();
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
@ -400,7 +404,10 @@ export const AddTemplateFieldsFormPartial = ({
{showAdvancedSettings && currentField ? (
<FieldAdvancedSettings
title={msg`Advanced settings`}
description={msg`Configure the ${FRIENDLY_FIELD_TYPE[currentField.type]} field`}
description={msg`Configure the ${parseMessageDescriptor(
_,
FRIENDLY_FIELD_TYPE[currentField.type],
)} field`}
field={currentField}
fields={localFields}
onAdvancedSettings={handleAdvancedSettings}
@ -432,7 +439,7 @@ export const AddTemplateFieldsFormPartial = ({
width: fieldBounds.current.width,
}}
>
{FRIENDLY_FIELD_TYPE[selectedField]}
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[selectedField])}
</div>
)}
@ -501,8 +508,7 @@ export const AddTemplateFieldsFormPartial = ({
{recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
<CommandGroup key={roleIndex}>
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
{/* Todo: Translations - Add plural translations. */}
{`${RECIPIENT_ROLES_DESCRIPTION_ENG[role].roleName}s`}
{_(RECIPIENT_ROLES_DESCRIPTION_ENG[role].roleNamePlural)}
</div>
{roleRecipients.length === 0 && (
@ -785,7 +791,7 @@ export const AddTemplateFieldsFormPartial = ({
)}
>
<CheckSquare className="h-4 w-4" />
<Trans>Checkbox</Trans>
Checkbox
</p>
</CardContent>
</Card>

View File

@ -4,11 +4,11 @@ import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { type Field, type Recipient } from '@documenso/prisma/client';
@ -74,8 +74,6 @@ export const AddTemplateSettingsFormPartial = ({
template,
onSubmit,
}: AddTemplateSettingsFormProps) => {
const { _ } = useLingui();
const { documentAuthOption } = extractDocumentAuthMethods({
documentAuth: template.authOptions,
});
@ -93,6 +91,7 @@ export const AddTemplateSettingsFormPartial = ({
timezone: template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
dateFormat: template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
redirectUrl: template.templateMeta?.redirectUrl ?? '',
language: template.templateMeta?.language ?? 'en',
},
},
});
@ -102,7 +101,7 @@ export const AddTemplateSettingsFormPartial = ({
// We almost always want to set the timezone to the user's local timezone to avoid confusion
// when the document is signed.
useEffect(() => {
if (!form.formState.touchedFields.meta?.timezone) {
if (!form.formState.touchedFields.meta?.timezone && !template.templateMeta?.timezone) {
form.setValue('meta.timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
}
}, [form, form.setValue, form.formState.touchedFields.meta?.timezone]);
@ -142,6 +141,46 @@ export const AddTemplateSettingsFormPartial = ({
)}
/>
<FormField
control={form.control}
name="meta.language"
render={({ field }) => (
<FormItem>
<FormLabel className="inline-flex items-center">
<Trans>Language</Trans>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
Controls the language for the document, including the language to be used
for email notifications, and the final certificate that is generated and
attached to the document.
</TooltipContent>
</Tooltip>
</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger className="bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(SUPPORTED_LANGUAGES).map(([code, language]) => (
<SelectItem key={code} value={code}>
{language.full}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="globalAccessAuth"

Some files were not shown because too many files have changed in this diff Show More