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,9 @@
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { NEXT_PUBLIC_WEBAPP_URL, WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
@ -21,6 +24,8 @@ import { LocaleDate } from '~/components/formatter/locale-date';
import { LeaveTeamDialog } from '../dialogs/leave-team-dialog';
export const CurrentUserTeamsDataTable = () => {
const { _ } = useLingui();
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
@ -57,7 +62,7 @@ export const CurrentUserTeamsDataTable = () => {
<DataTable
columns={[
{
header: 'Team',
header: _(msg`Team`),
accessorKey: 'name',
cell: ({ row }) => (
<Link href={`/t/${row.original.url}`} scroll={false}>
@ -74,15 +79,15 @@ export const CurrentUserTeamsDataTable = () => {
),
},
{
header: 'Role',
header: _(msg`Role`),
accessorKey: 'role',
cell: ({ row }) =>
row.original.ownerUserId === row.original.currentTeamMember.userId
? 'Owner'
: TEAM_MEMBER_ROLE_MAP[row.original.currentTeamMember.role],
: _(TEAM_MEMBER_ROLE_MAP[row.original.currentTeamMember.role]),
},
{
header: 'Member Since',
header: _(msg`Member Since`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
},
@ -92,7 +97,9 @@ export const CurrentUserTeamsDataTable = () => {
<div className="flex justify-end space-x-2">
{canExecuteTeamAction('MANAGE_TEAM', row.original.currentTeamMember.role) && (
<Button variant="outline" asChild>
<Link href={`/t/${row.original.url}/settings`}>Manage</Link>
<Link href={`/t/${row.original.url}/settings`}>
<Trans>Manage</Trans>
</Link>
</Button>
)}
@ -107,7 +114,7 @@ export const CurrentUserTeamsDataTable = () => {
disabled={row.original.ownerUserId === row.original.currentTeamMember.userId}
onSelect={(e) => e.preventDefault()}
>
Leave
<Trans>Leave</Trans>
</Button>
}
/>

View File

@ -1,3 +1,6 @@
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@ -14,21 +17,23 @@ export const PendingUserTeamsDataTableActions = ({
pendingTeamId,
onPayClick,
}: PendingUserTeamsDataTableActionsProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { mutateAsync: deleteTeamPending, isLoading: deletingTeam } =
trpc.team.deleteTeamPending.useMutation({
onSuccess: () => {
toast({
title: 'Success',
description: 'Pending team deleted.',
title: _(msg`Success`),
description: _(msg`Pending team deleted.`),
});
},
onError: () => {
toast({
title: 'Something went wrong',
description:
'We encountered an unknown error while attempting to delete the pending team. Please try again later.',
title: _(msg`Something went wrong`),
description: _(
msg`We encountered an unknown error while attempting to delete the pending team. Please try again later.`,
),
duration: 10000,
variant: 'destructive',
});
@ -38,7 +43,7 @@ export const PendingUserTeamsDataTableActions = ({
return (
<fieldset disabled={deletingTeam} className={cn('flex justify-end space-x-2', className)}>
<Button variant="outline" onClick={() => onPayClick(pendingTeamId)}>
Pay
<Trans>Pay</Trans>
</Button>
<Button
@ -46,7 +51,7 @@ export const PendingUserTeamsDataTableActions = ({
loading={deletingTeam}
onClick={async () => deleteTeamPending({ pendingTeamId: pendingTeamId })}
>
Remove
<Trans>Remove</Trans>
</Button>
</fieldset>
);

View File

@ -4,6 +4,9 @@ import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
@ -20,6 +23,8 @@ import { CreateTeamCheckoutDialog } from '../dialogs/create-team-checkout-dialog
import { PendingUserTeamsDataTableActions } from './pending-user-teams-data-table-actions';
export const PendingUserTeamsDataTable = () => {
const { _ } = useLingui();
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
@ -68,7 +73,7 @@ export const PendingUserTeamsDataTable = () => {
<DataTable
columns={[
{
header: 'Team',
header: _(msg`Team`),
accessorKey: 'name',
cell: ({ row }) => (
<AvatarWithText
@ -82,7 +87,7 @@ export const PendingUserTeamsDataTable = () => {
),
},
{
header: 'Created on',
header: _(msg`Created on`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
},

View File

@ -2,6 +2,8 @@
import Link from 'next/link';
import { Plural, Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { File } from 'lucide-react';
import { DateTime } from 'luxon';
@ -17,6 +19,8 @@ export type TeamBillingInvoicesDataTableProps = {
};
export const TeamBillingInvoicesDataTable = ({ teamId }: TeamBillingInvoicesDataTableProps) => {
const { _ } = useLingui();
const { data, isLoading, isInitialLoading, isLoadingError } = trpc.team.findTeamInvoices.useQuery(
{
teamId,
@ -46,7 +50,7 @@ export const TeamBillingInvoicesDataTable = ({ teamId }: TeamBillingInvoicesData
<DataTable
columns={[
{
header: 'Invoice',
header: _(msg`Invoice`),
accessorKey: 'created',
cell: ({ row }) => (
<div className="flex max-w-xs items-center gap-2">
@ -57,27 +61,27 @@ export const TeamBillingInvoicesDataTable = ({ teamId }: TeamBillingInvoicesData
{DateTime.fromSeconds(row.original.created).toFormat('MMMM yyyy')}
</span>
<span className="text-muted-foreground">
{row.original.quantity} {row.original.quantity > 1 ? 'Seats' : 'Seat'}
<Plural value={row.original.quantity} one="# Seat" other="# Seats" />
</span>
</div>
</div>
),
},
{
header: 'Status',
header: _(msg`Status`),
accessorKey: 'status',
cell: ({ row }) => {
const { status, paid } = row.original;
if (!status) {
return paid ? 'Paid' : 'Unpaid';
return paid ? <Trans>Paid</Trans> : <Trans>Unpaid</Trans>;
}
return status.charAt(0).toUpperCase() + status.slice(1);
},
},
{
header: 'Amount',
header: _(msg`Amount`),
accessorKey: 'total',
cell: ({ row }) => formatCurrency(row.original.currency, row.original.total / 100),
},
@ -91,7 +95,7 @@ export const TeamBillingInvoicesDataTable = ({ teamId }: TeamBillingInvoicesData
disabled={typeof row.original.hostedInvoicePdf !== 'string'}
>
<Link href={row.original.hostedInvoicePdf ?? ''} target="_blank">
View
<Trans>View</Trans>
</Link>
</Button>
@ -101,7 +105,7 @@ export const TeamBillingInvoicesDataTable = ({ teamId }: TeamBillingInvoicesData
disabled={typeof row.original.hostedInvoicePdf !== 'string'}
>
<Link href={row.original.invoicePdf ?? ''} target="_blank">
Download
<Trans>Download</Trans>
</Link>
</Button>
</div>

View File

@ -2,6 +2,8 @@
import { useSearchParams } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { History, MoreHorizontal, Trash2 } from 'lucide-react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
@ -32,6 +34,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const { _ } = useLingui();
const { toast } = useToast();
const parsedSearchParams = ZBaseTableSearchParamsSchema.parse(
@ -55,14 +58,14 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
trpc.team.resendTeamMemberInvitation.useMutation({
onSuccess: () => {
toast({
title: 'Success',
description: 'Invitation has been resent',
title: _(msg`Success`),
description: _(msg`Invitation has been resent`),
});
},
onError: () => {
toast({
title: 'Something went wrong',
description: 'Unable to resend invitation. Please try again.',
title: _(msg`Something went wrong`),
description: _(msg`Unable to resend invitation. Please try again.`),
variant: 'destructive',
});
},
@ -72,14 +75,14 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
trpc.team.deleteTeamMemberInvitations.useMutation({
onSuccess: () => {
toast({
title: 'Success',
description: 'Invitation has been deleted',
title: _(msg`Success`),
description: _(msg`Invitation has been deleted`),
});
},
onError: () => {
toast({
title: 'Something went wrong',
description: 'Unable to delete invitation. Please try again.',
title: _(msg`Something went wrong`),
description: _(msg`Unable to delete invitation. Please try again.`),
variant: 'destructive',
});
},
@ -103,7 +106,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
<DataTable
columns={[
{
header: 'Team Member',
header: _(msg`Team Member`),
cell: ({ row }) => {
return (
<AvatarWithText
@ -117,17 +120,17 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
},
},
{
header: 'Role',
header: _(msg`Role`),
accessorKey: 'role',
cell: ({ row }) => TEAM_MEMBER_ROLE_MAP[row.original.role] ?? row.original.role,
cell: ({ row }) => _(TEAM_MEMBER_ROLE_MAP[row.original.role]) ?? row.original.role,
},
{
header: 'Invited At',
header: _(msg`Invited At`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
},
{
header: 'Actions',
header: _(msg`Actions`),
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
@ -135,7 +138,9 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuLabel>
<Trans>Actions</Trans>
</DropdownMenuLabel>
<DropdownMenuItem
onClick={async () =>
@ -146,7 +151,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
}
>
<History className="mr-2 h-4 w-4" />
Resend
<Trans>Resend</Trans>
</DropdownMenuItem>
<DropdownMenuItem
@ -158,7 +163,7 @@ export const TeamMemberInvitesDataTable = ({ teamId }: TeamMemberInvitesDataTabl
}
>
<Trash2 className="mr-2 h-4 w-4" />
Remove
<Trans>Remove</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -2,6 +2,8 @@
import { useSearchParams } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Edit, MoreHorizontal, Trash2 } from 'lucide-react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
@ -42,6 +44,8 @@ export const TeamMembersDataTable = ({
teamId,
teamName,
}: TeamMembersDataTableProps) => {
const { _ } = useLingui();
const searchParams = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
@ -79,7 +83,7 @@ export const TeamMembersDataTable = ({
<DataTable
columns={[
{
header: 'Team Member',
header: _(msg`Team Member`),
cell: ({ row }) => {
const avatarFallbackText = row.original.user.name
? extractInitials(row.original.user.name)
@ -98,20 +102,20 @@ export const TeamMembersDataTable = ({
},
},
{
header: 'Role',
header: _(msg`Role`),
accessorKey: 'role',
cell: ({ row }) =>
teamOwnerUserId === row.original.userId
? 'Owner'
: TEAM_MEMBER_ROLE_MAP[row.original.role],
: _(TEAM_MEMBER_ROLE_MAP[row.original.role]),
},
{
header: 'Member Since',
header: _(msg`Member Since`),
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
},
{
header: 'Actions',
header: _(msg`Actions`),
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
@ -119,7 +123,9 @@ export const TeamMembersDataTable = ({
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuLabel>
<Trans>Actions</Trans>
</DropdownMenuLabel>
<UpdateTeamMemberDialog
currentUserTeamRole={currentUserTeamRole}
@ -137,7 +143,7 @@ export const TeamMembersDataTable = ({
title="Update team member role"
>
<Edit className="mr-2 h-4 w-4" />
Update role
<Trans>Update role</Trans>
</DropdownMenuItem>
}
/>
@ -155,10 +161,10 @@ export const TeamMembersDataTable = ({
teamOwnerUserId === row.original.userId ||
!isTeamRoleWithinUserHierarchy(currentUserTeamRole, row.original.role)
}
title="Remove team member"
title={_(msg`Remove team member`)}
>
<Trash2 className="mr-2 h-4 w-4" />
Remove
<Trans>Remove</Trans>
</DropdownMenuItem>
}
/>

View File

@ -5,6 +5,9 @@ import { useEffect, useState } from 'react';
import Link from 'next/link';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import type { TeamMemberRole } from '@documenso/prisma/client';
import { Input } from '@documenso/ui/primitives/input';
@ -26,6 +29,8 @@ export const TeamsMemberPageDataTable = ({
teamName,
teamOwnerUserId,
}: TeamsMemberPageDataTableProps) => {
const { _ } = useLingui();
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
@ -61,17 +66,21 @@ export const TeamsMemberPageDataTable = ({
<Input
defaultValue={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search"
placeholder={_(msg`Search`)}
/>
<Tabs value={currentTab} className="flex-shrink-0 overflow-x-auto">
<TabsList>
<TabsTrigger className="min-w-[60px]" value="members" asChild>
<Link href={pathname ?? '/'}>Active</Link>
<Link href={pathname ?? '/'}>
<Trans>Active</Trans>
</Link>
</TabsTrigger>
<TabsTrigger className="min-w-[60px]" value="invites" asChild>
<Link href={`${pathname}?tab=invites`}>Pending</Link>
<Link href={`${pathname}?tab=invites`}>
<Trans>Pending</Trans>
</Link>
</TabsTrigger>
</TabsList>
</Tabs>

View File

@ -5,6 +5,9 @@ import { useEffect, useState } from 'react';
import Link from 'next/link';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { trpc } from '@documenso/trpc/react';
import { Input } from '@documenso/ui/primitives/input';
@ -14,6 +17,8 @@ import { CurrentUserTeamsDataTable } from './current-user-teams-data-table';
import { PendingUserTeamsDataTable } from './pending-user-teams-data-table';
export const UserSettingsTeamsPageDataTable = () => {
const { _ } = useLingui();
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
@ -56,18 +61,20 @@ export const UserSettingsTeamsPageDataTable = () => {
<Input
defaultValue={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search"
placeholder={_(msg`Search`)}
/>
<Tabs value={currentTab} className="flex-shrink-0 overflow-x-auto">
<TabsList>
<TabsTrigger className="min-w-[60px]" value="active" asChild>
<Link href={pathname ?? '/'}>Active</Link>
<Link href={pathname ?? '/'}>
<Trans>Active</Trans>
</Link>
</TabsTrigger>
<TabsTrigger className="min-w-[60px]" value="pending" asChild>
<Link href={`${pathname}?tab=pending`}>
Pending
<Trans>Pending</Trans>
{data && data.count > 0 && (
<span className="ml-1 hidden opacity-50 md:inline-block">{data.count}</span>
)}