feat: web i18n (#1286)

This commit is contained in:
David Nguyen
2024-08-27 20:34:39 +09:00
committed by GitHub
parent 0829311214
commit 75c8772a02
294 changed files with 14846 additions and 2229 deletions

View File

@ -4,6 +4,8 @@ import { useEffect, useMemo, useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@ -39,8 +41,10 @@ export const CheckboxField = ({
onSignField,
onUnsignField,
}: CheckboxFieldProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const router = useRouter();
const [isPending, startTransition] = useTransition();
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
@ -115,8 +119,8 @@ export const CheckboxField = ({
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while signing the document.',
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@ -144,8 +148,8 @@ export const CheckboxField = ({
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while removing the signature.',
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
variant: 'destructive',
});
}
@ -205,8 +209,8 @@ export const CheckboxField = ({
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while updating the signature.',
title: _(msg`Error`),
description: _(msg`An error occurred while updating the signature.`),
variant: 'destructive',
});
} finally {

View File

@ -3,6 +3,8 @@
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
@ -49,8 +51,10 @@ export const ZClaimAccountFormSchema = z
export type TClaimAccountFormSchema = z.infer<typeof ZClaimAccountFormSchema>;
export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) => {
const analytics = useAnalytics();
const { _ } = useLingui();
const { toast } = useToast();
const analytics = useAnalytics();
const router = useRouter();
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
@ -71,9 +75,10 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
router.push(`/unverified-account`);
toast({
title: 'Registration Successful',
description:
'You have successfully registered. Please verify your account by clicking on the link you received in the email.',
title: _(msg`Registration Successful`),
description: _(
msg`You have successfully registered. Please verify your account by clicking on the link you received in the email.`,
),
duration: 5000,
});
@ -84,15 +89,16 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
} catch (error) {
if (error instanceof TRPCClientError && error.data?.code === 'BAD_REQUEST') {
toast({
title: 'An error occurred',
title: _(msg`An error occurred`),
description: error.message,
variant: 'destructive',
});
} else {
toast({
title: 'An unknown error occurred',
description:
'We encountered an unknown error while attempting to sign you up. Please try again later.',
title: _(msg`An unknown error occurred`),
description: _(
msg`We encountered an unknown error while attempting to sign you up. Please try again later.`,
),
variant: 'destructive',
});
}
@ -109,9 +115,11 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter your name" />
<Input {...field} placeholder={_(msg`Enter your name`)} />
</FormControl>
<FormMessage />
</FormItem>
@ -122,9 +130,11 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
control={form.control}
render={({ field }) => (
<FormItem className="mt-4">
<FormLabel>Email address</FormLabel>
<FormLabel>
<Trans>Email address</Trans>
</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter your email" />
<Input {...field} placeholder={_(msg`Enter your email`)} />
</FormControl>
<FormMessage />
</FormItem>
@ -135,9 +145,11 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
control={form.control}
render={({ field }) => (
<FormItem className="mt-4">
<FormLabel>Set a password</FormLabel>
<FormLabel>
<Trans>Set a password</Trans>
</FormLabel>
<FormControl>
<PasswordInput {...field} placeholder="Pick a password" />
<PasswordInput {...field} placeholder={_(msg`Pick a password`)} />
</FormControl>
<FormMessage />
</FormItem>
@ -145,7 +157,7 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
/>
<Button type="submit" className="mt-6 w-full" loading={form.formState.isSubmitting}>
Claim account
<Trans>Claim account</Trans>
</Button>
</fieldset>
</form>

View File

@ -2,6 +2,7 @@
import { useState } from 'react';
import { Trans } from '@lingui/macro';
import { FileSearch } from 'lucide-react';
import type { DocumentData } from '@documenso/prisma/client';
@ -30,7 +31,7 @@ export const DocumentPreviewButton = ({
{...props}
>
<FileSearch className="mr-2 h-5 w-5" strokeWidth={1.7} />
View Original Document
<Trans>View Original Document</Trans>
</Button>
<DocumentDialog documentData={documentData} open={showDialog} onOpenChange={setShowDialog} />

View File

@ -1,5 +1,7 @@
import React from 'react';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { RefreshOnFocus } from '~/components/(dashboard)/refresh-on-focus/refresh-on-focus';
export type SigningLayoutProps = {
@ -7,6 +9,8 @@ export type SigningLayoutProps = {
};
export default function SigningLayout({ children }: SigningLayoutProps) {
setupI18nSSR();
return (
<div>
{children}

View File

@ -1,12 +1,15 @@
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { CheckCircle2, Clock8 } from 'lucide-react';
import { getServerSession } from 'next-auth';
import { env } from 'next-runtime-env';
import { match } from 'ts-pattern';
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
@ -37,6 +40,10 @@ export type CompletedSigningPageProps = {
export default async function CompletedSigningPage({
params: { token },
}: CompletedSigningPageProps) {
setupI18nSSR();
const { _ } = useLingui();
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
if (!token) {
@ -122,47 +129,58 @@ export default async function CompletedSigningPage({
/>
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
Document
{recipient.role === RecipientRole.SIGNER && ' Signed '}
{recipient.role === RecipientRole.VIEWER && ' Viewed '}
{recipient.role === RecipientRole.APPROVER && ' Approved '}
{recipient.role === RecipientRole.SIGNER && <Trans>Document Signed</Trans>}
{recipient.role === RecipientRole.VIEWER && <Trans>Document Viewed</Trans>}
{recipient.role === RecipientRole.APPROVER && <Trans>Document Approved</Trans>}
</h2>
{match({ status: document.status, deletedAt: document.deletedAt })
.with({ status: DocumentStatus.COMPLETED }, () => (
<div className="text-documenso-700 mt-4 flex items-center text-center">
<CheckCircle2 className="mr-2 h-5 w-5" />
<span className="text-sm">Everyone has signed</span>
<span className="text-sm">
<Trans>Everyone has signed</Trans>
</span>
</div>
))
.with({ deletedAt: null }, () => (
<div className="mt-4 flex items-center text-center text-blue-600">
<Clock8 className="mr-2 h-5 w-5" />
<span className="text-sm">Waiting for others to sign</span>
<span className="text-sm">
<Trans>Waiting for others to sign</Trans>
</span>
</div>
))
.otherwise(() => (
<div className="flex items-center text-center text-red-600">
<Clock8 className="mr-2 h-5 w-5" />
<span className="text-sm">Document no longer available to sign</span>
<span className="text-sm">
<Trans>Document no longer available to sign</Trans>
</span>
</div>
))}
{match({ status: document.status, deletedAt: document.deletedAt })
.with({ status: DocumentStatus.COMPLETED }, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
Everyone has signed! You will receive an Email copy of the signed document.
<Trans>
Everyone has signed! You will receive an Email copy of the signed document.
</Trans>
</p>
))
.with({ deletedAt: null }, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
You will receive an Email copy of the signed document once everyone has signed.
<Trans>
You will receive an Email copy of the signed document once everyone has signed.
</Trans>
</p>
))
.otherwise(() => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
This document has been cancelled by the owner and is no longer available for others
to sign.
<Trans>
This document has been cancelled by the owner and is no longer available for
others to sign.
</Trans>
</p>
))}
@ -179,7 +197,7 @@ export default async function CompletedSigningPage({
) : (
<DocumentPreviewButton
className="text-[11px]"
title="Signatures will appear once the document has been completed"
title={_(msg`Signatures will appear once the document has been completed`)}
documentData={documentData}
/>
)}
@ -189,11 +207,11 @@ export default async function CompletedSigningPage({
{canSignUp && (
<div className={`flex max-w-xl flex-col items-center justify-center p-4 md:p-12`}>
<h2 className="mt-8 text-center text-xl font-semibold md:mt-0">
Need to sign documents?
<Trans>Need to sign documents?</Trans>
</h2>
<p className="text-muted-foreground/60 mt-4 max-w-[55ch] text-center leading-normal">
Create your account and start using state-of-the-art document signing.
<Trans>Create your account and start using state-of-the-art document signing.</Trans>
</p>
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
@ -202,7 +220,7 @@ export default async function CompletedSigningPage({
{isLoggedIn && (
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600 mt-36">
Go Back Home
<Trans>Go Back Home</Trans>
</Link>
)}
</div>

View File

@ -4,6 +4,8 @@ import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
import {
@ -44,6 +46,7 @@ export const DateField = ({
}: DateFieldProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const [isPending, startTransition] = useTransition();
@ -62,7 +65,9 @@ export const DateField = ({
const isDifferentTime = field.inserted && localDateString !== field.customText;
const tooltipText = `"${field.customText}" will appear on the document as it has a timezone of "${timezone}".`;
const tooltipText = _(
msg`"${field.customText}" will appear on the document as it has a timezone of "${timezone}".`,
);
const onSign = async (authOptions?: TRecipientActionAuth) => {
try {
@ -91,8 +96,8 @@ export const DateField = ({
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while signing the document.',
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@ -117,8 +122,8 @@ export const DateField = ({
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while removing the signature.',
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
variant: 'destructive',
});
}
@ -140,7 +145,7 @@ export const DateField = ({
{!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground duration-200 group-hover:text-yellow-300">
Date
<Trans>Date</Trans>
</p>
)}

View File

@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/macro';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
@ -100,15 +101,20 @@ export const DocumentActionAuth2FA = ({
<Alert variant="warning">
<AlertDescription>
<p>
{recipient.role === RecipientRole.VIEWER && actionTarget === 'DOCUMENT'
? 'You need to setup 2FA to mark this document as viewed.'
: `You need to setup 2FA to ${actionVerb.toLowerCase()} this ${actionTarget.toLowerCase()}.`}
{recipient.role === RecipientRole.VIEWER && actionTarget === 'DOCUMENT' ? (
<Trans>You need to setup 2FA to mark this document as viewed.</Trans>
) : (
// Todo: Translate
`You need to setup 2FA to ${actionVerb.toLowerCase()} this ${actionTarget.toLowerCase()}.`
)}
</p>
{user?.identityProvider === 'DOCUMENSO' && (
<p className="mt-2">
By enabling 2FA, you will be required to enter a code from your authenticator app
every time you sign in.
<Trans>
By enabling 2FA, you will be required to enter a code from your authenticator app
every time you sign in.
</Trans>
</p>
)}
</AlertDescription>
@ -116,7 +122,7 @@ export const DocumentActionAuth2FA = ({
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Close
<Trans>Close</Trans>
</Button>
<EnableAuthenticatorAppDialog onSuccess={() => setIs2FASetupSuccessful(true)} />
@ -156,20 +162,24 @@ export const DocumentActionAuth2FA = ({
{formErrorCode && (
<Alert variant="destructive">
<AlertTitle>Unauthorized</AlertTitle>
<AlertTitle>
<Trans>Unauthorized</Trans>
</AlertTitle>
<AlertDescription>
We were unable to verify your details. Please try again or contact support
<Trans>
We were unable to verify your details. Please try again or contact support
</Trans>
</AlertDescription>
</Alert>
)}
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
<Trans>Cancel</Trans>
</Button>
<Button type="submit" loading={isCurrentlyAuthenticating}>
Sign
<Trans>Sign</Trans>
</Button>
</DialogFooter>
</div>

View File

@ -1,5 +1,6 @@
import { useState } from 'react';
import { Trans } from '@lingui/macro';
import { DateTime } from 'luxon';
import { signOut } from 'next-auth/react';
@ -53,11 +54,14 @@ export const DocumentActionAuthAccount = ({
<AlertDescription>
{actionTarget === 'DOCUMENT' && recipient.role === RecipientRole.VIEWER ? (
<span>
To mark this document as viewed, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
<Trans>
To mark this document as viewed, you need to be logged in as{' '}
<strong>{recipient.email}</strong>
</Trans>
</span>
) : (
<span>
{/* Todo: Translate */}
To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be logged
in as <strong>{recipient.email}</strong>
</span>
@ -67,11 +71,11 @@ export const DocumentActionAuthAccount = ({
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
<Trans>Cancel</Trans>
</Button>
<Button onClick={async () => handleChangeAccount(recipient.email)} loading={isSigningOut}>
Login
<Trans>Login</Trans>
</Button>
</DialogFooter>
</fieldset>

View File

@ -1,3 +1,4 @@
import { Trans } from '@lingui/macro';
import { P, match } from 'ts-pattern';
import {
@ -55,10 +56,10 @@ export const DocumentActionAuthDialog = ({
<Dialog open={open} onOpenChange={handleOnOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title || 'Sign field'}</DialogTitle>
<DialogTitle>{title || <Trans>Sign field</Trans>}</DialogTitle>
<DialogDescription>
{description || 'Reauthentication is required to sign this field'}
{description || <Trans>Reauthentication is required to sign this field</Trans>}
</DialogDescription>
</DialogHeader>

View File

@ -1,6 +1,8 @@
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser';
import { Loader } from 'lucide-react';
import { useForm } from 'react-hook-form';
@ -54,6 +56,8 @@ export const DocumentActionAuthPasskey = ({
open,
onOpenChange,
}: DocumentActionAuthPasskeyProps) => {
const { _ } = useLingui();
const {
recipient,
passkeyData,
@ -123,6 +127,7 @@ export const DocumentActionAuthPasskey = ({
<div className="space-y-4">
<Alert variant="warning">
<AlertDescription>
{/* Todo: Translate */}
Your browser does not support passkeys, which is required to {actionVerb.toLowerCase()}{' '}
this {actionTarget.toLowerCase()}.
</AlertDescription>
@ -130,7 +135,7 @@ export const DocumentActionAuthPasskey = ({
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Close
<Trans>Close</Trans>
</Button>
</DialogFooter>
</div>
@ -149,16 +154,18 @@ export const DocumentActionAuthPasskey = ({
return (
<div className="h-28 space-y-4">
<Alert variant="destructive">
<AlertDescription>Something went wrong while loading your passkeys.</AlertDescription>
<AlertDescription>
<Trans>Something went wrong while loading your passkeys.</Trans>
</AlertDescription>
</Alert>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
<Trans>Cancel</Trans>
</Button>
<Button type="button" onClick={() => void refetchPasskeys()}>
Retry
<Trans>Retry</Trans>
</Button>
</DialogFooter>
</div>
@ -170,6 +177,7 @@ export const DocumentActionAuthPasskey = ({
<div className="space-y-4">
<Alert variant="warning">
<AlertDescription>
{/* Todo: Translate */}
{recipient.role === RecipientRole.VIEWER && actionTarget === 'DOCUMENT'
? 'You need to setup a passkey to mark this document as viewed.'
: `You need to setup a passkey to ${actionVerb.toLowerCase()} this ${actionTarget.toLowerCase()}.`}
@ -178,12 +186,16 @@ export const DocumentActionAuthPasskey = ({
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
<Trans>Cancel</Trans>
</Button>
<CreatePasskeyDialog
onSuccess={async () => refetchPasskeys()}
trigger={<Button>Setup</Button>}
trigger={
<Button>
<Trans>Setup</Trans>
</Button>
}
/>
</DialogFooter>
</div>
@ -207,7 +219,7 @@ export const DocumentActionAuthPasskey = ({
<SelectTrigger className="bg-background text-muted-foreground">
<SelectValue
data-testid="documentAccessSelectValue"
placeholder="Select passkey"
placeholder={_(msg`Select passkey`)}
/>
</SelectTrigger>

View File

@ -4,6 +4,8 @@ import { useEffect, useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@ -43,8 +45,10 @@ export const DropdownField = ({
onSignField,
onUnsignField,
}: DropdownFieldProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const router = useRouter();
const [isPending, startTransition] = useTransition();
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
@ -98,8 +102,8 @@ export const DropdownField = ({
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while signing the document.',
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@ -129,8 +133,8 @@ export const DropdownField = ({
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while removing the signature.',
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
variant: 'destructive',
});
}
@ -185,7 +189,7 @@ export const DropdownField = ({
},
)}
>
<SelectValue placeholder={'-- Select --'} />
<SelectValue placeholder={`-- ${_(msg`Select`)} --`} />
</SelectTrigger>
<SelectContent className="w-full ring-0 focus:ring-0" position="popper">
{parsedFieldMeta?.values?.map((item, index) => (

View File

@ -4,6 +4,8 @@ import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@ -31,6 +33,7 @@ export type EmailFieldProps = {
export const EmailField = ({ field, recipient, onSignField, onUnsignField }: EmailFieldProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const { email: providedEmail } = useRequiredSigningContext();
@ -77,8 +80,8 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while signing the document.',
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@ -103,8 +106,8 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while removing the signature.',
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
variant: 'destructive',
});
}
@ -120,7 +123,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
{!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground duration-200 group-hover:text-yellow-300">
Email
<Trans>Email</Trans>
</p>
)}

View File

@ -4,6 +4,7 @@ import { useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Trans } from '@lingui/macro';
import { useSession } from 'next-auth/react';
import { useForm } from 'react-hook-form';
@ -103,7 +104,7 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin
>
{validateUninsertedFields && uninsertedFields[0] && (
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
Click to insert field
<Trans>Click to insert field</Trans>
</FieldToolTip>
)}
@ -123,7 +124,7 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin
{recipient.role === RecipientRole.VIEWER ? (
<>
<p className="text-muted-foreground mt-2 text-sm">
Please mark as viewed to complete
<Trans>Please mark as viewed to complete</Trans>
</p>
<hr className="border-border mb-8 mt-4" />
@ -139,7 +140,7 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin
disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={() => router.back()}
>
Cancel
<Trans>Cancel</Trans>
</Button>
<SignDialog

View File

@ -1,5 +1,6 @@
import React from 'react';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
@ -12,6 +13,8 @@ export type SigningLayoutProps = {
};
export default async function SigningLayout({ children }: SigningLayoutProps) {
setupI18nSSR();
const { user, session } = await getServerComponentSession();
let teams: GetTeamsResponse = [];

View File

@ -4,6 +4,8 @@ import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@ -36,6 +38,7 @@ export type NameFieldProps = {
export const NameField = ({ field, recipient, onSignField, onUnsignField }: NameFieldProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const { fullName: providedFullName, setFullName: setProvidedFullName } =
@ -115,8 +118,8 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while signing the document.',
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@ -141,8 +144,8 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while removing the signature.',
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
variant: 'destructive',
});
}
@ -164,7 +167,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
{!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground duration-200 group-hover:text-yellow-300">
Name
<Trans>Name</Trans>
</p>
)}
@ -177,12 +180,18 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
<Dialog open={showFullNameModal} onOpenChange={setShowFullNameModal}>
<DialogContent>
<DialogTitle>
Sign as {recipient.name}{' '}
<div className="text-muted-foreground">({recipient.email})</div>
<Trans>
Sign as
<div>
{recipient.name} <div className="text-muted-foreground">({recipient.email})</div>
</div>
</Trans>
</DialogTitle>
<div>
<Label htmlFor="signature">Full Name</Label>
<Label htmlFor="signature">
<Trans>Full Name</Trans>
</Label>
<Input
type="text"
@ -203,7 +212,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
setLocalFullName('');
}}
>
Cancel
<Trans>Cancel</Trans>
</Button>
<Button
@ -212,7 +221,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
disabled={!localFullName}
onClick={() => onDialogSignClick()}
>
Sign
<Trans>Sign</Trans>
</Button>
</div>
</DialogFooter>

View File

@ -4,6 +4,7 @@ import React from 'react';
import Link from 'next/link';
import { Trans } from '@lingui/macro';
import { Clock8 } from 'lucide-react';
import { useSession } from 'next-auth/react';
@ -35,31 +36,37 @@ export const NoLongerAvailable = ({
<div className="relative mt-2 flex w-full flex-col items-center">
<div className="mt-8 flex items-center text-center text-red-600">
<Clock8 className="mr-2 h-5 w-5" />
<span className="text-sm">Document Cancelled</span>
<span className="text-sm">
<Trans>Document Cancelled</Trans>
</span>
</div>
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
<span className="mt-1.5 block">"{document.title}"</span>
is no longer available to sign
<Trans>
<span className="mt-1.5 block">"{document.title}"</span>
is no longer available to sign
</Trans>
</h2>
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
This document has been cancelled by the owner.
<Trans>This document has been cancelled by the owner.</Trans>
</p>
{session?.user ? (
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600 mt-36">
Go Back Home
<Trans>Go Back Home</Trans>
</Link>
) : (
<p className="text-muted-foreground/60 mt-36 text-sm">
Want to send slick signing links like this one?{' '}
<Link
href="https://documenso.com"
className="text-documenso-700 hover:text-documenso-600"
>
Check out Documenso.
</Link>
<Trans>
Want to send slick signing links like this one?{' '}
<Link
href="https://documenso.com"
className="text-documenso-700 hover:text-documenso-600"
>
Check out Documenso.
</Link>
</Trans>
</p>
)}
</div>

View File

@ -4,6 +4,8 @@ import { useEffect, useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Hash, Loader } from 'lucide-react';
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
@ -43,8 +45,10 @@ export type NumberFieldProps = {
};
export const NumberField = ({ field, recipient, onSignField, onUnsignField }: NumberFieldProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [showRadioModal, setShowRadioModal] = useState(false);
@ -142,8 +146,8 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while signing the document.',
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@ -187,8 +191,8 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while removing the signature.',
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
variant: 'destructive',
});
}
@ -263,7 +267,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
<Dialog open={showRadioModal} onOpenChange={setShowRadioModal}>
<DialogContent>
<DialogTitle>
{parsedFieldMeta?.label ? parsedFieldMeta?.label : 'Add number'}
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Add number</Trans>}
</DialogTitle>
<div>
@ -320,7 +324,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
setLocalNumber('');
}}
>
Cancel
<Trans>Cancel</Trans>
</Button>
<Button
@ -329,7 +333,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
disabled={!localNumber || userInputHasErrors}
onClick={() => onDialogSignClick()}
>
Save
<Trans>Save</Trans>
</Button>
</div>
</DialogFooter>

View File

@ -1,6 +1,7 @@
import { headers } from 'next/headers';
import { notFound, redirect } from 'next/navigation';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
@ -29,6 +30,8 @@ export type SigningPageProps = {
};
export default async function SigningPage({ params: { token } }: SigningPageProps) {
setupI18nSSR();
if (!token) {
return notFound();
}

View File

@ -4,6 +4,8 @@ import { useEffect, useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@ -32,8 +34,10 @@ export type RadioFieldProps = {
};
export const RadioField = ({ field, recipient, onSignField, onUnsignField }: RadioFieldProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const router = useRouter();
const [isPending, startTransition] = useTransition();
const parsedFieldMeta = ZRadioFieldMeta.parse(field.fieldMeta);
@ -94,8 +98,8 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while signing the document.',
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@ -121,8 +125,8 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while removing the signature.',
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
variant: 'destructive',
});
}

View File

@ -1,5 +1,7 @@
import { useState } from 'react';
import { Trans } from '@lingui/macro';
import type { Field } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
@ -53,36 +55,42 @@ export const SignDialog = ({
onClick={fieldsValidated}
loading={isSubmitting}
>
{isComplete ? 'Complete' : 'Next field'}
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>
<div className="text-foreground text-xl font-semibold">
{role === RecipientRole.VIEWER && 'Complete Viewing'}
{role === RecipientRole.SIGNER && 'Complete Signing'}
{role === RecipientRole.APPROVER && 'Complete Approval'}
{role === RecipientRole.VIEWER && <Trans>Complete Viewing</Trans>}
{role === RecipientRole.SIGNER && <Trans>Complete Signing</Trans>}
{role === RecipientRole.APPROVER && <Trans>Complete Approval</Trans>}
</div>
</DialogTitle>
<div className="text-muted-foreground max-w-[50ch]">
{role === RecipientRole.VIEWER && (
<span>
You are about to complete viewing "{truncatedTitle}".
<br /> Are you sure?
<Trans>
You are about to complete viewing "{truncatedTitle}".
<br /> Are you sure?
</Trans>
</span>
)}
{role === RecipientRole.SIGNER && (
<span>
You are about to complete signing "{truncatedTitle}".
<br /> Are you sure?
<Trans>
You are about to complete signing "{truncatedTitle}".
<br /> Are you sure?
</Trans>
</span>
)}
{role === RecipientRole.APPROVER && (
<span>
You are about to complete approving "{truncatedTitle}".
<br /> Are you sure?
<Trans>
You are about to complete approving "{truncatedTitle}".
<br /> Are you sure?
</Trans>
</span>
)}
</div>
@ -93,13 +101,13 @@ export const SignDialog = ({
<div className="flex w-full flex-1 flex-nowrap gap-4">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
variant="secondary"
onClick={() => {
setShowDialog(false);
}}
>
Cancel
<Trans>Cancel</Trans>
</Button>
<Button
@ -109,9 +117,9 @@ export const SignDialog = ({
loading={isSubmitting}
onClick={onSignatureComplete}
>
{role === RecipientRole.VIEWER && 'Mark as Viewed'}
{role === RecipientRole.SIGNER && 'Sign'}
{role === RecipientRole.APPROVER && 'Approve'}
{role === RecipientRole.VIEWER && <Trans>Mark as Viewed</Trans>}
{role === RecipientRole.SIGNER && <Trans>Sign</Trans>}
{role === RecipientRole.APPROVER && <Trans>Approve</Trans>}
</Button>
</div>
</DialogFooter>

View File

@ -4,6 +4,8 @@ import { useMemo, useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@ -45,6 +47,7 @@ export const SignatureField = ({
}: SignatureFieldProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const { signature: providedSignature, setSignature: setProvidedSignature } =
@ -142,8 +145,8 @@ export const SignatureField = ({
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while signing the document.',
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@ -168,8 +171,8 @@ export const SignatureField = ({
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while removing the signature.',
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
variant: 'destructive',
});
}
@ -191,7 +194,7 @@ export const SignatureField = ({
{state === 'empty' && (
<p className="group-hover:text-primary font-signature text-muted-foreground text-xl duration-200 group-hover:text-yellow-300">
Signature
<Trans>Signature</Trans>
</p>
)}
@ -213,12 +216,16 @@ export const SignatureField = ({
<Dialog open={showSignatureModal} onOpenChange={setShowSignatureModal}>
<DialogContent>
<DialogTitle>
Sign as {recipient.name}{' '}
<div className="text-muted-foreground h-5">({recipient.email})</div>
<Trans>
Sign as {recipient.name}{' '}
<div className="text-muted-foreground h-5">({recipient.email})</div>
</Trans>
</DialogTitle>
<div className="">
<Label htmlFor="signature">Signature</Label>
<Label htmlFor="signature">
<Trans>Signature</Trans>
</Label>
<SignaturePad
id="signature"
@ -233,14 +240,14 @@ export const SignatureField = ({
<div className="flex w-full flex-1 flex-nowrap gap-4">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
variant="secondary"
onClick={() => {
setShowSignatureModal(false);
setLocalSignature(null);
}}
>
Cancel
<Trans>Cancel</Trans>
</Button>
<Button
@ -249,7 +256,7 @@ export const SignatureField = ({
disabled={!localSignature}
onClick={() => onDialogSignClick()}
>
Sign
<Trans>Sign</Trans>
</Button>
</div>
</DialogFooter>

View File

@ -2,6 +2,8 @@
import { useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import { signOut } from 'next-auth/react';
@ -15,6 +17,7 @@ export type SigningAuthPageViewProps = {
};
export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageViewProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const [isSigningOut, setIsSigningOut] = useState(false);
@ -37,8 +40,8 @@ export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageV
});
} catch {
toast({
title: 'Something went wrong',
description: 'We were unable to log you out at this time.',
title: _(msg`Something went wrong`),
description: _(msg`We were unable to log you out at this time.`),
duration: 10000,
variant: 'destructive',
});
@ -50,10 +53,14 @@ export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageV
return (
<div className="mx-auto flex h-[70vh] w-full max-w-md flex-col items-center justify-center">
<div>
<h1 className="text-3xl font-semibold">Authentication required</h1>
<h1 className="text-3xl font-semibold">
<Trans>Authentication required</Trans>
</h1>
<p className="text-muted-foreground mt-2 text-sm">
You need to be logged in as <strong>{email}</strong> to view this page.
<Trans>
You need to be logged in as <strong>{email}</strong> to view this page.
</Trans>
</p>
<Button
@ -62,7 +69,7 @@ export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageV
onClick={async () => handleChangeAccount(email)}
loading={isSigningOut}
>
{emailHasAccount ? 'Login' : 'Sign up'}
{emailHasAccount ? <Trans>Login</Trans> : <Trans>Sign up</Trans>}
</Button>
</div>
</div>

View File

@ -2,6 +2,7 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import { X } from 'lucide-react';
import { type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@ -140,7 +141,7 @@ export const SigningFieldContainer = ({
{readOnlyField && (
<button className="bg-background/40 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 duration-200 group-hover:opacity-100">
<span className="bg-foreground/50 dark:bg-background/50 text-background dark:text-foreground rounded-xl p-2">
Read only field
<Trans>Read only field</Trans>
</span>
</button>
)}
@ -152,7 +153,7 @@ export const SigningFieldContainer = ({
className="text-destructive bg-background/40 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 duration-200 group-hover:opacity-100"
onClick={onRemoveSignedFieldClick}
>
Remove
<Trans>Remove</Trans>
</button>
</TooltipTrigger>
@ -176,7 +177,7 @@ export const SigningFieldContainer = ({
className="text-destructive bg-background/50 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 duration-200 group-hover:opacity-100"
onClick={onRemoveSignedFieldClick}
>
Remove
<Trans>Remove</Trans>
</button>
)}

View File

@ -1,3 +1,4 @@
import { Trans } from '@lingui/macro';
import { match } from 'ts-pattern';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
@ -63,10 +64,17 @@ export const SigningPageView = ({
</p>
</div>
<p className="text-muted-foreground">
({document.User.email}) has invited you to{' '}
{recipient.role === RecipientRole.VIEWER && 'view'}
{recipient.role === RecipientRole.SIGNER && 'sign'}
{recipient.role === RecipientRole.APPROVER && 'approve'} this document.
{match(recipient.role)
.with(RecipientRole.VIEWER, () => (
<Trans>({document.User.email}) has invited you to view this document</Trans>
))
.with(RecipientRole.SIGNER, () => (
<Trans>({document.User.email}) has invited you to sign this document</Trans>
))
.with(RecipientRole.APPROVER, () => (
<Trans>({document.User.email}) has invited you to approve this document</Trans>
))
.otherwise(() => null)}
</p>
<div className="mt-8 grid grid-cols-12 gap-y-8 lg:gap-x-8 lg:gap-y-0">

View File

@ -4,6 +4,8 @@ import { useEffect, useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Plural, Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Loader, Type } from 'lucide-react';
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
@ -35,9 +37,11 @@ export type TextFieldProps = {
};
export const TextField = ({ field, recipient, onSignField, onUnsignField }: TextFieldProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const router = useRouter();
const initialErrors: Record<string, string[]> = {
required: [],
characterLimit: [],
@ -160,8 +164,8 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while signing the document.',
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@ -188,8 +192,8 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while removing the text.',
title: _(msg`Error`),
description: _(msg`An error occurred while removing the text.`),
variant: 'destructive',
});
}
@ -220,7 +224,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
? parsedField?.text.substring(0, 20) + '...'
: undefined;
const fieldDisplayName = labelDisplay ? labelDisplay : textDisplay ? textDisplay : 'Add text';
const fieldDisplayName = labelDisplay ? labelDisplay : textDisplay;
const charactersRemaining = (parsedFieldMeta?.characterLimit ?? 0) - (localText.length ?? 0);
return (
@ -249,7 +253,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
>
<span className="flex items-center justify-center gap-x-1">
<Type />
{fieldDisplayName}
{fieldDisplayName || <Trans>Add text</Trans>}
</span>
</p>
)}
@ -264,12 +268,14 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
<Dialog open={showCustomTextModal} onOpenChange={setShowCustomTextModal}>
<DialogContent>
<DialogTitle>{parsedFieldMeta?.label ? parsedFieldMeta?.label : 'Add Text'}</DialogTitle>
<DialogTitle>
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Add Text</Trans>}
</DialogTitle>
<div>
<Textarea
id="custom-text"
placeholder={parsedFieldMeta?.placeholder ?? 'Enter your text here'}
placeholder={parsedFieldMeta?.placeholder ?? _(msg`Enter your text here`)}
className={cn('mt-2 w-full rounded-md', {
'border-2 border-red-300 ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 focus-visible:border-red-400 focus-visible:ring-4 focus-visible:ring-red-200 focus-visible:ring-offset-2 focus-visible:ring-offset-red-200':
userInputHasErrors,
@ -283,7 +289,11 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
parsedFieldMeta?.characterLimit > 0 &&
!userInputHasErrors && (
<div className="text-muted-foreground text-sm">
{charactersRemaining} characters remaining
<Plural
value={charactersRemaining}
one="1 character remaining"
other={`${charactersRemaining} characters remaining`}
/>
</div>
)}
@ -297,7 +307,13 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
{errors.characterLimit.map((error, index) => (
<p key={index} className="text-red-500">
{error}{' '}
{charactersRemaining < 0 && `(${Math.abs(charactersRemaining)} characters over)`}
{charactersRemaining < 0 && (
<Plural
value={Math.abs(charactersRemaining)}
one="(1 character over)"
other="(# characters over)"
/>
)}
</p>
))}
</div>
@ -314,7 +330,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
setLocalCustomText('');
}}
>
Cancel
<Trans>Cancel</Trans>
</Button>
<Button
@ -323,7 +339,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
disabled={!localText || userInputHasErrors}
onClick={() => onDialogSignClick()}
>
Save
<Trans>Save</Trans>
</Button>
</div>
</DialogFooter>