Implement i18n translation formatting for team accounts

Integrated i18n translations in team account components, enhancing the application's multi-language support. This includes updating dialog container text, button labels, and placeholders for improved localization. Also added translations for table headers, button labels, and form fields across various components.
This commit is contained in:
giancarlo
2024-03-28 16:41:37 +08:00
parent 9796f109ba
commit 2afa7f5be1
9 changed files with 274 additions and 206 deletions

View File

@@ -6,7 +6,13 @@
"yourTeam": "Your Teams", "yourTeam": "Your Teams",
"createTeam": "Create Team", "createTeam": "Create Team",
"membersTabLabel": "Members", "membersTabLabel": "Members",
"emailSettingsTab": "Email", "memberName": "Name",
"youLabel": "You",
"emailLabel": "Email",
"roleLabel": "Role",
"primaryOwnerLabel": "Primary Owner",
"joinedAtLabel": "Joined at",
"invitedAtLabel": "Invited at",
"membersTabSubheading": "Manage and Invite members", "membersTabSubheading": "Manage and Invite members",
"inviteMembersPageSubheading": "Invite members to your Team", "inviteMembersPageSubheading": "Invite members to your Team",
"createTeamModalHeading": "Create Team", "createTeamModalHeading": "Create Team",
@@ -20,7 +26,7 @@
"settingsPageLabel": "General", "settingsPageLabel": "General",
"createTeamDropdownLabel": "New team", "createTeamDropdownLabel": "New team",
"changeRole": "Change Role", "changeRole": "Change Role",
"removeMember": "Remove", "removeMember": "Remove from Account",
"inviteMembersSuccess": "Members invited successfully!", "inviteMembersSuccess": "Members invited successfully!",
"inviteMembersError": "Sorry, we encountered an error! Please try again", "inviteMembersError": "Sorry, we encountered an error! Please try again",
"inviteMembersLoading": "Inviting members...", "inviteMembersLoading": "Inviting members...",
@@ -28,20 +34,29 @@
"addAnotherMemberButtonLabel": "Add another one", "addAnotherMemberButtonLabel": "Add another one",
"inviteMembersSubmitLabel": "Send Invites", "inviteMembersSubmitLabel": "Send Invites",
"removeMemberModalHeading": "You are removing this user", "removeMemberModalHeading": "You are removing this user",
"removeMemberModalDescription": "Remove this member from the team. They will no longer have access to the team.",
"removeMemberSuccessMessage": "Member removed successfully", "removeMemberSuccessMessage": "Member removed successfully",
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again", "removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.", "removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.",
"removeMemberLoadingMessage": "Removing member...", "removeMemberLoadingMessage": "Removing member...",
"removeMemberSubmitLabel": "Remove User from Team", "removeMemberSubmitLabel": "Remove User from Team",
"chooseDifferentRoleError": "Role is the same as the current one", "chooseDifferentRoleError": "Role is the same as the current one",
"updateRole": "Update Role",
"updateRoleLoadingMessage": "Updating role...", "updateRoleLoadingMessage": "Updating role...",
"updateRoleSuccessMessage": "Role updated successfully", "updateRoleSuccessMessage": "Role updated successfully",
"updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.", "updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.",
"updateMemberRoleModalHeading": "Update Member's Role", "updateMemberRoleModalHeading": "Update Member's Role",
"updateMemberRoleModalDescription": "Change the role of the selected member. The role determines the permissions of the member.",
"roleMustBeDifferent": "Role must be different from the current one",
"memberRoleInputLabel": "Member role", "memberRoleInputLabel": "Member role",
"updateRoleDescription": "Pick a role for this member.",
"updateRoleSubmitLabel": "Update Role", "updateRoleSubmitLabel": "Update Role",
"transferOwnership": "Transfer Ownership", "transferOwnership": "Transfer Ownership",
"deleteInviteModalHeading": "Deleting Invite", "transferOwnershipDescription": "Transfer ownership of the team to another member.",
"transferOwnershipInputLabel": "Please type TRANSFER to confirm the transfer of ownership.",
"transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary owner of the team.",
"deleteInvitation": "Delete Invitation",
"deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the team.",
"deleteInviteSuccessMessage": "Invite deleted successfully", "deleteInviteSuccessMessage": "Invite deleted successfully",
"deleteInviteErrorMessage": "Invite not deleted. Please try again.", "deleteInviteErrorMessage": "Invite not deleted. Please try again.",
"deleteInviteLoadingMessage": "Deleting invite. Please wait...", "deleteInviteLoadingMessage": "Deleting invite. Please wait...",
@@ -60,10 +75,12 @@
"teamLogoInputHeading": "Upload your team's Logo", "teamLogoInputHeading": "Upload your team's Logo",
"teamLogoInputSubheading": "Please choose a photo to upload as your team logo.", "teamLogoInputSubheading": "Please choose a photo to upload as your team logo.",
"updateTeamSubmitLabel": "Update Team", "updateTeamSubmitLabel": "Update Team",
"inviteMembersPageHeading": "Invite Members", "inviteMembersHeading": "Invite Members to your Team",
"goBackToMembersPage": "Go back to members", "inviteMembersDescription": "Invite members to your team by entering their email and role.",
"emailPlaceholder": "member@email.com",
"membersPageHeading": "Members", "membersPageHeading": "Members",
"inviteMembersButtonLabel": "Invite Members", "inviteMembersButtonLabel": "Invite Members",
"invitingMembers": "Inviting members...",
"pendingInvitesHeading": "Pending Invites", "pendingInvitesHeading": "Pending Invites",
"pendingInvitesSubheading": "Manage invites not yet accepted", "pendingInvitesSubheading": "Manage invites not yet accepted",
"noPendingInvites": "No pending invites found", "noPendingInvites": "No pending invites found",
@@ -95,5 +112,8 @@
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your team.", "transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your team.",
"transferTeamErrorMessage": "We encountered an error transferring ownership of your team. Please try again.", "transferTeamErrorMessage": "We encountered an error transferring ownership of your team. Please try again.",
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected member.", "updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected member.",
"updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again." "updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.",
"searchInvitations": "Search Invitations",
"updateInvitation": "Update Invitation",
"removeInvitation": "Remove Invitation"
} }

