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

@ -2,6 +2,9 @@
import React from 'react';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
@ -21,6 +24,8 @@ export type AvatarWithRecipientProps = {
export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRecipientProps) {
const [, copy] = useCopyToClipboard();
const { _ } = useLingui();
const { toast } = useToast();
const signingToken = documentStatus === DocumentStatus.PENDING ? recipient.token : null;
@ -32,8 +37,8 @@ export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRec
void copy(`${NEXT_PUBLIC_WEBAPP_URL()}/sign/${signingToken}`).then(() => {
toast({
title: 'Copied to clipboard',
description: 'The signing link has been copied to your clipboard.',
title: _(msg`Copied to clipboard`),
description: _(msg`The signing link has been copied to your clipboard.`),
});
});
};
@ -44,7 +49,7 @@ export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRec
'cursor-pointer hover:underline': signingToken,
})}
role={signingToken ? 'button' : undefined}
title={signingToken ? 'Click to copy signing link for sending to recipient' : undefined}
title={signingToken ? _(msg`Click to copy signing link for sending to recipient`) : undefined}
onClick={onRecipientClick}
>
<StackAvatar
@ -56,11 +61,13 @@ export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRec
<div
className="text-muted-foreground text-sm"
title={signingToken ? 'Click to copy signing link for sending to recipient' : undefined}
title={
signingToken ? _(msg`Click to copy signing link for sending to recipient`) : undefined
}
>
<p>{recipient.email}</p>
<p className="text-muted-foreground/70 text-xs">
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
</p>
</div>
</div>

View File

