mirror of
https://github.com/documenso/documenso.git
synced 2025-11-26 14:34:05 +10:00
feat: web i18n (#1286)
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { startRegistration } from '@simplewebauthn/browser';
|
||||
import { KeyRoundIcon } from 'lucide-react';
|
||||
@@ -53,6 +55,7 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
|
||||
const [open, setOpen] = useState(false);
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const form = useForm<TCreatePasskeyFormSchema>({
|
||||
@@ -81,7 +84,7 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
|
||||
});
|
||||
|
||||
toast({
|
||||
description: 'Successfully created passkey',
|
||||
description: _(msg`Successfully created passkey`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
@@ -140,17 +143,22 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
|
||||
{trigger ?? (
|
||||
<Button variant="secondary" loading={isLoading}>
|
||||
<KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />
|
||||
Add passkey
|
||||
<Trans>Add passkey</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent position="center">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add passkey</DialogTitle>
|
||||
<DialogTitle>
|
||||
<Trans>Add passkey</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription className="mt-4">
|
||||
Passkeys allow you to sign in and authenticate using biometrics, password managers, etc.
|
||||
<Trans>
|
||||
Passkeys allow you to sign in and authenticate using biometrics, password managers,
|
||||
etc.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -165,7 +173,9 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
|
||||
name="passkeyName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>Passkey name</FormLabel>
|
||||
<FormLabel required>
|
||||
<Trans>Passkey name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input className="bg-background" placeholder="eg. Mac" {...field} />
|
||||
</FormControl>
|
||||
@@ -176,13 +186,17 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
|
||||
|
||||
<Alert variant="neutral">
|
||||
<AlertDescription>
|
||||
When you click continue, you will be prompted to add the first available
|
||||
authenticator on your system.
|
||||
<Trans>
|
||||
When you click continue, you will be prompted to add the first available
|
||||
authenticator on your system.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
|
||||
<AlertDescription className="mt-2">
|
||||
If you do not want to use the authenticator prompted, you can close it, which will
|
||||
then display the next available authenticator.
|
||||
<Trans>
|
||||
If you do not want to use the authenticator prompted, you can close it, which
|
||||
will then display the next available authenticator.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -190,30 +204,40 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
|
||||
<Alert variant="destructive">
|
||||
{match(formError)
|
||||
.with('ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED', () => (
|
||||
<AlertDescription>This passkey has already been registered.</AlertDescription>
|
||||
<AlertDescription>
|
||||
<Trans>This passkey has already been registered.</Trans>
|
||||
</AlertDescription>
|
||||
))
|
||||
.with('TOO_MANY_PASSKEYS', () => (
|
||||
<AlertDescription>
|
||||
You cannot have more than {MAXIMUM_PASSKEYS} passkeys.
|
||||
<Trans>You cannot have more than {MAXIMUM_PASSKEYS} passkeys.</Trans>
|
||||
</AlertDescription>
|
||||
))
|
||||
.with('InvalidStateError', () => (
|
||||
<>
|
||||
<AlertTitle className="text-sm">
|
||||
Passkey creation cancelled due to one of the following reasons:
|
||||
<Trans>
|
||||
Passkey creation cancelled due to one of the following reasons:
|
||||
</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<ul className="mt-1 list-inside list-disc">
|
||||
<li>Cancelled by user</li>
|
||||
<li>Passkey already exists for the provided authenticator</li>
|
||||
<li>Exceeded timeout</li>
|
||||
<li>
|
||||
<Trans>Cancelled by user</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>Passkey already exists for the provided authenticator</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>Exceeded timeout</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<AlertDescription>
|
||||
Something went wrong. Please try again or contact support.
|
||||
<Trans>Something went wrong. Please try again or contact support.</Trans>
|
||||
</AlertDescription>
|
||||
))}
|
||||
</Alert>
|
||||
@@ -221,11 +245,11 @@ export const CreatePasskeyDialog = ({ trigger, onSuccess, ...props }: CreatePass
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
Continue
|
||||
<Trans>Continue</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
||||
|
||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||
@@ -13,6 +17,9 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default async function SettingsManagePasskeysPage() {
|
||||
setupI18nSSR();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const isPasskeyEnabled = await getServerComponentFlag('app_passkey');
|
||||
|
||||
if (!isPasskeyEnabled) {
|
||||
@@ -21,7 +28,11 @@ export default async function SettingsManagePasskeysPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader title="Passkeys" subtitle="Manage your passkeys." hideDivider={true}>
|
||||
<SettingsHeader
|
||||
title={_(msg`Passkeys`)}
|
||||
subtitle={_(msg`Manage your passkeys.`)}
|
||||
hideDivider={true}
|
||||
>
|
||||
<CreatePasskeyDialog />
|
||||
</SettingsHeader>
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
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';
|
||||
|
||||
@@ -45,6 +47,7 @@ export const UserPasskeysDataTableActions = ({
|
||||
passkeyId,
|
||||
passkeyName,
|
||||
}: UserPasskeysDataTableActionsProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
@@ -61,15 +64,16 @@ export const UserPasskeysDataTableActions = ({
|
||||
trpc.auth.updatePasskey.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Passkey has been updated',
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`Passkey has been updated`),
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
description:
|
||||
'We are unable to update this passkey at the moment. Please try again later.',
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(
|
||||
msg`We are unable to update this passkey at the moment. Please try again later.`,
|
||||
),
|
||||
duration: 10000,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -80,15 +84,16 @@ export const UserPasskeysDataTableActions = ({
|
||||
trpc.auth.deletePasskey.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Passkey has been removed',
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`Passkey has been removed`),
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
description:
|
||||
'We are unable to remove this passkey at the moment. Please try again later.',
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(
|
||||
msg`We are unable to remove this passkey at the moment. Please try again later.`,
|
||||
),
|
||||
duration: 10000,
|
||||
variant: 'destructive',
|
||||
});
|
||||
@@ -102,15 +107,21 @@ export const UserPasskeysDataTableActions = ({
|
||||
onOpenChange={(value) => !isUpdatingPasskey && setIsUpdateDialogOpen(value)}
|
||||
>
|
||||
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
|
||||
<Button variant="outline">Edit</Button>
|
||||
<Button variant="outline">
|
||||
<Trans>Edit</Trans>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent position="center">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update passkey</DialogTitle>
|
||||
<DialogTitle>
|
||||
<Trans>Update passkey</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription className="mt-4">
|
||||
You are currently updating the <strong>{passkeyName}</strong> passkey.
|
||||
<Trans>
|
||||
You are currently updating the <strong>{passkeyName}</strong> passkey.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -129,7 +140,9 @@ export const UserPasskeysDataTableActions = ({
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel required>Name</FormLabel>
|
||||
<FormLabel required>
|
||||
<Trans>Name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@@ -141,12 +154,12 @@ export const UserPasskeysDataTableActions = ({
|
||||
<DialogFooter className="mt-4">
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="secondary">
|
||||
Cancel
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button type="submit" loading={isUpdatingPasskey}>
|
||||
Update
|
||||
<Trans>Update</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
@@ -160,15 +173,21 @@ export const UserPasskeysDataTableActions = ({
|
||||
onOpenChange={(value) => !isDeletingPasskey && setIsDeleteDialogOpen(value)}
|
||||
>
|
||||
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
|
||||
<Button variant="destructive">Delete</Button>
|
||||
<Button variant="destructive">
|
||||
<Trans>Delete</Trans>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent position="center">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete passkey</DialogTitle>
|
||||
<DialogTitle>
|
||||
<Trans>Delete passkey</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription className="mt-4">
|
||||
Are you sure you want to remove the <strong>{passkeyName}</strong> passkey.
|
||||
<Trans>
|
||||
Are you sure you want to remove the <strong>{passkeyName}</strong> passkey.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -176,7 +195,7 @@ export const UserPasskeysDataTableActions = ({
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="secondary">
|
||||
Cancel
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
@@ -189,7 +208,7 @@ export const UserPasskeysDataTableActions = ({
|
||||
variant="destructive"
|
||||
loading={isDeletingPasskey}
|
||||
>
|
||||
Delete
|
||||
<Trans>Delete</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
@@ -15,6 +17,8 @@ import { TableCell } from '@documenso/ui/primitives/table';
|
||||
import { UserPasskeysDataTableActions } from './user-passkeys-data-table-actions';
|
||||
|
||||
export const UserPasskeysDataTable = () => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -52,22 +56,22 @@ export const UserPasskeysDataTable = () => {
|
||||
<DataTable
|
||||
columns={[
|
||||
{
|
||||
header: 'Name',
|
||||
header: _(msg`Name`),
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'Created',
|
||||
header: _(msg`Created`),
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => DateTime.fromJSDate(row.original.createdAt).toRelative(),
|
||||
},
|
||||
|
||||
{
|
||||
header: 'Last used',
|
||||
header: _(msg`Last used`),
|
||||
accessorKey: 'updatedAt',
|
||||
cell: ({ row }) =>
|
||||
row.original.lastUsedAt
|
||||
? DateTime.fromJSDate(row.original.lastUsedAt).toRelative()
|
||||
: 'Never',
|
||||
: msg`Never`,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
|
||||
Reference in New Issue
Block a user