View File

@@ -4,6 +4,7 @@ import { useMemo, useState } from 'react';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import { Ellipsis } from 'lucide-react'; import { Ellipsis } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Database } from '@kit/supabase/database'; import { Database } from '@kit/supabase/database';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
@@ -17,6 +18,7 @@ import {
import { If } from '@kit/ui/if'; import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input'; import { Input } from '@kit/ui/input';
import { ProfileAvatar } from '@kit/ui/profile-avatar'; import { ProfileAvatar } from '@kit/ui/profile-avatar';
import { Trans } from '@kit/ui/trans';
import { RoleBadge } from '../members/role-badge'; import { RoleBadge } from '../members/role-badge';
import { DeleteInvitationDialog } from './delete-invitation-dialog'; import { DeleteInvitationDialog } from './delete-invitation-dialog';
@@ -38,8 +40,9 @@ export function AccountInvitationsTable({
invitations, invitations,
permissions, permissions,
}: AccountInvitationsTableProps) { }: AccountInvitationsTableProps) {
const { t } = useTranslation('teams');
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const columns = useMemo(() => getColumns(permissions), [permissions]); const columns = useGetColumns(permissions);
const filteredInvitations = invitations.filter((member) => { const filteredInvitations = invitations.filter((member) => {
const searchString = search.toLowerCase(); const searchString = search.toLowerCase();
@@ -56,7 +59,7 @@ export function AccountInvitationsTable({
<Input <Input
value={search} value={search}
onInput={(e) => setSearch((e.target as HTMLInputElement).value)} onInput={(e) => setSearch((e.target as HTMLInputElement).value)}
placeholder={'Search Invitation'} placeholder={t(`searchInvitations`)}
/> />
<DataTable columns={columns} data={filteredInvitations} /> <DataTable columns={columns} data={filteredInvitations} />
@@ -64,51 +67,59 @@ export function AccountInvitationsTable({
); );
} }
function getColumns(permissions: { function useGetColumns(permissions: {
canUpdateInvitation: boolean; canUpdateInvitation: boolean;
canRemoveInvitation: boolean; canRemoveInvitation: boolean;
}): ColumnDef<Invitations[0]>[] { }): ColumnDef<Invitations[0]>[] {
return [ const { t } = useTranslation('teams');
{
header: 'Email',
size: 200,
cell: ({ row }) => {
const member = row.original;
const email = member.email;
return ( return useMemo(
<span className={'flex items-center space-x-4 text-left'}> () => [
<span> {
<ProfileAvatar text={email} /> header: t('emailLabel'),
size: 200,
cell: ({ row }) => {
const member = row.original;
const email = member.email;
return (
<span className={'flex items-center space-x-4 text-left'}>
<span>
<ProfileAvatar text={email} />
</span>
<span>{email}</span>
</span> </span>
);
},
},
{
header: t('roleLabel'),
cell: ({ row }) => {
const { role } = row.original;
<span>{email}</span> return <RoleBadge role={role} />;
</span> },
);
}, },
}, {
{ header: t('invitedAtLabel'),
header: 'Role', cell: ({ row }) => {
cell: ({ row }) => { return new Date(row.original.created_at).toLocaleDateString();
const { role } = row.original; },
return <RoleBadge role={role} />;
}, },
}, {
{ header: '',
header: 'Invited At', id: 'actions',
cell: ({ row }) => { cell: ({ row }) => (
return new Date(row.original.created_at).toLocaleDateString(); <ActionsDropdown
permissions={permissions}
invitation={row.original}
/>
),
}, },
}, ],
{ [permissions, t],
header: '', );
id: 'actions',
cell: ({ row }) => (
<ActionsDropdown permissions={permissions} invitation={row.original} />
),
},
];
} }
function ActionsDropdown({ function ActionsDropdown({
@@ -133,13 +144,13 @@ function ActionsDropdown({
<DropdownMenuContent> <DropdownMenuContent>
<If condition={permissions.canUpdateInvitation}> <If condition={permissions.canUpdateInvitation}>
<DropdownMenuItem onClick={() => setIsUpdatingRole(true)}> <DropdownMenuItem onClick={() => setIsUpdatingRole(true)}>
Update Invitation <Trans i18nKey={'teams:updateInvitation'} />
</DropdownMenuItem> </DropdownMenuItem>
</If> </If>
<If condition={permissions.canRemoveInvitation}> <If condition={permissions.canRemoveInvitation}>
<DropdownMenuItem onClick={() => setIsDeletingInvite(true)}> <DropdownMenuItem onClick={() => setIsDeletingInvite(true)}>
Remove <Trans i18nKey={'teams:removeInvitation'} />
</DropdownMenuItem> </DropdownMenuItem>
</If> </If>
</DropdownMenuContent> </DropdownMenuContent>

View File

@@ -1,14 +1,16 @@
import { useState, useTransition } from 'react'; import { useState, useTransition } from 'react';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Button } from '@kit/ui/button';
import { import {
Dialog, AlertDialog,
DialogContent, AlertDialogCancel,
DialogDescription, AlertDialogContent,
DialogHeader, AlertDialogDescription,
DialogTitle, AlertDialogFooter,
} from '@kit/ui/dialog'; AlertDialogHeader,
AlertDialogTitle,
} from '@kit/ui/alert-dialog';
import { Button } from '@kit/ui/button';
import { If } from '@kit/ui/if'; import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
@@ -20,24 +22,24 @@ export const DeleteInvitationDialog: React.FC<{
invitationId: number; invitationId: number;
}> = ({ isOpen, setIsOpen, invitationId }) => { }> = ({ isOpen, setIsOpen, invitationId }) => {
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <AlertDialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent> <AlertDialogContent>
<DialogHeader> <AlertDialogHeader>
<DialogTitle> <AlertDialogTitle>
<Trans i18nKey="team:deleteInvitationDialogTitle" /> <Trans i18nKey="team:deleteInvitation" />
</DialogTitle> </AlertDialogTitle>
<DialogDescription> <AlertDialogDescription>
Remove the invitation to join this account. <Trans i18nKey="team:deleteInvitationDialogDescription" />
</DialogDescription> </AlertDialogDescription>
</DialogHeader> </AlertDialogHeader>
<DeleteInvitationForm <DeleteInvitationForm
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
invitationId={invitationId} invitationId={invitationId}
/> />
</DialogContent> </AlertDialogContent>
</Dialog> </AlertDialog>
); );
}; };
@@ -66,7 +68,7 @@ function DeleteInvitationForm({
return ( return (
<form action={onInvitationRemoved}> <form action={onInvitationRemoved}>
<div className={'flex flex-col space-y-6'}> <div className={'flex flex-col space-y-6'}>
<p className={'text-sm'}> <p className={'text-muted-foreground text-sm'}>
<Trans i18nKey={'common:modalConfirmationQuestion'} /> <Trans i18nKey={'common:modalConfirmationQuestion'} />
</p> </p>
@@ -74,13 +76,19 @@ function DeleteInvitationForm({
<RemoveInvitationErrorAlert /> <RemoveInvitationErrorAlert />
</If> </If>
<Button <AlertDialogFooter>
data-test={'confirm-delete-invitation'} <AlertDialogCancel>
variant={'destructive'} <Trans i18nKey={'common:cancel'} />
disabled={isSubmitting} </AlertDialogCancel>
>
Delete Invitation <Button
</Button> data-test={'confirm-delete-invitation'}
variant={'destructive'}
disabled={isSubmitting}
>
<Trans i18nKey={'teams:deleteInvitation'} />
</Button>
</AlertDialogFooter>
</div> </div>
</form> </form>
); );

View File

@@ -2,6 +2,7 @@ import { useState, useTransition } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Database } from '@kit/supabase/database'; import { Database } from '@kit/supabase/database';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
@@ -69,6 +70,7 @@ function UpdateInvitationForm({
userRole: Role; userRole: Role;
setIsOpen: (isOpen: boolean) => void; setIsOpen: (isOpen: boolean) => void;
}>) { }>) {
const { t } = useTranslation('teams');
const [pending, startTransition] = useTransition(); const [pending, startTransition] = useTransition();
const [error, setError] = useState<boolean>(); const [error, setError] = useState<boolean>();
@@ -94,7 +96,7 @@ function UpdateInvitationForm({
return data.role !== userRole; return data.role !== userRole;
}, },
{ {
message: 'Role must be different from the current role.', message: t('roleMustBeDifferent'),
path: ['role'], path: ['role'],
}, },
), ),
@@ -121,7 +123,10 @@ function UpdateInvitationForm({
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem> <FormItem>
<FormLabel>New Role</FormLabel> <FormLabel>
<Trans i18nKey={'teams:roleLabel'} />
</FormLabel>
<FormControl> <FormControl>
<MembershipRoleSelector <MembershipRoleSelector
currentUserRole={userRole} currentUserRole={userRole}
@@ -130,7 +135,9 @@ function UpdateInvitationForm({
/> />
</FormControl> </FormControl>
<FormDescription>Pick a role for this member.</FormDescription> <FormDescription>
<Trans i18nKey={'teams:updateRoleDescription'} />
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@@ -4,6 +4,7 @@ import { useMemo, useState } from 'react';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import { Ellipsis } from 'lucide-react'; import { Ellipsis } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Database } from '@kit/supabase/database'; import { Database } from '@kit/supabase/database';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
@@ -17,6 +18,7 @@ import {
import { If } from '@kit/ui/if'; import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input'; import { Input } from '@kit/ui/input';
import { ProfileAvatar } from '@kit/ui/profile-avatar'; import { ProfileAvatar } from '@kit/ui/profile-avatar';
import { Trans } from '@kit/ui/trans';
import { RemoveMemberDialog } from './remove-member-dialog'; import { RemoveMemberDialog } from './remove-member-dialog';
import { RoleBadge } from './role-badge'; import { RoleBadge } from './role-badge';
@@ -44,11 +46,8 @@ export function AccountMembersTable({
currentUserId, currentUserId,
}: AccountMembersTableProps) { }: AccountMembersTableProps) {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const { t } = useTranslation('teams');
const columns = useMemo( const columns = useGetColumns(permissions, currentUserId);
() => getColumns(permissions, currentUserId),
[currentUserId, permissions],
);
const filteredMembers = members.filter((member) => { const filteredMembers = members.filter((member) => {
const searchString = search.toLowerCase(); const searchString = search.toLowerCase();
@@ -65,7 +64,7 @@ export function AccountMembersTable({
<Input <Input
value={search} value={search}
onInput={(e) => setSearch((e.target as HTMLInputElement).value)} onInput={(e) => setSearch((e.target as HTMLInputElement).value)}
placeholder={'Search Member'} placeholder={t(`searchMembersPlaceholder`)}
/> />
<DataTable columns={columns} data={filteredMembers} /> <DataTable columns={columns} data={filteredMembers} />
@@ -73,7 +72,7 @@ export function AccountMembersTable({
); );
} }
function getColumns( function useGetColumns(
permissions: { permissions: {
canUpdateRole: boolean; canUpdateRole: boolean;
canTransferOwnership: boolean; canTransferOwnership: boolean;
@@ -81,87 +80,92 @@ function getColumns(
}, },
currentUserId: string, currentUserId: string,
): ColumnDef<Members[0]>[] { ): ColumnDef<Members[0]>[] {
return [ const { t } = useTranslation('teams');
{
header: 'Name',
size: 200,
cell: ({ row }) => {
const member = row.original;
const displayName = member.name ?? member.email.split('@')[0];
const isSelf = member.user_id === currentUserId;
return ( return useMemo(
<span className={'flex items-center space-x-4 text-left'}> () => [
<span> {
<ProfileAvatar header: t('memberName'),
displayName={displayName} size: 200,
pictureUrl={member.picture_url} cell: ({ row }) => {
/> const member = row.original;
const displayName = member.name ?? member.email.split('@')[0];
const isSelf = member.user_id === currentUserId;
return (
<span className={'flex items-center space-x-4 text-left'}>
<span>
<ProfileAvatar
displayName={displayName}
pictureUrl={member.picture_url}
/>
</span>
<span>{displayName}</span>
<If condition={isSelf}>
<span
className={
'bg-muted rounded-md px-2.5 py-1 text-xs font-medium'
}
>
{t('youLabel')}
</span>
</If>
</span> </span>
);
<span>{displayName}</span> },
<If condition={isSelf}>
<span
className={
'bg-muted rounded-md px-2.5 py-1 text-xs font-medium'
}
>
You
</span>
</If>
</span>
);
}, },
}, {
{ header: t('emailLabel'),
header: 'Email', accessorKey: 'email',
accessorKey: 'email', cell: ({ row }) => {
cell: ({ row }) => { return row.original.email ?? '-';
return row.original.email ?? '-'; },
}, },
}, {
{ header: t('roleLabel'),
header: 'Role', cell: ({ row }) => {
cell: ({ row }) => { const { role, primary_owner_user_id, user_id } = row.original;
const { role, primary_owner_user_id, user_id } = row.original; const isPrimaryOwner = primary_owner_user_id === user_id;
const isPrimaryOwner = primary_owner_user_id === user_id;
return ( return (
<span className={'flex items-center space-x-1'}> <span className={'flex items-center space-x-1'}>
<RoleBadge role={role} /> <RoleBadge role={role} />
<If condition={isPrimaryOwner}> <If condition={isPrimaryOwner}>
<span <span
className={ className={
'rounded-md bg-yellow-400 px-2.5 py-1 text-xs font-medium dark:text-black' 'rounded-md bg-yellow-400 px-2.5 py-1 text-xs font-medium dark:text-black'
} }
> >
Primary {t('primaryOwnerLabel')}
</span> </span>
</If> </If>
</span> </span>
); );
},
}, },
}, {
{ header: t('joinedAtLabel'),
header: 'Joined At', cell: ({ row }) => {
cell: ({ row }) => { return new Date(row.original.created_at).toLocaleDateString();
return new Date(row.original.created_at).toLocaleDateString(); },
}, },
}, {
{ header: '',
header: '', id: 'actions',
id: 'actions', cell: ({ row }) => (
cell: ({ row }) => ( <ActionsDropdown
<ActionsDropdown permissions={permissions}
permissions={permissions} member={row.original}
member={row.original} currentUserId={currentUserId}
currentUserId={currentUserId} />
/> ),
), },
}, ],
]; [permissions, currentUserId, t],
);
} }
function ActionsDropdown({ function ActionsDropdown({
@@ -196,19 +200,19 @@ function ActionsDropdown({
<DropdownMenuContent> <DropdownMenuContent>
<If condition={permissions.canUpdateRole}> <If condition={permissions.canUpdateRole}>
<DropdownMenuItem onClick={() => setIsUpdatingRole(true)}> <DropdownMenuItem onClick={() => setIsUpdatingRole(true)}>
Update Role <Trans i18nKey={'teams:updateRole'} />
</DropdownMenuItem> </DropdownMenuItem>
</If> </If>
<If condition={permissions.canTransferOwnership}> <If condition={permissions.canTransferOwnership}>
<DropdownMenuItem onClick={() => setIsTransferring(true)}> <DropdownMenuItem onClick={() => setIsTransferring(true)}>
Transfer Ownership <Trans i18nKey={'teams:transferOwnership'} />
</DropdownMenuItem> </DropdownMenuItem>
</If> </If>
<If condition={permissions.canRemoveFromAccount}> <If condition={permissions.canRemoveFromAccount}>
<DropdownMenuItem onClick={() => setIsRemoving(true)}> <DropdownMenuItem onClick={() => setIsRemoving(true)}>
Remove from Account <Trans i18nKey={'teams:removeMember'} />
</DropdownMenuItem> </DropdownMenuItem>
</If> </If>
</DropdownMenuContent> </DropdownMenuContent>

View File

@@ -56,10 +56,12 @@ export function InviteMembersDialogContainer({
<DialogContent onInteractOutside={(e) => e.preventDefault()}> <DialogContent onInteractOutside={(e) => e.preventDefault()}>
<DialogHeader> <DialogHeader>
<DialogTitle>Invite Members to Organization</DialogTitle> <DialogTitle>
<Trans i18nKey={'teams:inviteMembersHeading'} />
</DialogTitle>
<DialogDescription> <DialogDescription>
Invite members to your team by entering their email and role. <Trans i18nKey={'teams:inviteMembersDescription'} />
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -88,7 +90,7 @@ function InviteMembersForm({
onSubmit: (data: { invitations: InviteModel[] }) => void; onSubmit: (data: { invitations: InviteModel[] }) => void;
pending: boolean; pending: boolean;
}) { }) {
const { t } = useTranslation('team'); const { t } = useTranslation('teams');
const form = useForm({ const form = useForm({
resolver: zodResolver(InviteMembersSchema), resolver: zodResolver(InviteMembersSchema),
@@ -130,7 +132,7 @@ function InviteMembersForm({
<FormControl> <FormControl>
<Input <Input
data-test={'invite-email-input'} data-test={'invite-email-input'}
placeholder="member@email.com" placeholder={t('emailPlaceholder')}
type="email" type="email"
required required
{...field} {...field}
@@ -148,7 +150,9 @@ function InviteMembersForm({
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem> <FormItem>
<FormLabel>Role</FormLabel> <FormLabel>
<Trans i18nKey={'teams:roleLabel'} />
</FormLabel>
<FormControl> <FormControl>
<MembershipRoleSelector <MembershipRoleSelector
@@ -199,26 +203,30 @@ function InviteMembersForm({
<Button <Button
data-test={'append-new-invite-button'} data-test={'append-new-invite-button'}
type={'button'} type={'button'}
variant={'outline'} variant={'link'}
size={'sm'} size={'sm'}
disabled={pending} disabled={pending}
onClick={() => { onClick={() => {
fieldArray.append(createEmptyInviteModel()); fieldArray.append(createEmptyInviteModel());
}} }}
> >
<span className={'flex items-center space-x-2'}> <Plus className={'mr-1 h-3'} />
<Plus className={'h-4'} />
<span> <span>
<Trans i18nKey={'teams:addAnotherMemberButtonLabel'} /> <Trans i18nKey={'teams:addAnotherMemberButtonLabel'} />
</span>
</span> </span>
</Button> </Button>
</div> </div>
</div> </div>
<Button disabled={pending}> <Button disabled={pending}>
{pending ? 'Inviting...' : 'Invite Members'} <Trans
i18nKey={
pending
? 'teams:invitingMembers'
: 'teams:inviteMembersButtonLabel'
}
/>
</Button> </Button>
</form> </form>
</Form> </Form>

View File

@@ -1,14 +1,16 @@
import { useState, useTransition } from 'react'; import { useState, useTransition } from 'react';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Button } from '@kit/ui/button';
import { import {
Dialog, AlertDialog,
DialogContent, AlertDialogCancel,
DialogDescription, AlertDialogContent,
DialogHeader, AlertDialogDescription,
DialogTitle, AlertDialogFooter,
} from '@kit/ui/dialog'; AlertDialogHeader,
AlertDialogTitle,
} from '@kit/ui/alert-dialog';
import { Button } from '@kit/ui/button';
import { If } from '@kit/ui/if'; import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
@@ -21,25 +23,25 @@ export const RemoveMemberDialog: React.FC<{
userId: string; userId: string;
}> = ({ isOpen, setIsOpen, accountId, userId }) => { }> = ({ isOpen, setIsOpen, accountId, userId }) => {
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <AlertDialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent> <AlertDialogContent>
<DialogHeader> <AlertDialogHeader>
<DialogTitle> <AlertDialogTitle>
<Trans i18nKey="team:removeMemberModalHeading" /> <Trans i18nKey="teamS:removeMemberModalHeading" />
</DialogTitle> </AlertDialogTitle>
<DialogDescription> <AlertDialogDescription>
Remove this member from the team. <Trans i18nKey={'teams:removeMemberModalDescription'} />
</DialogDescription> </AlertDialogDescription>
</DialogHeader> </AlertDialogHeader>
<RemoveMemberForm <RemoveMemberForm
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
accountId={accountId} accountId={accountId}
userId={userId} userId={userId}
/> />
</DialogContent> </AlertDialogContent>
</Dialog> </AlertDialog>
); );
}; };
@@ -70,7 +72,7 @@ function RemoveMemberForm({
return ( return (
<form action={onMemberRemoved}> <form action={onMemberRemoved}>
<div className={'flex flex-col space-y-6'}> <div className={'flex flex-col space-y-6'}>
<p className={'text-sm'}> <p className={'text-muted-foreground text-sm'}>
<Trans i18nKey={'common:modalConfirmationQuestion'} /> <Trans i18nKey={'common:modalConfirmationQuestion'} />
</p> </p>
@@ -78,14 +80,20 @@ function RemoveMemberForm({
<RemoveMemberErrorAlert /> <RemoveMemberErrorAlert />
</If> </If>
<Button <AlertDialogFooter>
data-test={'confirm-remove-member'} <AlertDialogCancel>
variant={'destructive'} <Trans i18nKey={'common:cancel'} />
disabled={isSubmitting} </AlertDialogCancel>
onClick={onMemberRemoved}
> <Button
<Trans i18nKey={'teams:removeMemberSubmitLabel'} /> data-test={'confirm-remove-member'}
</Button> variant={'destructive'}
disabled={isSubmitting}
onClick={onMemberRemoved}
>
<Trans i18nKey={'teams:removeMemberSubmitLabel'} />
</Button>
</AlertDialogFooter>
</div> </div>
</form> </form>
); );

View File

@@ -46,7 +46,7 @@ export const TransferOwnershipDialog: React.FC<{
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
Transfer ownership of the team to another member. <Trans i18nKey="team:transferOwnershipDescription" />
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -127,7 +127,7 @@ function TransferOrganizationOwnershipForm({
return ( return (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
Please type TRANSFER to confirm the transfer of ownership. <Trans i18nKey={'teams:transferOwnershipInputLabel'} />
</FormLabel> </FormLabel>
<FormControl> <FormControl>
@@ -135,8 +135,7 @@ function TransferOrganizationOwnershipForm({
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Please make sure you understand the implications of this <Trans i18nKey={'teams:transferOwnershipInputDescription'} />
action.
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />

View File

@@ -2,6 +2,7 @@ import { useState, useTransition } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Database } from '@kit/supabase/database'; import { Database } from '@kit/supabase/database';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
@@ -75,6 +76,7 @@ function UpdateMemberForm({
}>) { }>) {
const [pending, startTransition] = useTransition(); const [pending, startTransition] = useTransition();
const [error, setError] = useState<boolean>(); const [error, setError] = useState<boolean>();
const { t } = useTranslation('teams');
const onSubmit = ({ role }: { role: Role }) => { const onSubmit = ({ role }: { role: Role }) => {
startTransition(async () => { startTransition(async () => {
@@ -95,7 +97,7 @@ function UpdateMemberForm({
return data.role !== userRole; return data.role !== userRole;
}, },
{ {
message: 'Role must be different from the current role.', message: t(`roleMustBeDifferent`),
path: ['role'], path: ['role'],
}, },
), ),
@@ -122,7 +124,8 @@ function UpdateMemberForm({
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem> <FormItem>
<FormLabel>New Role</FormLabel> <FormLabel>{t('memberRole')}</FormLabel>
<FormControl> <FormControl>
<MembershipRoleSelector <MembershipRoleSelector
currentUserRole={userRole} currentUserRole={userRole}
@@ -131,7 +134,7 @@ function UpdateMemberForm({
/> />
</FormControl> </FormControl>
<FormDescription>Pick a role for this member.</FormDescription> <FormDescription>{t('updateRoleDescription')}</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>