fix: remaining localization gaps (#156)

Co-authored-by: Lê Trung Hiếu <[email protected]>
This commit is contained in:
Lê Trung HiếuandLê Trung Hiếu authored and GitHub committed 2026-04-14 12:59:57 +08:00
1 parent 440a934280
commit 7093c982ba
13 files changed
+838 -208

No files matched your search

+3 -2
View File
@@ -12,7 +12,7 @@ import {
} from '@/components/ui/dropdown-menu';
import { MoreVertical, Trash2, RefreshCw } from 'lucide-react';
import { formatDistanceToNow, type Locale } from 'date-fns';
import { enUS, zhCN, ru } from 'date-fns/locale';
import { enUS, zhCN, ru, vi } from 'date-fns/locale';
import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
@@ -22,6 +22,7 @@ const DATE_LOCALE_MAP: Record<string, Locale> = {
en: enUS,
'zh-CN': zhCN,
ru: ru,
vi: vi,
};
interface AccountCardProps {
@@ -162,7 +163,7 @@ export const AccountCard: React.FC<AccountCardProps> = ({
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="h-4 w-4" />
<span className="sr-only">Open menu</span>
<span className="sr-only">{t('common.openMenu')}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
+20 -12
View File
@@ -316,14 +316,16 @@ export function CloudAccountList() {
if (isNumber(credits)) {
toast({
title: t('cloud.toast.quotaRefreshed'),
description: `AI credits: $${credits.toFixed(2)}`,
description: t('cloud.toast.refreshCreditsAvailable', {
amount: credits.toFixed(2),
}),
});
return;
}
toast({
title: t('cloud.toast.quotaRefreshed'),
description: 'AI credits not available for this refresh.',
description: t('cloud.toast.refreshCreditsUnavailable'),
});
},
onError: () => toast({ title: t('cloud.toast.refreshFailed'), variant: 'destructive' }),
@@ -483,7 +485,7 @@ export function CloudAccountList() {
if (file.size > 5 * 1024 * 1024) {
toast({
title: t('cloud.error.loadFailed'),
description: 'File size exceeds 5MB limit',
description: t('cloud.exportImport.fileTooLarge'),
variant: 'destructive',
});
return;
@@ -496,10 +498,10 @@ export function CloudAccountList() {
const content = event.target?.result as string;
JSON.parse(content);
setImportFileContent(content);
} catch (err) {
} catch {
toast({
title: t('cloud.error.loadFailed'),
description: 'Invalid JSON file format',
description: t('cloud.exportImport.invalidJson'),
variant: 'destructive',
});
setImportFileName('');
@@ -509,7 +511,7 @@ export function CloudAccountList() {
reader.onerror = () => {
toast({
title: t('cloud.error.loadFailed'),
description: 'Failed to read file',
description: t('cloud.exportImport.readFileFailed'),
variant: 'destructive',
});
};
@@ -588,12 +590,15 @@ export function CloudAccountList() {
if (failed === 0) {
toast({
title: t('cloud.toast.quotaRefreshed'),
description: `Successfully refreshed ${successful} accounts.`,
description: t('cloud.toast.batchRefreshSuccess', { count: successful }),
});
} else {
toast({
title: `Refresh completed with issues`,
description: `Refreshed ${successful} accounts, ${failed} failed.`,
title: t('cloud.toast.batchRefreshPartial.title'),
description: t('cloud.toast.batchRefreshPartial.description', {
successful,
failed,
}),
variant: 'destructive',
});
}
@@ -614,12 +619,15 @@ export function CloudAccountList() {
if (failed === 0) {
toast({
title: t('cloud.toast.deleted'),
description: `Successfully deleted ${successful} accounts.`,
description: t('cloud.toast.batchDeleteSuccess', { count: successful }),
});
} else {
toast({
title: `Deletion completed with issues`,
description: `Deleted ${successful} accounts, ${failed} failed.`,
title: t('cloud.toast.batchDeletePartial.title'),
description: t('cloud.toast.batchDeletePartial.description', {
successful,
failed,
}),
variant: 'destructive',
});
}
+45 -41
View File
@@ -1,4 +1,4 @@
import { type JSX, useEffect, useMemo, useRef, useState } from 'react';
import { type JSX, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Fingerprint,
@@ -48,11 +48,14 @@ function formatError(error: unknown): string {
return String(error);
}
function renderProfile(profile?: DeviceProfile): JSX.Element {
function renderProfile(
profile: DeviceProfile | undefined,
t: ReturnType<typeof useTranslation>['t'],
): JSX.Element {
if (!profile) {
return (
<div className="text-muted-foreground rounded-lg border border-dashed px-3 py-4 text-xs">
N/A
{t('common.notAvailable')}
</div>
);
}
@@ -95,41 +98,44 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
const history = useMemo(() => sortHistory(snapshot?.history || []), [snapshot?.history]);
const showLoadingPlaceholder = initialLoading && !snapshot;
const refreshProfiles = async (options?: { silent?: boolean }) => {
if (!account) {
setSnapshot(null);
return;
}
const silent = options?.silent === true;
if (silent) {
setRefreshing(true);
} else {
setInitialLoading(true);
}
try {
const result = await getCloudIdentityProfiles({ accountId: account.id });
setSnapshot(result);
} catch (error) {
toast({
title: t('cloud.toast.actionFailed', { defaultValue: 'Action failed' }),
description: formatError(error),
variant: 'destructive',
});
} finally {
if (silent) {
setRefreshing(false);
} else {
setInitialLoading(false);
const refreshProfiles = useCallback(
async (options?: { silent?: boolean }) => {
if (!account) {
setSnapshot(null);
return;
}
}
};
const silent = options?.silent === true;
if (silent) {
setRefreshing(true);
} else {
setInitialLoading(true);
}
try {
const result = await getCloudIdentityProfiles({ accountId: account.id });
setSnapshot(result);
} catch (error) {
toast({
title: t('cloud.toast.actionFailed'),
description: formatError(error),
variant: 'destructive',
});
} finally {
if (silent) {
setRefreshing(false);
} else {
setInitialLoading(false);
}
}
},
[account, t, toast],
);
useEffect(() => {
if (!open) {
return;
}
refreshProfiles();
}, [open, account?.id]);
void refreshProfiles();
}, [open, refreshProfiles]);
const runAction = async (key: string, action: () => Promise<void>) => {
if (actionLockRef.current) {
@@ -142,7 +148,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
await refreshProfiles({ silent: true });
} catch (error) {
toast({
title: t('cloud.toast.actionFailed', { defaultValue: 'Action failed' }),
title: t('cloud.toast.actionFailed'),
description: formatError(error),
variant: 'destructive',
});
@@ -247,7 +253,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
<CardTitle className="text-sm">{t('cloud.identity.previewTitle')}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{renderProfile(previewProfile)}
{renderProfile(previewProfile, t)}
<div className="flex flex-wrap gap-2">
<Button
size="sm"
@@ -295,7 +301,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
{t('cloud.identity.loading')}
</div>
) : (
renderProfile(snapshot?.currentStorage)
renderProfile(snapshot?.currentStorage, t)
)}
</CardContent>
</Card>
@@ -313,7 +319,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
{t('cloud.identity.loading')}
</div>
) : (
renderProfile(snapshot?.boundProfile)
renderProfile(snapshot?.boundProfile, t)
)}
</CardContent>
</Card>
@@ -353,7 +359,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
) : null}
</div>
{renderProfile(version.profile)}
{renderProfile(version.profile, t)}
<div className="mt-3 flex flex-wrap gap-2">
<Button
@@ -402,15 +408,13 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
<Card className="shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="text-sm">
{t('cloud.identity.baseline', { defaultValue: 'Reference Profile' })}
</CardTitle>
<CardTitle className="text-sm">{t('cloud.identity.baseline')}</CardTitle>
</CardHeader>
<CardContent>
{showLoadingPlaceholder ? (
<div className="text-muted-foreground text-xs">{t('cloud.identity.loading')}</div>
) : (
renderProfile(snapshot?.baseline)
renderProfile(snapshot?.baseline, t)
)}
</CardContent>
</Card>
+3 -1
View File
@@ -130,7 +130,9 @@ export function ModelVisibilitySettings() {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<span>{t('settings.modelVisibility.title')}</span>
<Badge variant="secondary">{filteredModelIds.length} models</Badge>
<Badge variant="secondary">
{t('settings.providerGroupings.models', { count: filteredModelIds.length })}
</Badge>
</CardTitle>
<CardDescription>{t('settings.modelVisibility.description')}</CardDescription>
</CardHeader>