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

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