fix: remaining localization gaps (#156)
Co-authored-by: Lê Trung Hiếu <[email protected]>
This commit is contained in:
1 parent
440a934280
commit
7093c982ba
13 files changed
+838
-208
No files matched your search
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user