@ -1,5 +1,8 @@
'use client';
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
@ -23,6 +26,8 @@ export const StackAvatarsWithTooltip = ({
position,
children,
}: StackAvatarsWithTooltipProps) => {
const { _ } = useLingui();
const waitingRecipients = recipients.filter(
(recipient) => getRecipientType(recipient) === 'waiting',
);
@ -49,7 +54,9 @@ export const StackAvatarsWithTooltip = ({
>
{completedRecipients.length > 0 && (
<div>
<h1 className="text-base font-medium">Completed</h1>
<h1 className="text-base font-medium">
<Trans>Completed</Trans>
</h1>
{completedRecipients.map((recipient: Recipient) => (
<div key={recipient.id} className="my-1 flex items-center gap-2">
<StackAvatar
@ -61,7 +68,7 @@ export const StackAvatarsWithTooltip = ({
<div className="">
<p className="text-muted-foreground text-sm">{recipient.email}</p>
<p className="text-muted-foreground/70 text-xs">
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
</p>
</div>
</div>
@ -71,7 +78,9 @@ export const StackAvatarsWithTooltip = ({
{waitingRecipients.length > 0 && (
<div>
<h1 className="text-base font-medium">Waiting</h1>
<h1 className="text-base font-medium">
<Trans>Waiting</Trans>
</h1>
{waitingRecipients.map((recipient: Recipient) => (
<AvatarWithRecipient
key={recipient.id}
@ -84,7 +93,9 @@ export const StackAvatarsWithTooltip = ({
{openedRecipients.length > 0 && (
<div>
<h1 className="text-base font-medium">Opened</h1>
<h1 className="text-base font-medium">
<Trans>Opened</Trans>
</h1>
{openedRecipients.map((recipient: Recipient) => (
<AvatarWithRecipient
key={recipient.id}
@ -97,7 +108,9 @@ export const StackAvatarsWithTooltip = ({
{uncompletedRecipients.length > 0 && (
<div>
<h1 className="text-base font-medium">Uncompleted</h1>
<h1 className="text-base font-medium">
<Trans>Uncompleted</Trans>
</h1>
{uncompletedRecipients.map((recipient: Recipient) => (
<AvatarWithRecipient
key={recipient.id}

View File

@ -4,6 +4,9 @@ import { useCallback, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import type { MessageDescriptor } from '@lingui/core';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Loader, Monitor, Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { useHotkeys } from 'react-hotkeys-hook';
@ -31,22 +34,22 @@ import { THEMES_TYPE } from '@documenso/ui/primitives/constants';
const DOCUMENTS_PAGES = [
{
label: 'All documents',
label: msg`All documents`,
path: '/documents?status=ALL',
shortcut: DOCUMENTS_PAGE_SHORTCUT.replace('+', ''),
},
{ label: 'Draft documents', path: '/documents?status=DRAFT' },
{ label: msg`Draft documents`, path: '/documents?status=DRAFT' },
{
label: 'Completed documents',
label: msg`Completed documents`,
path: '/documents?status=COMPLETED',
},
{ label: 'Pending documents', path: '/documents?status=PENDING' },
{ label: 'Inbox documents', path: '/documents?status=INBOX' },
{ label: msg`Pending documents`, path: '/documents?status=PENDING' },
{ label: msg`Inbox documents`, path: '/documents?status=INBOX' },
];
const TEMPLATES_PAGES = [
{
label: 'All templates',
label: msg`All templates`,
path: '/templates',
shortcut: TEMPLATES_PAGE_SHORTCUT.replace('+', ''),
},
@ -54,12 +57,12 @@ const TEMPLATES_PAGES = [
const SETTINGS_PAGES = [
{
label: 'Settings',
label: msg`Settings`,
path: '/settings',
shortcut: SETTINGS_PAGE_SHORTCUT.replace('+', ''),
},
{ label: 'Profile', path: '/settings/profile' },
{ label: 'Password', path: '/settings/password' },
{ label: msg`Profile`, path: '/settings/profile' },
{ label: msg`Password`, path: '/settings/password' },
];
export type CommandMenuProps = {
@ -68,6 +71,7 @@ export type CommandMenuProps = {
};
export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
const { _ } = useLingui();
const { setTheme } = useTheme();
const router = useRouter();
@ -174,7 +178,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
<CommandInput
value={search}
onValueChange={setSearch}
placeholder="Type a command or search..."
placeholder={_(msg`Type a command or search...`)}
/>
<CommandList>
@ -187,26 +191,28 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
</div>
</CommandEmpty>
) : (
<CommandEmpty>No results found.</CommandEmpty>
<CommandEmpty>
<Trans>No results found.</Trans>
</CommandEmpty>
)}
{!currentPage && (
<>
<CommandGroup className="mx-2 p-0 pb-2" heading="Documents">
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Documents`)}>
<Commands push={push} pages={DOCUMENTS_PAGES} />
</CommandGroup>
<CommandGroup className="mx-2 p-0 pb-2" heading="Templates">
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Templates`)}>
<Commands push={push} pages={TEMPLATES_PAGES} />
</CommandGroup>
<CommandGroup className="mx-2 p-0 pb-2" heading="Settings">
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Settings`)}>
<Commands push={push} pages={SETTINGS_PAGES} />
</CommandGroup>
<CommandGroup className="mx-2 p-0 pb-2" heading="Preferences">
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Preferences`)}>
<CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('theme')}>
Change theme
</CommandItem>
</CommandGroup>
{searchResults.length > 0 && (
<CommandGroup className="mx-2 p-0 pb-2" heading="Your documents">
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Your documents`)}>
<Commands push={push} pages={searchResults} />
</CommandGroup>
)}
@ -223,27 +229,31 @@ const Commands = ({
pages,
}: {
push: (_path: string) => void;
pages: { label: string; path: string; shortcut?: string; value?: string }[];
pages: { label: MessageDescriptor | string; path: string; shortcut?: string; value?: string }[];
}) => {
const { _ } = useLingui();
return pages.map((page, idx) => (
<CommandItem
className="-mx-2 -my-1 rounded-lg"
key={page.path + idx}
value={page.value ?? page.label}
value={page.value ?? (typeof page.label === 'string' ? page.label : _(page.label))}
onSelect={() => push(page.path)}
>
{page.label}
{typeof page.label === 'string' ? page.label : _(page.label)}
{page.shortcut && <CommandShortcut>{page.shortcut}</CommandShortcut>}
</CommandItem>
));
};
const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) => {
const { _ } = useLingui();
const THEMES = useMemo(
() => [
{ label: 'Light Mode', theme: THEMES_TYPE.LIGHT, icon: Sun },
{ label: 'Dark Mode', theme: THEMES_TYPE.DARK, icon: Moon },
{ label: 'System Theme', theme: THEMES_TYPE.SYSTEM, icon: Monitor },
{ label: msg`Light Mode`, theme: THEMES_TYPE.LIGHT, icon: Sun },
{ label: msg`Dark Mode`, theme: THEMES_TYPE.DARK, icon: Moon },
{ label: msg`System Theme`, theme: THEMES_TYPE.SYSTEM, icon: Monitor },
],
[],
);
@ -255,7 +265,7 @@ const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) =>
className="-my-1 mx-2 rounded-lg first:mt-2 last:mb-2"
>
<theme.icon className="mr-2" />
{theme.label}
{_(theme.label)}
</CommandItem>
));
};

View File

@ -4,6 +4,8 @@ import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useParams, usePathname } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Search } from 'lucide-react';
import { getRootHref } from '@documenso/lib/utils/params';
@ -13,11 +15,11 @@ import { Button } from '@documenso/ui/primitives/button';
const navigationLinks = [
{
href: '/documents',
label: 'Documents',
label: msg`Documents`,
},
{
href: '/templates',
label: 'Templates',
label: msg`Templates`,
},
];
@ -26,6 +28,8 @@ export type DesktopNavProps = HTMLAttributes<HTMLDivElement> & {
};
export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: DesktopNavProps) => {
const { _ } = useLingui();
const pathname = usePathname();
const params = useParams();
@ -62,7 +66,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
},
)}
>
{label}
{_(label)}
</Link>
))}
</div>
@ -74,7 +78,7 @@ export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: Deskto
>
<div className="flex items-center">
<Search className="mr-2 h-5 w-5" />
Search
<Trans>Search</Trans>
</div>
<div>

View File

@ -3,6 +3,8 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { motion } from 'framer-motion';
import { CheckCircle2, ChevronsUpDown, Plus, Settings2 } from 'lucide-react';
import { signOut } from 'next-auth/react';
@ -35,6 +37,8 @@ export type MenuSwitcherProps = {
};
export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProps) => {
const { _ } = useLingui();
const pathname = usePathname();
const isUserAdmin = isAdmin(user);
@ -65,14 +69,14 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
const formatSecondaryAvatarText = (team?: typeof selectedTeam) => {
if (!team) {
return 'Personal Account';
return _(msg`Personal Account`);
}
if (team.ownerUserId === user.id) {
return 'Owner';
return _(msg`Owner`);
}
return TEAM_MEMBER_ROLE_MAP[team.currentTeamMember.role];
return _(TEAM_MEMBER_ROLE_MAP[team.currentTeamMember.role]);
};
/**
@ -121,7 +125,9 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
>
{teams ? (
<>
<DropdownMenuLabel>Personal</DropdownMenuLabel>
<DropdownMenuLabel>
<Trans>Personal</Trans>
</DropdownMenuLabel>
<DropdownMenuItem asChild>
<Link href={formatRedirectUrlOnSwitch()}>
@ -147,12 +153,14 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
<DropdownMenuLabel>
<div className="flex flex-row items-center justify-between">
<p>Teams</p>
<p>
<Trans>Teams</Trans>
</p>
<div className="flex flex-row space-x-2">
<DropdownMenuItem asChild>
<Button
title="Manage teams"
title={_(msg`Manage teams`)}
variant="ghost"
className="text-muted-foreground flex h-5 w-5 items-center justify-center p-0"
asChild
@ -165,7 +173,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
<DropdownMenuItem asChild>
<Button
title="Create team"
title={_(msg`Create team`)}
variant="ghost"
className="text-muted-foreground flex h-5 w-5 items-center justify-center p-0"
asChild
@ -235,7 +243,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
href="/settings/teams?action=add-team"
className="flex items-center justify-between"
>
Create team
<Trans>Create team</Trans>
<Plus className="ml-2 h-4 w-4" />
</Link>
</DropdownMenuItem>
@ -245,18 +253,24 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
{isUserAdmin && (
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
<Link href="/admin">Admin panel</Link>
<Link href="/admin">
<Trans>Admin panel</Trans>
</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
<Link href="/settings/profile">User settings</Link>
<Link href="/settings/profile">
<Trans>User settings</Trans>
</Link>
</DropdownMenuItem>
{selectedTeam &&
canExecuteTeamAction('MANAGE_TEAM', selectedTeam.currentTeamMember.role) && (
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
<Link href={`/t/${selectedTeam.url}/settings/`}>Team settings</Link>
<Link href={`/t/${selectedTeam.url}/settings/`}>
<Trans>Team settings</Trans>
</Link>
</DropdownMenuItem>
)}
@ -268,7 +282,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
})
}
>
Sign Out
<Trans>Sign Out</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -4,6 +4,8 @@ import Image from 'next/image';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { signOut } from 'next-auth/react';
import LogoImage from '@documenso/assets/logo.png';
@ -17,6 +19,8 @@ export type MobileNavigationProps = {
};
export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
const { _ } = useLingui();
const params = useParams();
const handleMenuItemClick = () => {
@ -28,19 +32,19 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
const menuNavigationLinks = [
{
href: `${rootHref}/documents`,
text: 'Documents',
text: msg`Documents`,
},
{
href: `${rootHref}/templates`,
text: 'Templates',
text: msg`Templates`,
},
{
href: '/settings/teams',
text: 'Teams',
text: msg`Teams`,
},
{
href: '/settings/profile',
text: 'Settings',
text: msg`Settings`,
},
];
@ -65,7 +69,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
href={href}
onClick={() => handleMenuItemClick()}
>
{text}
{_(text)}
</Link>
))}
@ -77,7 +81,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
})
}
>
Sign Out
<Trans>Sign Out</Trans>
</button>
</div>

View File

@ -2,6 +2,8 @@
import { useEffect, useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { AlertTriangle } from 'lucide-react';
import { ONE_DAY, ONE_SECOND } from '@documenso/lib/constants/time';
@ -22,7 +24,9 @@ export type VerifyEmailBannerProps = {
const RESEND_CONFIRMATION_EMAIL_TIMEOUT = 20 * ONE_SECOND;
export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const [isOpen, setIsOpen] = useState(false);
const [isButtonDisabled, setIsButtonDisabled] = useState(false);
@ -37,8 +41,8 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
await sendConfirmationEmail({ email: email });
toast({
title: 'Success',
description: 'Verification email sent successfully.',
title: _(msg`Success`),
description: _(msg`Verification email sent successfully.`),
});
setIsOpen(false);
@ -47,8 +51,8 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
setIsButtonDisabled(false);
toast({
title: 'Error',
description: 'Something went wrong while sending the confirmation email.',
title: _(msg`Error`),
description: _(msg`Something went wrong while sending the confirmation email.`),
variant: 'destructive',
});
}
@ -81,7 +85,7 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
<div className="mx-auto flex max-w-screen-xl items-center justify-center gap-x-4 px-4 py-2 text-sm font-medium text-yellow-900">
<div className="flex items-center">
<AlertTriangle className="mr-2.5 h-5 w-5" />
Verify your email address to unlock all features.
<Trans>Verify your email address to unlock all features.</Trans>
</div>
<div>
@ -92,7 +96,11 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
onClick={() => setIsOpen(true)}
size="sm"
>
{isButtonDisabled ? 'Verification Email Sent' : 'Verify Now'}
{isButtonDisabled ? (
<Trans>Verification Email Sent</Trans>
) : (
<Trans>Verify Now</Trans>
)}
</Button>
</div>
</div>
@ -100,11 +108,15 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
<DialogTitle>Verify your email address</DialogTitle>
<DialogTitle>
<Trans>Verify your email address</Trans>
</DialogTitle>
<DialogDescription>
We've sent a confirmation email to <strong>{email}</strong>. Please check your inbox and
click the link in the email to verify your account.
<Trans>
We've sent a confirmation email to <strong>{email}</strong>. Please check your inbox
and click the link in the email to verify your account.
</Trans>
</DialogDescription>
<div>
@ -113,7 +125,7 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
loading={isLoading}
onClick={onResendConfirmationEmail}
>
{isLoading ? 'Sending...' : 'Resend Confirmation Email'}
{isLoading ? <Trans>Sending...</Trans> : <Trans>Resend Confirmation Email</Trans>}
</Button>
</div>
</DialogContent>

View File

@ -4,6 +4,8 @@ import { useMemo } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { Trans } from '@lingui/macro';
import {
Select,
SelectContent,
@ -49,10 +51,18 @@ export const PeriodSelector = () => {
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value="all">All Time</SelectItem>
<SelectItem value="7d">Last 7 days</SelectItem>
<SelectItem value="14d">Last 14 days</SelectItem>
<SelectItem value="30d">Last 30 days</SelectItem>
<SelectItem value="all">
<Trans>All Time</Trans>
</SelectItem>
<SelectItem value="7d">
<Trans>Last 7 days</Trans>
</SelectItem>
<SelectItem value="14d">
<Trans>Last 14 days</Trans>
</SelectItem>
<SelectItem value="30d">
<Trans>Last 30 days</Trans>
</SelectItem>
</SelectContent>
</Select>
);

View File

@ -2,6 +2,8 @@
import { useRouter } from 'next/navigation';
import { Trans } from '@lingui/macro';
import { Button } from '@documenso/ui/primitives/button';
export default function ActivityPageBackButton() {
@ -15,7 +17,7 @@ export default function ActivityPageBackButton() {
void router.back();
}}
>
Back
<Trans>Back</Trans>
</Button>
</div>
);

View File

@ -5,6 +5,7 @@ import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Trans } from '@lingui/macro';
import { Braces, CreditCard, Globe2Icon, Lock, User, Users, Webhook } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
@ -32,7 +33,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
)}
>
<User className="mr-2 h-5 w-5" />
Profile
<Trans>Profile</Trans>
</Button>
</Link>
@ -46,7 +47,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
)}
>
<Globe2Icon className="mr-2 h-5 w-5" />
Public Profile
<Trans>Public Profile</Trans>
</Button>
</Link>
)}
@ -60,7 +61,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
)}
>
<Users className="mr-2 h-5 w-5" />
Teams
<Trans>Teams</Trans>
</Button>
</Link>
@ -73,7 +74,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
)}
>
<Lock className="mr-2 h-5 w-5" />
Security
<Trans>Security</Trans>
</Button>
</Link>
@ -86,7 +87,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
)}
>
<Braces className="mr-2 h-5 w-5" />
API Tokens
<Trans>API Tokens</Trans>
</Button>
</Link>
@ -99,7 +100,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
)}
>
<Webhook className="mr-2 h-5 w-5" />
Webhooks
<Trans>Webhooks</Trans>
</Button>
</Link>
@ -113,7 +114,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
)}
>
<CreditCard className="mr-2 h-5 w-5" />
Billing
<Trans>Billing</Trans>
</Button>
</Link>
)}

View File

@ -5,6 +5,7 @@ import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Trans } from '@lingui/macro';
import { Braces, CreditCard, Globe2Icon, Lock, User, Users, Webhook } from 'lucide-react';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
@ -35,7 +36,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
)}
>
<User className="mr-2 h-5 w-5" />
Profile
<Trans>Profile</Trans>
</Button>
</Link>
@ -49,7 +50,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
)}
>
<Globe2Icon className="mr-2 h-5 w-5" />
Public Profile
<Trans>Public Profile</Trans>
</Button>
</Link>
)}
@ -63,7 +64,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
)}
>
<Users className="mr-2 h-5 w-5" />
Teams
<Trans>Teams</Trans>
</Button>
</Link>
@ -76,7 +77,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
)}
>
<Lock className="mr-2 h-5 w-5" />
Security
<Trans>Security</Trans>
</Button>
</Link>
@ -89,7 +90,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
)}
>
<Braces className="mr-2 h-5 w-5" />
API Tokens
<Trans>API Tokens</Trans>
</Button>
</Link>
@ -102,7 +103,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
)}
>
<Webhook className="mr-2 h-5 w-5" />
Webhooks
<Trans>Webhooks</Trans>
</Button>
</Link>
@ -116,7 +117,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
)}
>
<CreditCard className="mr-2 h-5 w-5" />
Billing
<Trans>Billing</Trans>
</Button>
</Link>
)}

View File

@ -5,6 +5,8 @@ import { useEffect, useState } from 'react';
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';
@ -44,16 +46,18 @@ export default function DeleteTokenDialog({
onDelete,
children,
}: DeleteTokenDialogProps) {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const deleteMessage = `delete ${token.name}`;
const ZDeleteTokenDialogSchema = z.object({
tokenName: z.literal(deleteMessage, {
errorMap: () => ({ message: `You must enter '${deleteMessage}' to proceed` }),
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
}),
});
@ -80,8 +84,8 @@ export default function DeleteTokenDialog({
});
toast({
title: 'Token deleted',
description: 'The token was deleted successfully.',
title: _(msg`Token deleted`),
description: _(msg`The token was deleted successfully.`),
duration: 5000,
});
@ -90,11 +94,12 @@ export default function DeleteTokenDialog({
router.refresh();
} catch (error) {
toast({
title: 'An unknown error occurred',
title: _(msg`An unknown error occurred`),
description: _(
msg`We encountered an unknown error while attempting to delete this token. Please try again later.`,
),
variant: 'destructive',
duration: 5000,
description:
'We encountered an unknown error while attempting to delete this token. Please try again later.',
});
}
};
@ -113,18 +118,22 @@ export default function DeleteTokenDialog({
<DialogTrigger asChild={true}>
{children ?? (
<Button className="mr-4" variant="destructive">
Delete
<Trans>Delete</Trans>
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure you want to delete this token?</DialogTitle>
<DialogTitle>
<Trans>Are you sure you want to delete this token?</Trans>
</DialogTitle>
<DialogDescription>
Please note that this action is irreversible. Once confirmed, your token will be
permanently deleted.
<Trans>
Please note that this action is irreversible. Once confirmed, your token will be
permanently deleted.
</Trans>
</DialogDescription>
</DialogHeader>
@ -140,10 +149,12 @@ export default function DeleteTokenDialog({
render={({ field }) => (
<FormItem>
<FormLabel>
Confirm by typing:{' '}
<span className="font-sm text-destructive font-semibold">
{deleteMessage}
</span>
<Trans>
Confirm by typing:{' '}
<span className="font-sm text-destructive font-semibold">
{deleteMessage}
</span>
</Trans>
</FormLabel>
<FormControl>
@ -162,7 +173,7 @@ export default function DeleteTokenDialog({
className="flex-1"
onClick={() => setIsOpen(false)}
>
Cancel
<Trans>Cancel</Trans>
</Button>
<Button
@ -172,7 +183,7 @@ export default function DeleteTokenDialog({
disabled={!form.formState.isValid}
loading={form.formState.isSubmitting}
>
I'm sure! Delete it
<Trans>I'm sure! Delete it</Trans>
</Button>
</div>
</DialogFooter>

View File

@ -5,6 +5,8 @@ import { useState } from 'react';
import { useRouter } from 'next/navigation';
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 { useForm } from 'react-hook-form';
import type { z } from 'zod';
@ -48,9 +50,11 @@ export type CreateWebhookDialogProps = {
} & Omit<DialogPrimitive.DialogProps, 'children'>;
export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const router = useRouter();
const team = useOptionalCurrentTeam();
const [open, setOpen] = useState(false);
@ -85,8 +89,8 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
setOpen(false);
toast({
title: 'Webhook created',
description: 'The webhook was successfully created.',
title: _(msg`Webhook created`),
description: _(msg`The webhook was successfully created.`),
});
form.reset();
@ -94,8 +98,8 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
router.refresh();
} catch (err) {
toast({
title: 'Error',
description: 'An error occurred while creating the webhook. Please try again.',
title: _(msg`Error`),
description: _(msg`An error occurred while creating the webhook. Please try again.`),
variant: 'destructive',
});
}
@ -108,13 +112,21 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
{...props}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? <Button className="flex-shrink-0">Create Webhook</Button>}
{trigger ?? (
<Button className="flex-shrink-0">
<Trans>Create Webhook</Trans>
</Button>
)}
</DialogTrigger>
<DialogContent className="max-w-lg" position="center">
<DialogHeader>
<DialogTitle>Create webhook</DialogTitle>
<DialogDescription>On this page, you can create a new webhook.</DialogDescription>
<DialogTitle>
<Trans>Create webhook</Trans>
</DialogTitle>
<DialogDescription>
<Trans>On this page, you can create a new webhook.</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
@ -129,13 +141,15 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
name="webhookUrl"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel required>Webhook URL</FormLabel>
<FormLabel required>
<Trans>Webhook URL</Trans>
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} />
</FormControl>
<FormDescription>
The URL for Documenso to send webhook events to.
<Trans>The URL for Documenso to send webhook events to.</Trans>
</FormDescription>
<FormMessage />
@ -148,7 +162,9 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>Enabled</FormLabel>
<FormLabel>
<Trans>Enabled</Trans>
</FormLabel>
<div>
<FormControl>
@ -171,7 +187,9 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
name="eventTriggers"
render={({ field: { onChange, value } }) => (
<FormItem className="flex flex-col gap-2">
<FormLabel required>Triggers</FormLabel>
<FormLabel required>
<Trans>Triggers</Trans>
</FormLabel>
<FormControl>
<TriggerMultiSelectCombobox
listValues={value}
@ -182,7 +200,7 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
</FormControl>
<FormDescription>
The events that will trigger a webhook to be sent to your URL.
<Trans>The events that will trigger a webhook to be sent to your URL.</Trans>
</FormDescription>
<FormMessage />
@ -195,7 +213,9 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
name="secret"
render={({ field }) => (
<FormItem>
<FormLabel>Secret</FormLabel>
<FormLabel>
<Trans>Secret</Trans>
</FormLabel>
<FormControl>
<PasswordInput
className="bg-background"
@ -205,8 +225,11 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
</FormControl>
<FormDescription>
A secret that will be sent to your URL so you can verify that the request has
been sent by Documenso.
<Trans>
A secret that will be sent to your URL so you can verify that the request
has been sent by Documenso
</Trans>
.
</FormDescription>
<FormMessage />
</FormItem>
@ -216,10 +239,10 @@ export const CreateWebhookDialog = ({ trigger, ...props }: CreateWebhookDialogPr
<DialogFooter>
<div className="flex w-full flex-nowrap gap-4">
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
Cancel
<Trans>Cancel</Trans>
</Button>
<Button type="submit" loading={form.formState.isSubmitting}>
Create
<Trans>Create</Trans>
</Button>
</div>
</DialogFooter>

View File

@ -5,6 +5,8 @@ import { useEffect, useState } from 'react';
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';
@ -40,9 +42,11 @@ export type DeleteWebhookDialogProps = {
};
export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const router = useRouter();
const team = useOptionalCurrentTeam();
const [open, setOpen] = useState(false);
@ -51,7 +55,7 @@ export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogPr
const ZDeleteWebhookFormSchema = z.object({
webhookUrl: z.literal(deleteMessage, {
errorMap: () => ({ message: `You must enter '${deleteMessage}' to proceed` }),
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
}),
});
@ -71,9 +75,9 @@ export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogPr
await deleteWebhook({ id: webhook.id, teamId: team?.id });
toast({
title: 'Webhook deleted',
title: _(msg`Webhook deleted`),
description: _(msg`The webhook has been successfully deleted.`),
duration: 5000,
description: 'The webhook has been successfully deleted.',
});
setOpen(false);
@ -81,11 +85,12 @@ export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogPr
router.refresh();
} catch (error) {
toast({
title: 'An unknown error occurred',
title: _(msg`An unknown error occurred`),
description: _(
msg`We encountered an unknown error while attempting to delete it. Please try again later.`,
),
variant: 'destructive',
duration: 5000,
description:
'We encountered an unknown error while attempting to delete it. Please try again later.',
});
}
};
@ -101,18 +106,22 @@ export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogPr
<DialogTrigger asChild>
{children ?? (
<Button className="mr-4" variant="destructive">
Delete
<Trans>Delete</Trans>
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Webhook</DialogTitle>
<DialogTitle>
<Trans>Delete Webhook</Trans>
</DialogTitle>
<DialogDescription>
Please note that this action is irreversible. Once confirmed, your webhook will be
permanently deleted.
<Trans>
Please note that this action is irreversible. Once confirmed, your webhook will be
permanently deleted.
</Trans>
</DialogDescription>
</DialogHeader>
@ -128,10 +137,12 @@ export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogPr
render={({ field }) => (
<FormItem>
<FormLabel>
Confirm by typing:{' '}
<span className="font-sm text-destructive font-semibold">
{deleteMessage}
</span>
<Trans>
Confirm by typing:{' '}
<span className="font-sm text-destructive font-semibold">
{deleteMessage}
</span>
</Trans>
</FormLabel>
<FormControl>
<Input className="bg-background" type="text" {...field} />
@ -149,7 +160,7 @@ export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogPr
className="flex-1"
onClick={() => setOpen(false)}
>
Cancel
<Trans>Cancel</Trans>
</Button>
<Button
@ -159,7 +170,7 @@ export const DeleteWebhookDialog = ({ webhook, children }: DeleteWebhookDialogPr
disabled={!form.formState.isValid}
loading={form.formState.isSubmitting}
>
I'm sure! Delete it
<Trans>I'm sure! Delete it</Trans>
</Button>
</div>
</DialogFooter>

View File

@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { Plural, Trans } from '@lingui/macro';
import { WebhookTriggerEvents } from '@prisma/client/';
import { Check, ChevronsUpDown } from 'lucide-react';
@ -60,7 +61,7 @@ export const TriggerMultiSelectCombobox = ({
aria-expanded={isOpen}
className="w-[200px] justify-between"
>
{selectedValues.length > 0 ? selectedValues.length + ' selected...' : 'Select values...'}
<Plural value={selectedValues.length} zero="Select values" other="# selected..." />
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
@ -72,7 +73,9 @@ export const TriggerMultiSelectCombobox = ({
15,
)}
/>
<CommandEmpty>No value found.</CommandEmpty>
<CommandEmpty>
<Trans>No value found.</Trans>
</CommandEmpty>
<CommandGroup>
{allEvents.map((value: string, i: number) => (
<CommandItem key={i} onSelect={() => handleSelect(value)}>