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

@@ -1,3 +1,4 @@
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -11,6 +12,8 @@ export type DocumentPageProps = {
};
export default async function TeamsDocumentEditPage({ params }: DocumentPageProps) {
setupI18nSSR();
const { teamUrl } = params;
const { user } = await getRequiredServerComponentSession();

View File

@@ -1,3 +1,4 @@
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -11,6 +12,8 @@ export type TeamDocumentsLogsPageProps = {
};
export default async function TeamsDocumentsLogsPage({ params }: TeamDocumentsLogsPageProps) {
setupI18nSSR();
const { teamUrl } = params;
const { user } = await getRequiredServerComponentSession();

View File

@@ -1,3 +1,4 @@
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -11,6 +12,8 @@ export type DocumentPageProps = {
};
export default async function DocumentPage({ params }: DocumentPageProps) {
setupI18nSSR();
const { teamUrl } = params;
const { user } = await getRequiredServerComponentSession();

View File

@@ -1,3 +1,4 @@
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -15,6 +16,8 @@ export default async function TeamsDocumentPage({
params,
searchParams = {},
}: TeamsDocumentPageProps) {
setupI18nSSR();
const { teamUrl } = params;
const { user } = await getRequiredServerComponentSession();

View File

@@ -3,6 +3,9 @@
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import type { MessageDescriptor } from '@lingui/core';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { ChevronLeft } from 'lucide-react';
import { AppErrorCode } from '@documenso/lib/errors/app-error';
@@ -13,24 +16,28 @@ type ErrorProps = {
};
export default function ErrorPage({ error }: ErrorProps) {
const { _ } = useLingui();
const router = useRouter();
let errorMessage = 'Unknown error';
let errorDetails = '';
let errorMessage = msg`Unknown error`;
let errorDetails: MessageDescriptor | null = null;
if (error.message === AppErrorCode.UNAUTHORIZED) {
errorMessage = 'Unauthorized';
errorDetails = 'You are not authorized to view this page.';
errorMessage = msg`Unauthorized`;
errorDetails = msg`You are not authorized to view this page.`;
}
return (
<div className="mx-auto flex min-h-[80vh] w-full items-center justify-center py-32">
<div>
<p className="text-muted-foreground font-semibold">{errorMessage}</p>
<p className="text-muted-foreground font-semibold">{_(errorMessage)}</p>
<h1 className="mt-3 text-2xl font-bold md:text-3xl">Oops! Something went wrong.</h1>
<h1 className="mt-3 text-2xl font-bold md:text-3xl">
<Trans>Oops! Something went wrong.</Trans>
</h1>
<p className="text-muted-foreground mt-4 text-sm">{errorDetails}</p>
<p className="text-muted-foreground mt-4 text-sm">{errorDetails ? _(errorDetails) : ''}</p>
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
<Button
@@ -41,11 +48,13 @@ export default function ErrorPage({ error }: ErrorProps) {
}}
>
<ChevronLeft className="mr-2 h-4 w-4" />
Go Back
<Trans>Go Back</Trans>
</Button>
<Button asChild>
<Link href="/settings/teams">View teams</Link>
<Link href="/settings/teams">
<Trans>View teams</Trans>
</Link>
</Button>
</div>
</div>

View File

@@ -2,6 +2,8 @@
import { useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { AlertTriangle } from 'lucide-react';
import { match } from 'ts-pattern';
@@ -31,6 +33,7 @@ export const LayoutBillingBanner = ({
teamId,
userRole,
}: LayoutBillingBannerProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const [isOpen, setIsOpen] = useState(false);
@@ -47,9 +50,10 @@ export const LayoutBillingBanner = ({
setIsOpen(false);
} catch (err) {
toast({
title: 'Something went wrong',
description:
'We are unable to proceed to the billing portal at this time. Please try again, or contact support.',
title: _(msg`Something went wrong`),
description: _(
msg`We are unable to proceed to the billing portal at this time. Please try again, or contact support.`,
),
variant: 'destructive',
duration: 10000,
});
@@ -75,8 +79,8 @@ export const LayoutBillingBanner = ({
<AlertTriangle className="mr-2.5 h-5 w-5" />
{match(subscription.status)
.with(SubscriptionStatus.PAST_DUE, () => 'Payment overdue')
.with(SubscriptionStatus.INACTIVE, () => 'Teams restricted')
.with(SubscriptionStatus.PAST_DUE, () => <Trans>Payment overdue</Trans>)
.with(SubscriptionStatus.INACTIVE, () => <Trans>Teams restricted</Trans>)
.exhaustive()}
</div>
@@ -92,26 +96,32 @@ export const LayoutBillingBanner = ({
onClick={() => setIsOpen(true)}
size="sm"
>
Resolve
<Trans>Resolve</Trans>
</Button>
</div>
</div>
<Dialog open={isOpen} onOpenChange={(value) => !isLoading && setIsOpen(value)}>
<DialogContent>
<DialogTitle>Payment overdue</DialogTitle>
<DialogTitle>
<Trans>Payment overdue</Trans>
</DialogTitle>
{match(subscription.status)
.with(SubscriptionStatus.PAST_DUE, () => (
<DialogDescription>
Your payment for teams is overdue. Please settle the payment to avoid any service
disruptions.
<Trans>
Your payment for teams is overdue. Please settle the payment to avoid any service
disruptions.
</Trans>
</DialogDescription>
))
.with(SubscriptionStatus.INACTIVE, () => (
<DialogDescription>
Due to an unpaid invoice, your team has been restricted. Please settle the payment
to restore full access to your team.
<Trans>
Due to an unpaid invoice, your team has been restricted. Please settle the payment
to restore full access to your team.
</Trans>
</DialogDescription>
))
.otherwise(() => null)}
@@ -119,7 +129,7 @@ export const LayoutBillingBanner = ({
{canExecuteTeamAction('MANAGE_BILLING', userRole) && (
<DialogFooter>
<Button loading={isLoading} onClick={handleCreatePortal}>
Resolve payment
<Trans>Resolve payment</Trans>
</Button>
</DialogFooter>
)}

View File

@@ -3,6 +3,7 @@ import React from 'react';
import { RedirectType, redirect } from 'next/navigation';
import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/server';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
@@ -26,6 +27,8 @@ export default async function AuthenticatedTeamsLayout({
children,
params,
}: AuthenticatedTeamsLayoutProps) {
setupI18nSSR();
const { session, user } = await getServerComponentSession();
if (!session || !user) {

View File

@@ -2,6 +2,7 @@
import Link from 'next/link';
import { Trans } from '@lingui/macro';
import { ChevronLeft } from 'lucide-react';
import { Button } from '@documenso/ui/primitives/button';
@@ -10,19 +11,25 @@ export default function NotFound() {
return (
<div className="mx-auto flex min-h-[80vh] w-full items-center justify-center py-32">
<div>
<p className="text-muted-foreground font-semibold">404 Team not found</p>
<p className="text-muted-foreground font-semibold">
<Trans>404 Team not found</Trans>
</p>
<h1 className="mt-3 text-2xl font-bold md:text-3xl">Oops! Something went wrong.</h1>
<h1 className="mt-3 text-2xl font-bold md:text-3xl">
<Trans>Oops! Something went wrong.</Trans>
</h1>
<p className="text-muted-foreground mt-4 text-sm">
The team you are looking for may have been removed, renamed or may have never existed.
<Trans>
The team you are looking for may have been removed, renamed or may have never existed.
</Trans>
</p>
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
<Button asChild className="w-32">
<Link href="/settings/teams">
<ChevronLeft className="mr-2 h-4 w-4" />
Go Back
<Trans>Go Back</Trans>
</Link>
</Button>
</div>

View File

@@ -1,6 +1,9 @@
import { Plural, Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import type Stripe from 'stripe';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { stripe } from '@documenso/lib/server-only/stripe';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -18,6 +21,10 @@ export type TeamsSettingsBillingPageProps = {
};
export default async function TeamsSettingBillingPage({ params }: TeamsSettingsBillingPageProps) {
setupI18nSSR();
const { _ } = useLingui();
const session = await getRequiredServerComponentSession();
const team = await getTeamByUrl({ userId: session.user.id, teamUrl: params.teamUrl });
@@ -32,29 +39,34 @@ export default async function TeamsSettingBillingPage({ params }: TeamsSettingsB
const formatTeamSubscriptionDetails = (subscription: Stripe.Subscription | null) => {
if (!subscription) {
return 'No payment required';
return <Trans>No payment required</Trans>;
}
const numberOfSeats = subscription.items.data[0].quantity ?? 0;
const formattedTeamMemberQuanity = numberOfSeats > 1 ? `${numberOfSeats} members` : '1 member';
const formattedTeamMemberQuanity = (
<Plural value={numberOfSeats} one="# member" other="# members" />
);
const formattedDate = DateTime.fromSeconds(subscription.current_period_end).toFormat(
'LLL dd, yyyy',
);
return `${formattedTeamMemberQuanity} • Monthly • Renews: ${formattedDate}`;
return _(msg`${formattedTeamMemberQuanity} • Monthly • Renews: ${formattedDate}`);
};
return (
<div>
<SettingsHeader title="Billing" subtitle="Your subscription is currently active." />
<SettingsHeader
title={_(msg`Billing`)}
subtitle={_(msg`Your subscription is currently active.`)}
/>
<Card gradient className="shadow-sm">
<CardContent className="flex flex-row items-center justify-between p-4">
<div className="flex flex-col text-sm">
<p className="text-foreground font-semibold">
Current plan: {teamSubscription ? 'Team' : 'Early Adopter Team'}
<Trans>Current plan: {teamSubscription ? 'Team' : 'Early Adopter Team'}</Trans>
</p>
<p className="text-muted-foreground mt-0.5">
@@ -66,8 +78,8 @@ export default async function TeamsSettingBillingPage({ params }: TeamsSettingsB
<div
title={
canManageBilling
? 'Manage team subscription.'
: 'You must be an admin of this team to manage billing.'
? _(msg`Manage team subscription.`)
: _(msg`You must be an admin of this team to manage billing.`)
}
>
<TeamBillingPortalButton teamId={team.id} />

View File

@@ -2,6 +2,9 @@ import React from 'react';
import { notFound } from 'next/navigation';
import { Trans } from '@lingui/macro';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -21,6 +24,8 @@ export default async function TeamsSettingsLayout({
children,
params: { teamUrl },
}: TeamSettingsLayoutProps) {
setupI18nSSR();
const session = await getRequiredServerComponentSession();
try {
@@ -41,7 +46,9 @@ export default async function TeamsSettingsLayout({
return (
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<h1 className="text-4xl font-semibold">Team Settings</h1>
<h1 className="text-4xl font-semibold">
<Trans>Team Settings</Trans>
</h1>
<div className="mt-4 grid grid-cols-12 gap-x-8 md:mt-8">
<DesktopNav className="hidden md:col-span-3 md:flex" />

View File

@@ -1,3 +1,7 @@
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -12,6 +16,9 @@ export type TeamsSettingsMembersPageProps = {
};
export default async function TeamsSettingsMembersPage({ params }: TeamsSettingsMembersPageProps) {
setupI18nSSR();
const { _ } = useLingui();
const { teamUrl } = params;
const session = await getRequiredServerComponentSession();
@@ -20,7 +27,10 @@ export default async function TeamsSettingsMembersPage({ params }: TeamsSettings
return (
<div>
<SettingsHeader title="Members" subtitle="Manage the members or invite new members.">
<SettingsHeader
title={_(msg`Members`)}
subtitle={_(msg`Manage the members or invite new members.`)}
>
<InviteTeamMembersDialog
teamId={team.id}
currentUserTeamRole={team.currentTeamMember.role}

View File

@@ -1,6 +1,8 @@
import { Trans } from '@lingui/macro';
import { CheckCircle2, Clock } from 'lucide-react';
import { P, match } from 'ts-pattern';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -26,6 +28,8 @@ export type TeamsSettingsPageProps = {
};
export default async function TeamsSettingsPage({ params }: TeamsSettingsPageProps) {
setupI18nSSR();
const { teamUrl } = params;
const session = await getRequiredServerComponentSession();
@@ -53,11 +57,15 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
<section className="mt-6 space-y-6">
{(team.teamEmail || team.emailVerification) && (
<Alert className="p-6" variant="neutral">
<AlertTitle>Team email</AlertTitle>
<AlertTitle>
<Trans>Team email</Trans>
</AlertTitle>
<AlertDescription className="mr-2">
You can view documents associated with this email and use this identity when sending
documents.
<Trans>
You can view documents associated with this email and use this identity when sending
documents.
</Trans>
</AlertDescription>
<hr className="border-border/50 mt-2" />
@@ -90,7 +98,7 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
.with({ teamEmail: P.not(null) }, () => (
<>
<CheckCircle2 className="mr-1.5 text-green-500 dark:text-green-300" />
Active
<Trans>Active</Trans>
</>
))
.with(
@@ -103,14 +111,14 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
() => (
<>
<Clock className="mr-1.5 text-yellow-500 dark:text-yellow-200" />
Expired
<Trans>Expired</Trans>
</>
),
)
.with({ emailVerification: P.not(null) }, () => (
<>
<Clock className="mr-1.5 text-blue-600 dark:text-blue-300" />
Awaiting email confirmation
<Trans>Awaiting email confirmation</Trans>
</>
))
.otherwise(() => null)}
@@ -128,7 +136,9 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
variant="neutral"
>
<div className="mb-4 sm:mb-0">
<AlertTitle>Team email</AlertTitle>
<AlertTitle>
<Trans>Team email</Trans>
</AlertTitle>
<AlertDescription className="mr-2">
<ul className="text-muted-foreground mt-0.5 list-inside list-disc text-sm">
@@ -136,7 +146,9 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
{/* <li>Display this name and email when sending documents</li> */}
{/* <li>View documents associated with this email</li> */}
<span>View documents associated with this email</span>
<span>
<Trans>View documents associated with this email</Trans>
</span>
</ul>
</AlertDescription>
</div>
@@ -153,10 +165,12 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
variant="neutral"
>
<div className="mb-4 sm:mb-0">
<AlertTitle>Transfer team</AlertTitle>
<AlertTitle>
<Trans>Transfer team</Trans>
</AlertTitle>
<AlertDescription className="mr-2">
Transfer the ownership of the team to another team member.
<Trans>Transfer the ownership of the team to another team member.</Trans>
</AlertDescription>
</div>
@@ -173,11 +187,15 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
variant="neutral"
>
<div className="mb-4 sm:mb-0">
<AlertTitle>Delete team</AlertTitle>
<AlertTitle>
<Trans>Delete team</Trans>
</AlertTitle>
<AlertDescription className="mr-2">
This team, and any associated data excluding billing invoices will be permanently
deleted.
<Trans>
This team, and any associated data excluding billing invoices will be
permanently deleted.
</Trans>
</AlertDescription>
</div>

View File

@@ -1,3 +1,4 @@
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { getTeamPublicProfile } from '@documenso/lib/server-only/team/get-team-public-profile';
@@ -13,6 +14,8 @@ export type TeamsSettingsPublicProfilePageProps = {
export default async function TeamsSettingsPublicProfilePage({
params,
}: TeamsSettingsPublicProfilePageProps) {
setupI18nSSR();
const { teamUrl } = params;
const { user } = await getRequiredServerComponentSession();

View File

@@ -1,5 +1,7 @@
'use client';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Edit, Loader, Mail, MoreHorizontal, X } from 'lucide-react';
import type { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -20,23 +22,24 @@ export type TeamsSettingsPageProps = {
};
export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { mutateAsync: resendEmailVerification, isLoading: isResendingEmailVerification } =
trpc.team.resendTeamEmailVerification.useMutation({
onSuccess: () => {
toast({
title: 'Success',
description: 'Email verification has been resent',
title: _(msg`Success`),
description: _(msg`Email verification has been resent`),
duration: 5000,
});
},
onError: () => {
toast({
title: 'Something went wrong',
title: _(msg`Something went wrong`),
description: _(msg`Unable to resend verification at this time. Please try again.`),
variant: 'destructive',
duration: 10000,
description: 'Unable to resend verification at this time. Please try again.',
});
},
});
@@ -61,7 +64,7 @@ export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
) : (
<Mail className="mr-2 h-4 w-4" />
)}
Resend verification
<Trans>Resend verification</Trans>
</DropdownMenuItem>
)}
@@ -71,7 +74,7 @@ export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
trigger={
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<Edit className="mr-2 h-4 w-4" />
Edit
<Trans>Edit</Trans>
</DropdownMenuItem>
}
/>
@@ -83,7 +86,7 @@ export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
trigger={
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<X className="mr-2 h-4 w-4" />
Remove
<Trans>Remove</Trans>
</DropdownMenuItem>
}
/>

View File

@@ -2,6 +2,8 @@
import { useRouter } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { AnimatePresence } from 'framer-motion';
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
@@ -29,6 +31,7 @@ export const TeamTransferStatus = ({
}: TeamTransferStatusProps) => {
const router = useRouter();
const { _ } = useLingui();
const { toast } = useToast();
const isExpired = transferVerification && isTokenExpired(transferVerification.expiresAt);
@@ -38,8 +41,8 @@ export const TeamTransferStatus = ({
onSuccess: () => {
if (!isExpired) {
toast({
title: 'Success',
description: 'The team transfer invitation has been successfully deleted.',
title: _(msg`Success`),
description: _(msg`The team transfer invitation has been successfully deleted.`),
duration: 5000,
});
}
@@ -48,10 +51,11 @@ export const TeamTransferStatus = ({
},
onError: () => {
toast({
title: 'An unknown error occurred',
title: _(msg`An unknown error occurred`),
description: _(
msg`We encountered an unknown error while attempting to remove this transfer. Please try again or contact support.`,
),
variant: 'destructive',
description:
'We encountered an unknown error while attempting to remove this transfer. Please try again or contact support.',
});
},
});
@@ -69,26 +73,36 @@ export const TeamTransferStatus = ({
>
<div>
<AlertTitle>
{isExpired ? 'Team transfer request expired' : 'Team transfer in progress'}
{isExpired ? (
<Trans>Team transfer request expired</Trans>
) : (
<Trans>Team transfer in progress</Trans>
)}
</AlertTitle>
<AlertDescription>
{isExpired ? (
<p className="text-sm">
The team transfer request to <strong>{transferVerification.name}</strong> has
expired.
<Trans>
The team transfer request to <strong>{transferVerification.name}</strong> has
expired.
</Trans>
</p>
) : (
<section className="text-sm">
<p>
A request to transfer the ownership of this team has been sent to{' '}
<strong>
{transferVerification.name} ({transferVerification.email})
</strong>
<Trans>
A request to transfer the ownership of this team has been sent to{' '}
<strong>
{transferVerification.name} ({transferVerification.email})
</strong>
</Trans>
</p>
<p>
If they accept this request, the team will be transferred to their account.
<Trans>
If they accept this request, the team will be transferred to their account.
</Trans>
</p>
</section>
)}
@@ -104,7 +118,7 @@ export const TeamTransferStatus = ({
'hover:bg-transparent hover:text-blue-800': !isExpired,
})}
>
{isExpired ? 'Close' : 'Cancel'}
{isExpired ? <Trans>Close</Trans> : <Trans>Cancel</Trans>}
</Button>
)}
</Alert>

View File

@@ -1,6 +1,8 @@
import { Trans } from '@lingui/macro';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
@@ -20,6 +22,8 @@ type ApiTokensPageProps = {
};
export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
setupI18nSSR();
const { teamUrl } = params;
const { user } = await getRequiredServerComponentSession();
@@ -35,7 +39,9 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
return (
<div>
<h3 className="text-2xl font-semibold">API Tokens</h3>
<h3 className="text-2xl font-semibold">
<Trans>API Tokens</Trans>
</h3>
<p className="text-muted-foreground mt-2 text-sm">
{match(error.code)
.with(AppErrorCode.UNAUTHORIZED, () => error.message)
@@ -47,18 +53,22 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
return (
<div>
<h3 className="text-2xl font-semibold">API Tokens</h3>
<h3 className="text-2xl font-semibold">
<Trans>API Tokens</Trans>
</h3>
<p className="text-muted-foreground mt-2 text-sm">
On this page, you can create new API tokens and manage the existing ones. <br />
You can view our swagger docs{' '}
<a
className="text-primary underline"
href={`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/openapi`}
target="_blank"
>
here
</a>
<Trans>
On this page, you can create new API tokens and manage the existing ones. <br />
You can view our swagger docs{' '}
<a
className="text-primary underline"
href={`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/openapi`}
target="_blank"
>
here
</a>
</Trans>
</p>
<hr className="my-4" />
@@ -67,12 +77,14 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
<hr className="mb-4 mt-8" />
<h4 className="text-xl font-medium">Your existing tokens</h4>
<h4 className="text-xl font-medium">
<Trans>Your existing tokens</Trans>
</h4>
{tokens.length === 0 && (
<div className="mb-4">
<p className="text-muted-foreground mt-2 text-sm italic">
Your tokens will be shown here once you create them.
<Trans>Your tokens will be shown here once you create them.</Trans>
</p>
</div>
)}
@@ -86,22 +98,30 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
<h5 className="text-base">{token.name}</h5>
<p className="text-muted-foreground mt-2 text-xs">
Created on <LocaleDate date={token.createdAt} format={DateTime.DATETIME_FULL} />
<Trans>
Created on{' '}
<LocaleDate date={token.createdAt} format={DateTime.DATETIME_FULL} />
</Trans>
</p>
{token.expires ? (
<p className="text-muted-foreground mt-1 text-xs">
Expires on <LocaleDate date={token.expires} format={DateTime.DATETIME_FULL} />
<Trans>
Expires on{' '}
<LocaleDate date={token.expires} format={DateTime.DATETIME_FULL} />
</Trans>
</p>
) : (
<p className="text-muted-foreground mt-1 text-xs">
Token doesn't have an expiration date
<Trans>Token doesn't have an expiration date</Trans>
</p>
)}
</div>
<div>
<DeleteTokenDialog token={token} teamId={team.id}>
<Button variant="destructive">Delete</Button>
<Button variant="destructive">
<Trans>Delete</Trans>
</Button>
</DeleteTokenDialog>
</div>
</div>

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 { Loader } from 'lucide-react';
import { useForm } from 'react-hook-form';
import type { z } from 'zod';
@@ -39,7 +41,9 @@ export type WebhookPageOptions = {
};
export default function WebhookPage({ params }: WebhookPageOptions) {
const { _ } = useLingui();
const { toast } = useToast();
const router = useRouter();
const team = useCurrentTeam();
@@ -73,16 +77,18 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
});
toast({
title: 'Webhook updated',
description: 'The webhook has been updated successfully.',
title: _(msg`Webhook updated`),
description: _(msg`The webhook has been updated successfully.`),
duration: 5000,
});
router.refresh();
} catch (err) {
toast({
title: 'Failed to update webhook',
description: 'We encountered an error while updating the webhook. Please try again later.',
title: _(msg`Failed to update webhook`),
description: _(
msg`We encountered an error while updating the webhook. Please try again later.`,
),
variant: 'destructive',
});
}
@@ -91,8 +97,8 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
return (
<div>
<SettingsHeader
title="Edit webhook"
subtitle="On this page, you can edit the webhook and its settings."
title={_(msg`Edit webhook`)}
subtitle={_(msg`On this page, you can edit the webhook and its settings.`)}
/>
{isLoading && (
@@ -119,7 +125,7 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
</FormControl>
<FormDescription>
The URL for Documenso to send webhook events to.
<Trans>The URL for Documenso to send webhook events to.</Trans>
</FormDescription>
<FormMessage />
@@ -132,7 +138,9 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>Enabled</FormLabel>
<FormLabel>
<Trans>Enabled</Trans>
</FormLabel>
<div>
<FormControl>
@@ -155,7 +163,9 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
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}
@@ -166,7 +176,7 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
</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 />
@@ -185,8 +195,10 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
</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>
@@ -195,7 +207,7 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
<div className="mt-4">
<Button type="submit" loading={form.formState.isSubmitting}>
Update webhook
<Trans>Update webhook</Trans>
</Button>
</div>
</fieldset>

View File

@@ -2,6 +2,8 @@
import Link from 'next/link';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
import { DateTime } from 'luxon';
@@ -18,6 +20,8 @@ import { LocaleDate } from '~/components/formatter/locale-date';
import { useCurrentTeam } from '~/providers/team';
export default function WebhookPage() {
const { _ } = useLingui();
const team = useCurrentTeam();
const { data: webhooks, isLoading } = trpc.webhook.getTeamWebhooks.useQuery({
@@ -27,8 +31,8 @@ export default function WebhookPage() {
return (
<div>
<SettingsHeader
title="Webhooks"
subtitle="On this page, you can create new Webhooks and manage the existing ones."
title={_(msg`Webhooks`)}
subtitle={_(msg`On this page, you can create new Webhooks and manage the existing ones.`)}
>
<CreateWebhookDialog />
</SettingsHeader>
@@ -43,7 +47,9 @@ export default function WebhookPage() {
// TODO: Perhaps add some illustrations here to make the page more engaging
<div className="mb-4">
<p className="text-muted-foreground mt-2 text-sm italic">
You have no webhooks yet. Your webhooks will be shown here once you create them.
<Trans>
You have no webhooks yet. Your webhooks will be shown here once you create them.
</Trans>
</p>
</div>
)}
@@ -71,29 +77,37 @@ export default function WebhookPage() {
</h5>
<Badge variant={webhook.enabled ? 'neutral' : 'warning'} size="small">
{webhook.enabled ? 'Enabled' : 'Disabled'}
{webhook.enabled ? <Trans>Enabled</Trans> : <Trans>Disabled</Trans>}
</Badge>
</div>
<p className="text-muted-foreground mt-2 text-xs">
Listening to{' '}
{webhook.eventTriggers
.map((trigger) => toFriendlyWebhookEventName(trigger))
.join(', ')}
<Trans>
Listening to{' '}
{webhook.eventTriggers
.map((trigger) => toFriendlyWebhookEventName(trigger))
.join(', ')}
</Trans>
</p>
<p className="text-muted-foreground mt-2 text-xs">
Created on{' '}
<LocaleDate date={webhook.createdAt} format={DateTime.DATETIME_FULL} />
<Trans>
Created on{' '}
<LocaleDate date={webhook.createdAt} format={DateTime.DATETIME_FULL} />
</Trans>
</p>
</div>
<div className="mt-4 flex flex-shrink-0 gap-4 sm:mt-0">
<Button asChild variant="outline">
<Link href={`/t/${team.url}/settings/webhooks/${webhook.id}`}>Edit</Link>
<Link href={`/t/${team.url}/settings/webhooks/${webhook.id}`}>
<Trans>Edit</Trans>
</Link>
</Button>
<DeleteWebhookDialog webhook={webhook}>
<Button variant="destructive">Delete</Button>
<Button variant="destructive">
<Trans>Delete</Trans>
</Button>
</DeleteWebhookDialog>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -13,6 +14,8 @@ type TeamTemplatePageProps = {
};
export default async function TeamTemplatePage({ params }: TeamTemplatePageProps) {
setupI18nSSR();
const { teamUrl } = params;
const { user } = await getRequiredServerComponentSession();

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -17,6 +18,8 @@ export default async function TeamTemplatesPage({
searchParams = {},
params,
}: TeamTemplatesPageProps) {
setupI18nSSR();
const { teamUrl } = params;
const { user } = await getRequiredServerComponentSession();