Merge branch 'main' into feat/external-2fa-codes

This commit is contained in:
Ephraim Duncan
2026-05-27 13:45:46 +00:00
committed by GitHub
88 changed files with 4121 additions and 1673 deletions
+4
View File
@@ -9,6 +9,10 @@ If you plan to contribute to Documenso, please take a moment to feel awesome ✨
- Consider the results from the discussion on the issue
- Accept the [Contributor License Agreement](https://documen.so/cla) to ensure we can accept your contributions.
## English only PRs and Issues
Please write all issues, pull requests, and related comments in English so maintainers and the wider contributor community can follow the discussion.
## Taking issues
Before taking an issue, ensure that:
@@ -12,7 +12,7 @@ import { Callout } from 'fumadocs-ui/components/callout';
| 21 CFR Part 11 | Compliant (Enterprise) |
| SOC 2 | Compliant |
| ISO 27001 | Planned |
| HIPAA | Planned |
| HIPAA | Compliant (Enterprise) |
## 21 CFR Part 11
@@ -97,12 +97,12 @@ Documenso implements digital signatures with the following characteristics:
- **Timestamps**: RFC 3161 timestamps can be applied to signatures
- **Signature visualization**: Signed documents include visual signature representations
For specific implementation details and configuration options, refer to the [signing certificates](/signing-certificates/overview) documentation.
For specific implementation details and configuration options, refer to the [signing certificates](/docs/concepts/signing-certificates) documentation.
Self-hosted deployments can configure their own signing certificates and timestamp authorities to meet specific compliance requirements.
## Related
- [Legal Validity](/compliance/legal-validity) - Legal frameworks for electronic signatures
- [Signing Certificates Overview](/signing-certificates/overview) - Certificate configuration
- [Audit Log](/features/audit-log) - Document activity tracking
- [E-Sign Compliance](/docs/compliance/esign) - Legal frameworks for electronic signatures
- [Signing Certificates](/docs/concepts/signing-certificates) - Certificate configuration
- [Signing Workflow](/docs/concepts/signing-workflow) - Document activity and audit trail
@@ -167,5 +167,5 @@ To enable sequential signing:
## Related
- [Add Recipients](/users/documents/add-recipients) - How to add recipients to a document
- [Field Types](/concepts/field-types) - Learn about the different field types you can assign to recipients
- [Add Recipients](/docs/users/documents/add-recipients) - How to add recipients to a document
- [Field Types](/docs/concepts/field-types) - Learn about the different field types you can assign to recipients
@@ -0,0 +1,45 @@
---
title: Common Errors
description: A comprehensive troubleshooting matrix for Documenso API and Webhook integration errors.
---
This guide provides a comprehensive troubleshooting matrix for the standard error codes returned by the Documenso API. Use this reference to diagnose and resolve integration issues related to envelopes, recipients, and webhooks.
## Application Error Codes
| Error Code | Description | Recommended Action |
| :--- | :--- | :--- |
| `ALREADY_EXISTS` | The resource you are attempting to create already exists. | Verify if the entity (e.g., user, envelope, webhook) has already been instantiated. Use a `PUT` or `PATCH` request to update the existing resource instead of `POST`. |
| `EXPIRED_CODE` | The provided access code or token has expired. | Generate a new access code or request a new invitation link before retrying the request. |
| `INVALID_BODY` | The request payload is malformed. | Inspect your JSON payload structure. Ensure it strictly adheres to the expected schema and that no required fields are missing. |
| `INVALID_REQUEST` | The overall request is malformed or invalid. | Review your API call parameters, including the URL, query parameters, and headers. Correct the request syntax. |
| `RECIPIENT_EXPIRED` | The signing link or recipient access has expired. | Generate and resend a new invitation to the affected recipient. |
| `LIMIT_EXCEEDED` | Your account usage quota has been exceeded. | Check your current plan limits. Upgrade your subscription or wait until your billing cycle renews. |
| `NOT_FOUND` | The requested resource could not be found (404). | Verify the resource ID (envelope, document, webhook) passed in the URL. Ensure the resource has not been deleted. |
| `NOT_IMPLEMENTED` | The requested feature is not currently supported by the server. | Consult the API documentation to verify available methods. Do not use this endpoint at this time. |
| `NOT_SETUP` | The required configuration for this action is incomplete. | Access your account or integration settings and complete the necessary configuration before retrying. |
| `INVALID_CAPTCHA` | Security token (Captcha) validation failed. | Ensure the Captcha token is correctly generated on the client side and transmitted without alteration in your request. |
| `UNAUTHORIZED` | Missing or invalid authentication (401). | Verify that your API key is correct, active, and properly formatted in the `Authorization` header (e.g., `Bearer <YOUR_API_KEY>`). |
| `FORBIDDEN` | Access to the resource is denied (403). | Ensure your API key or user account has the necessary permissions and roles to execute this specific action. |
| `UNKNOWN_ERROR` | An unexpected internal server error occurred (500). | Retry the request later. If the issue persists, contact technical support with your request payload and the timestamp of the incident. |
| `RETRY_EXCEPTION` | The operation failed temporarily but can be retried. | Implement an automatic retry logic in your integration, ideally using an exponential backoff strategy. |
| `SCHEMA_FAILED` | Strict data schema validation failed. | Verify that the data types sent (string, number, boolean) exactly match the OpenAPI specification. |
| `TOO_MANY_REQUESTS` | Rate limit exceeded (429). | Reduce the frequency of your API calls. Implement rate-limiting handling based on the response headers. |
| `TWO_FACTOR_AUTH_FAILED` | Two-factor authentication (2FA) failed. | Verify the provided 2FA code. Ensure it was entered correctly and has not expired. |
| `WEBHOOK_INVALID_REQUEST` | The webhook-related request is invalid. | Check your receiving endpoint configuration. Ensure the URL is correct and that your server accepts `POST` requests from Documenso. |
## Envelope State Errors
The following errors occur when attempting to perform actions on an envelope that are incompatible with its current state.
| Error Code | Description | Recommended Action |
| :--- | :--- | :--- |
| `ENVELOPE_DRAFT` | The action cannot be performed because the envelope is still in a draft state. | Finalize the envelope configuration and transition it to the `PENDING` (sent) state before attempting this operation. |
| `ENVELOPE_COMPLETED` | The action cannot be performed because the envelope is already completed. | No further modifications (e.g., adding signers, modifying documents) can be made to an envelope once the signing process is finished. |
| `ENVELOPE_REJECTED` | The action cannot be performed because the envelope was rejected by a recipient. | The signing flow is permanently halted. Create a new envelope if you wish to resubmit the document. |
| `ENVELOPE_LEGACY` | The action cannot be performed because the envelope uses an obsolete format. | This envelope was created with a legacy version of the system. Recreate the envelope using the current API version to interact with it. |
## See Also
- [Documents API](/docs/developers/api/documents)
- [Webhooks](/docs/developers/webhooks)
@@ -1,4 +1,14 @@
{
"title": "API Reference",
"pages": ["documents", "recipients", "fields", "templates", "teams", "rate-limits", "versioning", "developer-mode"]
"pages": [
"documents",
"recipients",
"fields",
"templates",
"teams",
"rate-limits",
"versioning",
"developer-mode",
"common-errors"
]
}
@@ -73,14 +73,14 @@ Include the token in the `Authorization` header of your HTTP requests.
### cURL
```bash
curl https://app.documenso.com/api/v2/documents \
curl https://app.documenso.com/api/v2/document \
-H "Authorization: api_xxxxxxxxxxxxxxxx"
```
### JavaScript / TypeScript
```typescript
const response = await fetch('https://app.documenso.com/api/v2/documents', {
const response = await fetch('https://app.documenso.com/api/v2/document', {
method: 'GET',
headers: {
Authorization: 'api_xxxxxxxxxxxxxxxx',
@@ -1,6 +1,6 @@
---
title: Storage Configuration
description: Configure file storage for uploaded documents and signed PDFs using database storage (default) or S3-compatible object storage.
description: Configure file storage for uploaded documents and signed PDFs using database storage (default), S3-compatible object storage, or Azure Blob Storage.
---
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
@@ -11,9 +11,10 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
## Storage Options
| Backend | Best For | Scalability | Configuration |
| ---------- | -------------------------------- | ----------- | ------------- |
| ------------ | --------------------------------------- | ----------- | ------------- |
| `database` | Small deployments, simplicity | Limited | None required |
| `s3` | Production, large files, backups | High | Required |
| `azure-blob` | Production on Azure, native Blob access | High | Required |
Select the storage backend with the `NEXT_PUBLIC_UPLOAD_TRANSPORT` environment variable:
@@ -23,6 +24,9 @@ NEXT_PUBLIC_UPLOAD_TRANSPORT=database
# S3-compatible storage
NEXT_PUBLIC_UPLOAD_TRANSPORT=s3
# Azure Blob Storage (native)
NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob
```
---
@@ -283,6 +287,111 @@ NEXT_PRIVATE_UPLOAD_REGION=us-east-1
---
## Azure Blob Storage
Azure Blob Storage is supported as a native transport (not S3-compatible). Documenso uses the official `@azure/storage-blob` SDK and signs SAS URLs with the Storage Account key for browser uploads and downloads.
### Required Variables
| Variable | Description |
| --------------------------------------- | ------------------------------------------------- |
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Set to `azure-blob` |
| `NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME` | Azure Storage Account name |
| `NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY` | Azure Storage Account access key |
| `NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER` | Container name where uploads are stored |
### Optional Variables
| Variable | Description | Default |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| `NEXT_PRIVATE_UPLOAD_AZURE_ENDPOINT` | Custom Blob endpoint URL. Useful for local development against Azurite (for example `http://127.0.0.1:10000`). | `https://<account>.blob.core.windows.net` |
### Azure Setup
{/* prettier-ignore */}
<Steps>
<Step>
### Create a Storage Account and Container
Create a Storage Account in the Azure Portal or via the Azure CLI, then create a container inside it:
```bash
az storage account create \
--name yourstorageaccount \
--resource-group your-rg \
--location eastus \
--sku Standard_LRS
az storage container create \
--name documenso-documents \
--account-name yourstorageaccount
```
</Step>
<Step>
### Configure CORS on the container
The browser uploads documents directly to Azure Blob using a SAS URL, and downloads them the same way, so the Storage Account needs CORS rules that allow your application origin:
```bash
az storage cors add \
--services b \
--methods GET PUT \
--origins https://your-documenso-domain.com \
--allowed-headers "Content-Type" "x-ms-blob-type" "Authorization" \
--exposed-headers "*" \
--max-age 3600 \
--account-name yourstorageaccount
```
</Step>
<Step>
### Configure Environment Variables
```bash
NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME=yourstorageaccount
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY=your-account-key
NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER=documenso-documents
```
</Step>
</Steps>
### Local Development with Azurite
Azurite is the official Azure Storage emulator. It supports the Blob REST API with account-key authentication.
```bash
docker run -d --name azurite \
-p 10000:10000 -p 10001:10001 -p 10002:10002 \
mcr.microsoft.com/azure-storage/azurite
```
Create the container against the well-known development account:
```bash
az storage container create \
--name documenso-documents \
--connection-string "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"
```
Configure environment variables to point at the emulator:
```bash
NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME=devstoreaccount1
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER=documenso-documents
NEXT_PRIVATE_UPLOAD_AZURE_ENDPOINT=http://127.0.0.1:10000
```
<Callout type="info">
The Azurite key shown above is the public well-known development key, published by Microsoft for emulator use. Never reuse it in production.
</Callout>
---
## CloudFront CDN (Optional)
Use Amazon CloudFront to serve documents with lower latency and reduced S3 costs. CloudFront integration uses signed URLs for secure access.
+2 -2
View File
@@ -16,7 +16,7 @@
"fumadocs-ui": "16.5.0",
"lucide-react": "^0.563.0",
"mermaid": "^11.12.2",
"next": "16.2.4",
"next": "16.2.6",
"next-plausible": "^3.12.5",
"next-themes": "^0.4.6",
"react": "^19.2.4",
@@ -29,7 +29,7 @@
"@types/node": "^25.1.0",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"postcss": "^8.5.6",
"postcss": "^8.5.14",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3"
}
+1 -1
View File
@@ -12,7 +12,7 @@
"dependencies": {
"@documenso/prisma": "*",
"luxon": "^3.7.2",
"next": "16.2.4"
"next": "16.2.6"
},
"devDependencies": {
"@types/node": "^20",
@@ -0,0 +1,152 @@
import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZCreateUserRequestSchema } from '@documenso/trpc/server/admin-router/create-user.types';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import type { z } from 'zod';
export type AdminUserCreateDialogProps = {
trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
const ZFormSchema = ZCreateUserRequestSchema;
type TFormSchema = z.infer<typeof ZFormSchema>;
export const AdminUserCreateDialog = ({ trigger, ...props }: AdminUserCreateDialogProps) => {
const { t } = useLingui();
const { toast } = useToast();
const [open, setOpen] = useState(false);
const navigate = useNavigate();
const form = useForm<TFormSchema>({
resolver: zodResolver(ZFormSchema),
defaultValues: {
email: '',
name: '',
},
});
const { mutateAsync: createUser } = trpc.admin.user.create.useMutation();
const onFormSubmit = async (data: TFormSchema) => {
try {
const result = await createUser(data);
await navigate(`/admin/users/${result.userId}`);
setOpen(false);
toast({
title: t`Success`,
description: t`User created and welcome email sent`,
duration: 5000,
});
} catch (err) {
const error = AppError.parseError(err);
console.error(error);
toast({
title: t`An error occurred`,
description: error.message || t`We encountered an error while creating the user. Please try again later.`,
variant: 'destructive',
});
}
};
useEffect(() => {
form.reset();
}, [open, form]);
return (
<Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">
<Trans>Create User</Trans>
</Button>
)}
</DialogTrigger>
<DialogContent position="center">
<DialogHeader>
<DialogTitle>
<Trans>Create User</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Create a new user. A welcome email will be sent with a link to set their password.</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>
<Trans>Email</Trans>
</FormLabel>
<FormControl>
<Input {...field} type="email" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel required>
<Trans>Name</Trans>
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" data-testid="dialog-create-user-button" loading={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</fieldset>
</form>
</Form>
</DialogContent>
</Dialog>
);
};
@@ -116,11 +116,12 @@ export const EditorFieldDropdownForm = ({
}
const newValues = [...currentValues];
const removedValue = currentValues[index].value;
newValues.splice(index, 1);
form.setValue('values', newValues);
if (form.getValues('defaultValue') === newValues[index].value) {
if (form.getValues('defaultValue') === removedValue) {
form.setValue('defaultValue', undefined);
}
};
+23 -15
View File
@@ -89,7 +89,6 @@ export const SignInForm = ({
const turnstileSiteKey = env('NEXT_PUBLIC_TURNSTILE_SITE_KEY');
const turnstileRef = useRef<TurnstileInstance>(null);
const twoFactorTurnstileRef = useRef<TurnstileInstance>(null);
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
@@ -197,13 +196,31 @@ export const SignInForm = ({
};
const onFormSubmit = async ({ email, password, totpCode, backupCode }: TSignInFormSchema) => {
const $turnstile = isTwoFactorAuthenticationDialogOpen ? twoFactorTurnstileRef.current : turnstileRef.current;
try {
let token: string | undefined;
if (turnstileSiteKey) {
token = await $turnstile?.getResponsePromise(3000).catch((_err) => undefined);
if (!token) {
toast({
title: _(msg`Human verification required`),
description: _(msg`Please complete the CAPTCHA challenge before signing in.`),
variant: 'destructive',
});
return;
}
}
await authClient.emailPassword.signIn({
email,
password,
totpCode,
backupCode,
captchaToken: captchaToken ?? undefined,
captchaToken: token ?? undefined,
redirectPath,
});
} catch (err) {
@@ -214,10 +231,6 @@ export const SignInForm = ({
if (error.code === 'TWO_FACTOR_MISSING_CREDENTIALS') {
setIsTwoFactorAuthenticationDialogOpen(true);
// Turnstile tokens are single-use. Clear the consumed one so the
// dialog's fresh widget mounts cleanly and the dialog can't be
// submitted with the stale token before a new one is issued.
setCaptchaToken(null);
return;
}
@@ -247,8 +260,7 @@ export const SignInForm = ({
variant: 'destructive',
});
turnstileRef.current?.reset();
setCaptchaToken(null);
$turnstile?.reset();
}
};
@@ -358,11 +370,9 @@ export const SignInForm = ({
<Turnstile
ref={turnstileRef}
siteKey={turnstileSiteKey}
onSuccess={setCaptchaToken}
onExpire={() => setCaptchaToken(null)}
options={{
size: 'flexible',
appearance: 'interaction-only',
appearance: 'always',
}}
/>
)}
@@ -499,11 +509,9 @@ export const SignInForm = ({
<Turnstile
ref={twoFactorTurnstileRef}
siteKey={turnstileSiteKey}
onSuccess={setCaptchaToken}
onExpire={() => setCaptchaToken(null)}
options={{
size: 'flexible',
appearance: 'interaction-only',
appearance: 'always',
}}
/>
</div>
@@ -518,7 +526,7 @@ export const SignInForm = ({
)}
</Button>
<Button type="submit" loading={isSubmitting} disabled={Boolean(turnstileSiteKey) && !captchaToken}>
<Button type="submit" loading={isSubmitting}>
{isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>}
</Button>
</DialogFooter>
+19 -8
View File
@@ -20,7 +20,7 @@ import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { TurnstileInstance } from '@marsidev/react-turnstile';
import { Turnstile } from '@marsidev/react-turnstile';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { FaIdCardClip } from 'react-icons/fa6';
import { FcGoogle } from 'react-icons/fc';
@@ -86,8 +86,6 @@ export const SignUpForm = ({
const turnstileSiteKey = env('NEXT_PUBLIC_TURNSTILE_SITE_KEY');
const turnstileRef = useRef<TurnstileInstance>(null);
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const hasSocialAuthEnabled = isGoogleSignupEnabled || isMicrosoftSignupEnabled || isOidcSignupEnabled;
const form = useForm<TSignUpFormSchema>({
@@ -105,12 +103,28 @@ export const SignUpForm = ({
const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => {
try {
let token: string | undefined;
if (turnstileSiteKey) {
token = await turnstileRef.current?.getResponsePromise(3000).catch((_err) => undefined);
if (!token) {
toast({
title: _(msg`Human verification required`),
description: _(msg`Please complete the CAPTCHA challenge before signing in.`),
variant: 'destructive',
});
return;
}
}
await authClient.emailPassword.signUp({
name,
email,
password,
signature,
captchaToken: captchaToken ?? undefined,
captchaToken: token ?? undefined,
});
await navigate(returnTo ? returnTo : '/unverified-account');
@@ -140,7 +154,6 @@ export const SignUpForm = ({
});
turnstileRef.current?.reset();
setCaptchaToken(null);
}
};
@@ -316,11 +329,9 @@ export const SignUpForm = ({
<Turnstile
ref={turnstileRef}
siteKey={turnstileSiteKey}
onSuccess={setCaptchaToken}
onExpire={() => setCaptchaToken(null)}
options={{
size: 'flexible',
appearance: 'interaction-only',
appearance: 'always',
}}
/>
)}
@@ -1,6 +1,7 @@
import { authClient } from '@documenso/auth/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { AppError } from '@documenso/lib/errors/app-error';
import { env } from '@documenso/lib/utils/env';
import { zEmail } from '@documenso/lib/utils/zod';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { Button } from '@documenso/ui/primitives/button';
@@ -12,6 +13,9 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { TurnstileInstance } from '@marsidev/react-turnstile';
import { Turnstile } from '@marsidev/react-turnstile';
import { useRef } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { z } from 'zod';
@@ -50,6 +54,9 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
const analytics = useAnalytics();
const navigate = useNavigate();
const turnstileSiteKey = env('NEXT_PUBLIC_TURNSTILE_SITE_KEY');
const turnstileRef = useRef<TurnstileInstance>(null);
const form = useForm<TClaimAccountFormSchema>({
values: {
name: defaultName ?? '',
@@ -61,7 +68,28 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
const onFormSubmit = async ({ name, email, password }: TClaimAccountFormSchema) => {
try {
await authClient.emailPassword.signUp({ name, email, password });
let token: string | undefined;
if (turnstileSiteKey) {
token = await turnstileRef.current?.getResponsePromise(3000).catch((_err) => undefined);
if (!token) {
toast({
title: _(msg`Human verification required`),
description: _(msg`Please complete the CAPTCHA challenge before signing in.`),
variant: 'destructive',
});
return;
}
}
await authClient.emailPassword.signUp({
name,
email,
password,
captchaToken: token ?? undefined,
});
await navigate(`/unverified-account`);
@@ -87,6 +115,8 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
description: _(errorMessage),
variant: 'destructive',
});
turnstileRef.current?.reset();
}
};
@@ -141,6 +171,19 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
)}
/>
{turnstileSiteKey && (
<div className="mt-4">
<Turnstile
ref={turnstileRef}
siteKey={turnstileSiteKey}
options={{
size: 'flexible',
appearance: 'always',
}}
/>
</div>
)}
<Button type="submit" className="mt-6 w-full" loading={form.formState.isSubmitting}>
<Trans>Claim account</Trans>
</Button>
@@ -97,7 +97,7 @@ export const DocumentSigningMobileWidget = () => {
layoutId="document-signing-mobile-widget-progress-bar"
className="absolute inset-y-0 left-0 bg-primary"
style={{
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
width: `${requiredRecipientFields.length === 0 ? 100 : 100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
}}
/>
</div>
@@ -142,7 +142,7 @@ export const DocumentSigningPageViewV1 = ({
return undefined;
}
const sortedRecipients = allRecipients.sort((a, b) => {
const sortedRecipients = [...allRecipients].sort((a, b) => {
// Sort by signingOrder first (nulls last), then by id
if (a.signingOrder === null && b.signingOrder === null) {
return a.id - b.id;
@@ -150,7 +150,7 @@ export const DocumentSigningPageViewV2 = () => {
layoutId="document-flow-container-step"
className="absolute inset-y-0 left-0 bg-primary"
style={{
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
width: `${requiredRecipientFields.length === 0 ? 100 : 100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
}}
/>
</div>
@@ -294,7 +294,7 @@ export const EnvelopeSigningProvider = ({
return null;
}
const sortedRecipients = envelope.recipients.sort((a, b) => {
const sortedRecipients = [...envelope.recipients].sort((a, b) => {
// Sort by signingOrder first (nulls last), then by id
if (a.signingOrder === null && b.signingOrder === null) {
return a.id - b.id;
@@ -334,8 +334,6 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
fieldGroup.add(loadingSpinnerGroup);
await signField(field.id, payload);
}
loadingSpinnerGroup.destroy();
})
.finally(() => {
loadingSpinnerGroup.destroy();
@@ -25,7 +25,7 @@ export const CardMetric = ({ icon: Icon, title, value, className, children }: Ca
</div>
)}
<h3 className="mb-2 flex items-end font-medium text-primary-forground text-sm leading-tight">{title}</h3>
<h3 className="mb-2 flex items-end font-medium text-sm leading-tight">{title}</h3>
</div>
{children || (
@@ -238,7 +238,7 @@ export const AdminOrganisationsTable = ({
}}
>
{(table) =>
!hidePaginationUntilOverflow || 1 > table.getPageCount() ? (
!hidePaginationUntilOverflow || table.getPageCount() > 1 ? (
<DataTablePagination additionalInformation="VisibleCount" table={table} />
) : null
}
@@ -1,6 +1,7 @@
import { findUsers } from '@documenso/lib/server-only/user/get-all-users';
import { Trans } from '@lingui/react/macro';
import { AdminUserCreateDialog } from '~/components/dialogs/admin-user-create-dialog';
import { AdminDashboardUsersTable } from '~/components/tables/admin-dashboard-users-table';
import type { Route } from './+types/users._index';
@@ -27,10 +28,14 @@ export default function AdminManageUsersPage({ loaderData }: Route.ComponentProp
return (
<div>
<div className="mb-6 flex items-center justify-between">
<h2 className="font-semibold text-4xl">
<Trans>Manage users</Trans>
</h2>
<AdminUserCreateDialog />
</div>
<AdminDashboardUsersTable users={users} totalPages={totalPages} page={page} perPage={perPage} />
</div>
);
@@ -40,6 +40,6 @@ export const handleInitialsFieldClick = async (
return {
type: FieldType.INITIALS,
value: initials,
value: initialsToInsert,
};
};
+1567 -1291
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -88,7 +88,7 @@
"dependencies": {
"@ai-sdk/google-vertex": "3.0.81",
"@documenso/prisma": "*",
"@libpdf/core": "^0.3.3",
"@libpdf/core": "^0.3.6",
"@lingui/conf": "^5.6.0",
"@lingui/core": "^5.6.0",
"@prisma/extension-read-replicas": "^0.4.1",
@@ -0,0 +1,387 @@
import { prisma } from '@documenso/prisma';
import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { apiSignin } from '../../fixtures/authentication';
test.describe.configure({ mode: 'parallel' });
/**
* Fill in the create-user dialog and submit it.
* Assumes the dialog trigger is already visible on the page.
*/
const submitCreateUserDialog = async ({
page,
email,
name,
}: {
page: import('@playwright/test').Page;
email: string;
name: string;
}) => {
await page.getByRole('button', { name: 'Create User' }).first().click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
await dialog.getByLabel('Email').fill(email);
await dialog.getByLabel('Name').fill(name);
await dialog.getByTestId('dialog-create-user-button').click();
};
// ─── Happy path ──────────────────────────────────────────────────────────────
test('[ADMIN][CREATE_USER]: admin can create a new user via the dialog', async ({ page }) => {
const { user: adminUser } = await seedUser({ isAdmin: true });
const newUserEmail = seedTestEmail();
const newUserName = 'New Created User';
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/users',
});
await expect(page.getByRole('heading', { name: 'Manage users' })).toBeVisible();
await submitCreateUserDialog({ page, email: newUserEmail, name: newUserName });
// After success the dialog closes and we navigate to /admin/users/:id.
await expect(page).toHaveURL(/\/admin\/users\/\d+$/, { timeout: 10_000 });
// The user-detail page renders the user's name in the heading.
await expect(page.getByRole('heading', { name: `Manage ${newUserName}'s profile` })).toBeVisible();
// The user exists in the database.
const created = await prisma.user.findUnique({
where: { email: newUserEmail.toLowerCase() },
});
expect(created).not.toBeNull();
expect(created?.name).toBe(newUserName);
});
// ─── emailVerified is set + password is null for admin-created users ────────
test('[ADMIN][CREATE_USER]: a newly created user has emailVerified set and no password', async ({ page }) => {
const { user: adminUser } = await seedUser({ isAdmin: true });
const newUserEmail = seedTestEmail();
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/users',
});
await submitCreateUserDialog({
page,
email: newUserEmail,
name: 'Pending Password User',
});
// Wait for redirect to confirm the request finished.
await expect(page).toHaveURL(/\/admin\/users\/\d+$/, { timeout: 10_000 });
// Admin-created users start with:
// - emailVerified set (the admin vouches for the email)
// - password null (user must set it via the welcome email reset link)
// The "password=null" state hard-blocks login at email-password.ts:101,
// forcing the user through the reset-link flow before they can sign in.
const created = await prisma.user.findUnique({
where: { email: newUserEmail.toLowerCase() },
select: { id: true, emailVerified: true, password: true },
});
expect(created, 'user should exist in the database').not.toBeNull();
expect(
created?.emailVerified,
'admin-created user should have emailVerified set — admin vouches for the email',
).not.toBeNull();
expect(
created?.password,
'admin-created user must have password=null — they must set one via the welcome reset link',
).toBeNull();
});
// ─── Welcome email side effect: a PasswordResetToken is issued ───────────────
test('[ADMIN][CREATE_USER]: creating a user issues a PasswordResetToken valid for ~24 hours', async ({ page }) => {
const { user: adminUser } = await seedUser({ isAdmin: true });
const newUserEmail = seedTestEmail();
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/users',
});
const beforeCreation = Date.now();
await submitCreateUserDialog({
page,
email: newUserEmail,
name: 'Token Recipient',
});
await expect(page).toHaveURL(/\/admin\/users\/\d+$/, { timeout: 10_000 });
const created = await prisma.user.findUniqueOrThrow({
where: { email: newUserEmail.toLowerCase() },
select: { id: true },
});
// The PasswordResetToken is created by an async background job
// (send.admin.user.created.email), so poll until it shows up.
await expect
.poll(
async () => {
const found = await prisma.passwordResetToken.findFirst({
where: { userId: created.id },
});
return found === null ? null : 'found';
},
{
message: `PasswordResetToken for user ${created.id} was not created by the welcome-email job in time`,
timeout: 30_000,
intervals: [250, 500, 1000],
},
)
.toBe('found');
// Now that we know it exists, fetch it with strict types.
const token = await prisma.passwordResetToken.findFirstOrThrow({
where: { userId: created.id },
});
// Token should be ~24h in the future (allow a generous fudge window).
const expiry = token.expiry.getTime();
const expectedExpiry = beforeCreation + 24 * 60 * 60 * 1000;
const driftMs = Math.abs(expiry - expectedExpiry);
// Allow up to 5 minutes of drift (test setup, db round-trips, clock skew,
// plus job scheduling delay).
expect(driftMs, `token expiry should be ~24h from now, drift was ${driftMs}ms`).toBeLessThan(5 * 60 * 1000);
// The token value should be a non-trivial hex string.
expect(token.token.length).toBeGreaterThanOrEqual(32);
expect(token.token).toMatch(/^[a-f0-9]+$/);
});
// ─── Duplicate email is rejected ─────────────────────────────────────────────
test('[ADMIN][CREATE_USER]: creating a user with an email that already exists is rejected', async ({ page }) => {
const { user: adminUser } = await seedUser({ isAdmin: true });
// Seed an existing user whose email we'll collide with.
const { user: existingUser } = await seedUser({ isPersonalOrganisation: true });
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/users',
});
await submitCreateUserDialog({
page,
email: existingUser.email,
name: 'Collision Attempt',
});
// The dialog should stay open OR an error toast should surface. Either way
// we must NOT navigate to a new user detail page.
await page.waitForTimeout(1000);
await expect(page).not.toHaveURL(/\/admin\/users\/\d+$/);
// The existing user record must not have been mutated by the attempt.
const stillExisting = await prisma.user.findUnique({
where: { email: existingUser.email },
select: { id: true, name: true, emailVerified: true },
});
expect(stillExisting?.id).toBe(existingUser.id);
expect(stillExisting?.name).toBe(existingUser.name);
// The seeded user was verified — make sure the failed create didn't
// somehow flip the flag.
expect(stillExisting?.emailVerified).not.toBeNull();
// Count of users with this email must still be 1.
const matching = await prisma.user.count({
where: { email: existingUser.email },
});
expect(matching).toBe(1);
});
// ─── Validation: empty form ──────────────────────────────────────────────────
test('[ADMIN][CREATE_USER]: submitting an empty form shows validation errors and does not create a user', async ({
page,
}) => {
const { user: adminUser } = await seedUser({ isAdmin: true });
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/users',
});
await page.getByRole('button', { name: 'Create User' }).first().click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
// Submit without filling anything.
await dialog.getByTestId('dialog-create-user-button').click();
// Validation errors are surfaced for both required fields. Their presence
// proves react-hook-form's zodResolver blocked the submit before the
// mutation ran, so no DB write could have happened.
await expect(dialog.getByLabel('Email')).toHaveAttribute('aria-invalid', 'true');
await expect(dialog.getByLabel('Name')).toHaveAttribute('aria-invalid', 'true');
// Dialog stays open and we must not have navigated to a user detail page.
await expect(dialog).toBeVisible();
await expect(page).not.toHaveURL(/\/admin\/users\/\d+$/);
});
// ─── Validation: malformed email ─────────────────────────────────────────────
test('[ADMIN][CREATE_USER]: a malformed email is rejected client-side', async ({ page }) => {
const { user: adminUser } = await seedUser({ isAdmin: true });
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/users',
});
await page.getByRole('button', { name: 'Create User' }).first().click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
const emailInput = dialog.getByLabel('Email');
await emailInput.fill('not-an-email');
await dialog.getByLabel('Name').fill('Some Name');
// The Email input is rendered with type="email" and the form does not set
// noValidate, so the browser's native HTML5 constraint validation rejects
// the malformed value and blocks the submit event from ever firing. (As a
// result react-hook-form's zodResolver never runs and `aria-invalid` is
// not flipped to true — the browser is the layer doing the rejection.) We
// assert directly on the input's ValidityState to prove the value is
// recognised as invalid client-side.
await expect(emailInput).toHaveJSProperty('validity.valid', false);
await dialog.getByTestId('dialog-create-user-button').click();
// Dialog stays open and we must not have navigated.
await expect(dialog).toBeVisible();
await expect(page).not.toHaveURL(/\/admin\/users\/\d+$/);
// The bogus email is definitely not present in the DB — a targeted check
// on a specific row, not a global count, so it's safe to run in parallel.
const bogus = await prisma.user.findFirst({
where: { email: 'not-an-email' },
});
expect(bogus).toBeNull();
});
// ─── Cancel button closes dialog without creating ───────────────────────────
test('[ADMIN][CREATE_USER]: clicking Cancel closes the dialog and does not create a user', async ({ page }) => {
const { user: adminUser } = await seedUser({ isAdmin: true });
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/users',
});
const newUserEmail = seedTestEmail();
await page.getByRole('button', { name: 'Create User' }).first().click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
// Fill in valid data but cancel anyway.
await dialog.getByLabel('Email').fill(newUserEmail);
await dialog.getByLabel('Name').fill('Cancelled User');
await dialog.getByRole('button', { name: 'Cancel' }).click();
await expect(dialog).not.toBeVisible();
// No user was created with that email.
const created = await prisma.user.findUnique({
where: { email: newUserEmail.toLowerCase() },
});
expect(created).toBeNull();
});
// ─── Email is lowercased when stored ─────────────────────────────────────────
test('[ADMIN][CREATE_USER]: email entered with mixed case is normalised to lowercase', async ({ page }) => {
const { user: adminUser } = await seedUser({ isAdmin: true });
// Build a known mixed-case email.
const rawEmail = seedTestEmail();
const mixedCaseEmail = rawEmail.replace(/^./, (c) => c.toUpperCase());
await apiSignin({
page,
email: adminUser.email,
redirectPath: '/admin/users',
});
await submitCreateUserDialog({
page,
email: mixedCaseEmail,
name: 'Mixed Case Email User',
});
await expect(page).toHaveURL(/\/admin\/users\/\d+$/, { timeout: 10_000 });
// Look up by lowercased form — that's the canonical storage.
const created = await prisma.user.findUnique({
where: { email: rawEmail.toLowerCase() },
select: { id: true, email: true, emailVerified: true },
});
expect(created).not.toBeNull();
expect(created?.email).toBe(rawEmail.toLowerCase());
// Verified — admin vouches for the email. Case normalisation must not
// affect verification state.
expect(created?.emailVerified).not.toBeNull();
});
// ─── Access control: non-admin cannot see the Create User affordance ────────
test('[ADMIN][CREATE_USER]: non-admin user redirected away from /admin/users and cannot see Create User button', async ({
page,
}) => {
const { user: nonAdminUser } = await seedUser({ isAdmin: false });
await apiSignin({
page,
email: nonAdminUser.email,
redirectPath: '/admin/users',
});
// Non-admins are redirected away from /admin/*; the admin heading must not
// be visible.
await expect(page.getByRole('heading', { name: 'Manage users' })).not.toBeVisible();
await expect(page.getByRole('button', { name: 'Create User' })).not.toBeVisible();
});
test('[ADMIN][CREATE_USER]: unauthenticated user cannot access /admin/users', async ({ page }) => {
// No apiSignin — just navigate directly.
await page.goto('/admin/users');
await expect(page).not.toHaveURL(/\/admin\/users$/);
await expect(page.getByRole('button', { name: 'Create User' })).not.toBeVisible();
});
@@ -1,4 +1,5 @@
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { FIELD_SIGNATURE_META_DEFAULT_VALUES } from '@documenso/lib/types/field-meta';
import { createDocumentAuthOptions } from '@documenso/lib/utils/document-auth';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
@@ -7,10 +8,11 @@ import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedDirectTemplate, seedTemplate } from '@documenso/prisma/seed/templates';
import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users';
import { expect, test } from '@playwright/test';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client';
import { customAlphabet } from 'nanoid';
import { apiSignin } from '../fixtures/authentication';
import { signSignaturePad } from '../fixtures/signature';
// Duped from `packages/lib/utils/teams.ts` due to errors when importing that file.
const formatDocumentsPath = (teamUrl: string) => `/t/${teamUrl}/documents`;
@@ -18,6 +20,47 @@ const formatTemplatesPath = (teamUrl: string) => `/t/${teamUrl}/templates`;
const nanoid = customAlphabet('1234567890abcdef', 10);
const expectSigningRequestJobForRecipient = async (recipientId: number) => {
const job = await prisma.backgroundJob.findFirst({
where: {
jobId: 'send.signing.requested.email',
payload: {
path: ['recipientId'],
equals: recipientId,
},
},
});
expect(job).not.toBeNull();
};
const seedSignatureFieldForRecipient = async (options: {
envelopeId: string;
recipientId: number;
positionY: number;
}) => {
const envelopeItem = await prisma.envelopeItem.findFirstOrThrow({
where: { envelopeId: options.envelopeId },
});
return await prisma.field.create({
data: {
envelopeId: options.envelopeId,
envelopeItemId: envelopeItem.id,
recipientId: options.recipientId,
type: FieldType.SIGNATURE,
page: 1,
positionX: 5,
positionY: options.positionY,
width: 20,
height: 5,
customText: '',
inserted: false,
fieldMeta: FIELD_SIGNATURE_META_DEFAULT_VALUES,
},
});
};
test('[DIRECT_TEMPLATES]: create direct link for template', async ({ page }) => {
const { team, owner, organisation } = await seedTeam({
createTeamMembers: 1,
@@ -256,11 +299,24 @@ test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with nex
},
});
const directTemplateRecipient = template.recipients[0];
if (!directTemplateRecipient) {
throw new Error('Expected direct template recipient to exist');
}
// All SIGNER recipients need a signature field for sendDocument to dispatch emails.
const directSignatureField = await seedSignatureFieldForRecipient({
envelopeId: template.id,
recipientId: directTemplateRecipient.id,
positionY: 10,
});
const originalName = 'Signer 2';
const originalSecondSignerEmail = seedTestEmail();
// Add another signer
await prisma.recipient.create({
const secondRecipient = await prisma.recipient.create({
data: {
signingOrder: 2,
envelopeId: template.id,
@@ -271,6 +327,12 @@ test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with nex
},
});
await seedSignatureFieldForRecipient({
envelopeId: template.id,
recipientId: secondRecipient.id,
positionY: 20,
});
// Check that the direct template link is accessible.
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
@@ -279,6 +341,12 @@ test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with nex
await page.getByPlaceholder('recipient@documenso.com').fill(seedTestEmail());
await page.getByRole('button', { name: 'Continue' }).click();
// Sign the direct template recipient's signature field via the UI.
await signSignaturePad(page);
await page.locator(`#field-${directSignatureField.id}`).getByRole('button').click();
await expect(page.locator(`#field-${directSignatureField.id}`)).toHaveAttribute('data-inserted', 'true');
await page.getByRole('button', { name: 'Complete' }).click();
await expect(page.getByText('Next Recipient Name')).toBeVisible();
@@ -309,8 +377,15 @@ test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with nex
const updatedSecondRecipient = createdEnvelopeRecipients.find((recipient) => recipient.signingOrder === 2);
expect(updatedSecondRecipient?.name).toBe(newName);
expect(updatedSecondRecipient?.email).toBe(newSecondSignerEmail);
expect(updatedSecondRecipient).toBeDefined();
if (!updatedSecondRecipient) {
throw new Error('Expected second recipient to exist');
}
expect(updatedSecondRecipient.name).toBe(newName);
expect(updatedSecondRecipient.email).toBe(newSecondSignerEmail);
await expectSigningRequestJobForRecipient(updatedSecondRecipient.id);
});
test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with next signer dictation', async ({
@@ -338,11 +413,24 @@ test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with nex
},
});
const directTemplateRecipient = template.recipients[0];
if (!directTemplateRecipient) {
throw new Error('Expected direct template recipient to exist');
}
// All SIGNER recipients need a signature field for sendDocument to dispatch emails.
const directSignatureField = await seedSignatureFieldForRecipient({
envelopeId: template.id,
recipientId: directTemplateRecipient.id,
positionY: 10,
});
const originalName = 'Signer 2';
const originalSecondSignerEmail = seedTestEmail();
// Add another signer
await prisma.recipient.create({
const secondRecipient = await prisma.recipient.create({
data: {
signingOrder: 2,
envelopeId: template.id,
@@ -353,10 +441,39 @@ test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with nex
},
});
await seedSignatureFieldForRecipient({
envelopeId: template.id,
recipientId: secondRecipient.id,
positionY: 20,
});
// Check that the direct template link is accessible.
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
await expect(page.getByRole('heading', { name: 'Team direct template link 1' })).toBeVisible();
await page.waitForTimeout(100);
// Wait for the PDF and the Konva canvas overlay to be ready.
await expect(page.locator('img[data-page-number]').first()).toBeVisible({ timeout: 30_000 });
const canvas = page.locator('.konva-container canvas').first();
await expect(canvas).toBeVisible({ timeout: 30_000 });
// Sign the direct template recipient's signature field via the canvas-based V2 UI.
await signSignaturePad(page);
const canvasBox = await canvas.boundingBox();
if (!canvasBox) {
throw new Error('Canvas bounding box not found');
}
const x =
(Number(directSignatureField.positionX) / 100) * canvasBox.width +
((Number(directSignatureField.width) / 100) * canvasBox.width) / 2;
const y =
(Number(directSignatureField.positionY) / 100) * canvasBox.height +
((Number(directSignatureField.height) / 100) * canvasBox.height) / 2;
await canvas.click({ position: { x, y } });
await expect(page.getByText('0 Fields Remaining').first()).toBeVisible({ timeout: 10_000 });
await page.getByRole('button', { name: 'Complete' }).click();
@@ -394,6 +511,13 @@ test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with nex
const updatedSecondRecipient = createdEnvelopeRecipients.find((recipient) => recipient.signingOrder === 2);
expect(updatedSecondRecipient?.name).toBe(newName);
expect(updatedSecondRecipient?.email).toBe(newSecondSignerEmail);
expect(updatedSecondRecipient).toBeDefined();
if (!updatedSecondRecipient) {
throw new Error('Expected second recipient to exist');
}
expect(updatedSecondRecipient.name).toBe(newName);
expect(updatedSecondRecipient.email).toBe(newSecondSignerEmail);
await expectSigningRequestJobForRecipient(updatedSecondRecipient.id);
});
@@ -0,0 +1,57 @@
import { Trans } from '@lingui/react/macro';
import { Button, Link, Section, Text } from '../components';
import { TemplateDocumentImage } from './template-document-image';
export type TemplateAdminUserCreatedProps = {
resetPasswordLink: string;
assetBaseUrl: string;
};
export const TemplateAdminUserCreated = ({ resetPasswordLink, assetBaseUrl }: TemplateAdminUserCreatedProps) => {
return (
<>
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Section className="flex-row items-center justify-center">
<Text className="mx-auto mb-0 max-w-[80%] text-center font-semibold text-lg text-primary">
<Trans>Welcome to Documenso!</Trans>
</Text>
<Text className="my-1 text-center text-base text-slate-400">
<Trans>An administrator has created a Documenso account for you.</Trans>
</Text>
<Text className="my-1 text-center text-base text-slate-400">
<Trans>To get started, please set your password by clicking the button below:</Trans>
</Text>
<Section className="mt-8 mb-6 text-center">
<Button
className="inline-flex items-center justify-center rounded-lg bg-documenso-500 px-6 py-3 text-center font-medium text-black text-sm no-underline"
href={resetPasswordLink}
>
<Trans>Set Password</Trans>
</Button>
<Text className="mt-8 text-center text-slate-400 text-sm italic">
<Trans>
You can also copy and paste this link into your browser: {resetPasswordLink} (link expires in 24 hours)
</Trans>
</Text>
</Section>
<Section className="mt-8">
<Text className="text-center text-slate-400 text-sm">
<Trans>
If you didn't expect this account or have any questions, please{' '}
<Link href="mailto:support@documenso.com" className="text-documenso-500">
contact support
</Link>
.
</Trans>
</Text>
</Section>
</Section>
</>
);
};
@@ -0,0 +1,45 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Body, Container, Head, Html, Img, Preview, Section } from '../components';
import type { TemplateAdminUserCreatedProps } from '../template-components/template-admin-user-created';
import { TemplateAdminUserCreated } from '../template-components/template-admin-user-created';
import { TemplateFooter } from '../template-components/template-footer';
export const AdminUserCreatedTemplate = ({
resetPasswordLink,
assetBaseUrl = 'http://localhost:3002',
}: TemplateAdminUserCreatedProps) => {
const { _ } = useLingui();
const previewText = msg`Set your password for Documenso`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return (
<Html>
<Head />
<Preview>{_(previewText)}</Preview>
<Body className="mx-auto my-auto bg-white font-sans">
<Section>
<Container className="mx-auto mt-8 mb-2 max-w-xl rounded-lg border border-slate-200 border-solid p-4 backdrop-blur-sm">
<Section>
<Img src={getAssetUrl('/static/logo.png')} alt="Documenso Logo" className="mb-4 h-6" />
<TemplateAdminUserCreated resetPasswordLink={resetPasswordLink} 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>
</Html>
);
};
export default AdminUserCreatedTemplate;
+1
View File
@@ -14,6 +14,7 @@ export const ZNameSchema = z
.string()
.trim()
.min(3, { message: 'Please enter a valid name.' })
.max(255, { message: 'Name cannot be more than 255 characters.' })
.refine((value) => !URL_PATTERN.test(value), {
message: 'Name cannot contain URLs.',
});
+4
View File
@@ -1,6 +1,8 @@
import { JobClient } from './client/client';
import { SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-admin-user-created-email';
import { SEND_CONFIRMATION_EMAIL_JOB_DEFINITION } from './definitions/emails/send-confirmation-email';
import { SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-cancelled-emails';
import { SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-completed-emails';
import { SEND_DOCUMENT_CREATED_FROM_DIRECT_TEMPLATE_EMAIL_JOB_DEFINITION } from './definitions/emails/send-document-created-from-direct-template-email';
import { SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-joined-email';
import { SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-left-email';
@@ -29,6 +31,7 @@ import { SYNC_EMAIL_DOMAINS_JOB_DEFINITION } from './definitions/internal/sync-e
* triggering jobs.
*/
export const jobsClient = new JobClient([
SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION,
SEND_SIGNING_EMAIL_JOB_DEFINITION,
SEND_CONFIRMATION_EMAIL_JOB_DEFINITION,
SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION,
@@ -39,6 +42,7 @@ export const jobsClient = new JobClient([
SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION,
SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION,
SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION,
SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION,
SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION,
SEND_DOCUMENT_CREATED_FROM_DIRECT_TEMPLATE_EMAIL_JOB_DEFINITION,
SEND_OWNER_RECIPIENT_EXPIRED_EMAIL_JOB_DEFINITION,
@@ -0,0 +1,67 @@
import { mailer } from '@documenso/email/mailer';
import { AdminUserCreatedTemplate } from '@documenso/email/templates/admin-user-created';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import crypto from 'crypto';
import { createElement } from 'react';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
import { DOCUMENSO_INTERNAL_EMAIL } from '../../../constants/email';
import { ONE_DAY } from '../../../constants/time';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendAdminUserCreatedEmailJobDefinition } from './send-admin-user-created-email';
/**
* Send notification email for admin-created users with password reset link.
*
* Creates a password reset token and sends an email explaining:
* - An administrator created their account
* - They need to set their password
* - Support contact if they didn't expect this
*/
export const run = async ({ payload, io }: { payload: TSendAdminUserCreatedEmailJobDefinition; io: JobRunIO }) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: payload.userId,
},
});
const token = await io.runTask(`create-password-reset-token`, async () => {
const passwordResetToken = await prisma.passwordResetToken.create({
data: {
token: crypto.randomBytes(18).toString('hex'),
expiry: new Date(Date.now() + ONE_DAY),
userId: user.id,
},
});
return passwordResetToken.token;
});
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const resetPasswordLink = `${assetBaseUrl}/reset-password/${token}`;
const emailTemplate = createElement(AdminUserCreatedTemplate, {
assetBaseUrl,
resetPasswordLink,
});
const [html, text] = await Promise.all([
renderEmailWithI18N(emailTemplate),
renderEmailWithI18N(emailTemplate, { plainText: true }),
]);
const i18n = await getI18nInstance();
return mailer.sendMail({
to: {
address: user.email,
name: user.name || '',
},
from: DOCUMENSO_INTERNAL_EMAIL,
subject: i18n._(msg`Welcome to Documenso`),
html,
text,
});
};
@@ -0,0 +1,31 @@
import { z } from 'zod';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION_ID = 'send.admin.user.created.email';
const SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
userId: z.number(),
});
export type TSendAdminUserCreatedEmailJobDefinition = z.infer<
typeof SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION_SCHEMA
>;
export const SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION = {
id: SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION_ID,
name: 'Send Admin User Created Email',
version: '1.0.0',
trigger: {
name: SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION_ID,
schema: SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION_SCHEMA,
},
handler: async ({ payload, io }) => {
const handler = await import('./send-admin-user-created-email.handler');
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION_ID,
TSendAdminUserCreatedEmailJobDefinition
>;
@@ -5,29 +5,26 @@ import { msg } from '@lingui/core/macro';
import { DocumentSource, EnvelopeType } from '@prisma/client';
import { createElement } from 'react';
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 { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFileServerSide } from '../../universal/upload/get-file.server';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import type { EnvelopeIdOptions } from '../../utils/envelope';
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
import { isRecipientEmailValidForSending } from '../../utils/recipients';
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { formatDocumentsPath } from '../../utils/teams';
import { getEmailContext } from '../email/get-email-context';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
import { getEmailContext } from '../../../server-only/email/get-email-context';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
import { getFileServerSide } from '../../../universal/upload/get-file.server';
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope';
import { isRecipientEmailValidForSending } from '../../../utils/recipients';
import { renderCustomEmailTemplate } from '../../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { formatDocumentsPath } from '../../../utils/teams';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendDocumentCompletedEmailsJobDefinition } from './send-document-completed-emails';
export interface SendDocumentOptions {
id: EnvelopeIdOptions;
requestMetadata?: RequestMetadata;
}
export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmailsJobDefinition; io: JobRunIO }) => {
const { envelopeId, requestMetadata } = payload;
export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOptions) => {
const envelope = await prisma.envelope.findUnique({
where: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
where: unsafeBuildEnvelopeIdQuery({ type: 'envelopeId', id: envelopeId }, EnvelopeType.DOCUMENT),
include: {
envelopeItems: {
include: {
@@ -0,0 +1,33 @@
import { z } from 'zod';
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION_ID = 'send.document.completed.emails';
const SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION_SCHEMA = z.object({
envelopeId: z.string(),
requestMetadata: ZRequestMetadataSchema.optional(),
});
export type TSendDocumentCompletedEmailsJobDefinition = z.infer<
typeof SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION_SCHEMA
>;
export const SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION = {
id: SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION_ID,
name: 'Send Document Completed Emails',
version: '1.0.0',
trigger: {
name: SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION_ID,
schema: SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION_SCHEMA,
},
handler: async ({ payload, io }) => {
const handler = await import('./send-document-completed-emails.handler');
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION_ID,
TSendDocumentCompletedEmailsJobDefinition
>;
@@ -14,7 +14,6 @@ import { groupBy } from 'remeda';
import { NEXT_PRIVATE_USE_PLAYWRIGHT_PDF } from '../../../constants/app';
import { AppError, AppErrorCode } from '../../../errors/app-error';
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
import { insertFieldInPDFV1 } from '../../../server-only/pdf/insert-field-in-pdf-v1';
@@ -31,6 +30,7 @@ import { fieldsContainUnsignedRequiredField } from '../../../utils/advanced-fiel
import { isDocumentCompleted } from '../../../utils/document';
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
import { mapDocumentIdToSecondaryId } from '../../../utils/envelope';
import { jobs } from '../../client';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSealDocumentJobDefinition } from './seal-document';
@@ -294,21 +294,6 @@ export const run = async ({ payload, io }: { payload: TSealDocumentJobDefinition
};
});
await io.runTask('send-completed-email', async () => {
let shouldSendCompletedEmail = sendEmail && !isResealing && !isRejected;
if (isResealing && !isDocumentCompleted(envelopeStatus)) {
shouldSendCompletedEmail = sendEmail;
}
if (shouldSendCompletedEmail) {
await sendCompletedEmail({
id: { type: 'envelopeId', id: envelopeId },
requestMetadata,
});
}
});
const updatedEnvelope = await prisma.envelope.findFirstOrThrow({
where: {
id: envelopeId,
@@ -325,6 +310,22 @@ export const run = async ({ payload, io }: { payload: TSealDocumentJobDefinition
userId: updatedEnvelope.userId,
teamId: updatedEnvelope.teamId ?? undefined,
});
let shouldSendCompletedEmail = sendEmail && !isResealing && !isRejected;
if (isResealing && !isDocumentCompleted(envelopeStatus)) {
shouldSendCompletedEmail = sendEmail;
}
if (shouldSendCompletedEmail) {
await jobs.triggerJob({
name: 'send.document.completed.emails',
payload: {
envelopeId,
requestMetadata,
},
});
}
};
type DecorateAndSignPdfOptions = {
+4 -3
View File
@@ -21,6 +21,7 @@
"@aws-sdk/cloudfront-signer": "^3.998.0",
"@aws-sdk/s3-request-presigner": "^3.998.0",
"@aws-sdk/signature-v4-crt": "^3.998.0",
"@azure/storage-blob": "^12.31.0",
"@bull-board/api": "^6.20.6",
"@bull-board/hono": "^6.20.6",
"@bull-board/ui": "^6.20.6",
@@ -48,7 +49,7 @@
"ioredis": "^5.10.1",
"jose": "^6.1.2",
"konva": "^10.0.9",
"kysely": "0.28.16",
"kysely": "0.29.2",
"luxon": "^3.7.2",
"nanoid": "^5.1.6",
"oslo": "^0.17.0",
@@ -57,8 +58,8 @@
"pino": "^9.14.0",
"pino-pretty": "^13.1.2",
"playwright": "1.56.1",
"postcss": "^8.5.6",
"postcss-selector-parser": "^7.1.0",
"postcss": "^8.5.14",
"postcss-selector-parser": "^7.1.1",
"posthog-js": "^1.297.2",
"posthog-node": "4.18.0",
"react": "^18",
@@ -104,10 +104,6 @@ export const sendDocument = async ({ id, userId, teamId, sendEmail, requestMetad
recipientsToNotify = envelope.recipients
.filter((r) => r.signingStatus === SigningStatus.NOT_SIGNED && r.role !== RecipientRole.CC)
.slice(0, 1);
// Secondary filter so we aren't resending if the current active recipient has already
// received the envelope.
recipientsToNotify.filter((r) => r.sendStatus !== SendStatus.SENT);
}
if (envelope.envelopeItems.length === 0) {
@@ -669,8 +669,6 @@ export const createDocumentFromDirectTemplate = async ({
await tx.recipient.update({
where: { id: nextRecipient.id },
data: {
sendStatus: SendStatus.SENT,
sentAt: new Date(),
...(nextSigner && documentMeta?.allowDictateNextSigner
? {
name: nextSigner.name,
@@ -0,0 +1,43 @@
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
export interface CreateAdminUserOptions {
name: string;
email: string;
}
/**
* Create a user for admin-initiated flows.
*
* Unlike normal signup, this function:
* - Leaves the password unset (`null`); the user must set it later via a password reset/onboarding link
* - Marks the email as verified immediately because this route is only called by admins
* - Does NOT create a personal organisation (user will be added to real org)
* - Returns the user immediately without side effects
*/
export const createAdminUser = async ({ name, email }: CreateAdminUserOptions) => {
const userExists = await prisma.user.findFirst({
where: {
email: email.toLowerCase(),
},
});
if (userExists) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
message: 'User with this email already exists',
});
}
const user = await prisma.user.create({
data: {
name,
email: email.toLowerCase(),
password: null,
// Verifying the email here instead of the password reset flow to reduce the
// attack surface. This route is only called by admins.
emailVerified: new Date(),
},
});
return user;
};
+15 -4
View File
@@ -26,8 +26,7 @@ export const createUser = async ({ name, email, password, signature }: CreateUse
throw new AppError(AppErrorCode.ALREADY_EXISTS);
}
const user = await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
const user = await prisma.user.create({
data: {
name,
email: email.toLowerCase(),
@@ -37,6 +36,18 @@ export const createUser = async ({ name, email, password, signature }: CreateUse
});
// Todo: (RR7) Migrate to use this after RR7.
// Note: If we actually ever proceed with this, there are multiple
// locations where we will need to update this.
// const user = await prisma.$transaction(async (tx) => {
// const user = await tx.user.create({
// data: {
// name,
// email: email.toLowerCase(),
// password: hashedPassword, // Todo: (RR7) Drop password.
// signature,
// },
// });
// await tx.account.create({
// data: {
// userId: user.id,
@@ -47,8 +58,8 @@ export const createUser = async ({ name, email, password, signature }: CreateUse
// },
// });
return user;
});
// return user;
// });
// Not used at the moment, uncomment if required.
await onCreateUserHook(user).catch((err) => {
+67 -3
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-13 06:46\n"
"PO-Revision-Date: 2026-05-22 05:21\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1605,6 +1605,10 @@ msgstr "Betrag"
msgid "An admin has deleted your document \"{documentName}\"."
msgstr "Ein Administrator hat dein Dokument \"{documentName}\" gelöscht."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "An administrator has created a Documenso account for you."
msgstr "Ein Administrator hat ein Documenso-Konto für Sie erstellt."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "An electronic signature provided by you on our platform, achieved through clicking through to a document and entering your name, or any other electronic signing method we provide, is legally binding. It carries the same weight and enforceability as a manual signature written with ink on paper."
msgstr "Eine elektronische Unterschrift, die Sie auf unserer Plattform bereitstellen, durch Klicken auf ein Dokument und Eingabe Ihres Namens oder einer anderen von uns bereitgestellten elektronischen Unterzeichnungsart, ist rechtlich bindend. Sie hat das gleiche Gewicht und die gleiche Durchsetzbarkeit wie eine handschriftliche Unterschrift auf Papier."
@@ -1624,6 +1628,7 @@ msgstr "Eine E-Mail mit dieser Adresse existiert bereits."
#: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-create-dialog.tsx
#: apps/remix/app/components/forms/avatar-image.tsx
#: apps/remix/app/components/forms/password.tsx
@@ -2394,6 +2399,7 @@ msgstr "Person nicht gefunden?"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/ai-recipient-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
@@ -3086,6 +3092,7 @@ msgid "Copy Value"
msgstr "Wert kopieren"
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
@@ -3111,6 +3118,10 @@ msgstr "Erstellen Sie eine neue E-Mail-Adresse für Ihre Organisation mit der Do
msgid "Create a new organisation with {planName} plan. Keep your current organisation on it's current plan"
msgstr "Erstellen Sie eine neue Organisation mit dem {planName} Plan. Behalten Sie Ihre aktuelle Organisation auf dem aktuellen Plan"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create a new user. A welcome email will be sent with a link to set their password."
msgstr "Neuen Benutzer erstellen. Eine Willkommens-E-Mail wird mit einem Link zum Festlegen des Passworts gesendet."
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
msgid "Create a support ticket"
msgstr "Erstellen Sie ein Support-Ticket"
@@ -3268,6 +3279,11 @@ msgstr "Erstellen Sie das Dokument als ausstehend und bereit zur Unterschrift."
msgid "Create token"
msgstr "Token erstellen"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create User"
msgstr "Benutzer erstellen"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
msgid "Create webhook"
msgstr "Webhook erstellen"
@@ -4594,6 +4610,7 @@ msgstr "Elektronische Zustellung von Dokumenten"
msgid "Electronic Signature Disclosure"
msgstr "Offenlegung der elektronischen Unterschrift"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
@@ -5835,6 +5852,12 @@ msgstr "Horizontal"
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr "Wie lange Empfänger Zeit haben, dieses Dokument nach dem Senden zu vervollständigen. Verwendet die Team-Standardeinstellung, wenn \"Vererben\" ausgewählt ist."
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Human verification required"
msgstr "Menschliche Überprüfung erforderlich"
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "Ich bin einverstanden, mein Konto mit dieser Organisation zu verknüpfen."
@@ -5899,6 +5922,10 @@ msgstr "Bei einem Problem mit Ihrem Abo kontaktieren Sie uns bitte unter <0>{SUP
msgid "If you are using staging, ensure that you have set the host prop on the embedding component to the staging domain (https://stg-app.documenso.com)"
msgstr "Wenn Sie die Staging-Umgebung verwenden, stellen Sie sicher, dass Sie die Host-Eigenschaft der Embedding-Komponente auf die Staging-Domain (https://stg-app.documenso.com) gesetzt haben"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "If you didn't expect this account or have any questions, please <0>contact support</0>."
msgstr "Wenn Sie dieses Konto nicht erwartet haben oder Fragen haben, <0>wenden Sie sich bitte an den Support</0>."
#: packages/email/template-components/template-access-auth-2fa.tsx
msgid "If you didn't request this verification code, you can safely ignore this email."
msgstr "Wenn Sie diesen Verifizierungscode nicht angefordert haben, können Sie diese E-Mail ignorieren."
@@ -6845,6 +6872,7 @@ msgstr "Mein Ordner"
msgid "N/A"
msgstr "N/A"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
@@ -7797,6 +7825,12 @@ msgstr "Bitte überprüfe deine E-Mail auf Updates."
msgid "Please choose your new password"
msgstr "Bitte wählen Sie Ihr neues Passwort"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Please complete the CAPTCHA challenge before signing in."
msgstr "Bitte schließen Sie die CAPTCHAPrüfung ab, bevor Sie sich anmelden."
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
@@ -9264,6 +9298,10 @@ msgstr "Sitzungen wurden widerrufen"
msgid "Set a password"
msgstr "Ein Passwort festlegen"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "Set Password"
msgstr "Passwort festlegen"
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
msgid "Set up your document properties and recipient information"
msgstr "Richten Sie Ihre Dokumenteigenschaften und Empfängerinformationen ein"
@@ -9272,6 +9310,10 @@ msgstr "Richten Sie Ihre Dokumenteigenschaften und Empfängerinformationen ein"
msgid "Set up your template properties and recipient information"
msgstr "Richten Sie Ihre Vorlageneigenschaften und Empfängerinformationen ein"
#: packages/email/templates/admin-user-created.tsx
msgid "Set your password for Documenso"
msgstr "Lege dein Passwort für Documenso fest"
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-nav-mobile.tsx
@@ -9543,8 +9585,8 @@ msgstr "Unterzeichnungszertifikat"
msgid "Signing certificate provided by"
msgstr "Unterzeichnungszertifikat bereitgestellt von"
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
msgid "Signing Complete!"
msgstr "Unterzeichnung abgeschlossen!"
@@ -9892,6 +9934,7 @@ msgstr "Abonnementstatus"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
@@ -11228,6 +11271,10 @@ msgstr "Um die Zwei-Faktor-Authentifizierung zu aktivieren, scannen Sie den folg
msgid "To gain access to your account, please confirm your email address by clicking on the confirmation link from your inbox."
msgstr "Um Zugang zu Ihrem Konto zu erhalten, bestätigen Sie bitte Ihre E-Mail-Adresse, indem Sie auf den Bestätigungslink in Ihrem Posteingang klicken."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "To get started, please set your password by clicking the button below:"
msgstr "Um zu beginnen, lege bitte dein Passwort fest, indem du auf den Button unten klickst:"
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To mark this document as viewed, you need to be logged in as <0>{0}</0>"
@@ -11902,6 +11949,10 @@ msgstr "Benutzer"
msgid "User Agent"
msgstr "Benutzer-Agent"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "User created and welcome email sent"
msgstr "Benutzer erstellt und Willkommens-E-Mail gesendet"
#: apps/remix/app/components/forms/password.tsx
msgid "User has no password."
msgstr "Benutzer hat kein Passwort."
@@ -12296,6 +12347,10 @@ msgstr "Beim Versuch, Ihre Änderungen zu speichern, ist ein Fehler aufgetreten.
msgid "We encountered an error while creating the email. Please try again later."
msgstr "Wir haben beim Erstellen der E-Mail einen Fehler festgestellt. Bitte versuchen Sie es später noch einmal."
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "We encountered an error while creating the user. Please try again later."
msgstr "Beim Erstellen des Benutzers ist ein Fehler aufgetreten. Bitte versuche es später erneut."
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "We encountered an error while removing the direct template link. Please try again later."
msgstr "Wir sind auf einen Fehler gestoßen, während wir den direkten Vorlagenlink entfernt haben. Bitte versuchen Sie es später erneut."
@@ -12665,6 +12720,11 @@ msgstr "Willkommen zurück! Hier ist ein Überblick über Ihr Konto."
msgid "Welcome to {organisationName}"
msgstr "Willkommen bei {organisationName}"
#: packages/lib/jobs/definitions/emails/send-admin-user-created-email.handler.ts
msgid "Welcome to Documenso"
msgstr "Willkommen bei Documenso"
#: packages/email/template-components/template-admin-user-created.tsx
#: packages/email/template-components/template-confirmation-email.tsx
msgid "Welcome to Documenso!"
msgstr "Willkommen bei Documenso!"
@@ -12955,6 +13015,10 @@ msgstr "Sie können im Editor manuell Empfänger hinzufügen."
msgid "You can also copy and paste this link into your browser: {confirmationLink} (link expires in 1 hour)"
msgstr "Du kannst diesen Link auch kopieren und in deinen Browser einfügen: {confirmationLink} (Link läuft in 1 Stunde ab)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "You can also copy and paste this link into your browser: {resetPasswordLink} (link expires in 24 hours)"
msgstr "Du kannst diesen Link auch kopieren und in deinen Browser einfügen: {resetPasswordLink} (Link läuft in 24 Stunden ab)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
msgid "You can choose to enable or disable the profile for public view."
msgstr "Sie können wählen, ob das Profil für die öffentliche Ansicht aktiviert oder deaktiviert werden soll."
+66 -2
View File
@@ -1600,6 +1600,10 @@ msgstr "Amount"
msgid "An admin has deleted your document \"{documentName}\"."
msgstr "An admin has deleted your document \"{documentName}\"."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "An administrator has created a Documenso account for you."
msgstr "An administrator has created a Documenso account for you."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "An electronic signature provided by you on our platform, achieved through clicking through to a document and entering your name, or any other electronic signing method we provide, is legally binding. It carries the same weight and enforceability as a manual signature written with ink on paper."
msgstr "An electronic signature provided by you on our platform, achieved through clicking through to a document and entering your name, or any other electronic signing method we provide, is legally binding. It carries the same weight and enforceability as a manual signature written with ink on paper."
@@ -1619,6 +1623,7 @@ msgstr "An email with this address already exists."
#: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-create-dialog.tsx
#: apps/remix/app/components/forms/avatar-image.tsx
#: apps/remix/app/components/forms/password.tsx
@@ -2389,6 +2394,7 @@ msgstr "Can't find someone?"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/ai-recipient-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
@@ -3081,6 +3087,7 @@ msgid "Copy Value"
msgstr "Copy Value"
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
@@ -3106,6 +3113,10 @@ msgstr "Create a new email address for your organisation using the domain <0>{0}
msgid "Create a new organisation with {planName} plan. Keep your current organisation on it's current plan"
msgstr "Create a new organisation with {planName} plan. Keep your current organisation on it's current plan"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create a new user. A welcome email will be sent with a link to set their password."
msgstr "Create a new user. A welcome email will be sent with a link to set their password."
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
msgid "Create a support ticket"
msgstr "Create a support ticket"
@@ -3263,6 +3274,11 @@ msgstr "Create the document as pending and ready to sign."
msgid "Create token"
msgstr "Create token"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create User"
msgstr "Create User"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
msgid "Create webhook"
msgstr "Create webhook"
@@ -4589,6 +4605,7 @@ msgstr "Electronic Delivery of Documents"
msgid "Electronic Signature Disclosure"
msgstr "Electronic Signature Disclosure"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
@@ -5830,6 +5847,12 @@ msgstr "Horizontal"
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Human verification required"
msgstr "Human verification required"
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "I agree to link my account with this organization"
@@ -5894,6 +5917,10 @@ msgstr "If there is any issue with your subscription, please contact us at <0>{S
msgid "If you are using staging, ensure that you have set the host prop on the embedding component to the staging domain (https://stg-app.documenso.com)"
msgstr "If you are using staging, ensure that you have set the host prop on the embedding component to the staging domain (https://stg-app.documenso.com)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "If you didn't expect this account or have any questions, please <0>contact support</0>."
msgstr "If you didn't expect this account or have any questions, please <0>contact support</0>."
#: packages/email/template-components/template-access-auth-2fa.tsx
msgid "If you didn't request this verification code, you can safely ignore this email."
msgstr "If you didn't request this verification code, you can safely ignore this email."
@@ -6840,6 +6867,7 @@ msgstr "My Folder"
msgid "N/A"
msgstr "N/A"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
@@ -7792,6 +7820,12 @@ msgstr "Please check your email for updates."
msgid "Please choose your new password"
msgstr "Please choose your new password"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Please complete the CAPTCHA challenge before signing in."
msgstr "Please complete the CAPTCHA challenge before signing in."
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
@@ -9259,6 +9293,10 @@ msgstr "Sessions have been revoked"
msgid "Set a password"
msgstr "Set a password"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "Set Password"
msgstr "Set Password"
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
msgid "Set up your document properties and recipient information"
msgstr "Set up your document properties and recipient information"
@@ -9267,6 +9305,10 @@ msgstr "Set up your document properties and recipient information"
msgid "Set up your template properties and recipient information"
msgstr "Set up your template properties and recipient information"
#: packages/email/templates/admin-user-created.tsx
msgid "Set your password for Documenso"
msgstr "Set your password for Documenso"
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-nav-mobile.tsx
@@ -9538,8 +9580,8 @@ msgstr "Signing Certificate"
msgid "Signing certificate provided by"
msgstr "Signing certificate provided by"
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
msgid "Signing Complete!"
msgstr "Signing Complete!"
@@ -9887,6 +9929,7 @@ msgstr "Subscription Status"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
@@ -11223,6 +11266,10 @@ msgstr "To enable two-factor authentication, scan the following QR code using yo
msgid "To gain access to your account, please confirm your email address by clicking on the confirmation link from your inbox."
msgstr "To gain access to your account, please confirm your email address by clicking on the confirmation link from your inbox."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "To get started, please set your password by clicking the button below:"
msgstr "To get started, please set your password by clicking the button below:"
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To mark this document as viewed, you need to be logged in as <0>{0}</0>"
@@ -11897,6 +11944,10 @@ msgstr "User"
msgid "User Agent"
msgstr "User Agent"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "User created and welcome email sent"
msgstr "User created and welcome email sent"
#: apps/remix/app/components/forms/password.tsx
msgid "User has no password."
msgstr "User has no password."
@@ -12291,6 +12342,10 @@ msgstr "We encountered an error while attempting to save your changes. Your chan
msgid "We encountered an error while creating the email. Please try again later."
msgstr "We encountered an error while creating the email. Please try again later."
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "We encountered an error while creating the user. Please try again later."
msgstr "We encountered an error while creating the user. Please try again later."
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "We encountered an error while removing the direct template link. Please try again later."
msgstr "We encountered an error while removing the direct template link. Please try again later."
@@ -12660,6 +12715,11 @@ msgstr "Welcome back! Here's an overview of your account."
msgid "Welcome to {organisationName}"
msgstr "Welcome to {organisationName}"
#: packages/lib/jobs/definitions/emails/send-admin-user-created-email.handler.ts
msgid "Welcome to Documenso"
msgstr "Welcome to Documenso"
#: packages/email/template-components/template-admin-user-created.tsx
#: packages/email/template-components/template-confirmation-email.tsx
msgid "Welcome to Documenso!"
msgstr "Welcome to Documenso!"
@@ -12950,6 +13010,10 @@ msgstr "You can add recipients manually in the editor."
msgid "You can also copy and paste this link into your browser: {confirmationLink} (link expires in 1 hour)"
msgstr "You can also copy and paste this link into your browser: {confirmationLink} (link expires in 1 hour)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "You can also copy and paste this link into your browser: {resetPasswordLink} (link expires in 24 hours)"
msgstr "You can also copy and paste this link into your browser: {resetPasswordLink} (link expires in 24 hours)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
msgid "You can choose to enable or disable the profile for public view."
msgstr "You can choose to enable or disable the profile for public view."
+67 -3
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-13 06:46\n"
"PO-Revision-Date: 2026-05-22 05:22\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1605,6 +1605,10 @@ msgstr "Cantidad"
msgid "An admin has deleted your document \"{documentName}\"."
msgstr "Un administrador ha eliminado tu documento \"{documentName}\"."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "An administrator has created a Documenso account for you."
msgstr "Un administrador ha creado una cuenta de Documenso para ti."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "An electronic signature provided by you on our platform, achieved through clicking through to a document and entering your name, or any other electronic signing method we provide, is legally binding. It carries the same weight and enforceability as a manual signature written with ink on paper."
msgstr "Una firma electrónica proporcionada por usted en nuestra plataforma, lograda mediante el clic en un documento e ingresando su nombre, o cualquier otro método de firma electrónica que proporcionemos, es legalmente vinculante. Tiene el mismo peso y exigibilidad que una firma manual escrita con tinta en papel."
@@ -1624,6 +1628,7 @@ msgstr "Ya existe un correo electrónico con esta dirección."
#: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-create-dialog.tsx
#: apps/remix/app/components/forms/avatar-image.tsx
#: apps/remix/app/components/forms/password.tsx
@@ -2394,6 +2399,7 @@ msgstr "¿No puedes encontrar a alguien?"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/ai-recipient-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
@@ -3086,6 +3092,7 @@ msgid "Copy Value"
msgstr "Copiar valor"
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
@@ -3111,6 +3118,10 @@ msgstr "Crea una nueva dirección de correo electrónico para tu organización u
msgid "Create a new organisation with {planName} plan. Keep your current organisation on it's current plan"
msgstr "Crea una nueva organización con el plan {planName}. Mantén tu organización actual en su plan actual"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create a new user. A welcome email will be sent with a link to set their password."
msgstr "Crea un nuevo usuario. Se enviará un correo de bienvenida con un enlace para configurar su contraseña."
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
msgid "Create a support ticket"
msgstr "Crea un ticket de soporte"
@@ -3268,6 +3279,11 @@ msgstr "Crear el documento como pendiente y listo para firmar."
msgid "Create token"
msgstr "Crear token"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create User"
msgstr "Crear usuario"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
msgid "Create webhook"
msgstr "Crear webhook"
@@ -4594,6 +4610,7 @@ msgstr "Entrega Electrónica de Documentos"
msgid "Electronic Signature Disclosure"
msgstr "Divulgación de Firma Electrónica"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
@@ -5835,6 +5852,12 @@ msgstr "Horizontal"
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr "Tiempo que tienen los destinatarios para completar este documento después de que se envía. Usa el valor predeterminado del equipo cuando está configurado como heredado."
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Human verification required"
msgstr "Se requiere verificación humana"
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "Acepto vincular mi cuenta con esta organización"
@@ -5899,6 +5922,10 @@ msgstr "Si tienes algún problema con tu suscripción, por favor contáctanos en
msgid "If you are using staging, ensure that you have set the host prop on the embedding component to the staging domain (https://stg-app.documenso.com)"
msgstr "Si estás utilizando el entorno de pruebas (staging), asegúrate de haber establecido la propiedad host del componente de incrustación al dominio de staging (https://stg-app.documenso.com)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "If you didn't expect this account or have any questions, please <0>contact support</0>."
msgstr "Si no esperabas esta cuenta o tienes alguna pregunta, por favor <0>contacta con soporte</0>."
#: packages/email/template-components/template-access-auth-2fa.tsx
msgid "If you didn't request this verification code, you can safely ignore this email."
msgstr "Si no solicitaste este código de verificación, puedes ignorar este correo electrónico con seguridad."
@@ -6845,6 +6872,7 @@ msgstr "Mi Carpeta"
msgid "N/A"
msgstr "N/A"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
@@ -7797,6 +7825,12 @@ msgstr "Por favor, revisa tu correo electrónico para actualizaciones."
msgid "Please choose your new password"
msgstr "Por favor, elige tu nueva contraseña"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Please complete the CAPTCHA challenge before signing in."
msgstr "Complete el desafío CAPTCHA antes de iniciar sesión."
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
@@ -9264,6 +9298,10 @@ msgstr "Las sesiones han sido revocadas"
msgid "Set a password"
msgstr "Establecer una contraseña"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "Set Password"
msgstr "Establecer contraseña"
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
msgid "Set up your document properties and recipient information"
msgstr "Configura las propiedades de tu documento y la información del destinatario"
@@ -9272,6 +9310,10 @@ msgstr "Configura las propiedades de tu documento y la información del destinat
msgid "Set up your template properties and recipient information"
msgstr "Configura las propiedades de tu plantilla y la información del destinatario"
#: packages/email/templates/admin-user-created.tsx
msgid "Set your password for Documenso"
msgstr "Establece tu contraseña para Documenso"
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-nav-mobile.tsx
@@ -9543,8 +9585,8 @@ msgstr "Certificado de Firma"
msgid "Signing certificate provided by"
msgstr "Certificado de firma proporcionado por"
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
msgid "Signing Complete!"
msgstr "¡Firma completa!"
@@ -9892,6 +9934,7 @@ msgstr "Estado de la suscripción"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
@@ -11228,6 +11271,10 @@ msgstr "Para habilitar la autenticación de dos factores, escanea el siguiente c
msgid "To gain access to your account, please confirm your email address by clicking on the confirmation link from your inbox."
msgstr "Para acceder a tu cuenta, por favor confirma tu dirección de correo electrónico haciendo clic en el enlace de confirmación de tu bandeja de entrada."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "To get started, please set your password by clicking the button below:"
msgstr "Para comenzar, establece tu contraseña haciendo clic en el botón de abajo:"
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To mark this document as viewed, you need to be logged in as <0>{0}</0>"
@@ -11902,6 +11949,10 @@ msgstr "Usuario"
msgid "User Agent"
msgstr "Agente de usuario"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "User created and welcome email sent"
msgstr "Usuario creado y correo de bienvenida enviado"
#: apps/remix/app/components/forms/password.tsx
msgid "User has no password."
msgstr "El usuario no tiene contraseña."
@@ -12296,6 +12347,10 @@ msgstr "Se encontró un error mientras se intentaba guardar sus cambios. Sus cam
msgid "We encountered an error while creating the email. Please try again later."
msgstr "Encontramos un error mientras creábamos el correo. Por favor, inténtalo de nuevo más tarde."
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "We encountered an error while creating the user. Please try again later."
msgstr "Se produjo un error al crear el usuario. Por favor, inténtalo de nuevo más tarde."
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "We encountered an error while removing the direct template link. Please try again later."
msgstr "Encontramos un error al eliminar el enlace directo de la plantilla. Por favor, inténtalo de nuevo más tarde."
@@ -12665,6 +12720,11 @@ msgstr "¡Bienvenido de nuevo! Aquí tienes un resumen de tu cuenta."
msgid "Welcome to {organisationName}"
msgstr "Bienvenido a {organisationName}"
#: packages/lib/jobs/definitions/emails/send-admin-user-created-email.handler.ts
msgid "Welcome to Documenso"
msgstr "Bienvenido a Documenso"
#: packages/email/template-components/template-admin-user-created.tsx
#: packages/email/template-components/template-confirmation-email.tsx
msgid "Welcome to Documenso!"
msgstr "¡Bienvenido a Documenso!"
@@ -12955,6 +13015,10 @@ msgstr "Puedes agregar destinatarios manualmente en el editor."
msgid "You can also copy and paste this link into your browser: {confirmationLink} (link expires in 1 hour)"
msgstr "También puedes copiar y pegar este enlace en tu navegador: {confirmationLink} (el enlace expira en 1 hora)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "You can also copy and paste this link into your browser: {resetPasswordLink} (link expires in 24 hours)"
msgstr "También puedes copiar y pegar este enlace en tu navegador: {resetPasswordLink} (el enlace expira en 24 horas)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
msgid "You can choose to enable or disable the profile for public view."
msgstr "Puedes elegir habilitar o deshabilitar el perfil para vista pública."
+67 -3
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-13 06:46\n"
"PO-Revision-Date: 2026-05-22 05:21\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@@ -1605,6 +1605,10 @@ msgstr "Montant"
msgid "An admin has deleted your document \"{documentName}\"."
msgstr "Un administrateur a supprimé votre document \"{documentName}\"."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "An administrator has created a Documenso account for you."
msgstr "Un administrateur a créé un compte Documenso pour vous."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "An electronic signature provided by you on our platform, achieved through clicking through to a document and entering your name, or any other electronic signing method we provide, is legally binding. It carries the same weight and enforceability as a manual signature written with ink on paper."
msgstr "Une signature électronique fournie par vous sur notre plateforme, obtenue en cliquant sur un document et en saisissant votre nom, ou toute autre méthode de signature électronique que nous fournis, est juridiquement contraignante. Elle a le même poids et la même force exécutoire qu'une signature manuelle écrite à l'encre sur papier."
@@ -1624,6 +1628,7 @@ msgstr "Un email avec cette adresse existe déjà."
#: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-create-dialog.tsx
#: apps/remix/app/components/forms/avatar-image.tsx
#: apps/remix/app/components/forms/password.tsx
@@ -2394,6 +2399,7 @@ msgstr "Vous ne trouvez pas quelquun ?"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/ai-recipient-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
@@ -3086,6 +3092,7 @@ msgid "Copy Value"
msgstr "Copier la valeur"
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
@@ -3111,6 +3118,10 @@ msgstr "Créez une nouvelle adresse e-mail pour votre organisation en utilisant
msgid "Create a new organisation with {planName} plan. Keep your current organisation on it's current plan"
msgstr "Créez une nouvelle organisation avec le plan {planName}. Conservez votre organisation actuelle sur son plan en cours"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create a new user. A welcome email will be sent with a link to set their password."
msgstr "Créez un nouvel utilisateur. Un e-mail de bienvenue lui sera envoyé avec un lien pour définir son mot de passe."
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
msgid "Create a support ticket"
msgstr "Créer un ticket de support"
@@ -3268,6 +3279,11 @@ msgstr "Créer le document comme en attente et prêt à signer."
msgid "Create token"
msgstr "Créer un token"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create User"
msgstr "Créer un utilisateur"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
msgid "Create webhook"
msgstr "Créer un webhook"
@@ -4594,6 +4610,7 @@ msgstr "Remise électronique de documents"
msgid "Electronic Signature Disclosure"
msgstr "Divulgation de signature électronique"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
@@ -5835,6 +5852,12 @@ msgstr "Horizontal"
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr "Durée pendant laquelle les destinataires disposent pour compléter ce document après son envoi. Utilise la valeur par défaut de l’équipe lorsque le mode hérité est sélectionné."
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Human verification required"
msgstr "Vérification humaine requise"
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "J'accepte de lier mon compte avec cette organisation"
@@ -5899,6 +5922,10 @@ msgstr "Si vous rencontrez un problème avec votre abonnement, veuillez nous con
msgid "If you are using staging, ensure that you have set the host prop on the embedding component to the staging domain (https://stg-app.documenso.com)"
msgstr "Si vous utilisez lenvironnement de staging, assurez-vous davoir défini la propriété host du composant dintégration sur le domaine de staging (https://stg-app.documenso.com)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "If you didn't expect this account or have any questions, please <0>contact support</0>."
msgstr "Si vous nattendiez pas ce compte ou si vous avez des questions, veuillez <0>contacter lassistance</0>."
#: packages/email/template-components/template-access-auth-2fa.tsx
msgid "If you didn't request this verification code, you can safely ignore this email."
msgstr "Si vous n'avez pas demandé ce code de vérification, vous pouvez ignorer cet email en toute sécurité."
@@ -6845,6 +6872,7 @@ msgstr "Mon Dossier"
msgid "N/A"
msgstr "N/A"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
@@ -7797,6 +7825,12 @@ msgstr "Veuillez vérifier votre e-mail pour des mises à jour."
msgid "Please choose your new password"
msgstr "Veuillez choisir votre nouveau mot de passe"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Please complete the CAPTCHA challenge before signing in."
msgstr "Veuillez compléter le test CAPTCHA avant de vous connecter."
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
@@ -9264,6 +9298,10 @@ msgstr "Les sessions ont été révoquées"
msgid "Set a password"
msgstr "Définir un mot de passe"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "Set Password"
msgstr "Définir le mot de passe"
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
msgid "Set up your document properties and recipient information"
msgstr "Configurez les propriétés de votre document et les informations du destinataire"
@@ -9272,6 +9310,10 @@ msgstr "Configurez les propriétés de votre document et les informations du des
msgid "Set up your template properties and recipient information"
msgstr "Configurez les propriétés de votre modèle et les informations du destinataire"
#: packages/email/templates/admin-user-created.tsx
msgid "Set your password for Documenso"
msgstr "Définissez votre mot de passe pour Documenso"
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-nav-mobile.tsx
@@ -9543,8 +9585,8 @@ msgstr "Certificat de signature"
msgid "Signing certificate provided by"
msgstr "Certificat de signature fourni par"
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
msgid "Signing Complete!"
msgstr "Signature Complète !"
@@ -9892,6 +9934,7 @@ msgstr "Statut de labonnement"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
@@ -11228,6 +11271,10 @@ msgstr "Pour activer l'authentification à deux facteurs, scannez le code QR sui
msgid "To gain access to your account, please confirm your email address by clicking on the confirmation link from your inbox."
msgstr "Pour accéder à votre compte, veuillez confirmer votre adresse e-mail en cliquant sur le lien de confirmation dans votre boîte de réception."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "To get started, please set your password by clicking the button below:"
msgstr "Pour commencer, veuillez définir votre mot de passe en cliquant sur le bouton ci-dessous :"
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To mark this document as viewed, you need to be logged in as <0>{0}</0>"
@@ -11902,6 +11949,10 @@ msgstr "Utilisateur"
msgid "User Agent"
msgstr "Agent utilisateur"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "User created and welcome email sent"
msgstr "Utilisateur créé et e-mail de bienvenue envoyé"
#: apps/remix/app/components/forms/password.tsx
msgid "User has no password."
msgstr "L'utilisateur n'a pas de mot de passe."
@@ -12296,6 +12347,10 @@ msgstr "Nous avons rencontré une erreur lors de la tentative d'enregistrement d
msgid "We encountered an error while creating the email. Please try again later."
msgstr "Nous avons rencontré une erreur lors de la création de l'e-mail. Veuillez réessayer plus tard."
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "We encountered an error while creating the user. Please try again later."
msgstr "Nous avons rencontré une erreur lors de la création de lutilisateur. Veuillez réessayer plus tard."
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "We encountered an error while removing the direct template link. Please try again later."
msgstr "Une erreur s'est produite lors de la suppression du lien direct vers le modèle. Veuillez réessayer plus tard."
@@ -12665,6 +12720,11 @@ msgstr "Bon retour ! Voici un aperçu de votre compte."
msgid "Welcome to {organisationName}"
msgstr "Bienvenue chez {organisationName}"
#: packages/lib/jobs/definitions/emails/send-admin-user-created-email.handler.ts
msgid "Welcome to Documenso"
msgstr "Bienvenue sur Documenso"
#: packages/email/template-components/template-admin-user-created.tsx
#: packages/email/template-components/template-confirmation-email.tsx
msgid "Welcome to Documenso!"
msgstr "Bienvenue sur Documenso !"
@@ -12955,6 +13015,10 @@ msgstr "Vous pouvez ajouter des destinataires manuellement dans l'éditeur."
msgid "You can also copy and paste this link into your browser: {confirmationLink} (link expires in 1 hour)"
msgstr "Vous pouvez également copier et coller ce lien dans votre navigateur : {confirmationLink} (le lien expire dans 1 heure)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "You can also copy and paste this link into your browser: {resetPasswordLink} (link expires in 24 hours)"
msgstr "Vous pouvez également copier et coller ce lien dans votre navigateur : {resetPasswordLink} (le lien expire dans 24 heures)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
msgid "You can choose to enable or disable the profile for public view."
msgstr "Vous pouvez choisir d'activer ou de désactiver le profil pour l'affichage public."
+67 -3
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: it\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-13 06:46\n"
"PO-Revision-Date: 2026-05-22 05:22\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1605,6 +1605,10 @@ msgstr "Importo"
msgid "An admin has deleted your document \"{documentName}\"."
msgstr "Un amministratore ha eliminato il tuo documento \"{documentName}\"."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "An administrator has created a Documenso account for you."
msgstr "Un amministratore ha creato un account Documenso per te."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "An electronic signature provided by you on our platform, achieved through clicking through to a document and entering your name, or any other electronic signing method we provide, is legally binding. It carries the same weight and enforceability as a manual signature written with ink on paper."
msgstr "Una firma elettronica fornita da te sulla nostra piattaforma, ottenuta cliccando su un documento e inserendo il tuo nome, o qualsiasi altro metodo di firma elettronica che forniamo, è legalmente vincolante. Ha lo stesso peso e validità giuridica di una firma manuale scritta con inchiostro su carta."
@@ -1624,6 +1628,7 @@ msgstr "Una email con questo indirizzo esiste già."
#: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-create-dialog.tsx
#: apps/remix/app/components/forms/avatar-image.tsx
#: apps/remix/app/components/forms/password.tsx
@@ -2394,6 +2399,7 @@ msgstr "Non riesci a trovare qualcuno?"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/ai-recipient-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
@@ -3086,6 +3092,7 @@ msgid "Copy Value"
msgstr "Copia valore"
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
@@ -3111,6 +3118,10 @@ msgstr "Crea un nuovo indirizzo email per la tua organizzazione utilizzando il d
msgid "Create a new organisation with {planName} plan. Keep your current organisation on it's current plan"
msgstr "Crea una nuova organizzazione con il piano {planName}. Mantieni la tua organizzazione attuale sul suo piano attuale"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create a new user. A welcome email will be sent with a link to set their password."
msgstr "Crea un nuovo utente. Verrà inviata un'email di benvenuto con un link per impostare la password."
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
msgid "Create a support ticket"
msgstr "Crea un ticket di supporto"
@@ -3268,6 +3279,11 @@ msgstr "Crea il documento come in attesa e pronto per la firma."
msgid "Create token"
msgstr "Crea token"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create User"
msgstr "Crea utente"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
msgid "Create webhook"
msgstr "Crea webhook"
@@ -4594,6 +4610,7 @@ msgstr "Consegna elettronica dei documenti"
msgid "Electronic Signature Disclosure"
msgstr "Divulgazione della firma elettronica"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
@@ -5835,6 +5852,12 @@ msgstr "Orizzontale"
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr "Per quanto tempo i destinatari hanno a disposizione per completare questo documento dopo che è stato inviato. Viene utilizzato il valore predefinito del team quando è impostato su eredita."
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Human verification required"
msgstr "Verifica umana richiesta"
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "Accetto di collegare il mio account con questa organizzazione"
@@ -5899,6 +5922,10 @@ msgstr "Se ci sono problemi con il tuo abbonamento, contattaci a <0>{SUPPORT_EMA
msgid "If you are using staging, ensure that you have set the host prop on the embedding component to the staging domain (https://stg-app.documenso.com)"
msgstr "Se stai utilizzando lambiente di staging, assicurati di aver impostato la proprietà host del componente di embedding sul dominio di staging (https:\\/\\/stg-app.documenso.com)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "If you didn't expect this account or have any questions, please <0>contact support</0>."
msgstr "Se non ti aspettavi questo account o hai domande, <0>contatta l'assistenza</0>."
#: packages/email/template-components/template-access-auth-2fa.tsx
msgid "If you didn't request this verification code, you can safely ignore this email."
msgstr "Se non hai richiesto questo codice di verifica, puoi tranquillamente ignorare questa email."
@@ -6845,6 +6872,7 @@ msgstr "La Mia Cartella"
msgid "N/A"
msgstr "N/A"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
@@ -7797,6 +7825,12 @@ msgstr "Per favore controlla la tua email per aggiornamenti."
msgid "Please choose your new password"
msgstr "Per favore scegli la tua nuova password"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Please complete the CAPTCHA challenge before signing in."
msgstr "Completa la verifica CAPTCHA prima di accedere."
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
@@ -9264,6 +9298,10 @@ msgstr "Le sessioni sono state revocate"
msgid "Set a password"
msgstr "Imposta una password"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "Set Password"
msgstr "Imposta password"
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
msgid "Set up your document properties and recipient information"
msgstr "Configura le proprietà del documento e le informazioni sui destinatari"
@@ -9272,6 +9310,10 @@ msgstr "Configura le proprietà del documento e le informazioni sui destinatari"
msgid "Set up your template properties and recipient information"
msgstr "Configura le proprietà del modello e le informazioni sui destinatari"
#: packages/email/templates/admin-user-created.tsx
msgid "Set your password for Documenso"
msgstr "Imposta la tua password per Documenso"
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-nav-mobile.tsx
@@ -9543,8 +9585,8 @@ msgstr "Certificato di Firma"
msgid "Signing certificate provided by"
msgstr "Certificato di firma fornito da"
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
msgid "Signing Complete!"
msgstr "Firma completata!"
@@ -9892,6 +9934,7 @@ msgstr "Stato dellabbonamento"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
@@ -11228,6 +11271,10 @@ msgstr "Per abilitare l'autenticazione a due fattori, scansiona il seguente codi
msgid "To gain access to your account, please confirm your email address by clicking on the confirmation link from your inbox."
msgstr "Per accedere al tuo account, conferma il tuo indirizzo email facendo clic sul link di conferma dalla tua casella di posta."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "To get started, please set your password by clicking the button below:"
msgstr "Per iniziare, imposta la tua password facendo clic sul pulsante qui sotto:"
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To mark this document as viewed, you need to be logged in as <0>{0}</0>"
@@ -11902,6 +11949,10 @@ msgstr "Utente"
msgid "User Agent"
msgstr "User Agent"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "User created and welcome email sent"
msgstr "Utente creato ed email di benvenuto inviata"
#: apps/remix/app/components/forms/password.tsx
msgid "User has no password."
msgstr "L'utente non ha password."
@@ -12296,6 +12347,10 @@ msgstr "Abbiamo riscontrato un errore durante il tentativo di salvare le modific
msgid "We encountered an error while creating the email. Please try again later."
msgstr "Abbiamo riscontrato un errore durante la creazione dell'email. Riprovare più tardi."
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "We encountered an error while creating the user. Please try again later."
msgstr "Si è verificato un errore durante la creazione dell'utente. Riprova più tardi."
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "We encountered an error while removing the direct template link. Please try again later."
msgstr "Abbiamo riscontrato un errore durante la rimozione del link diretto al modello. Per favore riprova più tardi."
@@ -12665,6 +12720,11 @@ msgstr "Bentornato! Ecco una panoramica del tuo account."
msgid "Welcome to {organisationName}"
msgstr "Benvenuto a {organisationName}"
#: packages/lib/jobs/definitions/emails/send-admin-user-created-email.handler.ts
msgid "Welcome to Documenso"
msgstr "Benvenuto in Documenso"
#: packages/email/template-components/template-admin-user-created.tsx
#: packages/email/template-components/template-confirmation-email.tsx
msgid "Welcome to Documenso!"
msgstr "Benvenuto su Documenso!"
@@ -12955,6 +13015,10 @@ msgstr "Puoi aggiungere manualmente i destinatari nell'editor."
msgid "You can also copy and paste this link into your browser: {confirmationLink} (link expires in 1 hour)"
msgstr "Puoi anche copiare e incollare questo link nel tuo browser: {confirmationLink} (il link scade tra 1 ora)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "You can also copy and paste this link into your browser: {resetPasswordLink} (link expires in 24 hours)"
msgstr "Puoi anche copiare e incollare questo link nel tuo browser: {resetPasswordLink} (il link scade tra 24 ore)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
msgid "You can choose to enable or disable the profile for public view."
msgstr "Puoi scegliere di abilitare o disabilitare il profilo per la visualizzazione pubblica."
+67 -3
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ja\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-13 06:46\n"
"PO-Revision-Date: 2026-05-22 05:22\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1605,6 +1605,10 @@ msgstr "金額"
msgid "An admin has deleted your document \"{documentName}\"."
msgstr "管理者によって、あなたのドキュメント「{documentName}」が削除されました。"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "An administrator has created a Documenso account for you."
msgstr "管理者があなたのために Documenso アカウントを作成しました。"
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "An electronic signature provided by you on our platform, achieved through clicking through to a document and entering your name, or any other electronic signing method we provide, is legally binding. It carries the same weight and enforceability as a manual signature written with ink on paper."
msgstr "当社のプラットフォーム上でお客様が行う電子署名(文書をクリックして氏名を入力する、その他当社が提供する方法を用いるなど)は、法的拘束力を持ちます。紙にインクで署名した手書き署名と同等の効力と強制力を持ちます。"
@@ -1624,6 +1628,7 @@ msgstr "このメールアドレスはすでに存在します。"
#: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-create-dialog.tsx
#: apps/remix/app/components/forms/avatar-image.tsx
#: apps/remix/app/components/forms/password.tsx
@@ -2394,6 +2399,7 @@ msgstr "メンバーが見つかりませんか?"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/ai-recipient-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
@@ -3086,6 +3092,7 @@ msgid "Copy Value"
msgstr "値をコピー"
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
@@ -3111,6 +3118,10 @@ msgstr "ドメイン <0>{0}</0> を使用して、組織用の新しいメール
msgid "Create a new organisation with {planName} plan. Keep your current organisation on it's current plan"
msgstr "{planName} プランの新しい組織を作成します。現在の組織はそのままのプランを維持します。"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create a new user. A welcome email will be sent with a link to set their password."
msgstr "新しいユーザーを作成します。パスワードを設定するためのリンクが記載された歓迎メールが送信されます。"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
msgid "Create a support ticket"
msgstr "サポートチケットを作成"
@@ -3268,6 +3279,11 @@ msgstr "文書を保留状態で作成し、署名可能にします。"
msgid "Create token"
msgstr "トークンを作成"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create User"
msgstr "ユーザーを作成"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
msgid "Create webhook"
msgstr "Webhook を作成"
@@ -4594,6 +4610,7 @@ msgstr "文書の電子的な配信"
msgid "Electronic Signature Disclosure"
msgstr "電子署名に関する開示"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
@@ -5835,6 +5852,12 @@ msgstr "横"
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr "この文書が送信されてから、受信者が完了するまでの猶予期間です。「継承」に設定すると、チームのデフォルト値が使用されます。"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Human verification required"
msgstr "本人確認が必要です"
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "この組織と自分のアカウントをリンクすることに同意します"
@@ -5899,6 +5922,10 @@ msgstr "サブスクリプションに問題がある場合は、<0>{SUPPORT_EMA
msgid "If you are using staging, ensure that you have set the host prop on the embedding component to the staging domain (https://stg-app.documenso.com)"
msgstr "ステージング環境を使用している場合は、埋め込みコンポーネントの host プロパティをステージングドメイン (https://stg-app.documenso.com) に設定していることを確認してください"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "If you didn't expect this account or have any questions, please <0>contact support</0>."
msgstr "このアカウントに覚えがない場合やご不明な点がある場合は、<0>サポートにお問い合わせください</0>。"
#: packages/email/template-components/template-access-auth-2fa.tsx
msgid "If you didn't request this verification code, you can safely ignore this email."
msgstr "この認証コードをリクエストしていない場合は、このメールは無視してかまいません。"
@@ -6845,6 +6872,7 @@ msgstr "マイフォルダ"
msgid "N/A"
msgstr "N/A"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
@@ -7797,6 +7825,12 @@ msgstr "更新についてはメールをご確認ください。"
msgid "Please choose your new password"
msgstr "新しいパスワードを選択してください"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Please complete the CAPTCHA challenge before signing in."
msgstr "サインインする前に CAPTCHA 認証を完了してください。"
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
@@ -9264,6 +9298,10 @@ msgstr "セッションは取り消されました"
msgid "Set a password"
msgstr "パスワードを設定"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "Set Password"
msgstr "パスワードを設定する"
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
msgid "Set up your document properties and recipient information"
msgstr "ドキュメントのプロパティと受信者情報を設定します"
@@ -9272,6 +9310,10 @@ msgstr "ドキュメントのプロパティと受信者情報を設定します
msgid "Set up your template properties and recipient information"
msgstr "テンプレートのプロパティと受信者情報を設定します"
#: packages/email/templates/admin-user-created.tsx
msgid "Set your password for Documenso"
msgstr "Documenso 用のパスワードを設定してください"
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-nav-mobile.tsx
@@ -9543,8 +9585,8 @@ msgstr "署名証明書"
msgid "Signing certificate provided by"
msgstr "署名証明書の提供元"
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
msgid "Signing Complete!"
msgstr "署名が完了しました"
@@ -9892,6 +9934,7 @@ msgstr "サブスクリプション状況"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
@@ -11228,6 +11271,10 @@ msgstr "二要素認証を有効にするには、認証アプリで次の QR
msgid "To gain access to your account, please confirm your email address by clicking on the confirmation link from your inbox."
msgstr "アカウントにアクセスするには、受信トレイの確認リンクをクリックしてメールアドレスを確認してください。"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "To get started, please set your password by clicking the button below:"
msgstr "開始するには、以下のボタンをクリックしてパスワードを設定してください。"
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To mark this document as viewed, you need to be logged in as <0>{0}</0>"
@@ -11902,6 +11949,10 @@ msgstr "ユーザー"
msgid "User Agent"
msgstr "ユーザーエージェント"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "User created and welcome email sent"
msgstr "ユーザーが作成され、ウェルカムメールが送信されました"
#: apps/remix/app/components/forms/password.tsx
msgid "User has no password."
msgstr "このユーザーにはパスワードが設定されていません。"
@@ -12296,6 +12347,10 @@ msgstr "変更内容の保存中にエラーが発生しました。現在、変
msgid "We encountered an error while creating the email. Please try again later."
msgstr "メールの作成中にエラーが発生しました。後でもう一度お試しください。"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "We encountered an error while creating the user. Please try again later."
msgstr "ユーザーの作成中にエラーが発生しました。後でもう一度お試しください。"
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "We encountered an error while removing the direct template link. Please try again later."
msgstr "ダイレクトテンプレートリンクの削除中にエラーが発生しました。後でもう一度お試しください。"
@@ -12665,6 +12720,11 @@ msgstr "お帰りなさい!アカウントの概要をご確認ください。
msgid "Welcome to {organisationName}"
msgstr "{organisationName} へようこそ"
#: packages/lib/jobs/definitions/emails/send-admin-user-created-email.handler.ts
msgid "Welcome to Documenso"
msgstr "Documenso へようこそ"
#: packages/email/template-components/template-admin-user-created.tsx
#: packages/email/template-components/template-confirmation-email.tsx
msgid "Welcome to Documenso!"
msgstr "Documenso へようこそ!"
@@ -12955,6 +13015,10 @@ msgstr "エディターで受信者を手動で追加できます。"
msgid "You can also copy and paste this link into your browser: {confirmationLink} (link expires in 1 hour)"
msgstr "次のリンクをブラウザにコピー&ペーストして使用することもできます: {confirmationLink}(リンクの有効期限は 1 時間です)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "You can also copy and paste this link into your browser: {resetPasswordLink} (link expires in 24 hours)"
msgstr "このリンクをブラウザにコピー&ペーストして利用することもできます: {resetPasswordLink}(リンクの有効期限は24時間です)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
msgid "You can choose to enable or disable the profile for public view."
msgstr "プロフィールを公開するかどうかを選択できます。"
+67 -3
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ko\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-13 06:46\n"
"PO-Revision-Date: 2026-05-22 05:22\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1605,6 +1605,10 @@ msgstr "금액"
msgid "An admin has deleted your document \"{documentName}\"."
msgstr "관리자가 귀하의 \"{documentName}\" 문서를 삭제했습니다."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "An administrator has created a Documenso account for you."
msgstr "관리자가 회원님을 위해 Documenso 계정을 생성했습니다."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "An electronic signature provided by you on our platform, achieved through clicking through to a document and entering your name, or any other electronic signing method we provide, is legally binding. It carries the same weight and enforceability as a manual signature written with ink on paper."
msgstr "플랫폼에서 클릭을 통해 문서에 접근해 이름을 입력하거나, 저희가 제공하는 기타 전자 서명 방식으로 제공된 전자 서명은 법적 효력을 갖습니다. 이는 종이 문서에 펜으로 직접 서명하는 것과 동일한 효력과 집행력을 가집니다."
@@ -1624,6 +1628,7 @@ msgstr "이 이메일 주소를 사용하는 이메일이 이미 있습니다."
#: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-create-dialog.tsx
#: apps/remix/app/components/forms/avatar-image.tsx
#: apps/remix/app/components/forms/password.tsx
@@ -2394,6 +2399,7 @@ msgstr "누군가를 찾을 수 없나요?"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/ai-recipient-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
@@ -3086,6 +3092,7 @@ msgid "Copy Value"
msgstr "값 복사"
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
@@ -3111,6 +3118,10 @@ msgstr "도메인 <0>{0}</0>을(를) 사용해 조직용 새 이메일 주소를
msgid "Create a new organisation with {planName} plan. Keep your current organisation on it's current plan"
msgstr "새 조직을 {planName} 요금제로 생성하고, 현재 조직은 기존 요금제로 유지합니다."
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create a new user. A welcome email will be sent with a link to set their password."
msgstr "새 사용자를 생성합니다. 사용자가 비밀번호를 설정할 수 있는 링크가 포함된 환영 이메일이 전송됩니다."
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
msgid "Create a support ticket"
msgstr "지원 티켓 생성"
@@ -3268,6 +3279,11 @@ msgstr "문서를 보류 상태이자 서명 준비 상태로 생성합니다."
msgid "Create token"
msgstr "토큰 생성"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create User"
msgstr "사용자 생성"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
msgid "Create webhook"
msgstr "웹훅 생성"
@@ -4594,6 +4610,7 @@ msgstr "문서의 전자 제공"
msgid "Electronic Signature Disclosure"
msgstr "전자 서명 고지"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
@@ -5835,6 +5852,12 @@ msgstr "가로"
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr "이 문서가 전송된 후 수신자가 이를 완료할 수 있는 기간입니다. 상속으로 설정된 경우 팀 기본값을 사용합니다."
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Human verification required"
msgstr "사람 인증이 필요합니다"
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "이 조직과 계정을 연결하는 데 동의합니다"
@@ -5899,6 +5922,10 @@ msgstr "구독 관련 문제가 있는 경우 <0>{SUPPORT_EMAIL}</0>로 연락
msgid "If you are using staging, ensure that you have set the host prop on the embedding component to the staging domain (https://stg-app.documenso.com)"
msgstr "스테이징 환경을 사용 중인 경우, 임베딩 컴포넌트의 host prop을 스테이징 도메인(https://stg-app.documenso.com)으로 설정했는지 확인하세요."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "If you didn't expect this account or have any questions, please <0>contact support</0>."
msgstr "이 계정을 예상하지 못했거나 궁금한 점이 있으시면 <0>지원팀에 문의해 주세요</0>."
#: packages/email/template-components/template-access-auth-2fa.tsx
msgid "If you didn't request this verification code, you can safely ignore this email."
msgstr "이 인증 코드를 요청하지 않았다면, 이 이메일은 무시하셔도 안전합니다."
@@ -6845,6 +6872,7 @@ msgstr "내 폴더"
msgid "N/A"
msgstr "해당 없음"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
@@ -7797,6 +7825,12 @@ msgstr "업데이트 내용을 이메일로 확인해 주세요."
msgid "Please choose your new password"
msgstr "새 비밀번호를 선택해 주세요"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Please complete the CAPTCHA challenge before signing in."
msgstr "로그인하기 전에 CAPTCHA 인증을 완료해 주세요."
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
@@ -9264,6 +9298,10 @@ msgstr "세션이 해지되었습니다."
msgid "Set a password"
msgstr "비밀번호 설정"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "Set Password"
msgstr "비밀번호 설정하기"
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
msgid "Set up your document properties and recipient information"
msgstr "문서 속성과 수신자 정보를 설정하세요."
@@ -9272,6 +9310,10 @@ msgstr "문서 속성과 수신자 정보를 설정하세요."
msgid "Set up your template properties and recipient information"
msgstr "템플릿 속성과 수신자 정보를 설정하세요."
#: packages/email/templates/admin-user-created.tsx
msgid "Set your password for Documenso"
msgstr "Documenso 계정의 비밀번호를 설정하세요"
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-nav-mobile.tsx
@@ -9543,8 +9585,8 @@ msgstr "서명 인증서"
msgid "Signing certificate provided by"
msgstr "서명 인증서 제공자"
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
msgid "Signing Complete!"
msgstr "서명이 완료되었습니다!"
@@ -9892,6 +9934,7 @@ msgstr "구독 상태"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
@@ -11228,6 +11271,10 @@ msgstr "2단계 인증을 활성화하려면, 인증 앱으로 아래 QR 코드
msgid "To gain access to your account, please confirm your email address by clicking on the confirmation link from your inbox."
msgstr "계정에 접근하려면 받은편지함의 확인 링크를 클릭해 이메일 주소를 확인해 주세요."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "To get started, please set your password by clicking the button below:"
msgstr "시작하려면 아래 버튼을 클릭하여 비밀번호를 설정해 주세요."
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To mark this document as viewed, you need to be logged in as <0>{0}</0>"
@@ -11902,6 +11949,10 @@ msgstr "사용자"
msgid "User Agent"
msgstr "User Agent"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "User created and welcome email sent"
msgstr "사용자가 생성되었으며 환영 이메일이 전송되었습니다"
#: apps/remix/app/components/forms/password.tsx
msgid "User has no password."
msgstr "사용자에게 설정된 비밀번호가 없습니다."
@@ -12296,6 +12347,10 @@ msgstr "변경 사항을 저장하는 동안 오류가 발생했습니다. 현
msgid "We encountered an error while creating the email. Please try again later."
msgstr "이메일을 생성하는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "We encountered an error while creating the user. Please try again later."
msgstr "사용자를 생성하는 동안 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "We encountered an error while removing the direct template link. Please try again later."
msgstr "직접 템플릿 링크를 제거하는 중 오류가 발생했습니다. 나중에 다시 시도해 주세요."
@@ -12665,6 +12720,11 @@ msgstr "다시 오신 것을 환영합니다! 계정 개요를 확인해 보세
msgid "Welcome to {organisationName}"
msgstr "{organisationName}에 오신 것을 환영합니다"
#: packages/lib/jobs/definitions/emails/send-admin-user-created-email.handler.ts
msgid "Welcome to Documenso"
msgstr "Documenso에 오신 것을 환영합니다"
#: packages/email/template-components/template-admin-user-created.tsx
#: packages/email/template-components/template-confirmation-email.tsx
msgid "Welcome to Documenso!"
msgstr "Documenso에 오신 것을 환영합니다!"
@@ -12955,6 +13015,10 @@ msgstr "편집기에서 수신자를 수동으로 추가할 수 있습니다."
msgid "You can also copy and paste this link into your browser: {confirmationLink} (link expires in 1 hour)"
msgstr "아래 링크를 브라우저에 복사해 붙여넣을 수도 있습니다: {confirmationLink} (링크는 1시간 후 만료됩니다)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "You can also copy and paste this link into your browser: {resetPasswordLink} (link expires in 24 hours)"
msgstr "다음 링크를 브라우저에 복사해 붙여넣어 사용할 수도 있습니다: {resetPasswordLink} (링크는 24시간 후 만료됩니다)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
msgid "You can choose to enable or disable the profile for public view."
msgstr "공개 프로필을 공개하거나 비공개로 전환할 수 있습니다."
+67 -3
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: nl\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-13 06:46\n"
"PO-Revision-Date: 2026-05-22 05:21\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1605,6 +1605,10 @@ msgstr "Bedrag"
msgid "An admin has deleted your document \"{documentName}\"."
msgstr "Een beheerder heeft je document \"{documentName}\" verwijderd."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "An administrator has created a Documenso account for you."
msgstr "Een beheerder heeft een Documenso-account voor je aangemaakt."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "An electronic signature provided by you on our platform, achieved through clicking through to a document and entering your name, or any other electronic signing method we provide, is legally binding. It carries the same weight and enforceability as a manual signature written with ink on paper."
msgstr "Een elektronische handtekening die je via ons platform zet, door door te klikken naar een document en je naam in te voeren of via een andere elektronische ondertekeningsmethode die wij aanbieden, is juridisch bindend. Deze heeft dezelfde rechtsgeldigheid en afdwingbaarheid als een met pen op papier geschreven handtekening."
@@ -1624,6 +1628,7 @@ msgstr "Er bestaat al een e-mailadres met dit adres."
#: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-create-dialog.tsx
#: apps/remix/app/components/forms/avatar-image.tsx
#: apps/remix/app/components/forms/password.tsx
@@ -2394,6 +2399,7 @@ msgstr "Kunt u niemand vinden?"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/ai-recipient-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
@@ -3086,6 +3092,7 @@ msgid "Copy Value"
msgstr "Waarde kopiëren"
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
@@ -3111,6 +3118,10 @@ msgstr "Maak een nieuw e-mailadres voor je organisatie aan met het domein <0>{0}
msgid "Create a new organisation with {planName} plan. Keep your current organisation on it's current plan"
msgstr "Maak een nieuwe organisatie aan met het {planName}-abonnement. Laat je huidige organisatie op het huidige abonnement staan"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create a new user. A welcome email will be sent with a link to set their password."
msgstr "Maak een nieuwe gebruiker aan. Er wordt een welkomstmail verzonden met een link om hun wachtwoord in te stellen."
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
msgid "Create a support ticket"
msgstr "Maak een supportticket aan"
@@ -3268,6 +3279,11 @@ msgstr "Maak het document aan als in behandeling en klaar om te ondertekenen."
msgid "Create token"
msgstr "Token aanmaken"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create User"
msgstr "Gebruiker aanmaken"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
msgid "Create webhook"
msgstr "Webhook aanmaken"
@@ -4594,6 +4610,7 @@ msgstr "Elektronische levering van documenten"
msgid "Electronic Signature Disclosure"
msgstr "Kennisgeving elektronische handtekening"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
@@ -5835,6 +5852,12 @@ msgstr "Horizontaal"
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr "Hoe lang ontvangers de tijd hebben om dit document te voltooien nadat het is verzonden. Gebruikt de standaardinstelling van het team wanneer overnemen is geselecteerd."
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Human verification required"
msgstr "Menselijke verificatie vereist"
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "Ik ga akkoord met het koppelen van mijn account aan deze organisatie"
@@ -5899,6 +5922,10 @@ msgstr "Als er een probleem is met je abonnement, neem dan contact met ons op vi
msgid "If you are using staging, ensure that you have set the host prop on the embedding component to the staging domain (https://stg-app.documenso.com)"
msgstr "Als je staging gebruikt, zorg er dan voor dat je de host-prop op de embeddingcomponent hebt ingesteld op het stagingdomein (https://stg-app.documenso.com)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "If you didn't expect this account or have any questions, please <0>contact support</0>."
msgstr "Als je dit account niet had verwacht of vragen hebt, <0>neem dan contact op met support</0>."
#: packages/email/template-components/template-access-auth-2fa.tsx
msgid "If you didn't request this verification code, you can safely ignore this email."
msgstr "Als u deze verificatiecode niet heeft aangevraagd, kunt u deze e-mail veilig negeren."
@@ -6845,6 +6872,7 @@ msgstr "Mijn map"
msgid "N/A"
msgstr "n.v.t."
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
@@ -7797,6 +7825,12 @@ msgstr "Controleer je email voor updates."
msgid "Please choose your new password"
msgstr "Kies je nieuwe wachtwoord"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Please complete the CAPTCHA challenge before signing in."
msgstr "Voltooi de CAPTCHA-uitdaging voordat je je aanmeldt."
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
@@ -9264,6 +9298,10 @@ msgstr "Sessies zijn ingetrokken"
msgid "Set a password"
msgstr "Stel een wachtwoord in"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "Set Password"
msgstr "Stel wachtwoord in"
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
msgid "Set up your document properties and recipient information"
msgstr "Stel je documenteigenschappen en ontvangerinformatie in"
@@ -9272,6 +9310,10 @@ msgstr "Stel je documenteigenschappen en ontvangerinformatie in"
msgid "Set up your template properties and recipient information"
msgstr "Stel je sjablooneigenschappen en ontvangerinformatie in"
#: packages/email/templates/admin-user-created.tsx
msgid "Set your password for Documenso"
msgstr "Stel uw wachtwoord voor Documenso in"
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-nav-mobile.tsx
@@ -9543,8 +9585,8 @@ msgstr "Ondertekeningscertificaat"
msgid "Signing certificate provided by"
msgstr "Ondertekeningscertificaat verstrekt door"
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
msgid "Signing Complete!"
msgstr "Ondertekening voltooid!"
@@ -9892,6 +9934,7 @@ msgstr "Abonnementsstatus"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
@@ -11228,6 +11271,10 @@ msgstr "Om tweefactorauthenticatie in te schakelen, scan je de volgende QR
msgid "To gain access to your account, please confirm your email address by clicking on the confirmation link from your inbox."
msgstr "Om toegang te krijgen tot je account, moet je je emailadres bevestigen door op de bevestigingslink in je inbox te klikken."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "To get started, please set your password by clicking the button below:"
msgstr "Om te beginnen, stel uw wachtwoord in door op de onderstaande knop te klikken:"
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To mark this document as viewed, you need to be logged in as <0>{0}</0>"
@@ -11902,6 +11949,10 @@ msgstr "Gebruiker"
msgid "User Agent"
msgstr "User Agent"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "User created and welcome email sent"
msgstr "Gebruiker aangemaakt en welkomst-e-mail verzonden"
#: apps/remix/app/components/forms/password.tsx
msgid "User has no password."
msgstr "Gebruiker heeft geen wachtwoord."
@@ -12296,6 +12347,10 @@ msgstr "Er is een fout opgetreden bij het opslaan van uw wijzigingen. Uw wijzigi
msgid "We encountered an error while creating the email. Please try again later."
msgstr "Er is een fout opgetreden bij het aanmaken van de e-mail. Probeer het later opnieuw."
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "We encountered an error while creating the user. Please try again later."
msgstr "Er is een fout opgetreden bij het aanmaken van de gebruiker. Probeer het later opnieuw."
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "We encountered an error while removing the direct template link. Please try again later."
msgstr "Er is een fout opgetreden bij het verwijderen van de directe sjabloonlink. Probeer het later opnieuw."
@@ -12665,6 +12720,11 @@ msgstr "Welkom terug! Hier is een overzicht van je account."
msgid "Welcome to {organisationName}"
msgstr "Welkom bij {organisationName}"
#: packages/lib/jobs/definitions/emails/send-admin-user-created-email.handler.ts
msgid "Welcome to Documenso"
msgstr "Welkom bij Documenso"
#: packages/email/template-components/template-admin-user-created.tsx
#: packages/email/template-components/template-confirmation-email.tsx
msgid "Welcome to Documenso!"
msgstr "Welkom bij Documenso!"
@@ -12955,6 +13015,10 @@ msgstr "U kunt ontvangers handmatig toevoegen in de editor."
msgid "You can also copy and paste this link into your browser: {confirmationLink} (link expires in 1 hour)"
msgstr "Je kunt deze link ook kopiëren en in je browser plakken: {confirmationLink} (link verloopt over 1 uur)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "You can also copy and paste this link into your browser: {resetPasswordLink} (link expires in 24 hours)"
msgstr "U kunt deze link ook kopiëren en in uw browser plakken: {resetPasswordLink} (link verloopt over 24 uur)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
msgid "You can choose to enable or disable the profile for public view."
msgstr "Je kunt ervoor kiezen het profiel in of uit te schakelen voor openbare weergave."
+67 -3
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-13 06:46\n"
"PO-Revision-Date: 2026-05-22 05:22\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
@@ -1605,6 +1605,10 @@ msgstr "Kwota"
msgid "An admin has deleted your document \"{documentName}\"."
msgstr "Administrator usunął dokument „{documentName}”."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "An administrator has created a Documenso account for you."
msgstr "Administrator utworzył dla Ciebie konto Documenso."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "An electronic signature provided by you on our platform, achieved through clicking through to a document and entering your name, or any other electronic signing method we provide, is legally binding. It carries the same weight and enforceability as a manual signature written with ink on paper."
msgstr "Podpis elektroniczny złożony na naszej platformie poprzez kliknięcie dokumentu oraz wpisanie swojego imienia i nazwiska lub za pomocą innej metody podpisywania elektronicznego, którą udostępniamy, jest prawnie wiążący. Ma on taką samą moc i wykonalność jak podpis ręczny złożony na papierze."
@@ -1624,6 +1628,7 @@ msgstr "Ten adres e-mail już istnieje."
#: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-create-dialog.tsx
#: apps/remix/app/components/forms/avatar-image.tsx
#: apps/remix/app/components/forms/password.tsx
@@ -2394,6 +2399,7 @@ msgstr "Nie możesz kogoś znaleźć?"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/ai-recipient-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
@@ -3086,6 +3092,7 @@ msgid "Copy Value"
msgstr "Kopiuj wartość"
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
@@ -3111,6 +3118,10 @@ msgstr "Utwórz nowy adres e-mail organizacji w domenie <0>{0}</0>."
msgid "Create a new organisation with {planName} plan. Keep your current organisation on it's current plan"
msgstr "Utwórz nową organizację z planem {planName}. Zachowaj obecną organizację w dotychczasowym planie"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create a new user. A welcome email will be sent with a link to set their password."
msgstr "Utwórz nowego użytkownika. Zostanie wysłany email powitalny z łączem do ustawienia hasła."
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
msgid "Create a support ticket"
msgstr "Utwórz zgłoszenie"
@@ -3268,6 +3279,11 @@ msgstr "Utwórz dokument jako oczekujący i gotowy do podpisania."
msgid "Create token"
msgstr "Utwórz token"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create User"
msgstr "Utwórz użytkownika"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
msgid "Create webhook"
msgstr "Utwórz webhook"
@@ -4594,6 +4610,7 @@ msgstr "Elektroniczne dostarczanie dokumentów"
msgid "Electronic Signature Disclosure"
msgstr "Informacje o podpisie elektronicznym"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
@@ -5835,6 +5852,12 @@ msgstr "Poziomo"
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr "Czas, w którym odbiorcy muszą zakończyć dokument. Opcja dziedziczenia korzysta z domyślnych ustawień zespołu."
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Human verification required"
msgstr "Wymagana weryfikacja za pomocą CAPTCHA"
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "Zgadzam się połączyć moje konto z organizacją"
@@ -5899,6 +5922,10 @@ msgstr "Jeśli masz problemy z subskrypcją, skontaktuj się z nami pod adresem
msgid "If you are using staging, ensure that you have set the host prop on the embedding component to the staging domain (https://stg-app.documenso.com)"
msgstr "Jeśli korzystasz ze środowiska „staging”, ustaw właściwość host na domenę https://stg-app.documenso.com"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "If you didn't expect this account or have any questions, please <0>contact support</0>."
msgstr "Jeśli nie spodziewałeś(-aś) się tego konta lub masz jakiekolwiek pytania, <0>skontaktuj się z pomocą techniczną</0>."
#: packages/email/template-components/template-access-auth-2fa.tsx
msgid "If you didn't request this verification code, you can safely ignore this email."
msgstr "Jeśli prośba wysłania kodu weryfikacyjnego nie została utworzona przez Ciebie, zignoruj wiadomość."
@@ -6845,6 +6872,7 @@ msgstr "Mój folder"
msgid "N/A"
msgstr "Nie dotyczy"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
@@ -7797,6 +7825,12 @@ msgstr "Sprawdź pocztę e-mail."
msgid "Please choose your new password"
msgstr "Wybierz nowe hasło"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Please complete the CAPTCHA challenge before signing in."
msgstr "Przed zalogowaniem się dokończ wyzwanie CAPTCHA."
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
@@ -9264,6 +9298,10 @@ msgstr "Sesje zostały unieważnione"
msgid "Set a password"
msgstr "Ustaw hasło"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "Set Password"
msgstr "Ustaw hasło"
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
msgid "Set up your document properties and recipient information"
msgstr "Skonfiguruj właściwości dokumentu i informacje o odbiorcach"
@@ -9272,6 +9310,10 @@ msgstr "Skonfiguruj właściwości dokumentu i informacje o odbiorcach"
msgid "Set up your template properties and recipient information"
msgstr "Skonfiguruj właściwości szablonu i informacje o odbiorcach"
#: packages/email/templates/admin-user-created.tsx
msgid "Set your password for Documenso"
msgstr "Ustaw swoje hasło do Documenso"
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-nav-mobile.tsx
@@ -9543,8 +9585,8 @@ msgstr "Certyfikat podpisu"
msgid "Signing certificate provided by"
msgstr "Certyfikat podpisu został dostarczony przez"
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
msgid "Signing Complete!"
msgstr "Podpisywanie zostało zakończone!"
@@ -9892,6 +9934,7 @@ msgstr "Status subskrypcji"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
@@ -11228,6 +11271,10 @@ msgstr "Aby włączyć weryfikację dwuetapową, zeskanuj kod QR za pomocą apli
msgid "To gain access to your account, please confirm your email address by clicking on the confirmation link from your inbox."
msgstr "Aby uzyskać dostęp do konta, potwierdź adres e-mail, klikając na link potwierdzający w wiadomości."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "To get started, please set your password by clicking the button below:"
msgstr "Aby rozpocząć, ustaw swoje hasło, klikając przycisk poniżej:"
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To mark this document as viewed, you need to be logged in as <0>{0}</0>"
@@ -11902,6 +11949,10 @@ msgstr "Użytkownik"
msgid "User Agent"
msgstr "User agent"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "User created and welcome email sent"
msgstr "Użytkownik został utworzony i wysłano wiadomość powitalną"
#: apps/remix/app/components/forms/password.tsx
msgid "User has no password."
msgstr "Użytkownik nie ma hasła."
@@ -12296,6 +12347,10 @@ msgstr "Wystąpił błąd podczas zapisania zmian. Zmiany nie mogą zostać zapi
msgid "We encountered an error while creating the email. Please try again later."
msgstr "Wystąpił błąd podczas tworzenia wiadomości. Spróbuj ponownie później."
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "We encountered an error while creating the user. Please try again later."
msgstr "Wystąpił błąd podczas tworzenia użytkownika. Spróbuj ponownie później."
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "We encountered an error while removing the direct template link. Please try again later."
msgstr "Wystąpił błąd podczas usuwania bezpośredniego linku do szablonu. Spróbuj ponownie później."
@@ -12665,6 +12720,11 @@ msgstr "Witaj z powrotem! Oto podsumowanie Twojego konta."
msgid "Welcome to {organisationName}"
msgstr "Witaj w organizacji {organisationName}"
#: packages/lib/jobs/definitions/emails/send-admin-user-created-email.handler.ts
msgid "Welcome to Documenso"
msgstr "Witaj w Documenso"
#: packages/email/template-components/template-admin-user-created.tsx
#: packages/email/template-components/template-confirmation-email.tsx
msgid "Welcome to Documenso!"
msgstr "Witaj w Documenso!"
@@ -12955,6 +13015,10 @@ msgstr "Możesz ręcznie dodać odbiorców w edytorze."
msgid "You can also copy and paste this link into your browser: {confirmationLink} (link expires in 1 hour)"
msgstr "Możesz także skopiować i wkleić link do przeglądarki: {confirmationLink} (link wygaśnie za 1 godzinę)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "You can also copy and paste this link into your browser: {resetPasswordLink} (link expires in 24 hours)"
msgstr "Możesz także skopiować i wkleić ten link do przeglądarki: {resetPasswordLink} (link wygaśnie za 24 godziny)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
msgid "You can choose to enable or disable the profile for public view."
msgstr "Możesz zarządzać widocznością profilu."
+66 -2
View File
@@ -1600,6 +1600,10 @@ msgstr "Valor"
msgid "An admin has deleted your document \"{documentName}\"."
msgstr "Um administrador excluiu seu documento \"{documentName}\"."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "An administrator has created a Documenso account for you."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "An electronic signature provided by you on our platform, achieved through clicking through to a document and entering your name, or any other electronic signing method we provide, is legally binding. It carries the same weight and enforceability as a manual signature written with ink on paper."
msgstr "Uma assinatura eletrônica fornecida por você em nossa plataforma, realizada clicando em um documento e inserindo seu nome, ou qualquer outro método de assinatura eletrônica que fornecemos, é legalmente vinculativa. Ela tem o mesmo peso e aplicabilidade que uma assinatura manual feita com tinta no papel."
@@ -1619,6 +1623,7 @@ msgstr "Um e-mail com este endereço já existe."
#: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-create-dialog.tsx
#: apps/remix/app/components/forms/avatar-image.tsx
#: apps/remix/app/components/forms/password.tsx
@@ -2389,6 +2394,7 @@ msgstr ""
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/ai-recipient-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
@@ -3081,6 +3087,7 @@ msgid "Copy Value"
msgstr ""
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
@@ -3106,6 +3113,10 @@ msgstr "Crie um novo endereço de e-mail para sua organização usando o domíni
msgid "Create a new organisation with {planName} plan. Keep your current organisation on it's current plan"
msgstr "Crie uma nova organização com o plano {planName}. Mantenha sua organização atual em seu plano atual"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create a new user. A welcome email will be sent with a link to set their password."
msgstr ""
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
msgid "Create a support ticket"
msgstr "Criar um ticket de suporte"
@@ -3263,6 +3274,11 @@ msgstr "Criar o documento como pendente e pronto para assinar."
msgid "Create token"
msgstr "Criar token"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create User"
msgstr ""
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
msgid "Create webhook"
msgstr "Criar webhook"
@@ -4589,6 +4605,7 @@ msgstr "Entrega Eletrônica de Documentos"
msgid "Electronic Signature Disclosure"
msgstr "Divulgação de Assinatura Eletrônica"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
@@ -5830,6 +5847,12 @@ msgstr "Horizontal"
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr ""
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Human verification required"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "Concordo em vincular minha conta a esta organização"
@@ -5894,6 +5917,10 @@ msgstr "Se houver algum problema com sua assinatura, entre em contato conosco em
msgid "If you are using staging, ensure that you have set the host prop on the embedding component to the staging domain (https://stg-app.documenso.com)"
msgstr ""
#: packages/email/template-components/template-admin-user-created.tsx
msgid "If you didn't expect this account or have any questions, please <0>contact support</0>."
msgstr ""
#: packages/email/template-components/template-access-auth-2fa.tsx
msgid "If you didn't request this verification code, you can safely ignore this email."
msgstr "Se você não solicitou este código de verificação, pode ignorar este e-mail com segurança."
@@ -6840,6 +6867,7 @@ msgstr "Minha Pasta"
msgid "N/A"
msgstr "N/A"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
@@ -7792,6 +7820,12 @@ msgstr "Por favor, verifique seu e-mail para atualizações."
msgid "Please choose your new password"
msgstr "Por favor, escolha sua nova senha"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Please complete the CAPTCHA challenge before signing in."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
@@ -9259,6 +9293,10 @@ msgstr "Sessões foram revogadas"
msgid "Set a password"
msgstr "Definir uma senha"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "Set Password"
msgstr ""
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
msgid "Set up your document properties and recipient information"
msgstr "Configure as propriedades do seu documento e informações do destinatário"
@@ -9267,6 +9305,10 @@ msgstr "Configure as propriedades do seu documento e informações do destinatá
msgid "Set up your template properties and recipient information"
msgstr "Configure as propriedades do seu modelo e informações do destinatário"
#: packages/email/templates/admin-user-created.tsx
msgid "Set your password for Documenso"
msgstr ""
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-nav-mobile.tsx
@@ -9538,8 +9580,8 @@ msgstr "Certificado de Assinatura"
msgid "Signing certificate provided by"
msgstr "Certificado de assinatura fornecido por"
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
msgid "Signing Complete!"
msgstr "Assinatura Concluída!"
@@ -9887,6 +9929,7 @@ msgstr "Status da Assinatura"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
@@ -11223,6 +11266,10 @@ msgstr "Para ativar a autenticação de dois fatores, escaneie o seguinte códig
msgid "To gain access to your account, please confirm your email address by clicking on the confirmation link from your inbox."
msgstr "Para obter acesso à sua conta, confirme seu endereço de e-mail clicando no link de confirmação em sua caixa de entrada."
#: packages/email/template-components/template-admin-user-created.tsx
msgid "To get started, please set your password by clicking the button below:"
msgstr ""
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To mark this document as viewed, you need to be logged in as <0>{0}</0>"
@@ -11897,6 +11944,10 @@ msgstr "Usuário"
msgid "User Agent"
msgstr "Agente de Usuário"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "User created and welcome email sent"
msgstr ""
#: apps/remix/app/components/forms/password.tsx
msgid "User has no password."
msgstr "O usuário não tem senha."
@@ -12291,6 +12342,10 @@ msgstr "Encontramos um erro ao tentar salvar suas alterações. Suas alteraçõe
msgid "We encountered an error while creating the email. Please try again later."
msgstr "Encontramos um erro ao criar o e-mail. Por favor, tente novamente mais tarde."
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "We encountered an error while creating the user. Please try again later."
msgstr ""
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "We encountered an error while removing the direct template link. Please try again later."
msgstr "Encontramos um erro ao remover o link direto do modelo. Por favor, tente novamente mais tarde."
@@ -12660,6 +12715,11 @@ msgstr "Bem-vindo de volta! Aqui está uma visão geral da sua conta."
msgid "Welcome to {organisationName}"
msgstr "Bem-vindo à {organisationName}"
#: packages/lib/jobs/definitions/emails/send-admin-user-created-email.handler.ts
msgid "Welcome to Documenso"
msgstr ""
#: packages/email/template-components/template-admin-user-created.tsx
#: packages/email/template-components/template-confirmation-email.tsx
msgid "Welcome to Documenso!"
msgstr "Bem-vindo ao Documenso!"
@@ -12950,6 +13010,10 @@ msgstr "Você pode adicionar destinatários manualmente no editor."
msgid "You can also copy and paste this link into your browser: {confirmationLink} (link expires in 1 hour)"
msgstr "Você também pode copiar e colar este link no seu navegador: {confirmationLink} (o link expira em 1 hora)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "You can also copy and paste this link into your browser: {resetPasswordLink} (link expires in 24 hours)"
msgstr ""
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
msgid "You can choose to enable or disable the profile for public view."
msgstr "Você pode optar por ativar ou desativar o perfil para visualização pública."
+67 -3
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-13 06:46\n"
"PO-Revision-Date: 2026-05-22 05:21\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1605,6 +1605,10 @@ msgstr "金额"
msgid "An admin has deleted your document \"{documentName}\"."
msgstr "管理员已删除您的文档“{documentName}”。"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "An administrator has created a Documenso account for you."
msgstr "管理员已为你创建了一个 Documenso 账户。"
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "An electronic signature provided by you on our platform, achieved through clicking through to a document and entering your name, or any other electronic signing method we provide, is legally binding. It carries the same weight and enforceability as a manual signature written with ink on paper."
msgstr "你在我们平台上提供的电子签名——无论是通过点击文档并输入姓名,还是通过我们提供的其他任何电子签署方式——都具有法律效力,其效力和可执行性与纸质手写签名完全相同。"
@@ -1624,6 +1628,7 @@ msgstr "已存在使用该地址的邮箱。"
#: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-organisation-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-create-dialog.tsx
#: apps/remix/app/components/forms/avatar-image.tsx
#: apps/remix/app/components/forms/password.tsx
@@ -2394,6 +2399,7 @@ msgstr "找不到某个人?"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/ai-recipient-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
@@ -3086,6 +3092,7 @@ msgid "Copy Value"
msgstr "复制值"
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
@@ -3111,6 +3118,10 @@ msgstr "使用域名 <0>{0}</0> 为您的组织创建一个新的邮箱地址。
msgid "Create a new organisation with {planName} plan. Keep your current organisation on it's current plan"
msgstr "创建一个新的组织并使用 {planName} 套餐。保留当前组织在其现有套餐上。"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create a new user. A welcome email will be sent with a link to set their password."
msgstr "创建新用户。系统将发送一封欢迎电子邮件,其中包含用于设置其密码的链接。"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx
msgid "Create a support ticket"
msgstr "创建支持工单"
@@ -3268,6 +3279,11 @@ msgstr "将文档创建为待处理状态,并准备好供签署。"
msgid "Create token"
msgstr "创建令牌"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "Create User"
msgstr "创建用户"
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
msgid "Create webhook"
msgstr "创建 Webhook"
@@ -4594,6 +4610,7 @@ msgstr "文档的电子交付"
msgid "Electronic Signature Disclosure"
msgstr "电子签名披露"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
@@ -5835,6 +5852,12 @@ msgstr "水平"
msgid "How long recipients have to complete this document after it is sent. Uses the team default when set to inherit."
msgstr "设置此文档在发送后,收件人可完成签署的时间长度。当设置为继承时,将使用团队默认值。"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Human verification required"
msgstr "需要进行人工验证"
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
msgid "I agree to link my account with this organization"
msgstr "我同意将我的账户与此组织关联"
@@ -5899,6 +5922,10 @@ msgstr "如果您的订阅出现任何问题,请通过 <0>{SUPPORT_EMAIL}</0>
msgid "If you are using staging, ensure that you have set the host prop on the embedding component to the staging domain (https://stg-app.documenso.com)"
msgstr "如果您在使用预发布环境,请确保已将嵌入组件的 host 属性设置为预发布域名 (https://stg-app.documenso.com)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "If you didn't expect this account or have any questions, please <0>contact support</0>."
msgstr "如果你并未预期会收到此账户,或者有任何疑问,请<0>联系支持团队</0>。"
#: packages/email/template-components/template-access-auth-2fa.tsx
msgid "If you didn't request this verification code, you can safely ignore this email."
msgstr "如果您没有请求此验证码,您可以放心忽略这封邮件。"
@@ -6845,6 +6872,7 @@ msgstr "我的文件夹"
msgid "N/A"
msgstr "N/A"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
@@ -7797,6 +7825,12 @@ msgstr "请留意邮箱更新。"
msgid "Please choose your new password"
msgstr "请选择你的新密码"
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/general/claim-account.tsx
msgid "Please complete the CAPTCHA challenge before signing in."
msgstr "在登录之前,请先完成 CAPTCHA 验证。"
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
#: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx
@@ -9264,6 +9298,10 @@ msgstr "会话已被撤销"
msgid "Set a password"
msgstr "设置密码"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "Set Password"
msgstr "设置密码"
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
msgid "Set up your document properties and recipient information"
msgstr "设置文档属性和收件人信息"
@@ -9272,6 +9310,10 @@ msgstr "设置文档属性和收件人信息"
msgid "Set up your template properties and recipient information"
msgstr "设置模板属性和收件人信息"
#: packages/email/templates/admin-user-created.tsx
msgid "Set your password for Documenso"
msgstr "为 Documenso 设置您的密码"
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-command-menu.tsx
#: apps/remix/app/components/general/app-nav-mobile.tsx
@@ -9543,8 +9585,8 @@ msgstr "签署证书"
msgid "Signing certificate provided by"
msgstr "签署证书由以下机构提供"
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/server-only/document/send-completed-email.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
#: packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts
msgid "Signing Complete!"
msgstr "签署完成!"
@@ -9892,6 +9934,7 @@ msgstr "订阅状态"
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
#: apps/remix/app/components/dialogs/admin-team-member-delete-dialog.tsx
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
@@ -11228,6 +11271,10 @@ msgstr "要启用双重验证,请使用验证器应用扫描以下二维码。
msgid "To gain access to your account, please confirm your email address by clicking on the confirmation link from your inbox."
msgstr "要访问你的账号,请点击收件箱中的确认链接以验证邮箱地址。"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "To get started, please set your password by clicking the button below:"
msgstr "要开始使用,请点击下方按钮设置您的密码:"
#. placeholder {0}: recipient.email
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
msgid "To mark this document as viewed, you need to be logged in as <0>{0}</0>"
@@ -11902,6 +11949,10 @@ msgstr "用户"
msgid "User Agent"
msgstr "用户代理"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "User created and welcome email sent"
msgstr "用户已创建并已发送欢迎电子邮件"
#: apps/remix/app/components/forms/password.tsx
msgid "User has no password."
msgstr "用户没有密码。"
@@ -12296,6 +12347,10 @@ msgstr "保存更改时遇到错误。您的更改目前无法保存。"
msgid "We encountered an error while creating the email. Please try again later."
msgstr "创建邮箱时遇到错误。请稍后重试。"
#: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx
msgid "We encountered an error while creating the user. Please try again later."
msgstr "创建用户时遇到错误。请稍后再试。"
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
msgid "We encountered an error while removing the direct template link. Please try again later."
msgstr "移除直接模板链接时出错。请稍后再试。"
@@ -12665,6 +12720,11 @@ msgstr "欢迎回来!以下是您的账户概览。"
msgid "Welcome to {organisationName}"
msgstr "欢迎加入 {organisationName}"
#: packages/lib/jobs/definitions/emails/send-admin-user-created-email.handler.ts
msgid "Welcome to Documenso"
msgstr "欢迎使用 Documenso"
#: packages/email/template-components/template-admin-user-created.tsx
#: packages/email/template-components/template-confirmation-email.tsx
msgid "Welcome to Documenso!"
msgstr "欢迎来到 Documenso"
@@ -12955,6 +13015,10 @@ msgstr "您可以在编辑器中手动添加收件人。"
msgid "You can also copy and paste this link into your browser: {confirmationLink} (link expires in 1 hour)"
msgstr "您也可以将此链接复制并粘贴到浏览器中:{confirmationLink}(链接 1 小时后过期)"
#: packages/email/template-components/template-admin-user-created.tsx
msgid "You can also copy and paste this link into your browser: {resetPasswordLink} (link expires in 24 hours)"
msgstr "您也可以将此链接复制并粘贴到浏览器中打开:{resetPasswordLink}(链接将在 24 小时后失效)"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx
msgid "You can choose to enable or disable the profile for public view."
msgstr "您可以选择启用或禁用公开个人资料。"
+5
View File
@@ -26,4 +26,9 @@ export type BaseApiLog = Partial<RootApiLog> & {
export type TrpcApiLog = BaseApiLog & {
trpcMiddleware: string;
unverifiedTeamId?: number | null;
/**
* Used to differentiate between batched TRPC requests sharing the same
* underlying HTTP `requestId`.
*/
nonBatchedRequestId?: string;
};
@@ -0,0 +1,100 @@
import path from 'node:path';
import {
BlobSASPermissions,
BlobServiceClient,
generateBlobSASQueryParameters,
StorageSharedKeyCredential,
} from '@azure/storage-blob';
import { env } from '@documenso/lib/utils/env';
import slugify from '@sindresorhus/slugify';
import { ONE_HOUR } from '../../../constants/time';
import { alphaid } from '../../id';
import type { PresignedUrl, StorageProvider, UploadFileInput, UploadFileResult } from './storage-provider';
export class AzureBlobProvider implements StorageProvider {
private serviceClient: BlobServiceClient;
private credential: StorageSharedKeyCredential;
private containerName: string;
constructor() {
const accountName = String(env('NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME'));
const accountKey = String(env('NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY'));
this.containerName = String(env('NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER'));
this.credential = new StorageSharedKeyCredential(accountName, accountKey);
const endpointOverride = env('NEXT_PRIVATE_UPLOAD_AZURE_ENDPOINT');
const url = endpointOverride
? `${endpointOverride}/${accountName}`
: `https://${accountName}.blob.core.windows.net`;
this.serviceClient = new BlobServiceClient(url, this.credential);
}
private buildSasUrl(key: string, permissions: BlobSASPermissions): string {
const expiresOn = new Date(Date.now() + ONE_HOUR);
const sasToken = generateBlobSASQueryParameters(
{
containerName: this.containerName,
blobName: key,
permissions,
expiresOn,
},
this.credential,
).toString();
const blobClient = this.serviceClient.getContainerClient(this.containerName).getBlobClient(key);
return `${blobClient.url}?${sasToken}`;
}
async getPresignPostUrl(fileName: string, _contentType: string, userId?: number): Promise<PresignedUrl> {
const { name, ext } = path.parse(fileName);
let slugified = slugify(name);
if (slugified.length === 0 || slugified.length > 100) {
slugified = alphaid(8);
}
let key = `${alphaid(12)}/${slugified}${ext}`;
if (userId) {
key = `${userId}/${key}`;
}
const url = this.buildSasUrl(key, BlobSASPermissions.parse('cw'));
return { key, url };
}
async getAbsolutePresignPostUrl(key: string): Promise<PresignedUrl> {
const url = this.buildSasUrl(key, BlobSASPermissions.parse('cw'));
return { key, url };
}
async getPresignGetUrl(key: string): Promise<PresignedUrl> {
const url = this.buildSasUrl(key, BlobSASPermissions.parse('r'));
return { key, url };
}
async uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
const { name, ext } = path.parse(input.name);
const key = `${alphaid(12)}/${slugify(name)}${ext}`;
const containerClient = this.serviceClient.getContainerClient(this.containerName);
const blockBlobClient = containerClient.getBlockBlobClient(key);
const body = input.body instanceof ArrayBuffer ? Buffer.from(input.body) : input.body;
await blockBlobClient.uploadData(body, {
blobHTTPHeaders: { blobContentType: input.type },
});
return { key };
}
async deleteFile(key: string): Promise<void> {
const containerClient = this.serviceClient.getContainerClient(this.containerName);
await containerClient.deleteBlob(key);
}
}
@@ -0,0 +1,28 @@
import { env } from '@documenso/lib/utils/env';
import { AzureBlobProvider } from './azure-blob-provider';
import { S3Provider } from './s3-provider';
import type { StorageProvider } from './storage-provider';
export type { PresignedUrl, StorageProvider, UploadFileInput, UploadFileResult } from './storage-provider';
let cached: StorageProvider | null = null;
export const getStorageProvider = (): StorageProvider => {
if (cached) {
return cached;
}
const transport = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
switch (transport) {
case 's3':
cached = new S3Provider();
return cached;
case 'azure-blob':
cached = new AzureBlobProvider();
return cached;
default:
throw new Error(`Invalid object storage transport: "${transport}". Expected "s3" or "azure-blob".`);
}
};
@@ -0,0 +1,120 @@
import path from 'node:path';
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { env } from '@documenso/lib/utils/env';
import slugify from '@sindresorhus/slugify';
import { ONE_HOUR, ONE_SECOND } from '../../../constants/time';
import { alphaid } from '../../id';
import type { PresignedUrl, StorageProvider, UploadFileInput, UploadFileResult } from './storage-provider';
export class S3Provider implements StorageProvider {
private client: S3Client;
constructor() {
const hasCredentials = env('NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID') && env('NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY');
this.client = new S3Client({
endpoint: env('NEXT_PRIVATE_UPLOAD_ENDPOINT') || undefined,
forcePathStyle: env('NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE') === 'true',
region: env('NEXT_PRIVATE_UPLOAD_REGION') || 'us-east-1',
credentials: hasCredentials
? {
accessKeyId: String(env('NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID')),
secretAccessKey: String(env('NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY')),
}
: undefined,
});
}
async getPresignPostUrl(fileName: string, contentType: string, userId?: number): Promise<PresignedUrl> {
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
const { name, ext } = path.parse(fileName);
let slugified = slugify(name);
if (slugified.length === 0 || slugified.length > 100) {
slugified = alphaid(8);
}
let key = `${alphaid(12)}/${slugified}${ext}`;
if (userId) {
key = `${userId}/${key}`;
}
const command = new PutObjectCommand({
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
ContentType: contentType,
});
const url = await getSignedUrl(this.client, command, { expiresIn: ONE_HOUR / ONE_SECOND });
return { key, url };
}
async getAbsolutePresignPostUrl(key: string): Promise<PresignedUrl> {
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
const command = new PutObjectCommand({
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
});
const url = await getSignedUrl(this.client, command, { expiresIn: ONE_HOUR / ONE_SECOND });
return { key, url };
}
async getPresignGetUrl(key: string): Promise<PresignedUrl> {
if (env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN')) {
const distributionUrl = new URL(key, `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN')}`);
const { getSignedUrl: getCloudfrontSignedUrl } = await import('@aws-sdk/cloudfront-signer');
const url = getCloudfrontSignedUrl({
url: distributionUrl.toString(),
keyPairId: `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_ID')}`,
privateKey: `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS')}`,
dateLessThan: new Date(Date.now() + ONE_HOUR).toISOString(),
});
return { key, url };
}
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
const command = new GetObjectCommand({
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
});
const url = await getSignedUrl(this.client, command, { expiresIn: ONE_HOUR / ONE_SECOND });
return { key, url };
}
async uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
const { name, ext } = path.parse(input.name);
const key = `${alphaid(12)}/${slugify(name)}${ext}`;
const body = input.body instanceof ArrayBuffer ? Buffer.from(input.body) : input.body;
await this.client.send(
new PutObjectCommand({
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
Body: body,
ContentType: input.type,
}),
);
return { key };
}
async deleteFile(key: string): Promise<void> {
await this.client.send(
new DeleteObjectCommand({
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
}),
);
}
}
@@ -0,0 +1,44 @@
export type PresignedUrl = {
key: string;
url: string;
};
export type UploadFileInput = {
name: string;
type: string;
body: ArrayBuffer | Buffer;
};
export type UploadFileResult = {
key: string;
};
export interface StorageProvider {
/**
* Generate a presigned URL to upload a file by name. The provider chooses the
* final object key (typically derived from a slugified file name plus a
* random prefix) and returns it along with the signed URL.
*/
getPresignPostUrl(fileName: string, contentType: string, userId?: number): Promise<PresignedUrl>;
/**
* Generate a presigned URL to upload to an already-known key (used for flows
* where the destination has been chosen previously).
*/
getAbsolutePresignPostUrl(key: string): Promise<PresignedUrl>;
/**
* Generate a presigned URL to download a file by key.
*/
getPresignGetUrl(key: string): Promise<PresignedUrl>;
/**
* Server-side upload of a file's bytes. Returns the chosen key.
*/
uploadFile(input: UploadFileInput): Promise<UploadFileResult>;
/**
* Server-side delete of a file by key.
*/
deleteFile(key: string): Promise<void>;
}
@@ -77,7 +77,8 @@ export const putFileServerSide = async (file: File) => {
const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
return await match(NEXT_PUBLIC_UPLOAD_TRANSPORT)
.with('s3', async () => putFileInS3(file))
.with('s3', async () => putFileInObjectStorage(file))
.with('azure-blob', async () => putFileInObjectStorage(file))
.otherwise(async () => putFileInDatabase(file));
};
@@ -94,7 +95,7 @@ const putFileInDatabase = async (file: File) => {
};
};
const putFileInS3 = async (file: File) => {
const putFileInObjectStorage = async (file: File) => {
const buffer = await file.arrayBuffer();
const blob = new Blob([buffer], { type: file.type });
+7 -5
View File
@@ -45,7 +45,8 @@ export const putFile = async (file: File) => {
const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
return await match(NEXT_PUBLIC_UPLOAD_TRANSPORT)
.with('s3', async () => putFileInS3(file))
.with('s3', async () => putFileInObjectStorage(file, {}))
.with('azure-blob', async () => putFileInObjectStorage(file, { 'x-ms-blob-type': 'BlockBlob' }))
.otherwise(async () => putFileInDatabase(file));
};
@@ -62,7 +63,7 @@ const putFileInDatabase = async (file: File) => {
};
};
const putFileInS3 = async (file: File) => {
const putFileInObjectStorage = async (file: File, extraHeaders: Record<string, string>) => {
const getPresignedUrlResponse = await fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/files/presigned-post-url`, {
method: 'POST',
headers: {
@@ -82,16 +83,17 @@ const putFileInS3 = async (file: File) => {
const body = await file.arrayBuffer();
const reponse = await fetch(url, {
const response = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/octet-stream',
...extraHeaders,
},
body,
});
if (!reponse.ok) {
throw new Error(`Failed to upload file "${file.name}", failed with status code ${reponse.status}`);
if (!response.ok) {
throw new Error(`Failed to upload file "${file.name}", failed with status code ${response.status}`);
}
return {
+14 -137
View File
@@ -1,154 +1,31 @@
import path from 'node:path';
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { env } from '@documenso/lib/utils/env';
import slugify from '@sindresorhus/slugify';
import { ONE_HOUR, ONE_SECOND } from '../../constants/time';
import { alphaid } from '../id';
import { getStorageProvider } from './providers';
export const getPresignPostUrl = async (fileName: string, contentType: string, userId?: number) => {
const client = getS3Client();
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
// Get the basename and extension for the file
const { name, ext } = path.parse(fileName);
let slugified = slugify(name);
// If the slugified name is empty or too long, generate a random string instead
//
// This is fine since we don't really need the filename in s3 since we store it
// in the database and can always get the original filename from there.
//
// The slugified name can be empty when a string contains only CJK or other
// special characters.
if (slugified.length === 0 || slugified.length > 100) {
slugified = alphaid(8);
}
let key = `${alphaid(12)}/${slugified}${ext}`;
if (userId) {
key = `${userId}/${key}`;
}
const putObjectCommand = new PutObjectCommand({
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
ContentType: contentType,
});
const url = await getSignedUrl(client, putObjectCommand, {
expiresIn: ONE_HOUR / ONE_SECOND,
});
return { key, url };
return getStorageProvider().getPresignPostUrl(fileName, contentType, userId);
};
export const getAbsolutePresignPostUrl = async (key: string) => {
const client = getS3Client();
const { getSignedUrl: getS3SignedUrl } = await import('@aws-sdk/s3-request-presigner');
const putObjectCommand = new PutObjectCommand({
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
});
const url = await getS3SignedUrl(client, putObjectCommand, {
expiresIn: ONE_HOUR / ONE_SECOND,
});
return { key, url };
return getStorageProvider().getAbsolutePresignPostUrl(key);
};
export const getPresignGetUrl = async (key: string) => {
if (env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN')) {
const distributionUrl = new URL(key, `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN')}`);
const { getSignedUrl: getCloudfrontSignedUrl } = await import('@aws-sdk/cloudfront-signer');
const url = getCloudfrontSignedUrl({
url: distributionUrl.toString(),
keyPairId: `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_ID')}`,
privateKey: `${env('NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS')}`,
dateLessThan: new Date(Date.now() + ONE_HOUR).toISOString(),
});
return { key, url };
}
const client = getS3Client();
const { getSignedUrl: getS3SignedUrl } = await import('@aws-sdk/s3-request-presigner');
const getObjectCommand = new GetObjectCommand({
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
});
const url = await getS3SignedUrl(client, getObjectCommand, {
expiresIn: ONE_HOUR / ONE_SECOND,
});
return { key, url };
return getStorageProvider().getPresignGetUrl(key);
};
/**
* Uploads a file to S3.
* Uploads a file server-side. Name preserved for backward compatibility with
* existing callers; underneath it delegates to the active storage provider.
*/
export const uploadS3File = async (file: File) => {
const client = getS3Client();
// Get the basename and extension for the file
const { name, ext } = path.parse(file.name);
const key = `${alphaid(12)}/${slugify(name)}${ext}`;
const fileBuffer = await file.arrayBuffer();
const response = await client.send(
new PutObjectCommand({
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
Body: Buffer.from(fileBuffer),
ContentType: file.type,
}),
);
return { key, response };
const buffer = await file.arrayBuffer();
const { key } = await getStorageProvider().uploadFile({
name: file.name,
type: file.type,
body: buffer,
});
return { key };
};
export const deleteS3File = async (key: string) => {
const client = getS3Client();
await client.send(
new DeleteObjectCommand({
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
Key: key,
}),
);
};
const getS3Client = () => {
const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
if (NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
throw new Error('Invalid upload transport');
}
const hasCredentials = env('NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID') && env('NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY');
return new S3Client({
endpoint: env('NEXT_PRIVATE_UPLOAD_ENDPOINT') || undefined,
forcePathStyle: env('NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE') === 'true',
region: env('NEXT_PRIVATE_UPLOAD_REGION') || 'us-east-1',
credentials: hasCredentials
? {
accessKeyId: String(env('NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID')),
secretAccessKey: String(env('NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY')),
}
: undefined,
});
return getStorageProvider().deleteFile(key);
};
+1 -1
View File
@@ -22,7 +22,7 @@
},
"dependencies": {
"@prisma/client": "^6.19.0",
"kysely": "0.28.16",
"kysely": "0.29.2",
"nanoid": "^5.1.6",
"prisma": "^6.19.0",
"prisma-extension-kysely": "^3.0.0",
+2
View File
@@ -63,4 +63,6 @@ const main = async () => {
}
};
if (require.main === module) {
void main();
}
+1 -1
View File
@@ -11,7 +11,7 @@
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.19",
"autoprefixer": "^10.4.22",
"postcss": "^8.5.6",
"postcss": "^8.5.14",
"tailwindcss": "^3.4.18",
"tailwindcss-animate": "^1.0.7"
},
@@ -0,0 +1,32 @@
import { jobsClient } from '@documenso/lib/jobs/client';
import { createAdminUser } from '@documenso/lib/server-only/user/create-admin-user';
import { adminProcedure } from '../trpc';
import { ZCreateUserRequestSchema, ZCreateUserResponseSchema } from './create-user.types';
export const createUserRoute = adminProcedure
.input(ZCreateUserRequestSchema)
.output(ZCreateUserResponseSchema)
.mutation(async ({ input, ctx }) => {
const { email, name } = input;
const user = await createAdminUser({
name,
email,
});
ctx.logger.info({
createdUserId: user.id,
});
await jobsClient.triggerJob({
name: 'send.admin.user.created.email',
payload: {
userId: user.id,
},
});
return {
userId: user.id,
};
});
@@ -0,0 +1,15 @@
import { ZNameSchema } from '@documenso/lib/constants/auth';
import { z } from 'zod';
export const ZCreateUserRequestSchema = z.object({
email: z.string().email().min(1),
name: ZNameSchema,
});
export type TCreateUserRequest = z.infer<typeof ZCreateUserRequestSchema>;
export const ZCreateUserResponseSchema = z.object({
userId: z.number(),
});
export type TCreateUserResponse = z.infer<typeof ZCreateUserResponseSchema>;
@@ -2,6 +2,7 @@ import { router } from '../trpc';
import { createAdminOrganisationRoute } from './create-admin-organisation';
import { createStripeCustomerRoute } from './create-stripe-customer';
import { createSubscriptionClaimRoute } from './create-subscription-claim';
import { createUserRoute } from './create-user';
import { deleteDocumentRoute } from './delete-document';
import { deleteOrganisationRoute } from './delete-organisation';
import { deleteAdminOrganisationMemberRoute } from './delete-organisation-member';
@@ -64,6 +65,7 @@ export const adminRouter = router({
},
user: {
get: getUserRoute,
create: createUserRoute,
update: updateUserRoute,
delete: deleteUserRoute,
enable: enableUserRoute,
@@ -3,6 +3,8 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { stripe } from '@documenso/lib/server-only/stripe';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { Prisma } from '@prisma/client';
import { z } from 'zod';
import { authenticatedProcedure } from '../trpc';
import { ZUpdateOrganisationRequestSchema, ZUpdateOrganisationResponseSchema } from './update-organisation.types';
@@ -36,7 +38,8 @@ export const updateOrganisationRoute = authenticatedProcedure
});
}
const updatedOrganisation = await prisma.organisation.update({
const updatedOrganisation = await prisma.organisation
.update({
where: {
id: organisationId,
},
@@ -44,6 +47,23 @@ export const updateOrganisationRoute = authenticatedProcedure
name: data.name,
url: data.url,
},
})
.catch((err) => {
console.error(err);
if (!(err instanceof Prisma.PrismaClientKnownRequestError)) {
throw err;
}
const target = z.array(z.string()).safeParse(err.meta?.target);
if (err.code === 'P2002' && target.success && target.data.includes('url')) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
message: 'Organisation URL already exists.',
});
}
throw err;
});
if (updatedOrganisation.customerId) {
+67 -30
View File
@@ -70,9 +70,12 @@ const t = initTRPC
* Middlewares
*/
export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path, meta }) => {
const infoToLog: TrpcApiLog = {
// Auth-independent log bindings. `auth` is set per-branch below since it
// depends on which auth path was taken; `ctx.metadata.auth` here is still
// `null` (the resolved value is set in the `next()` call below).
const baseLogAttributes: TrpcApiLog = {
path,
auth: ctx.metadata.auth,
auth: null,
source: ctx.metadata.source,
trpcMiddleware: 'authenticated',
unverifiedTeamId: ctx.teamId,
@@ -93,15 +96,21 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path, me
const apiToken = await getApiTokenByToken({ token });
ctx.logger.info({
...infoToLog,
const trpcApiV2Logger = ctx.logger.child({
...baseLogAttributes,
auth: 'api',
userId: apiToken.user.id,
apiTokenId: apiToken.id,
} satisfies TrpcApiLog);
trpcApiV2Logger.info({
position: 'trpcProcedure',
});
return await next({
ctx: {
...ctx,
logger: trpcApiV2Logger,
user: apiToken.user,
teamId: apiToken.teamId,
session: null,
@@ -131,17 +140,21 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path, me
});
}
// Recreate the logger with a sub request ID to differentiate between batched requests.
// Recreate the logger with a sub request ID to differentiate between batched
// requests, as well as identifying attributes so every subsequent log line
// (including errors) inherits them.
const trpcSessionLogger = ctx.logger.child({
...baseLogAttributes,
auth: 'session',
nonBatchedRequestId: alphaid(),
});
trpcSessionLogger.info({
...infoToLog,
userId: ctx.user.id,
apiTokenId: null,
} satisfies TrpcApiLog);
trpcSessionLogger.info({
position: 'trpcProcedure',
});
return await next({
ctx: {
...ctx,
@@ -163,14 +176,9 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path, me
});
export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next, path, meta }) => {
// Recreate the logger with a sub request ID to differentiate between batched requests.
const trpcSessionLogger = ctx.logger.child({
nonBatchedRequestId: alphaid(),
});
const infoToLog: TrpcApiLog = {
const baseLogAttributes: TrpcApiLog = {
path,
auth: ctx.metadata.auth,
auth: null,
source: ctx.metadata.source,
trpcMiddleware: 'maybeAuthenticated',
unverifiedTeamId: ctx.teamId,
@@ -191,15 +199,23 @@ export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next, pat
const apiToken = await getApiTokenByToken({ token });
ctx.logger.info({
...infoToLog,
// Attach identifying attributes to the logger so every subsequent log line
// within this request (including errors) inherits them.
const trpcApiV2Logger = ctx.logger.child({
...baseLogAttributes,
auth: 'api',
userId: apiToken.user.id,
apiTokenId: apiToken.id,
} satisfies TrpcApiLog);
trpcApiV2Logger.info({
position: 'trpcProcedure',
});
return await next({
ctx: {
...ctx,
logger: trpcApiV2Logger,
user: apiToken.user,
teamId: apiToken.teamId,
session: null,
@@ -222,12 +238,25 @@ export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next, pat
});
}
trpcSessionLogger.info({
...infoToLog,
// Resolve `auth` once so it stays in sync between the logger bindings and
// the outgoing metadata.
const auth = ctx.session ? 'session' : null;
// Recreate the logger with a sub request ID to differentiate between batched
// requests, as well as identifying attributes so every subsequent log line
// (including errors) inherits them.
const trpcSessionLogger = ctx.logger.child({
...baseLogAttributes,
auth,
nonBatchedRequestId: alphaid(),
userId: ctx.user?.id,
apiTokenId: null,
} satisfies TrpcApiLog);
trpcSessionLogger.info({
position: 'trpcProcedure',
});
return await next({
ctx: {
...ctx,
@@ -243,7 +272,7 @@ export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next, pat
email: ctx.user.email,
}
: undefined,
auth: ctx.session ? 'session' : null,
auth,
} satisfies ApiRequestMetadata,
},
});
@@ -266,20 +295,24 @@ export const adminMiddleware = t.middleware(async ({ ctx, next, path }) => {
});
}
// Recreate the logger with a sub request ID to differentiate between batched requests.
// Recreate the logger with a sub request ID to differentiate between batched
// requests, as well as identifying attributes so every subsequent log line
// (including errors) inherits them.
const trpcSessionLogger = ctx.logger.child({
nonBatchedRequestId: alphaid(),
});
trpcSessionLogger.info({
unverifiedTeamId: ctx.teamId,
path,
auth: ctx.metadata.auth,
auth: 'session',
source: ctx.metadata.source,
userId: ctx.user.id,
apiTokenId: null,
trpcMiddleware: 'admin',
} satisfies TrpcApiLog);
trpcSessionLogger.info({
position: 'trpcProcedure',
});
return await next({
ctx: {
...ctx,
@@ -300,12 +333,12 @@ export const adminMiddleware = t.middleware(async ({ ctx, next, path }) => {
});
export const procedureMiddleware = t.middleware(async ({ ctx, next, path }) => {
// Recreate the logger with a sub request ID to differentiate between batched requests.
// Recreate the logger with a sub request ID to differentiate between batched
// requests, as well as identifying attributes so every subsequent log line
// (including errors) inherits them.
const trpcSessionLogger = ctx.logger.child({
nonBatchedRequestId: alphaid(),
});
trpcSessionLogger.info({
unverifiedTeamId: ctx.teamId,
path,
auth: ctx.metadata.auth,
source: ctx.metadata.source,
@@ -314,6 +347,10 @@ export const procedureMiddleware = t.middleware(async ({ ctx, next, path }) => {
trpcMiddleware: 'procedure',
} satisfies TrpcApiLog);
trpcSessionLogger.info({
position: 'trpcProcedure',
});
return await next({
ctx: {
...ctx,
+5 -1
View File
@@ -22,7 +22,7 @@ declare namespace NodeJS {
NEXT_PRIVATE_STRIPE_API_KEY: string;
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
NEXT_PUBLIC_UPLOAD_TRANSPORT?: 'database' | 's3';
NEXT_PUBLIC_UPLOAD_TRANSPORT?: 'database' | 's3' | 'azure-blob';
NEXT_PRIVATE_UPLOAD_ENDPOINT?: string;
NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE?: string;
NEXT_PRIVATE_UPLOAD_REGION?: string;
@@ -32,6 +32,10 @@ declare namespace NodeJS {
NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN?: string;
NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_ID?: string;
NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS?: string;
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME?: string;
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY?: string;
NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER?: string;
NEXT_PRIVATE_UPLOAD_AZURE_ENDPOINT?: string;
NEXT_PRIVATE_SIGNING_TRANSPORT?: 'local' | 'http' | 'gcloud-hsm';
NEXT_PRIVATE_SIGNING_PASSPHRASE?: string;
@@ -41,7 +41,7 @@ export const CopyTextButton = ({
<Button
type="button"
variant="none"
className="ml-2 h-7 rounded border bg-neutral-50 px-0.5 font-normal dark:border dark:border-neutral-500 dark:bg-neutral-600"
className="ml-2 h-7 rounded-md border-border bg-muted px-0.5 font-normal"
onClick={async () => onCopy()}
>
<AnimatePresence mode="wait" initial={false}>
@@ -56,7 +56,7 @@ export const CopyTextButton = ({
<div
className={cn(
'flex h-6 w-6 items-center justify-center rounded transition-all hover:bg-neutral-200 hover:active:bg-neutral-300 dark:hover:bg-neutral-500 dark:hover:active:bg-neutral-400',
'flex h-6 w-6 items-center justify-center rounded transition-all hover:bg-muted-foreground/10 hover:active:bg-muted-foreground/20',
{
'ml-1': Boolean(badgeContentCopied || badgeContentUncopied),
},
+1 -1
View File
@@ -83,7 +83,7 @@ const CommandTextInput = React.forwardRef<
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
{
'!ring-red-500 ring-2 transition-all': props['aria-invalid'],
'!ring-destructive ring-2 transition-all': props['aria-invalid'],
},
)}
{...props}
+6 -6
View File
@@ -92,7 +92,7 @@ export const DocumentDropzone = ({
// Disabled State
<div className="flex">
<motion.div
className="a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border border-muted-foreground/20 bg-white/80 px-2 py-4 backdrop-blur-sm group-hover:border-destructive/10 group-hover:bg-destructive/2 dark:bg-muted/80"
className="a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border border-muted-foreground/20 bg-background/80 px-2 py-4 backdrop-blur-sm group-hover:border-destructive/10 group-hover:bg-destructive/2"
variants={DocumentDropzoneDisabledCardLeftVariants}
>
<div className="h-2 w-full rounded-[2px] bg-muted-foreground/10 group-hover:bg-destructive/10" />
@@ -100,7 +100,7 @@ export const DocumentDropzone = ({
<div className="h-2 w-full rounded-[2px] bg-muted-foreground/10 group-hover:bg-destructive/10" />
</motion.div>
<motion.div
className="z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border border-muted-foreground/20 bg-white/80 px-2 py-4 backdrop-blur-sm group-hover:border-destructive/50 group-hover:bg-destructive/5 dark:bg-muted/80"
className="z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border border-muted-foreground/20 bg-background/80 px-2 py-4 backdrop-blur-sm group-hover:border-destructive/50 group-hover:bg-destructive/5"
variants={DocumentDropzoneDisabledCardCenterVariants}
>
<AlertTriangle
@@ -109,7 +109,7 @@ export const DocumentDropzone = ({
/>
</motion.div>
<motion.div
className="z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border border-muted-foreground/20 bg-white/80 px-2 py-4 backdrop-blur-sm group-hover:border-destructive/10 group-hover:bg-destructive/2 dark:bg-muted/80"
className="z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border border-muted-foreground/20 bg-background/80 px-2 py-4 backdrop-blur-sm group-hover:border-destructive/10 group-hover:bg-destructive/2"
variants={DocumentDropzoneDisabledCardRightVariants}
>
<div className="h-2 w-full rounded-[2px] bg-muted-foreground/10 group-hover:bg-destructive/10" />
@@ -121,7 +121,7 @@ export const DocumentDropzone = ({
// Non Disabled State
<div className="flex">
<motion.div
className="a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border border-muted-foreground/20 bg-white/80 px-2 py-4 backdrop-blur-sm group-hover:border-documenso/80 dark:bg-muted/80"
className="a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border border-muted-foreground/20 bg-background/80 px-2 py-4 backdrop-blur-sm group-hover:border-documenso/80"
variants={DocumentDropzoneCardLeftVariants}
>
<div className="h-2 w-full rounded-[2px] bg-muted-foreground/20 group-hover:bg-documenso" />
@@ -129,13 +129,13 @@ export const DocumentDropzone = ({
<div className="h-2 w-full rounded-[2px] bg-muted-foreground/20 group-hover:bg-documenso" />
</motion.div>
<motion.div
className="z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border border-muted-foreground/20 bg-white/80 px-2 py-4 backdrop-blur-sm group-hover:border-documenso/80 dark:bg-muted/80"
className="z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border border-muted-foreground/20 bg-background/80 px-2 py-4 backdrop-blur-sm group-hover:border-documenso/80"
variants={DocumentDropzoneCardCenterVariants}
>
<Plus strokeWidth="2px" className="h-12 w-12 text-muted-foreground/20 group-hover:text-documenso" />
</motion.div>
<motion.div
className="z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border border-muted-foreground/20 bg-white/80 px-2 py-4 backdrop-blur-sm group-hover:border-documenso/80 dark:bg-muted/80"
className="z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border border-muted-foreground/20 bg-background/80 px-2 py-4 backdrop-blur-sm group-hover:border-documenso/80"
variants={DocumentDropzoneCardRightVariants}
>
<div className="h-2 w-full rounded-[2px] bg-muted-foreground/20 group-hover:bg-documenso" />
@@ -920,7 +920,7 @@ export const AddFieldsFormPartial = ({
{hasErrors && (
<div className="mt-4">
<ul>
<li className="text-red-500 text-sm">
<li className="text-destructive text-sm">
<Trans>
To proceed further, please set at least one value for the{' '}
{emptyCheckboxFields.length > 0 ? 'Checkbox' : emptyRadioFields.length > 0 ? 'Radio' : 'Select'}{' '}
@@ -326,7 +326,7 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
<div className="mt-4">
<ul>
{errors.map((error, index) => (
<li className="text-red-500 text-sm" key={index}>
<li className="text-destructive text-sm" key={index}>
{error}
</li>
))}
@@ -240,7 +240,7 @@ export const CheckboxFieldAdvancedSettings = ({
/>
<button
type="button"
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center text-slate-500 hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center text-muted-foreground hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => removeValue(index)}
>
<Trash className="h-5 w-5" />
@@ -162,7 +162,7 @@ export const DropdownFieldAdvancedSettings = ({
<Input className="w-1/2" value={value.value} onChange={(e) => handleValueChange(index, e.target.value)} />
<button
type="button"
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center text-slate-500 hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center text-muted-foreground hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => removeValue(index)}
>
<Trash className="h-5 w-5" />
@@ -176,7 +176,7 @@ export const RadioFieldAdvancedSettings = ({
/>
<button
type="button"
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center text-slate-500 hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50 dark:text-white"
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center text-muted-foreground hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => removeValue(value.id)}
>
<Trash className="h-5 w-5" />
@@ -38,7 +38,7 @@ export const FormErrorMessage = ({ error, className }: FormErrorMessageProps) =>
opacity: 0,
y: 10,
}}
className={cn('text-red-500 text-xs', className)}
className={cn('text-destructive text-xs', className)}
>
{errorMessage}
</motion.p>
+1 -1
View File
@@ -156,7 +156,7 @@ const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<
y: 10,
}}
>
<p ref={ref} id={formMessageId} className={cn('text-red-500 text-xs', className)} {...props}>
<p ref={ref} id={formMessageId} className={cn('text-destructive text-xs', className)} {...props}>
{body}
</p>
</motion.div>
+1 -1
View File
@@ -12,7 +12,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type,
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className,
{
'!ring-red-500 ring-2 transition-all': props['aria-invalid'],
'!ring-destructive ring-2 transition-all': props['aria-invalid'],
},
)}
ref={ref}
@@ -119,7 +119,7 @@ export function MultiSelectCombobox<T = OptionValue>({
<AnimatePresence>
{loading ? (
<div className="flex items-center justify-center">
<Loader className="h-5 w-5 animate-spin text-gray-500 dark:text-gray-100" />
<Loader className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : (
<AnimateGenericFadeInOut className="flex w-full justify-between">
@@ -142,7 +142,7 @@ export function MultiSelectCombobox<T = OptionValue>({
{showClearButton && !loading && (
<div className="absolute top-0 right-8 bottom-0 flex items-center justify-center">
<button
className="flex h-4 w-4 items-center justify-center rounded-full bg-gray-300 dark:bg-neutral-700"
className="flex h-4 w-4 items-center justify-center rounded-full bg-muted-foreground/20"
onClick={() => onChange([])}
>
<XIcon className="h-3.5 w-3.5 text-muted-foreground" />
+1 -1
View File
@@ -31,7 +31,7 @@ const SelectTrigger = React.forwardRef<
<AnimatePresence>
{loading ? (
<div className="flex w-full items-center justify-center">
<Loader className="h-5 w-5 animate-spin text-gray-500 dark:text-gray-100" />
<Loader className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : (
<AnimateGenericFadeInOut className="flex w-full justify-between">
@@ -28,7 +28,7 @@ export const SignaturePadType = ({ className, value, defaultValue, onChange }: S
<input
data-testid="signature-pad-type-input"
placeholder={t`Type your signature`}
className="w-full bg-transparent px-4 text-center font-signature text-7xl text-black placeholder:text-4xl focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 dark:text-white"
className="w-full bg-transparent px-4 text-center font-signature text-7xl text-foreground placeholder:text-4xl focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0"
// style={{ color: selectedColor }}
value={value}
onChange={(event) => {
@@ -169,26 +169,23 @@ export const SignaturePad = ({
<TabsContent
value="draw"
className="relative flex aspect-signature-pad items-center justify-center rounded-md border border-border bg-neutral-50 text-center dark:bg-background"
className="relative flex aspect-signature-pad items-center justify-center rounded-md border border-border bg-muted/25 text-center"
>
<SignaturePadDraw className="h-full w-full" onChange={onDrawSignatureChange} value={drawSignature} />
</TabsContent>
<TabsContent
value="text"
className="relative flex aspect-signature-pad items-center justify-center rounded-md border border-border bg-neutral-50 text-center dark:bg-background"
className="relative flex aspect-signature-pad items-center justify-center rounded-md border border-border bg-muted/25 text-center"
>
<SignaturePadType value={typedSignature} defaultValue={fullName} onChange={onTypedSignatureChange} />
</TabsContent>
<TabsContent
value="image"
className={cn(
'relative aspect-signature-pad rounded-md border border-border bg-neutral-50 dark:bg-background',
{
'bg-white': imageSignature,
},
)}
className={cn('relative aspect-signature-pad rounded-md border border-border bg-muted/25', {
'bg-background': imageSignature,
})}
>
<SignaturePadUpload value={imageSignature} onChange={onImageSignatureChange} />
</TabsContent>
@@ -944,7 +944,7 @@ export const AddTemplateFieldsFormPartial = ({
{hasErrors && (
<div className="mt-4">
<ul>
<li className="text-red-500 text-sm">
<li className="text-destructive text-sm">
<Trans>
To proceed further, please set at least one value for the{' '}
{emptyCheckboxFields.length > 0 ? 'Checkbox' : emptyRadioFields.length > 0 ? 'Radio' : 'Select'}{' '}
@@ -725,7 +725,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
{isSignerDirectRecipient(signer) ? (
<Tooltip>
<TooltipTrigger className="col-span-1 mt-auto inline-flex h-10 w-10 items-center justify-center text-slate-500 hover:opacity-80">
<TooltipTrigger className="col-span-1 mt-auto inline-flex h-10 w-10 items-center justify-center text-muted-foreground hover:opacity-80">
<Link2Icon className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="z-9999 max-w-md p-4 text-foreground">
@@ -744,7 +744,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
) : (
<button
type="button"
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center justify-center text-slate-500 hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center justify-center text-muted-foreground hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
disabled={isSubmitting || signers.length === 1}
onClick={() => onRemoveSigner(index)}
data-testid="remove-placeholder-recipient-button"
+1 -1
View File
@@ -11,7 +11,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ classNa
'flex h-20 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
{
'!ring-red-500 ring-2 transition-all': props['aria-invalid'],
'!ring-destructive ring-2 transition-all': props['aria-invalid'],
},
)}
ref={ref}