feat(i18n): add language switcher in top-right corner for multi-language support #9

This commit is contained in:
rustmailer
2025-11-24 21:51:16 +08:00
parent 5ebc7394d0
commit ea81d0298b
119 changed files with 15277 additions and 1662 deletions
@@ -33,6 +33,7 @@ import { useQuery } from '@tanstack/react-query'
import { Card, CardContent } from '@/components/ui/card'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { TableSkeleton } from '@/components/table-skeleton'
import { useTranslation } from 'react-i18next'
import { FileIcon } from 'lucide-react'
import { format, formatDistanceToNow } from 'date-fns'
import LongText from '@/components/long-text'
@@ -49,6 +50,7 @@ interface Props {
}
export function OAuth2TokensDialog({ currentRow, open, onOpenChange }: Props) {
const { t } = useTranslation()
const navigate = useNavigate()
const { data: oauth2Tokens, isLoading } = useQuery({
queryKey: ['oauth2-tokens', currentRow.id],
@@ -65,21 +67,21 @@ export function OAuth2TokensDialog({ currentRow, open, onOpenChange }: Props) {
await navigator.clipboard.writeText(token);
if (access) {
toast({
title: "Success",
description: "Access token copied to clipboard",
title: t('common.ok'),
description: t('accounts.accessTokenCopiedToClipboard'),
});
} else {
toast({
title: "Success",
description: "Refresh token copied to clipboard",
title: t('common.ok'),
description: t('accounts.refreshTokenCopiedToClipboard'),
});
}
} catch (err) {
toast({
variant: "destructive",
title: "Failed to copy text",
title: t('settings.failedToCopyText'),
description: (err as Error).message,
action: <ToastAction altText="Try again">Try again</ToastAction>,
action: <ToastAction altText={t('common.tryAgain')}>{t('common.tryAgain')}</ToastAction>,
});
}
}, []);
@@ -93,9 +95,9 @@ export function OAuth2TokensDialog({ currentRow, open, onOpenChange }: Props) {
>
<DialogContent className='sm:max-w-3xl'>
<DialogHeader className='text-left'>
<DialogTitle>OAuth2 Tokens</DialogTitle>
<DialogTitle>{t('accounts.oauth2Tokens')}</DialogTitle>
<DialogDescription>
Details of the OAuth2 tokens for the account.
{t('accounts.detailsOfTheOAuth2TokensForTheAccount')}
</DialogDescription>
</DialogHeader>
<Card>
@@ -106,20 +108,20 @@ export function OAuth2TokensDialog({ currentRow, open, onOpenChange }: Props) {
<Table className='w-full'>
<TableHeader>
<TableRow>
<TableHead>Field</TableHead>
<TableHead>Value</TableHead>
<TableHead>Action</TableHead>
<TableHead>{t('accounts.field')}</TableHead>
<TableHead>{t('accounts.value')}</TableHead>
<TableHead>{t('common.actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className='max-w-80'>OAuth2 Name</TableCell>
<TableCell className='max-w-80'>{t('oauth2.id')}</TableCell>
<TableCell>
<LongText className='max-w-[240px] sm:max-w-[430px]'>{oauth2Tokens.oauth2_name}</LongText>
<LongText className='max-w-[240px] sm:max-w-[430px]'>{oauth2Tokens.oauth2_id}</LongText>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='max-w-80'>Access Token</TableCell>
<TableCell className='max-w-80'>{t('accessTokens.token')}</TableCell>
<TableCell>
<LongText className='max-w-[240px] sm:max-w-[430px]'>{oauth2Tokens.access_token}</LongText>
</TableCell>
@@ -130,7 +132,7 @@ export function OAuth2TokensDialog({ currentRow, open, onOpenChange }: Props) {
</TableCell>
</TableRow>
<TableRow>
<TableCell className='max-w-80'>Refresh Token</TableCell>
<TableCell className='max-w-80'>{t('accounts.refreshToken')}</TableCell>
<TableCell>
<LongText className='max-w-[240px] sm:max-w-[430px]'>{oauth2Tokens.refresh_token}</LongText>
</TableCell>
@@ -141,13 +143,13 @@ export function OAuth2TokensDialog({ currentRow, open, onOpenChange }: Props) {
</TableCell>
</TableRow>
<TableRow>
<TableCell className='max-w-80'>Created At</TableCell>
<TableCell className='max-w-80'>{t('settings.createdAt')}</TableCell>
<TableCell>
{format(new Date(oauth2Tokens.created_at), 'yyyy-MM-dd HH:mm:ss')}
</TableCell>
</TableRow>
<TableRow>
<TableCell className='max-w-80'>Updated At</TableCell>
<TableCell className='max-w-80'>{t('settings.updatedAt')}</TableCell>
<TableCell>
{formatDistanceToNow(new Date(oauth2Tokens.updated_at), { addSuffix: true })}
</TableCell>
@@ -158,10 +160,11 @@ export function OAuth2TokensDialog({ currentRow, open, onOpenChange }: Props) {
<div className="flex h-[250px] mt-4 shrink-0 items-center justify-center rounded-md border border-dashed">
<div className="mx-auto flex max-w-[420px] flex-col items-center justify-center text-center">
<FileIcon className="h-10 w-10 text-muted-foreground" />
<h3 className="mt-4 text-lg font-semibold">No OAuth2 Tokens</h3>
<h3 className="mt-4 text-lg font-semibold">{t('accounts.noOAuth2Tokens')}</h3>
<p className="mb-4 mt-2 text-sm text-muted-foreground">
The account has not completed the authorization process. Please
<a onClick={() => navigate({ to: '/oauth2' })} className="ml-1 text-blue-500 underline cursor-pointer">click here</a> to authorize the account.
{t('accounts.theAccountHasNotCompletedTheAuthorizationProcess')}
<a onClick={() => navigate({ to: '/oauth2' })} className="ml-1 text-blue-500 underline cursor-pointer">{t('accounts.clickHere')}</a>
{t('accounts.toAuthorizeTheAccount')}
</p>
</div>
</div>
@@ -170,7 +173,7 @@ export function OAuth2TokensDialog({ currentRow, open, onOpenChange }: Props) {
</Card>
<DialogFooter>
<DialogClose asChild>
<Button variant='outline' className="px-2 py-1 text-sm h-auto">Close</Button>
<Button variant='outline' className="px-2 py-1 text-sm h-auto">{t('common.close')}</Button>
</DialogClose>
</DialogFooter>
</DialogContent>