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
@@ -46,6 +46,7 @@ import { ToastAction } from '@/components/ui/toast'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { Loader2 } from 'lucide-react'
import { add_proxy, update_proxy } from '@/api/system/api'
import { useTranslation } from 'react-i18next'
const proxyFormSchema = z.object({
@@ -127,6 +128,7 @@ const mapCurrentRowToFormValues = (currentRow: Proxy) => {
};
export function ProxyActionDialog({ currentRow, open, onOpenChange }: Props) {
const { t } = useTranslation()
const isEdit = !!currentRow
const queryClient = useQueryClient();
const form = useForm<ProxyForm>({
@@ -151,9 +153,9 @@ export function ProxyActionDialog({ currentRow, open, onOpenChange }: Props) {
function handleSuccess() {
toast({
title: `Proxy ${isEdit ? 'Updated' : 'Added'}`,
description: `Your Proxy has been successfully ${isEdit ? 'updated' : 'added'}.`,
action: <ToastAction altText="Close">Close</ToastAction>,
title: `${t('settings.proxy')} ${isEdit ? t('common.update') : t('common.add')}`,
description: t('settings.yourProxyHasBeenSuccessfully', { action: isEdit ? t('common.update').toLowerCase() : t('common.add').toLowerCase() }),
action: <ToastAction altText={t('common.close')}>{t('common.close')}</ToastAction>,
});
queryClient.invalidateQueries({ queryKey: ['proxy-list'] });
@@ -164,13 +166,13 @@ export function ProxyActionDialog({ currentRow, open, onOpenChange }: Props) {
function handleError(error: AxiosError) {
const errorMessage = (error.response?.data as { message?: string })?.message ||
error.message ||
`${isEdit ? 'Update' : 'Add'} failed, please try again later`;
t('settings.proxyUpdateOrAddFailed', { action: isEdit ? t('common.update') : t('common.add') });
toast({
variant: "destructive",
title: `Proxy ${isEdit ? 'Update' : 'Add'} Failed`,
title: `${t('settings.proxy')} ${isEdit ? t('common.update') : t('common.add')} ${t('common.error')}`,
description: errorMessage as string,
action: <ToastAction altText="Try again">Try again</ToastAction>,
action: <ToastAction altText={t('common.tryAgain')}>{t('common.tryAgain')}</ToastAction>,
});
console.error(error);
}
@@ -195,10 +197,10 @@ export function ProxyActionDialog({ currentRow, open, onOpenChange }: Props) {
>
<DialogContent className='max-w-xl'>
<DialogHeader className='text-left mb-4'>
<DialogTitle>{isEdit ? 'Edit Proxy' : 'Add New Proxy'}</DialogTitle>
<DialogTitle>{isEdit ? t('settings.edit') + ' ' + t('settings.proxy') : t('common.add') + ' ' + t('settings.proxy')}</DialogTitle>
<DialogDescription>
{isEdit ? 'Update the Proxy here. ' : 'Add new Proxy here. '}
Click save when you&apos;re done.
{isEdit ? t('settings.updateTheProxyHere') : t('settings.addNewProxyHere')}
{t('accounts.clickSaveWhenDone')}
</DialogDescription>
</DialogHeader>
<Form {...form}>
@@ -212,7 +214,7 @@ export function ProxyActionDialog({ currentRow, open, onOpenChange }: Props) {
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>Proxy URL</FormLabel>
<FormLabel>{t('settings.proxy')} URL</FormLabel>
<FormControl>
<Input
placeholder="socks5://127.0.0.1:22308"
@@ -221,7 +223,7 @@ export function ProxyActionDialog({ currentRow, open, onOpenChange }: Props) {
</FormControl>
<FormMessage />
<FormDescription>
Please use an IP address (e.g., 127.0.0.1) rather than a hostname or domain for better reliability.
{t('settings.pleaseUseAnIpAddressForBetterReliability')}
</FormDescription>
</FormItem>
)}
@@ -242,11 +244,11 @@ export function ProxyActionDialog({ currentRow, open, onOpenChange }: Props) {
<span>
{isEdit
? updateMutation.isPending
? "Saving..."
: "Save changes"
? t('oauth2.saving')
: t('accounts.saveChanges')
: createMutation.isPending
? "Adding..."
: "Add"}
? t('settings.adding')
: t('common.add')}
</span>
</span>
</Button>
@@ -25,11 +25,11 @@ import { DataTableColumnHeader } from './data-table-column-header'
import { DataTableRowActions } from './data-table-row-actions'
import { format } from 'date-fns'
export const columns: ColumnDef<Proxy>[] = [
export const getColumns = (t: (key: string) => string): ColumnDef<Proxy>[] => [
{
accessorKey: 'id',
header: ({ column }) => (
<DataTableColumnHeader column={column} title='Id' />
<DataTableColumnHeader column={column} title={t('settings.id')} />
),
cell: ({ row }) => (
<LongText className='max-w-72'>{`${row.original.id}`}</LongText>
@@ -41,7 +41,7 @@ export const columns: ColumnDef<Proxy>[] = [
{
accessorKey: "url",
header: ({ column }) => (
<DataTableColumnHeader column={column} title='Url' />
<DataTableColumnHeader column={column} title={t('settings.url')} />
),
cell: ({ row }) => {
return <LongText>{row.original.url}</LongText>
@@ -51,7 +51,7 @@ export const columns: ColumnDef<Proxy>[] = [
{
accessorKey: 'created_at',
header: ({ column }) => (
<DataTableColumnHeader column={column} title='Created At' />
<DataTableColumnHeader column={column} title={t('settings.createdAt')} />
),
cell: ({ row }) => {
const created_at = row.original.created_at;
@@ -63,7 +63,7 @@ export const columns: ColumnDef<Proxy>[] = [
{
accessorKey: 'updated_at',
header: ({ column }) => (
<DataTableColumnHeader column={column} title='Updated At' />
<DataTableColumnHeader column={column} title={t('settings.updatedAt')} />
),
cell: ({ row }) => {
const updated_at = row.original.updated_at;
@@ -21,7 +21,6 @@ import {
ArrowDownIcon,
ArrowUpIcon,
CaretSortIcon,
EyeNoneIcon,
} from '@radix-ui/react-icons'
import { Column } from '@tanstack/react-table'
import { cn } from '@/lib/utils'
@@ -30,9 +29,9 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useTranslation } from 'react-i18next'
interface DataTableColumnHeaderProps<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
@@ -48,7 +47,7 @@ export function DataTableColumnHeader<TData, TValue>({
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>
}
const { t } = useTranslation()
return (
<div className={cn('flex items-center space-x-2', className)}>
<DropdownMenu>
@@ -71,16 +70,11 @@ export function DataTableColumnHeader<TData, TValue>({
<DropdownMenuContent align='start'>
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUpIcon className='mr-2 h-3.5 w-3.5 text-muted-foreground/70' />
Asc
{t('table.asc')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDownIcon className='mr-2 h-3.5 w-3.5 text-muted-foreground/70' />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeNoneIcon className='mr-2 h-3.5 w-3.5 text-muted-foreground/70' />
Hide
{t('table.desc')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -31,6 +31,7 @@ import {
} from '@/components/ui/dropdown-menu'
import { useProxyContext } from '../context'
import { Proxy } from '../data/schema'
import { useTranslation } from 'react-i18next'
interface DataTableRowActionsProps {
row: Row<Proxy>
@@ -38,6 +39,7 @@ interface DataTableRowActionsProps {
export function DataTableRowActions({ row }: DataTableRowActionsProps) {
const { setOpen, setCurrentRow } = useProxyContext()
const { t } = useTranslation()
return (
<>
<DropdownMenu modal={false}>
@@ -57,7 +59,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
setOpen('edit')
}}
>
Edit
{t('table.edit')}
<DropdownMenuShortcut>
<IconEdit size={16} />
</DropdownMenuShortcut>
@@ -70,7 +72,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
}}
className='!text-red-500'
>
Delete
{t('table.delete')}
<DropdownMenuShortcut>
<IconTrash size={16} />
</DropdownMenuShortcut>
@@ -19,6 +19,7 @@
import { Table } from '@tanstack/react-table'
import { Input } from '@/components/ui/input'
import { useTranslation } from 'react-i18next'
interface DataTableToolbarProps<TData> {
table: Table<TData>
@@ -27,11 +28,12 @@ interface DataTableToolbarProps<TData> {
export function DataTableToolbar<TData>({
table,
}: DataTableToolbarProps<TData>) {
const { t } = useTranslation()
return (
<div className='flex items-center justify-between'>
<div className='flex flex-1 flex-col-reverse items-start gap-y-2 sm:flex-row sm:items-center sm:space-x-2'>
<Input
placeholder='Filter Proxy...'
placeholder={t('settings.filterProxy')}
value={(table.getState().globalFilter as string) ?? ''}
onChange={(event) => {
table.setGlobalFilter(event.target.value);
@@ -29,6 +29,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
import { ToastAction } from '@/components/ui/toast'
import { AxiosError } from 'axios'
import { delete_proxy } from '@/api/system/api'
import { useTranslation } from 'react-i18next'
interface Props {
open: boolean
@@ -37,15 +38,16 @@ interface Props {
}
export function ProxyDeleteDialog({ open, onOpenChange, currentRow }: Props) {
const { t } = useTranslation()
const [value, setValue] = useState(0)
const queryClient = useQueryClient();
function handleSuccess() {
toast({
title: 'Delete Success',
description: `Proxy has been successfully deleted.`,
action: <ToastAction altText="Close">Close</ToastAction>,
title: t('proxyDelete.successTitle'),
description: t('proxyDelete.successDesc'),
action: <ToastAction altText={t('proxyDelete.close')}>{t('proxyDelete.close')}</ToastAction>,
});
queryClient.invalidateQueries({ queryKey: ['proxy-list'] });
onOpenChange(false);
}
@@ -53,13 +55,13 @@ export function ProxyDeleteDialog({ open, onOpenChange, currentRow }: Props) {
function handleError(error: AxiosError) {
const errorMessage = error.response?.data ||
error.message ||
`Delete failed, please try again later`;
t('proxyDelete.failedDesc');
toast({
variant: "destructive",
title: `Proxy delete Failed`,
title: t('proxyDelete.failedTitle'),
description: errorMessage as string,
action: <ToastAction altText="Try again">Try again</ToastAction>,
action: <ToastAction altText={t('proxyDelete.tryAgain')}>{t('proxyDelete.tryAgain')}</ToastAction>,
});
console.error(error);
}
@@ -88,39 +90,39 @@ export function ProxyDeleteDialog({ open, onOpenChange, currentRow }: Props) {
className='mr-1 inline-block stroke-destructive'
size={18}
/>{' '}
Delete Proxy
{t('proxyDelete.title')}
</span>
}
desc={
<div className='space-y-4'>
<p className='mb-2'>
Are you sure you want to delete{' '}
<span className='font-bold'>{`${currentRow.id}`}</span>?
{t('proxyDelete.confirmText')} <span className='font-bold'>{`${currentRow.id}`}</span>?
<br />
This action will permanently remove the Proxy from the system. This cannot be undone.
{t('proxyDelete.permanent')}
</p>
<Label className='my-2'>
Proxy Id:
{t('proxyDelete.proxyIdLabel')}
<Input
type="number"
value={`${value}`}
onChange={(e) => setValue(parseInt(e.target.value, 10))}
placeholder='Enter Proxy Id to confirm deletion.'
placeholder={t('proxyDelete.proxyIdPlaceholder')}
className="mt-2"
/>
</Label>
<Alert variant='destructive'>
<AlertTitle>Warning!</AlertTitle>
<AlertTitle>{t('proxyDelete.warningTitle', 'Warning!')}</AlertTitle>
<AlertDescription>
Please be carefull, this operation can not be rolled back.
{t('proxyDelete.warningDesc')}
</AlertDescription>
</Alert>
</div>
}
confirmText='Delete'
confirmText={t('proxyDelete.deleteBtn')}
destructive
/>
)
}
@@ -44,6 +44,7 @@ import {
import { Proxy } from '../data/schema'
import { DataTablePagination } from './data-table-pagination'
import { DataTableToolbar } from './data-table-toolbar'
import { useTranslation } from 'react-i18next'
declare module '@tanstack/react-table' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -59,6 +60,7 @@ interface DataTableProps {
export function ProxyTable({ columns, data }: DataTableProps) {
const { t } = useTranslation()
const [rowSelection, setRowSelection] = useState({})
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
@@ -140,7 +142,7 @@ export function ProxyTable({ columns, data }: DataTableProps) {
colSpan={columns.length}
className='h-24 text-center'
>
No results.
{t('common.table.noResults')}
</TableCell>
</TableRow>
)}