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

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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',