mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
feat: start a document from pasted content or a link
A document no longer has to come off this machine. Paste text or Markdown and it is rendered to a PDF; give a link and the server fetches it, including a GitHub file page, which it reads as the file that page shows. Every source resolves to bytes in one place and is created by envelope.create, the same call an upload makes, so the editor and everything past it cannot tell them apart. The upload button and the drop zone now share that one creation path rather than each carrying a copy. The server dials http(s) only, and never an address that resolves inside the cluster — checked on the hostname, on every address it resolves to, and again on each redirect. It reads at most the upload limit and requires the bytes to be a PDF.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { useAnalytics } from '@hanzo/esign-lib/client-only/hooks/use-analytics';
|
||||
import { useSession } from '@hanzo/esign-lib/client-only/providers/session';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@hanzo/esign-lib/constants/time-zones';
|
||||
import { useLimits } from '@hanzo/esign-lib/server-only/limits/provider/client';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@hanzo/esign-lib/utils/teams';
|
||||
import type {
|
||||
TCreateEnvelopePayload,
|
||||
TCreateEnvelopeResponse,
|
||||
} from '@hanzo/esign-trpc/server/envelope-router/create-envelope.types';
|
||||
import { useZapMutation } from '@hanzo/esign-trpc/zap/react';
|
||||
import { useToast } from '@hanzo/esign-ui/primitives/use-toast';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { uploadErrorMessage } from './upload-error';
|
||||
|
||||
export type CreateEnvelopeOptions = {
|
||||
type: EnvelopeType;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an envelope from files and open it in the editor.
|
||||
*
|
||||
* Every way of starting a document ends here — a picked file, a drop, a paste,
|
||||
* a link. They differ only in how the bytes were come by; once they are Files
|
||||
* there is one call, one error message, one place the user lands.
|
||||
*/
|
||||
export const useCreateEnvelope = ({ type, folderId }: CreateEnvelopeOptions) => {
|
||||
const { t, i18n } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { user } = useSession();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
const navigate = useNavigate();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const { refreshLimits } = useLimits();
|
||||
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
const { mutateAsync: createEnvelope } = useZapMutation<TCreateEnvelopeResponse, FormData>(
|
||||
'envelope.create',
|
||||
);
|
||||
|
||||
const timezone =
|
||||
TIME_ZONES.find((zone) => zone === Intl.DateTimeFormat().resolvedOptions().timeZone) ??
|
||||
DEFAULT_DOCUMENT_TIME_ZONE;
|
||||
|
||||
/** Resolves the new envelope's id, or undefined once the failure is shown. */
|
||||
const create = async (files: File[]): Promise<string | undefined> => {
|
||||
try {
|
||||
setIsCreating(true);
|
||||
|
||||
const payload = {
|
||||
folderId,
|
||||
type,
|
||||
title: files[0].name,
|
||||
meta: {
|
||||
timezone,
|
||||
},
|
||||
} satisfies TCreateEnvelopePayload;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('payload', JSON.stringify(payload));
|
||||
|
||||
for (const file of files) {
|
||||
formData.append('files', file);
|
||||
}
|
||||
|
||||
const { id } = await createEnvelope(formData);
|
||||
|
||||
void refreshLimits();
|
||||
|
||||
toast({
|
||||
title: type === EnvelopeType.DOCUMENT ? t`Document uploaded` : t`Template uploaded`,
|
||||
description:
|
||||
type === EnvelopeType.DOCUMENT
|
||||
? t`Your document has been uploaded successfully.`
|
||||
: t`Your template has been uploaded successfully.`,
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
if (type === EnvelopeType.DOCUMENT) {
|
||||
analytics.capture('App: Document Uploaded', {
|
||||
userId: user.id,
|
||||
documentId: id,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
const pathPrefix =
|
||||
type === EnvelopeType.DOCUMENT
|
||||
? formatDocumentsPath(team.url)
|
||||
: formatTemplatesPath(team.url);
|
||||
|
||||
const aiQueryParam = team.preferences.aiFeaturesEnabled ? '?ai=true' : '';
|
||||
|
||||
await navigate(`${pathPrefix}/${id}/edit${aiQueryParam}`);
|
||||
|
||||
return id;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: t`Error`,
|
||||
description: i18n._(uploadErrorMessage(err, type)),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { create, isCreating };
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
@@ -14,25 +14,17 @@ import {
|
||||
import { Link, useNavigate, useParams } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useAnalytics } from '@hanzo/esign-lib/client-only/hooks/use-analytics';
|
||||
import { useCurrentOrganisation } from '@hanzo/esign-lib/client-only/providers/organisation';
|
||||
import { useSession } from '@hanzo/esign-lib/client-only/providers/session';
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@hanzo/esign-lib/constants/app';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@hanzo/esign-lib/constants/time-zones';
|
||||
import { useLimits } from '@hanzo/esign-lib/server-only/limits/provider/client';
|
||||
import { megabytesToBytes } from '@hanzo/esign-lib/universal/unit-convertions';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@hanzo/esign-lib/utils/teams';
|
||||
import type {
|
||||
TCreateEnvelopePayload,
|
||||
TCreateEnvelopeResponse,
|
||||
} from '@hanzo/esign-trpc/server/envelope-router/create-envelope.types';
|
||||
import { useZapMutation } from '@hanzo/esign-trpc/zap/react';
|
||||
import { cn } from '@hanzo/esign-ui/lib/utils';
|
||||
import { useToast } from '@hanzo/esign-ui/primitives/use-toast';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { uploadErrorMessage } from './upload-error';
|
||||
import { useCreateEnvelope } from './create';
|
||||
|
||||
export interface EnvelopeDropZoneWrapperProps {
|
||||
children: ReactNode;
|
||||
@@ -45,7 +37,7 @@ export const EnvelopeDropZoneWrapper = ({
|
||||
type,
|
||||
className,
|
||||
}: EnvelopeDropZoneWrapperProps) => {
|
||||
const { t, i18n } = useLingui();
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { user } = useSession();
|
||||
const { folderId } = useParams();
|
||||
@@ -53,20 +45,11 @@ export const EnvelopeDropZoneWrapper = ({
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const analytics = useAnalytics();
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { quota, remaining, maximumEnvelopeItemCount } = useLimits();
|
||||
|
||||
const userTimezone =
|
||||
TIME_ZONES.find((timezone) => timezone === Intl.DateTimeFormat().resolvedOptions().timeZone) ??
|
||||
DEFAULT_DOCUMENT_TIME_ZONE;
|
||||
|
||||
const { quota, remaining, refreshLimits, maximumEnvelopeItemCount } = useLimits();
|
||||
|
||||
const { mutateAsync: createEnvelope } = useZapMutation<TCreateEnvelopeResponse, FormData>(
|
||||
'envelope.create',
|
||||
);
|
||||
const { create, isCreating } = useCreateEnvelope({ type, folderId });
|
||||
|
||||
const isUploadDisabled = remaining.documents === 0 || !user.emailVerified;
|
||||
|
||||
@@ -76,67 +59,7 @@ export const EnvelopeDropZoneWrapper = ({
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const payload = {
|
||||
folderId,
|
||||
type,
|
||||
title: files[0].name,
|
||||
meta: {
|
||||
timezone: userTimezone,
|
||||
},
|
||||
} satisfies TCreateEnvelopePayload;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('payload', JSON.stringify(payload));
|
||||
|
||||
for (const file of files) {
|
||||
formData.append('files', file);
|
||||
}
|
||||
|
||||
const { id } = await createEnvelope(formData);
|
||||
|
||||
void refreshLimits();
|
||||
|
||||
toast({
|
||||
title: type === EnvelopeType.DOCUMENT ? t`Document uploaded` : t`Template uploaded`,
|
||||
description:
|
||||
type === EnvelopeType.DOCUMENT
|
||||
? t`Your document has been uploaded successfully.`
|
||||
: t`Your template has been uploaded successfully.`,
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
if (type === EnvelopeType.DOCUMENT) {
|
||||
analytics.capture('App: Document Uploaded', {
|
||||
userId: user.id,
|
||||
documentId: id,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
const pathPrefix =
|
||||
type === EnvelopeType.DOCUMENT
|
||||
? formatDocumentsPath(team.url)
|
||||
: formatTemplatesPath(team.url);
|
||||
|
||||
const aiQueryParam = team.preferences.aiFeaturesEnabled ? '?ai=true' : '';
|
||||
|
||||
await navigate(`${pathPrefix}/${id}/edit${aiQueryParam}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: t`Error`,
|
||||
description: i18n._(uploadErrorMessage(err, type)),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
await create(files);
|
||||
};
|
||||
|
||||
const onFileDropRejected = (fileRejections: FileRejection[]) => {
|
||||
@@ -257,7 +180,7 @@ export const EnvelopeDropZoneWrapper = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
{isCreating && (
|
||||
<div className="absolute inset-0 z-50 bg-muted/30 backdrop-blur-[2px]">
|
||||
<div className="pointer-events-none flex h-1/2 w-full flex-col items-center justify-center">
|
||||
<Loader className="h-12 w-12 animate-spin text-primary" />
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import type { EnvelopeType } from '@prisma/client';
|
||||
import { ClipboardTypeIcon, LinkIcon, PlusIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useSession } from '@hanzo/esign-lib/client-only/providers/session';
|
||||
import { useLimits } from '@hanzo/esign-lib/server-only/limits/provider/client';
|
||||
import type { TImportSource } from '@hanzo/esign-lib/universal/import';
|
||||
import { TEXT_LIMIT } from '@hanzo/esign-lib/universal/import';
|
||||
import { Button } from '@hanzo/esign-ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@hanzo/esign-ui/primitives/dialog';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@hanzo/esign-ui/primitives/dropdown-menu';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@hanzo/esign-ui/primitives/form/form';
|
||||
import { Input } from '@hanzo/esign-ui/primitives/input';
|
||||
import { Textarea } from '@hanzo/esign-ui/primitives/textarea';
|
||||
import { useToast } from '@hanzo/esign-ui/primitives/use-toast';
|
||||
|
||||
import { importPdf } from '../../../../server/api/files/files.client';
|
||||
import { useCreateEnvelope } from './create';
|
||||
import { uploadErrorMessage } from './upload-error';
|
||||
|
||||
export type EnvelopeImportMenuProps = {
|
||||
type: EnvelopeType;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
const ZPasteFormSchema = z.object({
|
||||
title: z.string().trim().max(255).optional(),
|
||||
text: z.string().trim().min(1).max(TEXT_LIMIT),
|
||||
});
|
||||
|
||||
const ZUrlFormSchema = z.object({
|
||||
url: z.string().trim().min(1).max(2048),
|
||||
});
|
||||
|
||||
type TPasteForm = z.infer<typeof ZPasteFormSchema>;
|
||||
type TUrlForm = z.infer<typeof ZUrlFormSchema>;
|
||||
|
||||
/** The first line that says something, as the document's name. */
|
||||
const firstLine = (text: string) =>
|
||||
text
|
||||
.split('\n')
|
||||
.map((line) =>
|
||||
line
|
||||
.replace(/^\s*(#{1,6}|[-*+]|\d{1,3}[.)])\s+/, '')
|
||||
.replace(/[*_`#]/g, '')
|
||||
.trim(),
|
||||
)
|
||||
.find((line) => line.length > 0)
|
||||
?.slice(0, 255);
|
||||
|
||||
/**
|
||||
* Start a document from something other than a file on this machine.
|
||||
*
|
||||
* Each entry collects what its source needs and hands back a source; the server
|
||||
* resolves it to a PDF and the envelope is created from those bytes, so nothing
|
||||
* past this menu knows where the document came from.
|
||||
*/
|
||||
export const EnvelopeImportMenu = ({ type, folderId }: EnvelopeImportMenuProps) => {
|
||||
const { t, i18n } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { user } = useSession();
|
||||
|
||||
const { remaining } = useLimits();
|
||||
|
||||
const { create, isCreating } = useCreateEnvelope({ type, folderId });
|
||||
|
||||
const [source, setSource] = useState<TImportSource['kind'] | null>(null);
|
||||
|
||||
const paste = useForm<TPasteForm>({
|
||||
resolver: zodResolver(ZPasteFormSchema),
|
||||
defaultValues: { title: '', text: '' },
|
||||
});
|
||||
|
||||
const link = useForm<TUrlForm>({
|
||||
resolver: zodResolver(ZUrlFormSchema),
|
||||
defaultValues: { url: '' },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (source === null) {
|
||||
paste.reset();
|
||||
link.reset();
|
||||
}
|
||||
}, [source, paste, link]);
|
||||
|
||||
const submit = async (resolved: TImportSource) => {
|
||||
try {
|
||||
if (await create([await importPdf(resolved)])) {
|
||||
setSource(null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: t`Import failed`,
|
||||
description: i18n._(uploadErrorMessage(err, type)),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const busy = isCreating || paste.formState.isSubmitting || link.formState.isSubmitting;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
loading={busy}
|
||||
disabled={remaining.documents === 0 || !user.emailVerified}
|
||||
data-testid="envelope-import-button"
|
||||
>
|
||||
{!busy && <PlusIcon className="mr-2 h-4 w-4" />}
|
||||
<Trans>Import</Trans>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onSelect={() => setSource('text')}>
|
||||
<ClipboardTypeIcon className="mr-2 h-4 w-4" />
|
||||
<Trans>Paste content</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onSelect={() => setSource('url')}>
|
||||
<LinkIcon className="mr-2 h-4 w-4" />
|
||||
<Trans>Import from URL</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/*
|
||||
A source is one more item here and one more case in the resolver
|
||||
(esign-lib/server-only/import.ts). Google Drive is next and waits on
|
||||
a Google OAuth client id (drive.file) plus a Picker API key: the
|
||||
picker returns a file id, the resolver exchanges it for bytes.
|
||||
*/}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Dialog open={source === 'text'} onOpenChange={(open) => !open && setSource(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Paste content</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Paste text or Markdown. It becomes a PDF you can place fields on and send for
|
||||
signature.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...paste}>
|
||||
<form
|
||||
onSubmit={paste.handleSubmit(
|
||||
async ({ title, text }) =>
|
||||
await submit({
|
||||
kind: 'text',
|
||||
title: title?.trim() || firstLine(text) || t`Pasted document`,
|
||||
text,
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<fieldset disabled={busy} className="space-y-4">
|
||||
<FormField
|
||||
control={paste.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Title</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t`Taken from the first line`} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={paste.control}
|
||||
name="text"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Content</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="h-64 font-mono text-xs"
|
||||
placeholder={t`# Mutual NDA — both parties agree...`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => setSource(null)}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<Button type="submit" loading={busy}>
|
||||
<Trans>Create</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={source === 'url'} onOpenChange={(open) => !open && setSource(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Import from URL</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
The address of a PDF we can reach. A GitHub file page works — paste it as you see
|
||||
it.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...link}>
|
||||
<form
|
||||
onSubmit={link.handleSubmit(async ({ url }) => await submit({ kind: 'url', url }))}
|
||||
>
|
||||
<fieldset disabled={busy} className="space-y-4">
|
||||
<FormField
|
||||
control={link.control}
|
||||
name="url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Document URL</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="url"
|
||||
placeholder="https://github.com/hanzoai/sign/blob/main/terms.pdf"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => setSource(null)}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<Button type="submit" loading={busy}>
|
||||
<Trans>Import</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +1,15 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { msg, plural } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { ErrorCode as DropzoneErrorCode, type FileRejection } from 'react-dropzone';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { useCurrentOrganisation } from '@hanzo/esign-lib/client-only/providers/organisation';
|
||||
import { useSession } from '@hanzo/esign-lib/client-only/providers/session';
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@hanzo/esign-lib/constants/app';
|
||||
import { TIME_ZONES } from '@hanzo/esign-lib/constants/time-zones';
|
||||
import { useLimits } from '@hanzo/esign-lib/server-only/limits/provider/client';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@hanzo/esign-lib/utils/teams';
|
||||
import type {
|
||||
TCreateEnvelopePayload,
|
||||
TCreateEnvelopeResponse,
|
||||
} from '@hanzo/esign-trpc/server/envelope-router/create-envelope.types';
|
||||
import { useZapMutation } from '@hanzo/esign-trpc/zap/react';
|
||||
import { cn } from '@hanzo/esign-ui/lib/utils';
|
||||
import { DocumentUploadButton } from '@hanzo/esign-ui/primitives/document-upload-button';
|
||||
import {
|
||||
@@ -30,7 +22,7 @@ import { useToast } from '@hanzo/esign-ui/primitives/use-toast';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { uploadErrorMessage } from './upload-error';
|
||||
import { useCreateEnvelope } from './create';
|
||||
|
||||
export type EnvelopeUploadButtonProps = {
|
||||
className?: string;
|
||||
@@ -42,26 +34,17 @@ export type EnvelopeUploadButtonProps = {
|
||||
* Upload an envelope
|
||||
*/
|
||||
export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUploadButtonProps) => {
|
||||
const { t, i18n } = useLingui();
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { user } = useSession();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const userTimezone = TIME_ZONES.find(
|
||||
(timezone) => timezone === Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
);
|
||||
const { quota, remaining, maximumEnvelopeItemCount } = useLimits();
|
||||
|
||||
const { quota, remaining, refreshLimits, maximumEnvelopeItemCount } = useLimits();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { mutateAsync: createEnvelope } = useZapMutation<TCreateEnvelopeResponse, FormData>(
|
||||
'envelope.create',
|
||||
);
|
||||
const { create, isCreating } = useCreateEnvelope({ type, folderId });
|
||||
|
||||
const disabledMessage = useMemo(() => {
|
||||
if (organisation.subscription && remaining.documents === 0) {
|
||||
@@ -79,66 +62,6 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [remaining.documents, user.emailVerified, team]);
|
||||
|
||||
const onFileDrop = async (files: File[]) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const payload = {
|
||||
folderId,
|
||||
type,
|
||||
title: files[0].name,
|
||||
meta: {
|
||||
timezone: userTimezone,
|
||||
},
|
||||
} satisfies TCreateEnvelopePayload;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('payload', JSON.stringify(payload));
|
||||
|
||||
for (const file of files) {
|
||||
formData.append('files', file);
|
||||
}
|
||||
|
||||
const { id } = await createEnvelope(formData).catch((error) => {
|
||||
console.error(error);
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
void refreshLimits();
|
||||
|
||||
const pathPrefix =
|
||||
type === EnvelopeType.DOCUMENT
|
||||
? formatDocumentsPath(team.url)
|
||||
: formatTemplatesPath(team.url);
|
||||
|
||||
const aiQueryParam = team.preferences.aiFeaturesEnabled ? '?ai=true' : '';
|
||||
|
||||
await navigate(`${pathPrefix}/${id}/edit${aiQueryParam}`);
|
||||
|
||||
toast({
|
||||
title: type === EnvelopeType.DOCUMENT ? t`Document uploaded` : t`Template uploaded`,
|
||||
description:
|
||||
type === EnvelopeType.DOCUMENT
|
||||
? t`Your document has been uploaded successfully.`
|
||||
: t`Your template has been uploaded successfully.`,
|
||||
duration: 5000,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: t`Error`,
|
||||
description: i18n._(uploadErrorMessage(err, type)),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onFileDropRejected = (fileRejections: FileRejection[]) => {
|
||||
const maxItemsReached = fileRejections.some((fileRejection) =>
|
||||
fileRejection.errors.some((error) => error.code === DropzoneErrorCode.TooManyFiles),
|
||||
@@ -172,10 +95,10 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<DocumentUploadButton
|
||||
loading={isLoading}
|
||||
loading={isCreating}
|
||||
disabled={remaining.documents === 0 || !user.emailVerified}
|
||||
disabledMessage={disabledMessage}
|
||||
onDrop={onFileDrop}
|
||||
onDrop={(files) => void create(files)}
|
||||
onDropRejected={onFileDropRejected}
|
||||
type={type}
|
||||
internalVersion="2"
|
||||
|
||||
@@ -3,7 +3,9 @@ import { msg } from '@lingui/core/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@hanzo/esign-lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@hanzo/esign-lib/errors/app-error';
|
||||
import { ImportError } from '@hanzo/esign-lib/universal/import';
|
||||
|
||||
/**
|
||||
* Why an upload failed, worded for the thing being uploaded. Every upload
|
||||
@@ -12,6 +14,13 @@ import { AppError, AppErrorCode } from '@hanzo/esign-lib/errors/app-error';
|
||||
export const uploadErrorMessage = (err: unknown, type: EnvelopeType): MessageDescriptor =>
|
||||
match(AppError.parseError(err).code)
|
||||
.with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs.`)
|
||||
.with(ImportError.url, () => msg`That address cannot be read. Use a public http(s) link.`)
|
||||
.with(ImportError.fetch, () => msg`We could not reach that address.`)
|
||||
.with(ImportError.pdf, () => msg`That address does not lead to a PDF.`)
|
||||
.with(
|
||||
ImportError.size,
|
||||
() => msg`That document is larger than ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB.`,
|
||||
)
|
||||
.with(
|
||||
AppErrorCode.LIMIT_EXCEEDED,
|
||||
() => msg`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
|
||||
@@ -22,6 +22,7 @@ import { DocumentUploadButtonLegacy } from '~/components/general/document/docume
|
||||
import { FolderCard, FolderCardEmpty } from '~/components/general/folder/folder-card';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { EnvelopeImportMenu } from '../envelope/envelope-import-menu';
|
||||
import { EnvelopeUploadButton } from '../envelope/envelope-upload-button';
|
||||
|
||||
export type FolderGridProps = {
|
||||
@@ -102,6 +103,8 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
||||
<div className="flex gap-4 sm:flex-row sm:justify-end">
|
||||
<EnvelopeUploadButton type={type} folderId={parentId || undefined} />
|
||||
|
||||
<EnvelopeImportMenu type={type} folderId={parentId || undefined} />
|
||||
|
||||
{/* If you delete this, delete the component as well. */}
|
||||
{organisation.organisationClaim.flags.allowLegacyEnvelopes && (
|
||||
<DocumentUploadButtonLegacy type={type} />
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { AppError } from '@hanzo/esign-lib/errors/app-error';
|
||||
import type { TImportSource } from '@hanzo/esign-lib/universal/import';
|
||||
import { ImportError } from '@hanzo/esign-lib/universal/import';
|
||||
|
||||
/** The name the server gave the document it resolved. */
|
||||
const filename = (response: Response): string => {
|
||||
const header = response.headers.get('content-disposition') ?? '';
|
||||
|
||||
const encoded = /filename\*=UTF-8''([^;]+)/i.exec(header);
|
||||
|
||||
if (encoded) {
|
||||
return decodeURIComponent(encoded[1]);
|
||||
}
|
||||
|
||||
return /filename="?([^";]+)"?/i.exec(header)?.[1] ?? 'document.pdf';
|
||||
};
|
||||
|
||||
/**
|
||||
* Read an import source as a PDF.
|
||||
*
|
||||
* The server resolves it — a link is fetched there, so no address the browser
|
||||
* cannot reach is a problem and no source needs the browser's permission. What
|
||||
* comes back is a File, which is what the upload path already knows how to
|
||||
* create an envelope from.
|
||||
*/
|
||||
export const importPdf = async (source: TImportSource): Promise<File> => {
|
||||
const response = await fetch('/api/files/import', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(source),
|
||||
}).catch(() => null);
|
||||
|
||||
if (!response) {
|
||||
throw new AppError(ImportError.fetch);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const body: unknown = await response.json().catch(() => null);
|
||||
|
||||
throw AppError.parseFromJSON(body) ?? new AppError(ImportError.fetch);
|
||||
}
|
||||
|
||||
return new File([await response.blob()], filename(response), { type: 'application/pdf' });
|
||||
};
|
||||
@@ -1,12 +1,15 @@
|
||||
import { sValidator } from '@hono/standard-validator';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import contentDisposition from 'content-disposition';
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import { getOptionalSession } from '@hanzo/esign-auth/server/lib/utils/get-session';
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@hanzo/esign-lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@hanzo/esign-lib/errors/app-error';
|
||||
import { verifyEmbeddingPresignToken } from '@hanzo/esign-lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||
import { resolve } from '@hanzo/esign-lib/server-only/import';
|
||||
import { getTeamById } from '@hanzo/esign-lib/server-only/team/get-team';
|
||||
import { ZImportSourceSchema } from '@hanzo/esign-lib/universal/import';
|
||||
import { putNormalizedPdfFileServerSide } from '@hanzo/esign-lib/universal/upload/put-file.server';
|
||||
import { getPresignPostUrl } from '@hanzo/esign-lib/universal/upload/server-actions';
|
||||
import { prisma } from '@hanzo/esign-prisma';
|
||||
@@ -56,6 +59,38 @@ export const filesRoute = new Hono<HonoEnv>()
|
||||
return c.json({ error: 'Upload failed' }, 500);
|
||||
}
|
||||
})
|
||||
/**
|
||||
* Resolves an import source — a link, pasted content — to the PDF it stands
|
||||
* for. The browser turns the answer back into a File and creates the envelope
|
||||
* with it, so an import and an upload are the same thing from here on.
|
||||
*/
|
||||
.post('/import', sValidator('json', ZImportSourceSchema), async (c) => {
|
||||
const session = await getOptionalSession(c);
|
||||
|
||||
if (!session.user) {
|
||||
return c.json({ error: 'Unauthorized' }, 401);
|
||||
}
|
||||
|
||||
try {
|
||||
const { name, bytes } = await resolve(c.req.valid('json'));
|
||||
|
||||
// The bytes themselves, not their backing buffer — a rendered PDF sits in
|
||||
// a pooled one, and the whole pool would go over the wire.
|
||||
return new Response(bytes, {
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': contentDisposition(name),
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
c.get('logger').info({ event: 'files.import.failed', code: error.code });
|
||||
|
||||
return c.json(AppError.toJSON(error), error.statusCode === 400 ? 400 : 500);
|
||||
}
|
||||
})
|
||||
.post('/presigned-post-url', sValidator('json', ZGetPresignedPostUrlRequestSchema), async (c) => {
|
||||
const { fileName, contentType } = c.req.valid('json');
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ app.route('/api/auth', auth);
|
||||
|
||||
// Files route.
|
||||
app.use('/api/files/upload-pdf', fileRateLimitMiddleware);
|
||||
app.use('/api/files/import', fileRateLimitMiddleware);
|
||||
app.use('/api/files/presigned-post-url', fileRateLimitMiddleware);
|
||||
app.route('/api/files', filesRoute);
|
||||
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Importing a document from somewhere other than a file picker.
|
||||
*
|
||||
* Two things have to hold. The URL policy decides which addresses the server
|
||||
* will dial on a caller's behalf, so it is the boundary between "fetch a public
|
||||
* PDF" and "read something that only exists inside the cluster" — every refusal
|
||||
* below is an address the server must never reach. And a pasted note has to come
|
||||
* back as a real PDF, since it is handed to `envelope.create` as if it had been
|
||||
* uploaded.
|
||||
*
|
||||
* Run: npx tsx --test packages/lib/__tests__/import.test.ts
|
||||
*/
|
||||
import assert from 'node:assert/strict';
|
||||
import { createServer } from 'node:http';
|
||||
import type { AddressInfo } from 'node:net';
|
||||
import { describe, test } from 'node:test';
|
||||
|
||||
import { AppError } from '../errors/app-error';
|
||||
import { resolve } from '../server-only/import';
|
||||
import { ImportError, isBlocked, isPdf, parseUrl } from '../universal/import';
|
||||
|
||||
const refused = (address: string) => {
|
||||
const err = (() => {
|
||||
try {
|
||||
parseUrl(address);
|
||||
return null;
|
||||
} catch (caught) {
|
||||
return AppError.parseError(caught);
|
||||
}
|
||||
})();
|
||||
|
||||
assert.ok(err, `${address} was accepted`);
|
||||
assert.equal(err.code, ImportError.url);
|
||||
};
|
||||
|
||||
describe('import url policy', () => {
|
||||
test('only http(s) is fetchable', () => {
|
||||
for (const address of [
|
||||
'file:///etc/passwd',
|
||||
'ftp://example.com/a.pdf',
|
||||
'gopher://example.com/a.pdf',
|
||||
'data:application/pdf;base64,JVBERi0=',
|
||||
'not a url',
|
||||
'',
|
||||
]) {
|
||||
refused(address);
|
||||
}
|
||||
|
||||
assert.equal(parseUrl('https://example.com/a.pdf').protocol, 'https:');
|
||||
assert.equal(parseUrl('http://example.com/a.pdf').protocol, 'http:');
|
||||
});
|
||||
|
||||
test('an address inside the cluster is refused', () => {
|
||||
for (const address of [
|
||||
'http://localhost:3000/a.pdf',
|
||||
'http://app.localhost/a.pdf',
|
||||
'http://sign.internal/a.pdf',
|
||||
'http://printer.local/a.pdf',
|
||||
'http://metadata.google.internal/computeMetadata/v1/',
|
||||
'http://127.0.0.1/a.pdf',
|
||||
'http://127.1.2.3/a.pdf',
|
||||
'http://10.0.0.1/a.pdf',
|
||||
'http://172.16.0.1/a.pdf',
|
||||
'http://172.31.255.254/a.pdf',
|
||||
'http://192.168.1.1/a.pdf',
|
||||
'http://169.254.169.254/latest/meta-data/',
|
||||
'http://100.64.0.1/a.pdf',
|
||||
'http://0.0.0.0/a.pdf',
|
||||
'http://[::1]/a.pdf',
|
||||
'http://[::]/a.pdf',
|
||||
'http://[fd00::1]/a.pdf',
|
||||
'http://[fe80::1]/a.pdf',
|
||||
'http://[::ffff:127.0.0.1]/a.pdf',
|
||||
]) {
|
||||
refused(address);
|
||||
}
|
||||
});
|
||||
|
||||
test('the spellings a URL normalises away are refused too', () => {
|
||||
// Every one of these is 127.0.0.1 by the time the URL parser is done.
|
||||
for (const address of [
|
||||
'http://2130706433/a.pdf',
|
||||
'http://0177.0.0.1/a.pdf',
|
||||
'http://0x7f.0.0.1/a.pdf',
|
||||
'http://LOCALHOST/a.pdf',
|
||||
]) {
|
||||
refused(address);
|
||||
}
|
||||
});
|
||||
|
||||
test('credentials in the address are refused', () => {
|
||||
refused('https://user:[email protected]/a.pdf');
|
||||
});
|
||||
|
||||
test('a public address is accepted, 172.32 and 172.15 among them', () => {
|
||||
for (const address of [
|
||||
'https://example.com/a.pdf',
|
||||
'https://raw.githubusercontent.com/o/r/main/a.pdf',
|
||||
'http://172.32.0.1/a.pdf',
|
||||
'http://172.15.0.1/a.pdf',
|
||||
'http://8.8.8.8/a.pdf',
|
||||
'http://[2001:db8::1]/a.pdf',
|
||||
]) {
|
||||
assert.ok(parseUrl(address) instanceof URL, `${address} was refused`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the same predicate reads a resolved address', () => {
|
||||
assert.equal(isBlocked('169.254.169.254'), true);
|
||||
assert.equal(isBlocked('::1'), true);
|
||||
assert.equal(isBlocked('93.184.216.34'), false);
|
||||
});
|
||||
|
||||
test('a GitHub page URL is read as the file it shows', () => {
|
||||
assert.equal(
|
||||
parseUrl('https://github.com/hanzoai/sign/blob/main/docs/terms.pdf').toString(),
|
||||
'https://raw.githubusercontent.com/hanzoai/sign/main/docs/terms.pdf',
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
parseUrl('https://github.com/hanzoai/sign/raw/v1.2.3/a/b/terms.pdf').toString(),
|
||||
'https://raw.githubusercontent.com/hanzoai/sign/v1.2.3/a/b/terms.pdf',
|
||||
);
|
||||
|
||||
// A repo page carries no file, so it is left alone and answers as HTML —
|
||||
// which the PDF check then refuses.
|
||||
assert.equal(
|
||||
parseUrl('https://github.com/hanzoai/sign').toString(),
|
||||
'https://github.com/hanzoai/sign',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('import url fetch', () => {
|
||||
test('the resolver applies the policy, not just the parser', async () => {
|
||||
const server = createServer((_req, res) => {
|
||||
res.writeHead(200, { 'content-type': 'application/pdf' });
|
||||
res.end('%PDF-1.7\nnot yours\n%%EOF\n');
|
||||
});
|
||||
|
||||
await new Promise<void>((ready) => {
|
||||
server.listen(0, '127.0.0.1', ready);
|
||||
});
|
||||
|
||||
const { port } = server.address() as AddressInfo;
|
||||
let reached = false;
|
||||
|
||||
server.on('request', () => {
|
||||
reached = true;
|
||||
});
|
||||
|
||||
try {
|
||||
const err = await resolve({ kind: 'url', url: `http://127.0.0.1:${port}/a.pdf` }).then(
|
||||
() => null,
|
||||
(caught: unknown) => AppError.parseError(caught),
|
||||
);
|
||||
|
||||
assert.ok(err, 'a loopback address was fetched');
|
||||
assert.equal(err.code, ImportError.url);
|
||||
assert.equal(reached, false, 'the server was dialled anyway');
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('pdf recognition', () => {
|
||||
const bytes = (s: string) => new TextEncoder().encode(s);
|
||||
|
||||
test('a PDF is recognised by its header, junk in front of it or not', () => {
|
||||
assert.equal(isPdf(bytes('%PDF-1.7\n...')), true);
|
||||
assert.equal(isPdf(bytes(`${'\n'.repeat(40)}%PDF-1.4`)), true);
|
||||
});
|
||||
|
||||
test('anything else is not a PDF', () => {
|
||||
assert.equal(isPdf(bytes('<!doctype html><html>404</html>')), false);
|
||||
assert.equal(isPdf(bytes('')), false);
|
||||
assert.equal(isPdf(bytes('%PDF')), false);
|
||||
assert.equal(isPdf(bytes(`${'x'.repeat(2000)}%PDF-1.7`)), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pasted content', () => {
|
||||
test('text becomes a PDF named after its title', async () => {
|
||||
const { name, bytes } = await resolve({
|
||||
kind: 'text',
|
||||
title: 'Mutual NDA',
|
||||
text: '# Mutual NDA\n\nBoth parties agree.\n\n- One\n- Two\n',
|
||||
});
|
||||
|
||||
assert.equal(name, 'Mutual NDA.pdf');
|
||||
assert.equal(isPdf(bytes), true);
|
||||
assert.ok(bytes.length > 500, `only ${bytes.length} bytes`);
|
||||
});
|
||||
|
||||
test('a long paste runs onto further pages', async () => {
|
||||
const one = await resolve({ kind: 'text', title: 'One', text: 'A single line.' });
|
||||
const many = await resolve({
|
||||
kind: 'text',
|
||||
title: 'Many',
|
||||
text: Array.from({ length: 400 }, (_, i) => `Clause ${i + 1}. The parties agree.`).join('\n'),
|
||||
});
|
||||
|
||||
const pages = (bytes: Uint8Array) =>
|
||||
(Buffer.from(bytes).toString('latin1').match(/\/Type\s*\/Page[^s]/g) ?? []).length;
|
||||
|
||||
assert.equal(pages(one.bytes), 1);
|
||||
assert.ok(pages(many.bytes) > 5, `expected several pages, got ${pages(many.bytes)}`);
|
||||
});
|
||||
|
||||
test('typography a paste carries does not break the render', async () => {
|
||||
const { bytes } = await resolve({
|
||||
kind: 'text',
|
||||
title: 'Odd',
|
||||
text: 'A “quoted” em—dash, an ellipsis… a 🙂 and a tab\there.',
|
||||
});
|
||||
|
||||
assert.equal(isPdf(bytes), true);
|
||||
});
|
||||
|
||||
test('a title that already ends in .pdf is not doubled', async () => {
|
||||
const { name } = await resolve({ kind: 'text', title: 'terms.pdf', text: 'Hello.' });
|
||||
|
||||
assert.equal(name, 'terms.pdf');
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"test": "tsx --test __tests__/*.test.ts",
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import { PDF, StandardFonts, layoutText } from '@libpdf/core';
|
||||
import { lookup } from 'node:dns/promises';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '../constants/app';
|
||||
import { AppError } from '../errors/app-error';
|
||||
import type { TImportSource } from '../universal/import';
|
||||
import { ImportError, isBlocked, isPdf, parseUrl } from '../universal/import';
|
||||
import { megabytesToBytes } from '../universal/unit-convertions';
|
||||
|
||||
/**
|
||||
* Turn an import source into PDF bytes.
|
||||
*
|
||||
* This is the only place a source is read. Whatever it was — a pasted note, a
|
||||
* link, later a file picked out of a drive — it leaves here as bytes and is
|
||||
* created by `envelope.create`, exactly like a browser upload.
|
||||
*/
|
||||
|
||||
const LIMIT = megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT);
|
||||
const TIMEOUT = 15_000;
|
||||
const REDIRECTS = 4;
|
||||
|
||||
const failed = (message: string) => new AppError(ImportError.fetch, { message, statusCode: 400 });
|
||||
|
||||
/** Refuse a host that resolves inside the cluster, whatever it is called. */
|
||||
const resolveHost = async (url: URL) => {
|
||||
const addresses = await lookup(url.hostname.replace(/^\[|\]$/g, ''), { all: true }).catch(
|
||||
() => [],
|
||||
);
|
||||
|
||||
if (addresses.length === 0) {
|
||||
throw failed(`Cannot resolve ${url.hostname}`);
|
||||
}
|
||||
|
||||
if (addresses.some(({ address }) => isBlocked(address))) {
|
||||
throw new AppError(ImportError.url, {
|
||||
message: `Unreachable host: ${url.hostname}`,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** Read a response body, stopping at the limit rather than after it. */
|
||||
const read = async (response: Response): Promise<Uint8Array<ArrayBuffer>> => {
|
||||
const declared = Number(response.headers.get('content-length'));
|
||||
|
||||
if (Number.isFinite(declared) && declared > LIMIT) {
|
||||
throw new AppError(ImportError.size, { statusCode: 400 });
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
if (!reader) {
|
||||
throw failed('The response carried no body');
|
||||
}
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
let size = 0;
|
||||
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
size += value.length;
|
||||
|
||||
if (size > LIMIT) {
|
||||
await reader.cancel();
|
||||
|
||||
throw new AppError(ImportError.size, { statusCode: 400 });
|
||||
}
|
||||
|
||||
chunks.push(value);
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(size);
|
||||
let at = 0;
|
||||
|
||||
for (const chunk of chunks) {
|
||||
bytes.set(chunk, at);
|
||||
at += chunk.length;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/** The GitHub contents API answers with JSON unless asked for the file itself. */
|
||||
const accept = (url: URL) =>
|
||||
url.hostname === 'api.github.com' ? 'application/vnd.github.raw' : 'application/pdf, */*';
|
||||
|
||||
const filename = (url: URL): string => {
|
||||
const last = decodeURIComponent(url.pathname.split('/').pop() ?? '').trim();
|
||||
const name = last.replace(/[/\\]/g, '').slice(0, 200);
|
||||
|
||||
if (!name) {
|
||||
return 'document.pdf';
|
||||
}
|
||||
|
||||
return /\.pdf$/i.test(name) ? name : `${name}.pdf`;
|
||||
};
|
||||
|
||||
const fromUrl = async ({ url: address }: { url: string }) => {
|
||||
let url = parseUrl(address);
|
||||
let response: Response;
|
||||
|
||||
for (let hop = 0; ; hop += 1) {
|
||||
await resolveHost(url);
|
||||
|
||||
const dialled = url;
|
||||
|
||||
response = await fetch(dialled, {
|
||||
redirect: 'manual',
|
||||
signal: AbortSignal.timeout(TIMEOUT),
|
||||
headers: { accept: accept(dialled) },
|
||||
}).catch(() => {
|
||||
throw failed(`Could not reach ${dialled.hostname}`);
|
||||
});
|
||||
|
||||
if (response.status < 300 || response.status > 399) {
|
||||
break;
|
||||
}
|
||||
|
||||
const location = response.headers.get('location');
|
||||
|
||||
if (!location || hop === REDIRECTS) {
|
||||
throw failed('The address redirects too many times');
|
||||
}
|
||||
|
||||
url = parseUrl(new URL(location, dialled).toString());
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw failed(`The server answered ${response.status}`);
|
||||
}
|
||||
|
||||
const bytes = await read(response);
|
||||
|
||||
if (!isPdf(bytes)) {
|
||||
throw new AppError(ImportError.pdf, { statusCode: 400 });
|
||||
}
|
||||
|
||||
return { name: filename(url), bytes };
|
||||
};
|
||||
|
||||
const PAGE = { width: 612, height: 792 } as const;
|
||||
const MARGIN = 56;
|
||||
const BODY = 11;
|
||||
const LEADING = 1.45;
|
||||
|
||||
/** Standard 14 fonts speak WinAnsi. Fold the typography a paste usually carries
|
||||
* into it, and drop what it cannot say. */
|
||||
const FOLD: Record<string, string> = {
|
||||
'‘': "'",
|
||||
'’': "'",
|
||||
'‚': "'",
|
||||
'“': '"',
|
||||
'”': '"',
|
||||
'–': '-',
|
||||
'—': '--',
|
||||
'•': '-',
|
||||
'…': '...',
|
||||
'\u00a0': ' ',
|
||||
'\t': ' ',
|
||||
};
|
||||
|
||||
const encodable = (text: string) =>
|
||||
text.replace(/[‘’‚“”–—•…\u00a0\t]/g, (c) => FOLD[c]).replace(/[^\x20-\x7e\xa1-\xff]/g, '');
|
||||
|
||||
/** Markdown emphasis reads as noise once it is no longer being rendered. */
|
||||
const plain = (text: string) =>
|
||||
text
|
||||
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
|
||||
.replace(/(\*\*|__)(.+?)\1/g, '$2')
|
||||
.replace(/`([^`]*)`/g, '$1');
|
||||
|
||||
type Block = { text: string; size: number; bold: boolean; indent: number; marker: string };
|
||||
|
||||
const parse = (line: string): Block => {
|
||||
const heading = /^(#{1,3})\s+(.*)$/.exec(line);
|
||||
|
||||
if (heading) {
|
||||
return {
|
||||
text: heading[2],
|
||||
size: [18, 14, 12][heading[1].length - 1],
|
||||
bold: true,
|
||||
indent: 0,
|
||||
marker: '',
|
||||
};
|
||||
}
|
||||
|
||||
const bullet = /^\s*[-*+]\s+(.*)$/.exec(line);
|
||||
|
||||
if (bullet) {
|
||||
return { text: bullet[1], size: BODY, bold: false, indent: 16, marker: '-' };
|
||||
}
|
||||
|
||||
const ordered = /^\s*(\d{1,3}[.)])\s+(.*)$/.exec(line);
|
||||
|
||||
if (ordered) {
|
||||
return { text: ordered[2], size: BODY, bold: false, indent: 22, marker: ordered[1] };
|
||||
}
|
||||
|
||||
return { text: line, size: BODY, bold: false, indent: 0, marker: '' };
|
||||
};
|
||||
|
||||
const fromText = async ({ title, text }: { title: string; text: string }) => {
|
||||
const pdf = PDF.create();
|
||||
|
||||
let page = pdf.addPage({ size: 'letter' });
|
||||
let y = PAGE.height - MARGIN;
|
||||
|
||||
const turn = () => {
|
||||
page = pdf.addPage({ size: 'letter' });
|
||||
y = PAGE.height - MARGIN;
|
||||
};
|
||||
|
||||
for (const line of text.replace(/\r\n?/g, '\n').split('\n')) {
|
||||
const block = parse(line);
|
||||
const body = encodable(plain(block.text)).trimEnd();
|
||||
|
||||
if (!body) {
|
||||
y -= BODY;
|
||||
continue;
|
||||
}
|
||||
|
||||
const font = block.bold ? StandardFonts.HelveticaBold : StandardFonts.Helvetica;
|
||||
const leading = block.size * LEADING;
|
||||
const width = PAGE.width - MARGIN * 2 - block.indent;
|
||||
const { lines } = layoutText(body, font, block.size, width, leading);
|
||||
|
||||
lines.forEach(({ text: run }, index) => {
|
||||
if (y - leading < MARGIN) {
|
||||
turn();
|
||||
}
|
||||
|
||||
if (index === 0 && block.marker) {
|
||||
page.drawText(block.marker, { x: MARGIN, y: y - block.size, font, size: block.size });
|
||||
}
|
||||
|
||||
page.drawText(run, {
|
||||
x: MARGIN + block.indent,
|
||||
y: y - block.size,
|
||||
font,
|
||||
size: block.size,
|
||||
});
|
||||
|
||||
y -= leading;
|
||||
});
|
||||
|
||||
if (block.bold) {
|
||||
y -= block.size * 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
const name = title.replace(/\.pdf$/i, '').trim() || 'document';
|
||||
|
||||
// A copy the caller owns: `save` writes into a pooled buffer, and a view over
|
||||
// a pool is not the document.
|
||||
return { name: `${name}.pdf`, bytes: new Uint8Array(await pdf.save()) };
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a source to the bytes an envelope is made from.
|
||||
*
|
||||
* A new source is one more case here and one more member of ZImportSourceSchema
|
||||
* — nothing else moves. Google Drive is the next one and needs an OAuth client
|
||||
* id plus a Picker API key before it can be written: the picker hands back a
|
||||
* file id, this function exchanges it for bytes.
|
||||
*/
|
||||
export const resolve = async (
|
||||
source: TImportSource,
|
||||
): Promise<{ name: string; bytes: Uint8Array<ArrayBuffer> }> =>
|
||||
match(source).with({ kind: 'url' }, fromUrl).with({ kind: 'text' }, fromText).exhaustive();
|
||||
@@ -601,6 +601,10 @@ msgstr "{validationLength, plural, one {Wählen Sie genau # Option} other {Wähl
|
||||
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
|
||||
msgstr "{visibleRows, plural, one {Eine # Ergebnis wird angezeigt.} other {# Ergebnisse werden angezeigt.}}"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "# Mutual NDA — both parties agree..."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: document.title
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
@@ -2319,6 +2323,8 @@ msgstr "Person nicht gefunden?"
|
||||
#: apps/remix/app/components/general/document/document-attachments-popover.tsx
|
||||
#: apps/remix/app/components/general/document/embedded-editor-attachment-popover.tsx
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-usage.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -2718,6 +2724,7 @@ msgstr "Kontaktinformationen"
|
||||
msgid "Contact us"
|
||||
msgstr "Kontaktieren Sie uns"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
|
||||
msgid "Content"
|
||||
msgstr "Inhalt"
|
||||
@@ -2873,6 +2880,7 @@ msgstr "Wert kopieren"
|
||||
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create"
|
||||
msgstr "Erstellen"
|
||||
@@ -3909,11 +3917,14 @@ msgid "Document upload disabled due to unpaid invoices"
|
||||
msgstr "Dokumenten-Upload deaktiviert aufgrund unbezahlter Rechnungen"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Document uploaded"
|
||||
msgstr "Dokument hochgeladen"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Document URL"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document viewed"
|
||||
@@ -4698,8 +4709,7 @@ msgstr "Umschlag aktualisiert"
|
||||
#: apps/remix/app/components/general/document/document-edit-form.tsx
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
#: apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
@@ -5445,6 +5455,20 @@ msgstr "Wenn Sie den Bestätigungslink nicht in Ihrem Posteingang finden, könne
|
||||
msgid "If your authenticator app does not support QR codes, you can use the following code instead:"
|
||||
msgstr "Wenn Ihre Authenticator-App keine QR-Codes unterstützt, können Sie stattdessen den folgenden Code verwenden:"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import failed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import from URL"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "Important: What This Means"
|
||||
msgstr "Wichtig: Was dies bedeutet"
|
||||
@@ -6953,6 +6977,19 @@ msgctxt "Subscription status"
|
||||
msgid "Past Due"
|
||||
msgstr "Überfällig"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste content"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste text or Markdown. It becomes a PDF you can place fields on and send for signature."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Pasted document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "PDF-Dokument"
|
||||
@@ -9036,6 +9073,10 @@ msgstr "Systemanforderungen"
|
||||
msgid "System Theme"
|
||||
msgstr "Systemthema"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Taken from the first line"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
|
||||
msgid "Target Organisation"
|
||||
msgstr "Zielorganisation"
|
||||
@@ -9273,8 +9314,7 @@ msgstr "Vorlage aktualisiert"
|
||||
msgid "Template updated successfully"
|
||||
msgstr "Vorlage erfolgreich aktualisiert"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Template uploaded"
|
||||
msgstr "Vorlage hochgeladen"
|
||||
|
||||
@@ -9352,6 +9392,18 @@ msgstr "Danke, dass Sie den Signaturvorgang abgeschlossen haben."
|
||||
msgid "Thank you for using Hanzo Sign to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address cannot be read. Use a public http(s) link."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address does not lead to a PDF."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That document is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB."
|
||||
msgstr ""
|
||||
|
||||
#: packages/email/template-components/template-forgot-password.tsx
|
||||
msgid "That's okay, it happens! Click the button below to reset your password."
|
||||
msgstr "Das ist in Ordnung, das passiert! Klicke auf den Button unten, um dein Passwort zurückzusetzen."
|
||||
@@ -9368,6 +9420,10 @@ msgstr "Das Konto wurde erfolgreich deaktiviert."
|
||||
msgid "The account has been enabled successfully."
|
||||
msgstr "Das Konto wurde erfolgreich aktiviert."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "The address of a PDF we can reach. A GitHub file page works — paste it as you see it."
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx
|
||||
msgid "The authentication methods required for recipients to sign fields"
|
||||
msgstr "Die Authentifizierungsmethoden, die erforderlich sind, damit Empfänger Felder signieren"
|
||||
@@ -10036,6 +10092,7 @@ msgid "Time Zone"
|
||||
msgstr "Zeitzone"
|
||||
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table.tsx
|
||||
#: apps/remix/app/components/tables/inbox-table.tsx
|
||||
@@ -11076,6 +11133,10 @@ msgstr "Wir können diesen Schlüssel im Moment nicht entfernen. Bitte versuchen
|
||||
msgid "We are unable to update this passkey at the moment. Please try again later."
|
||||
msgstr "Wir können diesen Schlüssel im Moment nicht aktualisieren. Bitte versuchen Sie es später erneut."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "We could not reach that address."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/ai-features-enable-dialog.tsx
|
||||
msgid "We couldn't enable AI features right now. Please try again."
|
||||
msgstr "Wir konnten die KI-Funktionen gerade nicht aktivieren. Bitte versuchen Sie es erneut."
|
||||
@@ -12321,8 +12382,7 @@ msgid "Your document has been updated successfully"
|
||||
msgstr "Ihr Dokument wurde erfolgreich aktualisiert"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your document has been uploaded successfully."
|
||||
msgstr "Ihr Dokument wurde erfolgreich hochgeladen."
|
||||
|
||||
@@ -12483,8 +12543,7 @@ msgstr "Ihre Vorlage wurde erfolgreich gelöscht."
|
||||
msgid "Your template has been updated successfully"
|
||||
msgstr "Ihre Vorlage wurde erfolgreich aktualisiert"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your template has been uploaded successfully."
|
||||
msgstr "Ihre Vorlage wurde erfolgreich hochgeladen."
|
||||
|
||||
|
||||
@@ -596,6 +596,10 @@ msgstr "{validationLength, plural, one {Select exactly # option} other {Select e
|
||||
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
|
||||
msgstr "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "# Mutual NDA — both parties agree..."
|
||||
msgstr "# Mutual NDA — both parties agree..."
|
||||
|
||||
#. placeholder {0}: document.title
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
@@ -2314,6 +2318,8 @@ msgstr "Can't find someone?"
|
||||
#: apps/remix/app/components/general/document/document-attachments-popover.tsx
|
||||
#: apps/remix/app/components/general/document/embedded-editor-attachment-popover.tsx
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-usage.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -2713,6 +2719,7 @@ msgstr "Contact Information"
|
||||
msgid "Contact us"
|
||||
msgstr "Contact us"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
|
||||
msgid "Content"
|
||||
msgstr "Content"
|
||||
@@ -2868,6 +2875,7 @@ msgstr "Copy Value"
|
||||
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create"
|
||||
msgstr "Create"
|
||||
@@ -3904,11 +3912,14 @@ msgid "Document upload disabled due to unpaid invoices"
|
||||
msgstr "Document upload disabled due to unpaid invoices"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Document uploaded"
|
||||
msgstr "Document uploaded"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Document URL"
|
||||
msgstr "Document URL"
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document viewed"
|
||||
@@ -4693,8 +4704,7 @@ msgstr "Envelope updated"
|
||||
#: apps/remix/app/components/general/document/document-edit-form.tsx
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
#: apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
@@ -5440,6 +5450,20 @@ msgstr "If you don't find the confirmation link in your inbox, you can request a
|
||||
msgid "If your authenticator app does not support QR codes, you can use the following code instead:"
|
||||
msgstr "If your authenticator app does not support QR codes, you can use the following code instead:"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import"
|
||||
msgstr "Import"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import failed"
|
||||
msgstr "Import failed"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import from URL"
|
||||
msgstr "Import from URL"
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "Important: What This Means"
|
||||
msgstr "Important: What This Means"
|
||||
@@ -6948,6 +6972,19 @@ msgctxt "Subscription status"
|
||||
msgid "Past Due"
|
||||
msgstr "Past Due"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste content"
|
||||
msgstr "Paste content"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste text or Markdown. It becomes a PDF you can place fields on and send for signature."
|
||||
msgstr "Paste text or Markdown. It becomes a PDF you can place fields on and send for signature."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Pasted document"
|
||||
msgstr "Pasted document"
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "PDF Document"
|
||||
@@ -9031,6 +9068,10 @@ msgstr "System Requirements"
|
||||
msgid "System Theme"
|
||||
msgstr "System Theme"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Taken from the first line"
|
||||
msgstr "Taken from the first line"
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
|
||||
msgid "Target Organisation"
|
||||
msgstr "Target Organisation"
|
||||
@@ -9268,8 +9309,7 @@ msgstr "Template Updated"
|
||||
msgid "Template updated successfully"
|
||||
msgstr "Template updated successfully"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Template uploaded"
|
||||
msgstr "Template uploaded"
|
||||
|
||||
@@ -9347,6 +9387,18 @@ msgstr "Thank you for completing the signing process."
|
||||
msgid "Thank you for using Hanzo Sign to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
|
||||
msgstr "Thank you for using Hanzo Sign to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address cannot be read. Use a public http(s) link."
|
||||
msgstr "That address cannot be read. Use a public http(s) link."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address does not lead to a PDF."
|
||||
msgstr "That address does not lead to a PDF."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That document is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB."
|
||||
msgstr "That document is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB."
|
||||
|
||||
#: packages/email/template-components/template-forgot-password.tsx
|
||||
msgid "That's okay, it happens! Click the button below to reset your password."
|
||||
msgstr "That's okay, it happens! Click the button below to reset your password."
|
||||
@@ -9363,6 +9415,10 @@ msgstr "The account has been disabled successfully."
|
||||
msgid "The account has been enabled successfully."
|
||||
msgstr "The account has been enabled successfully."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "The address of a PDF we can reach. A GitHub file page works — paste it as you see it."
|
||||
msgstr "The address of a PDF we can reach. A GitHub file page works — paste it as you see it."
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx
|
||||
msgid "The authentication methods required for recipients to sign fields"
|
||||
msgstr "The authentication methods required for recipients to sign fields"
|
||||
@@ -10031,6 +10087,7 @@ msgid "Time Zone"
|
||||
msgstr "Time Zone"
|
||||
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table.tsx
|
||||
#: apps/remix/app/components/tables/inbox-table.tsx
|
||||
@@ -11071,6 +11128,10 @@ msgstr "We are unable to remove this passkey at the moment. Please try again lat
|
||||
msgid "We are unable to update this passkey at the moment. Please try again later."
|
||||
msgstr "We are unable to update this passkey at the moment. Please try again later."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "We could not reach that address."
|
||||
msgstr "We could not reach that address."
|
||||
|
||||
#: apps/remix/app/components/dialogs/ai-features-enable-dialog.tsx
|
||||
msgid "We couldn't enable AI features right now. Please try again."
|
||||
msgstr "We couldn't enable AI features right now. Please try again."
|
||||
@@ -12316,8 +12377,7 @@ msgid "Your document has been updated successfully"
|
||||
msgstr "Your document has been updated successfully"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your document has been uploaded successfully."
|
||||
msgstr "Your document has been uploaded successfully."
|
||||
|
||||
@@ -12478,8 +12538,7 @@ msgstr "Your template has been successfully deleted."
|
||||
msgid "Your template has been updated successfully"
|
||||
msgstr "Your template has been updated successfully"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your template has been uploaded successfully."
|
||||
msgstr "Your template has been uploaded successfully."
|
||||
|
||||
|
||||
@@ -601,6 +601,10 @@ msgstr "{validationLength, plural, one {Seleccione exactamente # opción} other
|
||||
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
|
||||
msgstr "{visibleRows, plural, one {Mostrando # resultado.} other {Mostrando # resultados.}}"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "# Mutual NDA — both parties agree..."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: document.title
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
@@ -2319,6 +2323,8 @@ msgstr "¿No puedes encontrar a alguien?"
|
||||
#: apps/remix/app/components/general/document/document-attachments-popover.tsx
|
||||
#: apps/remix/app/components/general/document/embedded-editor-attachment-popover.tsx
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-usage.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -2718,6 +2724,7 @@ msgstr "Información de Contacto"
|
||||
msgid "Contact us"
|
||||
msgstr "Contáctanos"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
|
||||
msgid "Content"
|
||||
msgstr "Contenido"
|
||||
@@ -2873,6 +2880,7 @@ msgstr "Copiar valor"
|
||||
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create"
|
||||
msgstr "Crear"
|
||||
@@ -3909,11 +3917,14 @@ msgid "Document upload disabled due to unpaid invoices"
|
||||
msgstr "La carga de documentos está deshabilitada debido a facturas impagadas"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Document uploaded"
|
||||
msgstr "Documento subido"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Document URL"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document viewed"
|
||||
@@ -4698,8 +4709,7 @@ msgstr "Sobre actualizado"
|
||||
#: apps/remix/app/components/general/document/document-edit-form.tsx
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
#: apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
@@ -5445,6 +5455,20 @@ msgstr "Si no encuentras el enlace de confirmación en tu bandeja de entrada, pu
|
||||
msgid "If your authenticator app does not support QR codes, you can use the following code instead:"
|
||||
msgstr "Si tu aplicación de autenticación no admite códigos QR, puedes usar el siguiente código en su lugar:"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import failed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import from URL"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "Important: What This Means"
|
||||
msgstr "Importante: Lo que esto significa"
|
||||
@@ -6953,6 +6977,19 @@ msgctxt "Subscription status"
|
||||
msgid "Past Due"
|
||||
msgstr "Vencida"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste content"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste text or Markdown. It becomes a PDF you can place fields on and send for signature."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Pasted document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "Documento PDF"
|
||||
@@ -9036,6 +9073,10 @@ msgstr "Requisitos del Sistema"
|
||||
msgid "System Theme"
|
||||
msgstr "Tema del sistema"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Taken from the first line"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
|
||||
msgid "Target Organisation"
|
||||
msgstr "Organización de destino"
|
||||
@@ -9273,8 +9314,7 @@ msgstr "Plantilla actualizada"
|
||||
msgid "Template updated successfully"
|
||||
msgstr "Plantilla actualizada con éxito"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Template uploaded"
|
||||
msgstr "Plantilla subida"
|
||||
|
||||
@@ -9352,6 +9392,18 @@ msgstr "Gracias por completar el proceso de firma."
|
||||
msgid "Thank you for using Hanzo Sign to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address cannot be read. Use a public http(s) link."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address does not lead to a PDF."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That document is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB."
|
||||
msgstr ""
|
||||
|
||||
#: packages/email/template-components/template-forgot-password.tsx
|
||||
msgid "That's okay, it happens! Click the button below to reset your password."
|
||||
msgstr "Está bien, ¡sucede! Haz clic en el botón de abajo para restablecer tu contraseña."
|
||||
@@ -9368,6 +9420,10 @@ msgstr "La cuenta ha sido deshabilitada exitosamente."
|
||||
msgid "The account has been enabled successfully."
|
||||
msgstr "La cuenta ha sido habilitada exitosamente."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "The address of a PDF we can reach. A GitHub file page works — paste it as you see it."
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx
|
||||
msgid "The authentication methods required for recipients to sign fields"
|
||||
msgstr "Los métodos de autenticación necesarios para que los destinatarios firmen campos"
|
||||
@@ -10036,6 +10092,7 @@ msgid "Time Zone"
|
||||
msgstr "Zona horaria"
|
||||
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table.tsx
|
||||
#: apps/remix/app/components/tables/inbox-table.tsx
|
||||
@@ -11076,6 +11133,10 @@ msgstr "No podemos eliminar esta clave de acceso en este momento. Por favor, int
|
||||
msgid "We are unable to update this passkey at the moment. Please try again later."
|
||||
msgstr "No podemos actualizar esta clave de acceso en este momento. Por favor, inténtalo de nuevo más tarde."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "We could not reach that address."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/ai-features-enable-dialog.tsx
|
||||
msgid "We couldn't enable AI features right now. Please try again."
|
||||
msgstr "No hemos podido habilitar las funciones de IA en este momento. Inténtalo de nuevo."
|
||||
@@ -12321,8 +12382,7 @@ msgid "Your document has been updated successfully"
|
||||
msgstr "Tu documento se ha actualizado correctamente"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your document has been uploaded successfully."
|
||||
msgstr "Tu documento ha sido subido con éxito."
|
||||
|
||||
@@ -12483,8 +12543,7 @@ msgstr "Tu plantilla ha sido eliminada con éxito."
|
||||
msgid "Your template has been updated successfully"
|
||||
msgstr "Tu plantilla se ha actualizado correctamente"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your template has been uploaded successfully."
|
||||
msgstr "Su plantilla ha sido subida exitosamente."
|
||||
|
||||
|
||||
@@ -601,6 +601,10 @@ msgstr "{validationLength, plural, one {Sélectionnez exactement # option} other
|
||||
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
|
||||
msgstr "{visibleRows, plural, one {Affichage de # résultat.} other {Affichage de # résultats.}}"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "# Mutual NDA — both parties agree..."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: document.title
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
@@ -2319,6 +2323,8 @@ msgstr "Vous ne trouvez pas quelqu’un ?"
|
||||
#: apps/remix/app/components/general/document/document-attachments-popover.tsx
|
||||
#: apps/remix/app/components/general/document/embedded-editor-attachment-popover.tsx
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-usage.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -2718,6 +2724,7 @@ msgstr "Coordonnées"
|
||||
msgid "Contact us"
|
||||
msgstr "Contactez-nous"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
|
||||
msgid "Content"
|
||||
msgstr "Contenu"
|
||||
@@ -2873,6 +2880,7 @@ msgstr "Copier la valeur"
|
||||
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create"
|
||||
msgstr "Créer"
|
||||
@@ -3909,11 +3917,14 @@ msgid "Document upload disabled due to unpaid invoices"
|
||||
msgstr "Importation de documents désactivé en raison de factures impayées"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Document uploaded"
|
||||
msgstr "Document importé"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Document URL"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document viewed"
|
||||
@@ -4698,8 +4709,7 @@ msgstr "Enveloppe mise à jour"
|
||||
#: apps/remix/app/components/general/document/document-edit-form.tsx
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
#: apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
@@ -5445,6 +5455,20 @@ msgstr "Si vous ne trouvez pas le lien de confirmation dans votre boîte de réc
|
||||
msgid "If your authenticator app does not support QR codes, you can use the following code instead:"
|
||||
msgstr "Si votre application d'authentification ne prend pas en charge les codes QR, vous pouvez utiliser le code suivant à la place :"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import failed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import from URL"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "Important: What This Means"
|
||||
msgstr "Important : Ce que cela signifie"
|
||||
@@ -6953,6 +6977,19 @@ msgctxt "Subscription status"
|
||||
msgid "Past Due"
|
||||
msgstr "En retard de paiement"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste content"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste text or Markdown. It becomes a PDF you can place fields on and send for signature."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Pasted document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "Document PDF"
|
||||
@@ -9036,6 +9073,10 @@ msgstr "Exigences du système"
|
||||
msgid "System Theme"
|
||||
msgstr "Thème système"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Taken from the first line"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
|
||||
msgid "Target Organisation"
|
||||
msgstr "Organisation cible"
|
||||
@@ -9273,8 +9314,7 @@ msgstr "Modèle mis à jour"
|
||||
msgid "Template updated successfully"
|
||||
msgstr "Modèle mis à jour avec succès"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Template uploaded"
|
||||
msgstr "Modèle de document téléchargé"
|
||||
|
||||
@@ -9352,6 +9392,18 @@ msgstr "Merci d'avoir complété le processus de signature."
|
||||
msgid "Thank you for using Hanzo Sign to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address cannot be read. Use a public http(s) link."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address does not lead to a PDF."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That document is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB."
|
||||
msgstr ""
|
||||
|
||||
#: packages/email/template-components/template-forgot-password.tsx
|
||||
msgid "That's okay, it happens! Click the button below to reset your password."
|
||||
msgstr "Ce n'est pas grave, cela arrive ! Cliquez sur le bouton ci-dessous pour réinitialiser votre mot de passe."
|
||||
@@ -9368,6 +9420,10 @@ msgstr "Le compte a été désactivé avec succès."
|
||||
msgid "The account has been enabled successfully."
|
||||
msgstr "Le compte a été activé avec succès."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "The address of a PDF we can reach. A GitHub file page works — paste it as you see it."
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx
|
||||
msgid "The authentication methods required for recipients to sign fields"
|
||||
msgstr "Les méthodes d'authentification requises pour que les destinataires signent les champs"
|
||||
@@ -10036,6 +10092,7 @@ msgid "Time Zone"
|
||||
msgstr "Fuseau horaire"
|
||||
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table.tsx
|
||||
#: apps/remix/app/components/tables/inbox-table.tsx
|
||||
@@ -11076,6 +11133,10 @@ msgstr "Nous ne pouvons pas supprimer cette clé de passkey pour le moment. Veui
|
||||
msgid "We are unable to update this passkey at the moment. Please try again later."
|
||||
msgstr "Nous ne pouvons pas mettre à jour cette clé de passkey pour le moment. Veuillez réessayer plus tard."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "We could not reach that address."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/ai-features-enable-dialog.tsx
|
||||
msgid "We couldn't enable AI features right now. Please try again."
|
||||
msgstr "Nous n’avons pas pu activer les fonctionnalités d’IA pour le moment. Veuillez réessayer."
|
||||
@@ -12321,8 +12382,7 @@ msgid "Your document has been updated successfully"
|
||||
msgstr "Votre document a été mis à jour avec succès"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your document has been uploaded successfully."
|
||||
msgstr "Votre document a été importé avec succès."
|
||||
|
||||
@@ -12483,8 +12543,7 @@ msgstr "Votre modèle a été supprimé avec succès."
|
||||
msgid "Your template has been updated successfully"
|
||||
msgstr "Votre modèle a été mis à jour avec succès"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your template has been uploaded successfully."
|
||||
msgstr "Votre modèle a été importé avec succès."
|
||||
|
||||
|
||||
@@ -601,6 +601,10 @@ msgstr "{validationLength, plural, one {Seleziona esattamente # opzione} other {
|
||||
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
|
||||
msgstr "{visibleRows, plural, one {Visualizzazione di # risultato.} other {Visualizzazione di # risultati.}}"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "# Mutual NDA — both parties agree..."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: document.title
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
@@ -2319,6 +2323,8 @@ msgstr "Non riesci a trovare qualcuno?"
|
||||
#: apps/remix/app/components/general/document/document-attachments-popover.tsx
|
||||
#: apps/remix/app/components/general/document/embedded-editor-attachment-popover.tsx
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-usage.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -2718,6 +2724,7 @@ msgstr "Informazioni di contatto"
|
||||
msgid "Contact us"
|
||||
msgstr "Contattaci"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
|
||||
msgid "Content"
|
||||
msgstr "Contenuto"
|
||||
@@ -2873,6 +2880,7 @@ msgstr "Copia valore"
|
||||
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create"
|
||||
msgstr "Crea"
|
||||
@@ -3909,11 +3917,14 @@ msgid "Document upload disabled due to unpaid invoices"
|
||||
msgstr "Caricamento del documento disabilitato a causa di fatture non pagate"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Document uploaded"
|
||||
msgstr "Documento caricato"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Document URL"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document viewed"
|
||||
@@ -4698,8 +4709,7 @@ msgstr "Busta aggiornata"
|
||||
#: apps/remix/app/components/general/document/document-edit-form.tsx
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
#: apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
@@ -5445,6 +5455,20 @@ msgstr "Se non trovi il link di conferma nella tua casella di posta, puoi richie
|
||||
msgid "If your authenticator app does not support QR codes, you can use the following code instead:"
|
||||
msgstr "Se la tua app autenticatrice non supporta i codici QR, puoi usare il seguente codice:"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import failed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import from URL"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "Important: What This Means"
|
||||
msgstr "Importante: Cosa Significa"
|
||||
@@ -6953,6 +6977,19 @@ msgctxt "Subscription status"
|
||||
msgid "Past Due"
|
||||
msgstr "In ritardo"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste content"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste text or Markdown. It becomes a PDF you can place fields on and send for signature."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Pasted document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "Documento PDF"
|
||||
@@ -9036,6 +9073,10 @@ msgstr "Requisiti di sistema"
|
||||
msgid "System Theme"
|
||||
msgstr "Tema del sistema"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Taken from the first line"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
|
||||
msgid "Target Organisation"
|
||||
msgstr "Organizzazione di destinazione"
|
||||
@@ -9273,8 +9314,7 @@ msgstr "Modello aggiornato"
|
||||
msgid "Template updated successfully"
|
||||
msgstr "Modello aggiornato con successo"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Template uploaded"
|
||||
msgstr "Modello caricato"
|
||||
|
||||
@@ -9352,6 +9392,18 @@ msgstr "Grazie per aver completato il processo di firma."
|
||||
msgid "Thank you for using Hanzo Sign to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address cannot be read. Use a public http(s) link."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address does not lead to a PDF."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That document is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB."
|
||||
msgstr ""
|
||||
|
||||
#: packages/email/template-components/template-forgot-password.tsx
|
||||
msgid "That's okay, it happens! Click the button below to reset your password."
|
||||
msgstr "Va bene, succede! Fai clic sul pulsante qui sotto per reimpostare la tua password."
|
||||
@@ -9368,6 +9420,10 @@ msgstr "L'account è stato disabilitato con successo."
|
||||
msgid "The account has been enabled successfully."
|
||||
msgstr "L'account è stato abilitato con successo."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "The address of a PDF we can reach. A GitHub file page works — paste it as you see it."
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx
|
||||
msgid "The authentication methods required for recipients to sign fields"
|
||||
msgstr "I metodi di autenticazione richiesti per i destinatari per firmare i campi"
|
||||
@@ -10036,6 +10092,7 @@ msgid "Time Zone"
|
||||
msgstr "Fuso orario"
|
||||
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table.tsx
|
||||
#: apps/remix/app/components/tables/inbox-table.tsx
|
||||
@@ -11076,6 +11133,10 @@ msgstr "Non siamo in grado di rimuovere questa chiave d'accesso al momento. Per
|
||||
msgid "We are unable to update this passkey at the moment. Please try again later."
|
||||
msgstr "Non siamo in grado di aggiornare questa chiave d'accesso al momento. Per favore riprova più tardi."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "We could not reach that address."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/ai-features-enable-dialog.tsx
|
||||
msgid "We couldn't enable AI features right now. Please try again."
|
||||
msgstr "Al momento non siamo riusciti ad abilitare le funzionalità di IA. Riprova."
|
||||
@@ -12321,8 +12382,7 @@ msgid "Your document has been updated successfully"
|
||||
msgstr "Il tuo documento è stato aggiornato correttamente"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your document has been uploaded successfully."
|
||||
msgstr "Il tuo documento è stato caricato correttamente."
|
||||
|
||||
@@ -12483,8 +12543,7 @@ msgstr "Il tuo modello è stato eliminato correttamente."
|
||||
msgid "Your template has been updated successfully"
|
||||
msgstr "Il tuo modello è stato aggiornato correttamente"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your template has been uploaded successfully."
|
||||
msgstr "Il tuo modello è stato caricato con successo."
|
||||
|
||||
|
||||
@@ -601,6 +601,10 @@ msgstr "{validationLength, plural, other {ちょうど # 個のオプション
|
||||
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
|
||||
msgstr "{visibleRows, plural, other {# 件の結果を表示中}}"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "# Mutual NDA — both parties agree..."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: document.title
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
@@ -2319,6 +2323,8 @@ msgstr "メンバーが見つかりませんか?"
|
||||
#: apps/remix/app/components/general/document/document-attachments-popover.tsx
|
||||
#: apps/remix/app/components/general/document/embedded-editor-attachment-popover.tsx
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-usage.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -2718,6 +2724,7 @@ msgstr "連絡先情報"
|
||||
msgid "Contact us"
|
||||
msgstr "お問い合わせ"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
|
||||
msgid "Content"
|
||||
msgstr "コンテンツ"
|
||||
@@ -2873,6 +2880,7 @@ msgstr "値をコピー"
|
||||
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create"
|
||||
msgstr "作成"
|
||||
@@ -3909,11 +3917,14 @@ msgid "Document upload disabled due to unpaid invoices"
|
||||
msgstr "未払いの請求書があるため、文書のアップロードは無効化されています"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Document uploaded"
|
||||
msgstr "文書をアップロードしました"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Document URL"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document viewed"
|
||||
@@ -4698,8 +4709,7 @@ msgstr "封筒を更新しました"
|
||||
#: apps/remix/app/components/general/document/document-edit-form.tsx
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
#: apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
@@ -5445,6 +5455,20 @@ msgstr "受信トレイに確認リンクが見当たらない場合は、下か
|
||||
msgid "If your authenticator app does not support QR codes, you can use the following code instead:"
|
||||
msgstr "認証アプリが QR コードに対応していない場合は、代わりに次のコードを使用できます。"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import failed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import from URL"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "Important: What This Means"
|
||||
msgstr "重要: これが意味すること"
|
||||
@@ -6953,6 +6977,19 @@ msgctxt "Subscription status"
|
||||
msgid "Past Due"
|
||||
msgstr "支払い遅延"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste content"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste text or Markdown. It becomes a PDF you can place fields on and send for signature."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Pasted document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "PDF文書"
|
||||
@@ -9036,6 +9073,10 @@ msgstr "システム要件"
|
||||
msgid "System Theme"
|
||||
msgstr "システムテーマ"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Taken from the first line"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
|
||||
msgid "Target Organisation"
|
||||
msgstr "対象の組織"
|
||||
@@ -9273,8 +9314,7 @@ msgstr "テンプレートを更新しました"
|
||||
msgid "Template updated successfully"
|
||||
msgstr "テンプレートは正常に更新されました"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Template uploaded"
|
||||
msgstr "テンプレートをアップロードしました"
|
||||
|
||||
@@ -9352,6 +9392,18 @@ msgstr "署名プロセスを完了していただきありがとうございま
|
||||
msgid "Thank you for using Hanzo Sign to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address cannot be read. Use a public http(s) link."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address does not lead to a PDF."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That document is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB."
|
||||
msgstr ""
|
||||
|
||||
#: packages/email/template-components/template-forgot-password.tsx
|
||||
msgid "That's okay, it happens! Click the button below to reset your password."
|
||||
msgstr "問題ありません!パスワードをリセットするには、下のボタンをクリックしてください。"
|
||||
@@ -9368,6 +9420,10 @@ msgstr "アカウントは正常に無効化されました。"
|
||||
msgid "The account has been enabled successfully."
|
||||
msgstr "アカウントは正常に有効化されました。"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "The address of a PDF we can reach. A GitHub file page works — paste it as you see it."
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx
|
||||
msgid "The authentication methods required for recipients to sign fields"
|
||||
msgstr "受信者がフィールドに署名する際に必要な認証方法"
|
||||
@@ -10036,6 +10092,7 @@ msgid "Time Zone"
|
||||
msgstr "タイムゾーン"
|
||||
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table.tsx
|
||||
#: apps/remix/app/components/tables/inbox-table.tsx
|
||||
@@ -11076,6 +11133,10 @@ msgstr "現在、このパスキーは削除できません。後でもう一度
|
||||
msgid "We are unable to update this passkey at the moment. Please try again later."
|
||||
msgstr "現在、このパスキーは更新できません。後でもう一度お試しください。"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "We could not reach that address."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/ai-features-enable-dialog.tsx
|
||||
msgid "We couldn't enable AI features right now. Please try again."
|
||||
msgstr "現在AI機能を有効にできませんでした。もう一度お試しください。"
|
||||
@@ -12321,8 +12382,7 @@ msgid "Your document has been updated successfully"
|
||||
msgstr "ドキュメントは正常に更新されました"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your document has been uploaded successfully."
|
||||
msgstr "文書を正常にアップロードしました。"
|
||||
|
||||
@@ -12483,8 +12543,7 @@ msgstr "テンプレートは正常に削除されました。"
|
||||
msgid "Your template has been updated successfully"
|
||||
msgstr "テンプレートは正常に更新されました"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your template has been uploaded successfully."
|
||||
msgstr "テンプレートを正常にアップロードしました。"
|
||||
|
||||
|
||||
@@ -601,6 +601,10 @@ msgstr "{validationLength, plural, other {정확히 #개의 옵션을 선택하
|
||||
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
|
||||
msgstr "{visibleRows, plural, other {총 #개 결과 중 표시 중입니다.}}"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "# Mutual NDA — both parties agree..."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: document.title
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
@@ -2319,6 +2323,8 @@ msgstr "누군가를 찾을 수 없나요?"
|
||||
#: apps/remix/app/components/general/document/document-attachments-popover.tsx
|
||||
#: apps/remix/app/components/general/document/embedded-editor-attachment-popover.tsx
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-usage.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -2718,6 +2724,7 @@ msgstr "연락처 정보"
|
||||
msgid "Contact us"
|
||||
msgstr "문의하기"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
|
||||
msgid "Content"
|
||||
msgstr "콘텐츠"
|
||||
@@ -2873,6 +2880,7 @@ msgstr "값 복사"
|
||||
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create"
|
||||
msgstr "생성"
|
||||
@@ -3909,11 +3917,14 @@ msgid "Document upload disabled due to unpaid invoices"
|
||||
msgstr "미납된 청구서로 인해 문서 업로드가 비활성화되었습니다"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Document uploaded"
|
||||
msgstr "문서가 업로드되었습니다"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Document URL"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document viewed"
|
||||
@@ -4698,8 +4709,7 @@ msgstr "봉투가 업데이트되었습니다"
|
||||
#: apps/remix/app/components/general/document/document-edit-form.tsx
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
#: apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
@@ -5445,6 +5455,20 @@ msgstr "받은편지함에서 확인 링크를 찾을 수 없다면 아래에서
|
||||
msgid "If your authenticator app does not support QR codes, you can use the following code instead:"
|
||||
msgstr "인증 앱이 QR 코드를 지원하지 않는 경우, 대신 다음 코드를 사용할 수 있습니다:"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import failed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import from URL"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "Important: What This Means"
|
||||
msgstr "중요: 이것이 의미하는 것"
|
||||
@@ -6953,6 +6977,19 @@ msgctxt "Subscription status"
|
||||
msgid "Past Due"
|
||||
msgstr "연체"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste content"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste text or Markdown. It becomes a PDF you can place fields on and send for signature."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Pasted document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "PDF 문서"
|
||||
@@ -9036,6 +9073,10 @@ msgstr "시스템 요구 사항"
|
||||
msgid "System Theme"
|
||||
msgstr "시스템 테마"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Taken from the first line"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
|
||||
msgid "Target Organisation"
|
||||
msgstr "대상 조직"
|
||||
@@ -9273,8 +9314,7 @@ msgstr "템플릿이 업데이트되었습니다."
|
||||
msgid "Template updated successfully"
|
||||
msgstr "템플릿이 성공적으로 업데이트되었습니다."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Template uploaded"
|
||||
msgstr "템플릿이 업로드되었습니다."
|
||||
|
||||
@@ -9352,6 +9392,18 @@ msgstr "서명 프로세스를 완료해 주셔서 감사합니다."
|
||||
msgid "Thank you for using Hanzo Sign to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address cannot be read. Use a public http(s) link."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address does not lead to a PDF."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That document is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB."
|
||||
msgstr ""
|
||||
|
||||
#: packages/email/template-components/template-forgot-password.tsx
|
||||
msgid "That's okay, it happens! Click the button below to reset your password."
|
||||
msgstr "괜찮습니다. 누구나 그럴 수 있죠! 아래 버튼을 클릭해 비밀번호를 재설정하세요."
|
||||
@@ -9368,6 +9420,10 @@ msgstr "계정이 성공적으로 비활성화되었습니다."
|
||||
msgid "The account has been enabled successfully."
|
||||
msgstr "계정이 성공적으로 활성화되었습니다."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "The address of a PDF we can reach. A GitHub file page works — paste it as you see it."
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx
|
||||
msgid "The authentication methods required for recipients to sign fields"
|
||||
msgstr "수신자가 필드를 서명할 때 필요한 인증 방식입니다."
|
||||
@@ -10036,6 +10092,7 @@ msgid "Time Zone"
|
||||
msgstr "시간대"
|
||||
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table.tsx
|
||||
#: apps/remix/app/components/tables/inbox-table.tsx
|
||||
@@ -11076,6 +11133,10 @@ msgstr "현재 이 패스키를 제거할 수 없습니다. 나중에 다시 시
|
||||
msgid "We are unable to update this passkey at the moment. Please try again later."
|
||||
msgstr "현재 이 패스키를 업데이트할 수 없습니다. 나중에 다시 시도해 주세요."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "We could not reach that address."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/ai-features-enable-dialog.tsx
|
||||
msgid "We couldn't enable AI features right now. Please try again."
|
||||
msgstr "지금은 AI 기능을 활성화할 수 없습니다. 다시 시도해 주세요."
|
||||
@@ -12321,8 +12382,7 @@ msgid "Your document has been updated successfully"
|
||||
msgstr "문서가 성공적으로 업데이트되었습니다."
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your document has been uploaded successfully."
|
||||
msgstr "문서가 성공적으로 업로드되었습니다."
|
||||
|
||||
@@ -12483,8 +12543,7 @@ msgstr "템플릿이 성공적으로 삭제되었습니다."
|
||||
msgid "Your template has been updated successfully"
|
||||
msgstr "템플릿이 성공적으로 업데이트되었습니다."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your template has been uploaded successfully."
|
||||
msgstr "템플릿이 성공적으로 업로드되었습니다."
|
||||
|
||||
|
||||
@@ -601,6 +601,10 @@ msgstr "{validationLength, plural, one {Selecteer precies # optie} other {Select
|
||||
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
|
||||
msgstr "{visibleRows, plural, one {# resultaat wordt getoond.} other {# resultaten worden getoond.}}"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "# Mutual NDA — both parties agree..."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: document.title
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
@@ -2319,6 +2323,8 @@ msgstr "Kunt u niemand vinden?"
|
||||
#: apps/remix/app/components/general/document/document-attachments-popover.tsx
|
||||
#: apps/remix/app/components/general/document/embedded-editor-attachment-popover.tsx
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-usage.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -2718,6 +2724,7 @@ msgstr "Contactgegevens"
|
||||
msgid "Contact us"
|
||||
msgstr "Neem contact met ons op"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
|
||||
msgid "Content"
|
||||
msgstr "Inhoud"
|
||||
@@ -2873,6 +2880,7 @@ msgstr "Waarde kopiëren"
|
||||
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create"
|
||||
msgstr "Aanmaken"
|
||||
@@ -3909,11 +3917,14 @@ msgid "Document upload disabled due to unpaid invoices"
|
||||
msgstr "Uploaden van documenten uitgeschakeld wegens onbetaalde facturen"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Document uploaded"
|
||||
msgstr "Document geüpload"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Document URL"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document viewed"
|
||||
@@ -4698,8 +4709,7 @@ msgstr "Envelope bijgewerkt"
|
||||
#: apps/remix/app/components/general/document/document-edit-form.tsx
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
#: apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
@@ -5445,6 +5455,20 @@ msgstr "Als je de bevestigingslink niet in je inbox vindt, kun je hieronder een
|
||||
msgid "If your authenticator app does not support QR codes, you can use the following code instead:"
|
||||
msgstr "Als je authenticator‑app geen QR‑codes ondersteunt, kun je in plaats daarvan de volgende code gebruiken:"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import failed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import from URL"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "Important: What This Means"
|
||||
msgstr "Belangrijk: wat dit betekent"
|
||||
@@ -6953,6 +6977,19 @@ msgctxt "Subscription status"
|
||||
msgid "Past Due"
|
||||
msgstr "Achterstallig"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste content"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste text or Markdown. It becomes a PDF you can place fields on and send for signature."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Pasted document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "PDF-document"
|
||||
@@ -9036,6 +9073,10 @@ msgstr "Systeemvereisten"
|
||||
msgid "System Theme"
|
||||
msgstr "Systeemthema"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Taken from the first line"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
|
||||
msgid "Target Organisation"
|
||||
msgstr "Doelorganisatie"
|
||||
@@ -9273,8 +9314,7 @@ msgstr "Sjabloon bijgewerkt"
|
||||
msgid "Template updated successfully"
|
||||
msgstr "Sjabloon succesvol bijgewerkt"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Template uploaded"
|
||||
msgstr "Sjabloon geüpload"
|
||||
|
||||
@@ -9352,6 +9392,18 @@ msgstr "Bedankt dat je het ondertekeningsproces hebt voltooid."
|
||||
msgid "Thank you for using Hanzo Sign to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address cannot be read. Use a public http(s) link."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address does not lead to a PDF."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That document is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB."
|
||||
msgstr ""
|
||||
|
||||
#: packages/email/template-components/template-forgot-password.tsx
|
||||
msgid "That's okay, it happens! Click the button below to reset your password."
|
||||
msgstr "Geeft niets, dat kan gebeuren! Klik op de onderstaande knop om je wachtwoord te resetten."
|
||||
@@ -9368,6 +9420,10 @@ msgstr "Het account is succesvol uitgeschakeld."
|
||||
msgid "The account has been enabled successfully."
|
||||
msgstr "Het account is succesvol ingeschakeld."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "The address of a PDF we can reach. A GitHub file page works — paste it as you see it."
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx
|
||||
msgid "The authentication methods required for recipients to sign fields"
|
||||
msgstr "De authenticatiemethoden die vereist zijn voor ontvangers om velden te ondertekenen"
|
||||
@@ -10036,6 +10092,7 @@ msgid "Time Zone"
|
||||
msgstr "Tijdzone"
|
||||
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table.tsx
|
||||
#: apps/remix/app/components/tables/inbox-table.tsx
|
||||
@@ -11076,6 +11133,10 @@ msgstr "We kunnen deze passkey momenteel niet verwijderen. Probeer het later opn
|
||||
msgid "We are unable to update this passkey at the moment. Please try again later."
|
||||
msgstr "We kunnen deze passkey momenteel niet bijwerken. Probeer het later opnieuw."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "We could not reach that address."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/ai-features-enable-dialog.tsx
|
||||
msgid "We couldn't enable AI features right now. Please try again."
|
||||
msgstr "We konden AI-functies nu niet inschakelen. Probeer het opnieuw."
|
||||
@@ -12321,8 +12382,7 @@ msgid "Your document has been updated successfully"
|
||||
msgstr "Je document is succesvol bijgewerkt"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your document has been uploaded successfully."
|
||||
msgstr "Je document is succesvol geüpload."
|
||||
|
||||
@@ -12483,8 +12543,7 @@ msgstr "Je sjabloon is succesvol verwijderd."
|
||||
msgid "Your template has been updated successfully"
|
||||
msgstr "Je sjabloon is succesvol bijgewerkt"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your template has been uploaded successfully."
|
||||
msgstr "Uw sjabloon is succesvol geüpload."
|
||||
|
||||
|
||||
@@ -601,6 +601,10 @@ msgstr "{validationLength, plural, one {Wybierz dokładnie # opcję} few {Wybier
|
||||
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
|
||||
msgstr "{visibleRows, plural, one {# wynik} few {# wyniki} many {# wyników} other {# wyników}}"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "# Mutual NDA — both parties agree..."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: document.title
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
@@ -2319,6 +2323,8 @@ msgstr "Nie możesz kogoś znaleźć?"
|
||||
#: apps/remix/app/components/general/document/document-attachments-popover.tsx
|
||||
#: apps/remix/app/components/general/document/embedded-editor-attachment-popover.tsx
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-usage.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -2718,6 +2724,7 @@ msgstr "Informacje kontaktowe"
|
||||
msgid "Contact us"
|
||||
msgstr "Skontaktuj się z nami"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
|
||||
msgid "Content"
|
||||
msgstr "Zawartość"
|
||||
@@ -2873,6 +2880,7 @@ msgstr "Kopiuj wartość"
|
||||
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create"
|
||||
msgstr "Utwórz"
|
||||
@@ -3909,11 +3917,14 @@ msgid "Document upload disabled due to unpaid invoices"
|
||||
msgstr "Przesyłanie dokumentów zostało wyłączone z powodu nieopłaconych faktur"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Document uploaded"
|
||||
msgstr "Dokument został przesłany"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Document URL"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document viewed"
|
||||
@@ -4698,8 +4709,7 @@ msgstr "Koperta została zaktualizowana"
|
||||
#: apps/remix/app/components/general/document/document-edit-form.tsx
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
#: apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
@@ -5445,6 +5455,20 @@ msgstr "Jeśli nie znajdziesz wiadomości z linkiem potwierdzającym, możesz po
|
||||
msgid "If your authenticator app does not support QR codes, you can use the following code instead:"
|
||||
msgstr "Jeśli Twoja aplikacja uwierzytelniająca nie obsługuje kodów QR, użyj poniższego kodu:"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import failed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import from URL"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "Important: What This Means"
|
||||
msgstr "Uwaga: Co to oznacza"
|
||||
@@ -6953,6 +6977,19 @@ msgctxt "Subscription status"
|
||||
msgid "Past Due"
|
||||
msgstr "Przeterminowana"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste content"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste text or Markdown. It becomes a PDF you can place fields on and send for signature."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Pasted document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "Dokument PDF"
|
||||
@@ -9036,6 +9073,10 @@ msgstr "Wymagania systemowe"
|
||||
msgid "System Theme"
|
||||
msgstr "Motyw systemowy"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Taken from the first line"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
|
||||
msgid "Target Organisation"
|
||||
msgstr "Docelowa organizacja"
|
||||
@@ -9273,8 +9314,7 @@ msgstr "Szablon zaktualizowany"
|
||||
msgid "Template updated successfully"
|
||||
msgstr "Szablon został zaktualizowany"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Template uploaded"
|
||||
msgstr "Szablon został przesłany"
|
||||
|
||||
@@ -9352,6 +9392,18 @@ msgstr "Podpisywanie zostało zakończone."
|
||||
msgid "Thank you for using Hanzo Sign to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address cannot be read. Use a public http(s) link."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address does not lead to a PDF."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That document is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB."
|
||||
msgstr ""
|
||||
|
||||
#: packages/email/template-components/template-forgot-password.tsx
|
||||
msgid "That's okay, it happens! Click the button below to reset your password."
|
||||
msgstr "Zdarza się nawet najlepszym! Kliknij przycisk poniżej, aby zresetować hasło."
|
||||
@@ -9368,6 +9420,10 @@ msgstr "Konto zostało pomyślnie wyłączone."
|
||||
msgid "The account has been enabled successfully."
|
||||
msgstr "Konto zostało pomyślnie włączone."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "The address of a PDF we can reach. A GitHub file page works — paste it as you see it."
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx
|
||||
msgid "The authentication methods required for recipients to sign fields"
|
||||
msgstr "Wymagane metody uwierzytelniania odbiorców do podpisywania pól"
|
||||
@@ -10038,6 +10094,7 @@ msgid "Time Zone"
|
||||
msgstr "Strefa czasowa"
|
||||
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table.tsx
|
||||
#: apps/remix/app/components/tables/inbox-table.tsx
|
||||
@@ -11078,6 +11135,10 @@ msgstr "Nie mogliśmy usunąć klucza dostępu. Spróbuj ponownie później."
|
||||
msgid "We are unable to update this passkey at the moment. Please try again later."
|
||||
msgstr "Nie mogliśmy zaktualizować klucza dostępu. Spróbuj ponownie później."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "We could not reach that address."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/ai-features-enable-dialog.tsx
|
||||
msgid "We couldn't enable AI features right now. Please try again."
|
||||
msgstr "Nie mogliśmy włączyć funkcji AI. Spróbuj ponownie."
|
||||
@@ -12323,8 +12384,7 @@ msgid "Your document has been updated successfully"
|
||||
msgstr "Twój dokument został pomyślnie zaktualizowany"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your document has been uploaded successfully."
|
||||
msgstr "Twój dokument został pomyślnie przesłany."
|
||||
|
||||
@@ -12485,8 +12545,7 @@ msgstr "Twój szablon został pomyślnie usunięty."
|
||||
msgid "Your template has been updated successfully"
|
||||
msgstr "Twój szablon został pomyślnie zaktualizowany"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your template has been uploaded successfully."
|
||||
msgstr "Twój szablon został pomyślnie przesłany."
|
||||
|
||||
|
||||
@@ -596,6 +596,10 @@ msgstr "{validationLength, plural, one {Selecione exatamente # opção} other {S
|
||||
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
|
||||
msgstr "{visibleRows, plural, one {Exibindo # resultado.} other {Exibindo # resultados.}}"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "# Mutual NDA — both parties agree..."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: document.title
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
@@ -2314,6 +2318,8 @@ msgstr ""
|
||||
#: apps/remix/app/components/general/document/document-attachments-popover.tsx
|
||||
#: apps/remix/app/components/general/document/embedded-editor-attachment-popover.tsx
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-usage.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -2713,6 +2719,7 @@ msgstr "Informações de Contato"
|
||||
msgid "Contact us"
|
||||
msgstr "Contate-nos"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
|
||||
msgid "Content"
|
||||
msgstr "Conteúdo"
|
||||
@@ -2868,6 +2875,7 @@ msgstr ""
|
||||
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create"
|
||||
msgstr "Criar"
|
||||
@@ -3904,11 +3912,14 @@ msgid "Document upload disabled due to unpaid invoices"
|
||||
msgstr "Upload de documentos desativado devido a faturas não pagas"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Document uploaded"
|
||||
msgstr "Documento carregado"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Document URL"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document viewed"
|
||||
@@ -4693,8 +4704,7 @@ msgstr "Envelope atualizado"
|
||||
#: apps/remix/app/components/general/document/document-edit-form.tsx
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
#: apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
@@ -5440,6 +5450,20 @@ msgstr "Se você não encontrar o link de confirmação em sua caixa de entrada,
|
||||
msgid "If your authenticator app does not support QR codes, you can use the following code instead:"
|
||||
msgstr "Se seu aplicativo autenticador não suportar códigos QR, você pode usar o seguinte código:"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import failed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import from URL"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "Important: What This Means"
|
||||
msgstr "Importante: O que isso significa"
|
||||
@@ -6948,6 +6972,19 @@ msgctxt "Subscription status"
|
||||
msgid "Past Due"
|
||||
msgstr "Vencido"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste content"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste text or Markdown. It becomes a PDF you can place fields on and send for signature."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Pasted document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "Documento PDF"
|
||||
@@ -9031,6 +9068,10 @@ msgstr "Requisitos do Sistema"
|
||||
msgid "System Theme"
|
||||
msgstr "Tema do Sistema"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Taken from the first line"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
|
||||
msgid "Target Organisation"
|
||||
msgstr ""
|
||||
@@ -9268,8 +9309,7 @@ msgstr ""
|
||||
msgid "Template updated successfully"
|
||||
msgstr "Modelo atualizado com sucesso"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Template uploaded"
|
||||
msgstr "Modelo enviado"
|
||||
|
||||
@@ -9347,6 +9387,18 @@ msgstr "Obrigado por concluir o processo de assinatura."
|
||||
msgid "Thank you for using Hanzo Sign to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address cannot be read. Use a public http(s) link."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address does not lead to a PDF."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That document is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB."
|
||||
msgstr ""
|
||||
|
||||
#: packages/email/template-components/template-forgot-password.tsx
|
||||
msgid "That's okay, it happens! Click the button below to reset your password."
|
||||
msgstr "Tudo bem, acontece! Clique no botão abaixo para redefinir sua senha."
|
||||
@@ -9363,6 +9415,10 @@ msgstr "A conta foi desativada com sucesso."
|
||||
msgid "The account has been enabled successfully."
|
||||
msgstr "A conta foi ativada com sucesso."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "The address of a PDF we can reach. A GitHub file page works — paste it as you see it."
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx
|
||||
msgid "The authentication methods required for recipients to sign fields"
|
||||
msgstr "Os métodos de autenticação necessários para os destinatários assinarem campos"
|
||||
@@ -10031,6 +10087,7 @@ msgid "Time Zone"
|
||||
msgstr "Fuso Horário"
|
||||
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table.tsx
|
||||
#: apps/remix/app/components/tables/inbox-table.tsx
|
||||
@@ -11071,6 +11128,10 @@ msgstr "Não conseguimos remover esta passkey no momento. Por favor, tente novam
|
||||
msgid "We are unable to update this passkey at the moment. Please try again later."
|
||||
msgstr "Não conseguimos atualizar esta passkey no momento. Por favor, tente novamente mais tarde."
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "We could not reach that address."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/ai-features-enable-dialog.tsx
|
||||
msgid "We couldn't enable AI features right now. Please try again."
|
||||
msgstr ""
|
||||
@@ -12316,8 +12377,7 @@ msgid "Your document has been updated successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your document has been uploaded successfully."
|
||||
msgstr "Seu documento foi enviado com sucesso."
|
||||
|
||||
@@ -12478,8 +12538,7 @@ msgstr "Seu modelo foi excluído com sucesso."
|
||||
msgid "Your template has been updated successfully"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your template has been uploaded successfully."
|
||||
msgstr "Seu modelo foi enviado com sucesso."
|
||||
|
||||
|
||||
@@ -601,6 +601,10 @@ msgstr "{validationLength, plural, other {必须正好选择 # 个选项}}"
|
||||
msgid "{visibleRows, plural, one {Showing # result.} other {Showing # results.}}"
|
||||
msgstr "{visibleRows, plural, other {正在显示 # 条结果。}}"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "# Mutual NDA — both parties agree..."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: document.title
|
||||
#. placeholder {0}: envelope.title
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx
|
||||
@@ -2319,6 +2323,8 @@ msgstr "找不到某个人?"
|
||||
#: apps/remix/app/components/general/document/document-attachments-popover.tsx
|
||||
#: apps/remix/app/components/general/document/embedded-editor-attachment-popover.tsx
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/teams/team-email-usage.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
@@ -2718,6 +2724,7 @@ msgstr "联系信息"
|
||||
msgid "Contact us"
|
||||
msgstr "联系我们"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/site-settings.tsx
|
||||
msgid "Content"
|
||||
msgstr "内容"
|
||||
@@ -2873,6 +2880,7 @@ msgstr "复制值"
|
||||
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/routes/_profile+/_layout.tsx
|
||||
msgid "Create"
|
||||
msgstr "创建"
|
||||
@@ -3909,11 +3917,14 @@ msgid "Document upload disabled due to unpaid invoices"
|
||||
msgstr "由于有未支付账单,已禁用文档上传"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Document uploaded"
|
||||
msgstr "文档已上传"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Document URL"
|
||||
msgstr ""
|
||||
|
||||
#: packages/lib/utils/document-audit-logs.ts
|
||||
msgctxt "Audit log format"
|
||||
msgid "Document viewed"
|
||||
@@ -4698,8 +4709,7 @@ msgstr "信封已更新"
|
||||
#: apps/remix/app/components/general/document/document-edit-form.tsx
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
#: apps/remix/app/components/general/pdf-viewer/pdf-viewer.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
@@ -5445,6 +5455,20 @@ msgstr "如果在收件箱中未找到确认链接,你可以在下方重新请
|
||||
msgid "If your authenticator app does not support QR codes, you can use the following code instead:"
|
||||
msgstr "如果你的验证器应用不支持二维码,你可以使用以下代码:"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import failed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Import from URL"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx
|
||||
msgid "Important: What This Means"
|
||||
msgstr "重要:这意味着什么"
|
||||
@@ -6953,6 +6977,19 @@ msgctxt "Subscription status"
|
||||
msgid "Past Due"
|
||||
msgstr "逾期"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste content"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Paste text or Markdown. It becomes a PDF you can place fields on and send for signature."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Pasted document"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgid "PDF Document"
|
||||
msgstr "PDF 文档"
|
||||
@@ -9036,6 +9073,10 @@ msgstr "系统要求"
|
||||
msgid "System Theme"
|
||||
msgstr "系统主题"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "Taken from the first line"
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx
|
||||
msgid "Target Organisation"
|
||||
msgstr "目标组织"
|
||||
@@ -9273,8 +9314,7 @@ msgstr "模板已更新"
|
||||
msgid "Template updated successfully"
|
||||
msgstr "模板更新成功"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Template uploaded"
|
||||
msgstr "模板已上传"
|
||||
|
||||
@@ -9352,6 +9392,18 @@ msgstr "感谢您完成签署流程。"
|
||||
msgid "Thank you for using Hanzo Sign to perform your electronic document signing. The purpose of this disclosure is to inform you about the process, legality, and your rights regarding the use of electronic signatures on our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined below."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address cannot be read. Use a public http(s) link."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That address does not lead to a PDF."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "That document is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB."
|
||||
msgstr ""
|
||||
|
||||
#: packages/email/template-components/template-forgot-password.tsx
|
||||
msgid "That's okay, it happens! Click the button below to reset your password."
|
||||
msgstr "没关系,会有这种情况!点击下方按钮重置您的密码。"
|
||||
@@ -9368,6 +9420,10 @@ msgstr "账户已成功禁用。"
|
||||
msgid "The account has been enabled successfully."
|
||||
msgstr "账户已成功启用。"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
msgid "The address of a PDF we can reach. A GitHub file page works — paste it as you see it."
|
||||
msgstr ""
|
||||
|
||||
#: packages/ui/components/recipient/recipient-action-auth-select.tsx
|
||||
msgid "The authentication methods required for recipients to sign fields"
|
||||
msgstr "收件人签署字段所需的认证方式"
|
||||
@@ -10036,6 +10092,7 @@ msgid "Time Zone"
|
||||
msgstr "时区"
|
||||
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-view.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-import-menu.tsx
|
||||
#: apps/remix/app/components/general/template/template-page-view-documents-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table.tsx
|
||||
#: apps/remix/app/components/tables/inbox-table.tsx
|
||||
@@ -11076,6 +11133,10 @@ msgstr "目前无法移除此通行密钥。请稍后再试。"
|
||||
msgid "We are unable to update this passkey at the moment. Please try again later."
|
||||
msgstr "目前无法更新此通行密钥。请稍后再试。"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/upload-error.ts
|
||||
msgid "We could not reach that address."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/dialogs/ai-features-enable-dialog.tsx
|
||||
msgid "We couldn't enable AI features right now. Please try again."
|
||||
msgstr "我们现在无法启用 AI 功能。请重试。"
|
||||
@@ -12321,8 +12382,7 @@ msgid "Your document has been updated successfully"
|
||||
msgstr "您的文档已成功更新"
|
||||
|
||||
#: apps/remix/app/components/general/document/document-upload-button-legacy.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your document has been uploaded successfully."
|
||||
msgstr "你的文档已成功上传。"
|
||||
|
||||
@@ -12483,8 +12543,7 @@ msgstr "你的模板已成功删除。"
|
||||
msgid "Your template has been updated successfully"
|
||||
msgstr "您的模板已成功更新"
|
||||
|
||||
#: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope/envelope-upload-button.tsx
|
||||
#: apps/remix/app/components/general/envelope/create.ts
|
||||
msgid "Your template has been uploaded successfully."
|
||||
msgstr "您的模板已成功上传。"
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AppError } from '../errors/app-error';
|
||||
|
||||
/**
|
||||
* Where a document can come from, and which URLs the server may dial to get it.
|
||||
*
|
||||
* Every source resolves to PDF bytes and is created through `envelope.create`,
|
||||
* so nothing downstream knows which one it was. Only the resolver (server-only/
|
||||
* import.ts) turns a source into bytes; this module is the contract and the
|
||||
* policy, and holds no IO so it can be read the same way on both sides.
|
||||
*/
|
||||
|
||||
/** Why an import failed. `uploadErrorMessage` words each one for the user. */
|
||||
export const ImportError = {
|
||||
url: 'IMPORT_URL_INVALID',
|
||||
fetch: 'IMPORT_FETCH_FAILED',
|
||||
pdf: 'IMPORT_NOT_PDF',
|
||||
size: 'IMPORT_TOO_LARGE',
|
||||
} as const;
|
||||
|
||||
/** A pasted document longer than this is a paste accident, not a contract. */
|
||||
export const TEXT_LIMIT = 500_000;
|
||||
|
||||
export const ZImportSourceSchema = z.discriminatedUnion('kind', [
|
||||
z.object({
|
||||
kind: z.literal('url'),
|
||||
url: z.string().trim().min(1).max(2048),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('text'),
|
||||
title: z.string().trim().min(1).max(255),
|
||||
text: z.string().min(1).max(TEXT_LIMIT),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type TImportSource = z.infer<typeof ZImportSourceSchema>;
|
||||
|
||||
const invalid = (message: string) => new AppError(ImportError.url, { message, statusCode: 400 });
|
||||
|
||||
/** Names that only resolve inside a cluster, however they are spelled. */
|
||||
const PRIVATE_NAME = /(^|\.)(localhost|local|internal|home\.arpa)$/;
|
||||
|
||||
/** Dotted-quad, or null when the host is not an IPv4 literal. `URL` has already
|
||||
* canonicalised the octal/hex/integer spellings by the time we see it. */
|
||||
const ipv4 = (host: string): number[] | null => {
|
||||
const parts = host.split('.');
|
||||
|
||||
if (parts.length !== 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const octets = parts.map((part) => (/^\d{1,3}$/.test(part) ? Number(part) : -1));
|
||||
|
||||
return octets.every((octet) => octet >= 0 && octet <= 255) ? octets : null;
|
||||
};
|
||||
|
||||
/** Eight hextets, or null when the host is not an IPv6 literal. */
|
||||
const ipv6 = (host: string): number[] | null => {
|
||||
if (!host.includes(':')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [head, tail, ...rest] = host.split('::');
|
||||
|
||||
if (rest.length > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const read = (part: string) =>
|
||||
part === ''
|
||||
? []
|
||||
: part.split(':').map((h) => (/^[0-9a-f]{1,4}$/.test(h) ? parseInt(h, 16) : -1));
|
||||
|
||||
const left = read(head);
|
||||
const right = tail === undefined ? [] : read(tail);
|
||||
const gap = 8 - left.length - right.length;
|
||||
|
||||
if (gap < 0 || (tail === undefined && gap !== 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hextets = [...left, ...Array(gap).fill(0), ...right];
|
||||
|
||||
return hextets.every((hextet) => hextet >= 0) ? hextets : null;
|
||||
};
|
||||
|
||||
const privateIpv4 = ([a, b]: number[]): boolean =>
|
||||
a === 0 || // this network
|
||||
a === 10 || // private
|
||||
a === 127 || // loopback
|
||||
(a === 100 && b >= 64 && b <= 127) || // carrier-grade NAT
|
||||
(a === 169 && b === 254) || // link-local, incl. the cloud metadata address
|
||||
(a === 172 && b >= 16 && b <= 31) || // private
|
||||
(a === 192 && b === 0) || // IETF protocol assignments
|
||||
(a === 192 && b === 168) || // private
|
||||
(a === 198 && b >= 18 && b <= 19) || // benchmarking
|
||||
a >= 224; // multicast and reserved
|
||||
|
||||
/** Whether a host is one the server must not reach out to. Read twice: once for
|
||||
* the hostname in the URL, once for every address that hostname resolves to. */
|
||||
export const isBlocked = (host: string): boolean => {
|
||||
const name = host.toLowerCase().replace(/^\[|\]$/g, '');
|
||||
|
||||
if (name === '' || PRIVATE_NAME.test(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const v4 = ipv4(name);
|
||||
|
||||
if (v4) {
|
||||
return privateIpv4(v4);
|
||||
}
|
||||
|
||||
const v6 = ipv6(name);
|
||||
|
||||
if (!v6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// IPv4-mapped (::ffff:a.b.c.d) carries a v4 address in the last two hextets.
|
||||
if (v6.slice(0, 5).every((hextet) => hextet === 0) && v6[5] === 0xffff) {
|
||||
return privateIpv4([v6[6] >> 8, v6[6] & 0xff, v6[7] >> 8, v6[7] & 0xff]);
|
||||
}
|
||||
|
||||
return (
|
||||
v6.every((hextet) => hextet === 0) || // unspecified
|
||||
(v6.slice(0, 7).every((hextet) => hextet === 0) && v6[7] === 1) || // loopback
|
||||
(v6[0] & 0xfe00) === 0xfc00 || // unique local
|
||||
(v6[0] & 0xffc0) === 0xfe80 // link local
|
||||
);
|
||||
};
|
||||
|
||||
/** A GitHub page URL points at HTML; the same file is served raw one host over.
|
||||
* Pasting the address from the browser is the natural thing to do, so accept
|
||||
* it and read the bytes GitHub would have shown. */
|
||||
const github = (url: URL): URL => {
|
||||
if (url.hostname !== 'github.com' && url.hostname !== 'www.github.com') {
|
||||
return url;
|
||||
}
|
||||
|
||||
const [owner, repo, kind, ...path] = url.pathname.replace(/^\//, '').split('/');
|
||||
|
||||
if (!owner || !repo || (kind !== 'blob' && kind !== 'raw') || path.length < 2) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return new URL(`https://raw.githubusercontent.com/${owner}/${repo}/${path.join('/')}`);
|
||||
};
|
||||
|
||||
/** Parse a URL the server is allowed to fetch, refusing everything else. */
|
||||
export const parseUrl = (raw: string): URL => {
|
||||
let url: URL;
|
||||
|
||||
try {
|
||||
url = new URL(raw.trim());
|
||||
} catch {
|
||||
throw invalid('The address is not a URL');
|
||||
}
|
||||
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||
throw invalid(`Unsupported scheme: ${url.protocol}`);
|
||||
}
|
||||
|
||||
if (url.username || url.password) {
|
||||
throw invalid('The address carries credentials');
|
||||
}
|
||||
|
||||
if (isBlocked(url.hostname)) {
|
||||
throw invalid(`Unreachable host: ${url.hostname}`);
|
||||
}
|
||||
|
||||
return github(url);
|
||||
};
|
||||
|
||||
/** Whether bytes are a PDF. A reader tolerates a little junk before the header,
|
||||
* so look for it the same way rather than only at offset zero. */
|
||||
export const isPdf = (bytes: Uint8Array): boolean => {
|
||||
const head = bytes.subarray(0, 1024);
|
||||
|
||||
for (let i = 0; i + 5 <= head.length; i += 1) {
|
||||
if (
|
||||
head[i] === 0x25 && // %
|
||||
head[i + 1] === 0x50 && // P
|
||||
head[i + 2] === 0x44 && // D
|
||||
head[i + 3] === 0x46 && // F
|
||||
head[i + 4] === 0x2d // -
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
Reference in New Issue
Block a user