Skip to content

Commit 7093c98

Browse files
authored
fix: remaining localization gaps (#156)
Co-authored-by: Lê Trung Hiếu <hieuck@users.noreply.github.com>
1 parent 440a934 commit 7093c98

13 files changed

Lines changed: 836 additions & 206 deletions

File tree

src/components/AccountCard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from '@/components/ui/dropdown-menu';
1313
import { MoreVertical, Trash2, RefreshCw } from 'lucide-react';
1414
import { formatDistanceToNow, type Locale } from 'date-fns';
15-
import { enUS, zhCN, ru } from 'date-fns/locale';
15+
import { enUS, zhCN, ru, vi } from 'date-fns/locale';
1616
import { cn } from '@/lib/utils';
1717
import { useTranslation } from 'react-i18next';
1818

@@ -22,6 +22,7 @@ const DATE_LOCALE_MAP: Record<string, Locale> = {
2222
en: enUS,
2323
'zh-CN': zhCN,
2424
ru: ru,
25+
vi: vi,
2526
};
2627

2728
interface AccountCardProps {
@@ -162,7 +163,7 @@ export const AccountCard: React.FC<AccountCardProps> = ({
162163
onClick={(e) => e.stopPropagation()}
163164
>
164165
<MoreVertical className="h-4 w-4" />
165-
<span className="sr-only">Open menu</span>
166+
<span className="sr-only">{t('common.openMenu')}</span>
166167
</Button>
167168
</DropdownMenuTrigger>
168169
<DropdownMenuContent align="end">

src/components/CloudAccountList.tsx

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -316,14 +316,16 @@ export function CloudAccountList() {
316316
if (isNumber(credits)) {
317317
toast({
318318
title: t('cloud.toast.quotaRefreshed'),
319-
description: `AI credits: $${credits.toFixed(2)}`,
319+
description: t('cloud.toast.refreshCreditsAvailable', {
320+
amount: credits.toFixed(2),
321+
}),
320322
});
321323
return;
322324
}
323325

324326
toast({
325327
title: t('cloud.toast.quotaRefreshed'),
326-
description: 'AI credits not available for this refresh.',
328+
description: t('cloud.toast.refreshCreditsUnavailable'),
327329
});
328330
},
329331
onError: () => toast({ title: t('cloud.toast.refreshFailed'), variant: 'destructive' }),
@@ -483,7 +485,7 @@ export function CloudAccountList() {
483485
if (file.size > 5 * 1024 * 1024) {
484486
toast({
485487
title: t('cloud.error.loadFailed'),
486-
description: 'File size exceeds 5MB limit',
488+
description: t('cloud.exportImport.fileTooLarge'),
487489
variant: 'destructive',
488490
});
489491
return;
@@ -496,10 +498,10 @@ export function CloudAccountList() {
496498
const content = event.target?.result as string;
497499
JSON.parse(content);
498500
setImportFileContent(content);
499-
} catch (err) {
501+
} catch {
500502
toast({
501503
title: t('cloud.error.loadFailed'),
502-
description: 'Invalid JSON file format',
504+
description: t('cloud.exportImport.invalidJson'),
503505
variant: 'destructive',
504506
});
505507
setImportFileName('');
@@ -509,7 +511,7 @@ export function CloudAccountList() {
509511
reader.onerror = () => {
510512
toast({
511513
title: t('cloud.error.loadFailed'),
512-
description: 'Failed to read file',
514+
description: t('cloud.exportImport.readFileFailed'),
513515
variant: 'destructive',
514516
});
515517
};
@@ -588,12 +590,15 @@ export function CloudAccountList() {
588590
if (failed === 0) {
589591
toast({
590592
title: t('cloud.toast.quotaRefreshed'),
591-
description: `Successfully refreshed ${successful} accounts.`,
593+
description: t('cloud.toast.batchRefreshSuccess', { count: successful }),
592594
});
593595
} else {
594596
toast({
595-
title: `Refresh completed with issues`,
596-
description: `Refreshed ${successful} accounts, ${failed} failed.`,
597+
title: t('cloud.toast.batchRefreshPartial.title'),
598+
description: t('cloud.toast.batchRefreshPartial.description', {
599+
successful,
600+
failed,
601+
}),
597602
variant: 'destructive',
598603
});
599604
}
@@ -614,12 +619,15 @@ export function CloudAccountList() {
614619
if (failed === 0) {
615620
toast({
616621
title: t('cloud.toast.deleted'),
617-
description: `Successfully deleted ${successful} accounts.`,
622+
description: t('cloud.toast.batchDeleteSuccess', { count: successful }),
618623
});
619624
} else {
620625
toast({
621-
title: `Deletion completed with issues`,
622-
description: `Deleted ${successful} accounts, ${failed} failed.`,
626+
title: t('cloud.toast.batchDeletePartial.title'),
627+
description: t('cloud.toast.batchDeletePartial.description', {
628+
successful,
629+
failed,
630+
}),
623631
variant: 'destructive',
624632
});
625633
}

src/components/IdentityProfileDialog.tsx

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type JSX, useEffect, useMemo, useRef, useState } from 'react';
1+
import { type JSX, useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import {
44
Fingerprint,
@@ -48,11 +48,14 @@ function formatError(error: unknown): string {
4848
return String(error);
4949
}
5050

51-
function renderProfile(profile?: DeviceProfile): JSX.Element {
51+
function renderProfile(
52+
profile: DeviceProfile | undefined,
53+
t: ReturnType<typeof useTranslation>['t'],
54+
): JSX.Element {
5255
if (!profile) {
5356
return (
5457
<div className="text-muted-foreground rounded-lg border border-dashed px-3 py-4 text-xs">
55-
N/A
58+
{t('common.notAvailable')}
5659
</div>
5760
);
5861
}
@@ -95,41 +98,44 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
9598
const history = useMemo(() => sortHistory(snapshot?.history || []), [snapshot?.history]);
9699
const showLoadingPlaceholder = initialLoading && !snapshot;
97100

98-
const refreshProfiles = async (options?: { silent?: boolean }) => {
99-
if (!account) {
100-
setSnapshot(null);
101-
return;
102-
}
103-
const silent = options?.silent === true;
104-
if (silent) {
105-
setRefreshing(true);
106-
} else {
107-
setInitialLoading(true);
108-
}
109-
try {
110-
const result = await getCloudIdentityProfiles({ accountId: account.id });
111-
setSnapshot(result);
112-
} catch (error) {
113-
toast({
114-
title: t('cloud.toast.actionFailed', { defaultValue: 'Action failed' }),
115-
description: formatError(error),
116-
variant: 'destructive',
117-
});
118-
} finally {
101+
const refreshProfiles = useCallback(
102+
async (options?: { silent?: boolean }) => {
103+
if (!account) {
104+
setSnapshot(null);
105+
return;
106+
}
107+
const silent = options?.silent === true;
119108
if (silent) {
120-
setRefreshing(false);
109+
setRefreshing(true);
121110
} else {
122-
setInitialLoading(false);
111+
setInitialLoading(true);
123112
}
124-
}
125-
};
113+
try {
114+
const result = await getCloudIdentityProfiles({ accountId: account.id });
115+
setSnapshot(result);
116+
} catch (error) {
117+
toast({
118+
title: t('cloud.toast.actionFailed'),
119+
description: formatError(error),
120+
variant: 'destructive',
121+
});
122+
} finally {
123+
if (silent) {
124+
setRefreshing(false);
125+
} else {
126+
setInitialLoading(false);
127+
}
128+
}
129+
},
130+
[account, t, toast],
131+
);
126132

127133
useEffect(() => {
128134
if (!open) {
129135
return;
130136
}
131-
refreshProfiles();
132-
}, [open, account?.id]);
137+
void refreshProfiles();
138+
}, [open, refreshProfiles]);
133139

134140
const runAction = async (key: string, action: () => Promise<void>) => {
135141
if (actionLockRef.current) {
@@ -142,7 +148,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
142148
await refreshProfiles({ silent: true });
143149
} catch (error) {
144150
toast({
145-
title: t('cloud.toast.actionFailed', { defaultValue: 'Action failed' }),
151+
title: t('cloud.toast.actionFailed'),
146152
description: formatError(error),
147153
variant: 'destructive',
148154
});
@@ -247,7 +253,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
247253
<CardTitle className="text-sm">{t('cloud.identity.previewTitle')}</CardTitle>
248254
</CardHeader>
249255
<CardContent className="space-y-4">
250-
{renderProfile(previewProfile)}
256+
{renderProfile(previewProfile, t)}
251257
<div className="flex flex-wrap gap-2">
252258
<Button
253259
size="sm"
@@ -295,7 +301,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
295301
{t('cloud.identity.loading')}
296302
</div>
297303
) : (
298-
renderProfile(snapshot?.currentStorage)
304+
renderProfile(snapshot?.currentStorage, t)
299305
)}
300306
</CardContent>
301307
</Card>
@@ -313,7 +319,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
313319
{t('cloud.identity.loading')}
314320
</div>
315321
) : (
316-
renderProfile(snapshot?.boundProfile)
322+
renderProfile(snapshot?.boundProfile, t)
317323
)}
318324
</CardContent>
319325
</Card>
@@ -353,7 +359,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
353359
) : null}
354360
</div>
355361

356-
{renderProfile(version.profile)}
362+
{renderProfile(version.profile, t)}
357363

358364
<div className="mt-3 flex flex-wrap gap-2">
359365
<Button
@@ -402,15 +408,13 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
402408

403409
<Card className="shadow-sm">
404410
<CardHeader className="pb-3">
405-
<CardTitle className="text-sm">
406-
{t('cloud.identity.baseline', { defaultValue: 'Reference Profile' })}
407-
</CardTitle>
411+
<CardTitle className="text-sm">{t('cloud.identity.baseline')}</CardTitle>
408412
</CardHeader>
409413
<CardContent>
410414
{showLoadingPlaceholder ? (
411415
<div className="text-muted-foreground text-xs">{t('cloud.identity.loading')}</div>
412416
) : (
413-
renderProfile(snapshot?.baseline)
417+
renderProfile(snapshot?.baseline, t)
414418
)}
415419
</CardContent>
416420
</Card>

src/components/ModelVisibilitySettings.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ export function ModelVisibilitySettings() {
130130
<CardHeader>
131131
<CardTitle className="flex items-center gap-2">
132132
<span>{t('settings.modelVisibility.title')}</span>
133-
<Badge variant="secondary">{filteredModelIds.length} models</Badge>
133+
<Badge variant="secondary">
134+
{t('settings.providerGroupings.models', { count: filteredModelIds.length })}
135+
</Badge>
134136
</CardTitle>
135137
<CardDescription>{t('settings.modelVisibility.description')}</CardDescription>
136138
</CardHeader>

src/hooks/useAppConfig.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useCallback, useEffect, useRef } from 'react';
22

33
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4+
import { useTranslation } from 'react-i18next';
45

56
import { toast } from '@/components/ui/use-toast';
67
import { ipc } from '@/ipc/manager';
@@ -9,6 +10,7 @@ import { AppConfig } from '@/types/config';
910
const SAVE_DEBOUNCE_MS = 400;
1011

1112
export function useAppConfig() {
13+
const { t } = useTranslation();
1214
const queryClient = useQueryClient();
1315

1416
const {
@@ -57,8 +59,8 @@ export function useAppConfig() {
5759
queryClient.setQueryData(['appConfig'], savedConfig);
5860
lastStableConfigRef.current = savedConfig;
5961
toast({
60-
title: 'Settings saved',
61-
description: 'Your configuration has been updated.',
62+
title: t('settings.toast.saved.title'),
63+
description: t('settings.toast.saved.description'),
6264
});
6365
for (const item of pendingBatch) {
6466
item.resolve();
@@ -72,15 +74,15 @@ export function useAppConfig() {
7274
queryClient.invalidateQueries({ queryKey: ['appConfig'] });
7375
}
7476
toast({
75-
title: 'Error saving settings',
77+
title: t('settings.toast.saveFailed.title'),
7678
description: error.message,
7779
variant: 'destructive',
7880
});
7981
for (const item of pendingBatch) {
8082
item.reject(error);
8183
}
8284
}
83-
}, [queryClient, updateConfig]);
85+
}, [queryClient, t, updateConfig]);
8486

8587
const saveConfig = useCallback(
8688
(newConfig: AppConfig) => {

src/ipc/tray/i18n.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,48 @@ const en: TrayTexts = {
2525
const zh: TrayTexts = {
2626
current: '当前账号',
2727
quota: '当前额度',
28-
switch_next: '切换下一个账号',
28+
switch_next: '切换到下一个账号',
2929
refresh_current: '刷新当前额度',
3030
show_window: '显示主窗口',
31-
quit: '退出程序',
31+
quit: '退出应用',
3232
no_account: '无账号',
3333
unknown_quota: '未知',
34-
forbidden: '账号无权限',
34+
forbidden: '账号已被禁用',
35+
};
36+
37+
const ru: TrayTexts = {
38+
current: 'Текущий',
39+
quota: 'Квота',
40+
switch_next: 'Переключить на следующий аккаунт',
41+
refresh_current: 'Обновить текущую квоту',
42+
show_window: 'Показать главное окно',
43+
quit: 'Выйти из приложения',
44+
no_account: 'Нет аккаунта',
45+
unknown_quota: 'Неизвестно',
46+
forbidden: 'Аккаунт заблокирован',
47+
};
48+
49+
const vi: TrayTexts = {
50+
current: 'Hiện tại',
51+
quota: 'Quota',
52+
switch_next: 'Chuyển sang tài khoản tiếp theo',
53+
refresh_current: 'Làm mới quota hiện tại',
54+
show_window: 'Hiện cửa sổ chính',
55+
quit: 'Thoát ứng dụng',
56+
no_account: 'Không có tài khoản',
57+
unknown_quota: 'Không rõ',
58+
forbidden: 'Tài khoản bị cấm',
3559
};
3660

3761
export function getTrayTexts(lang: string = 'en'): TrayTexts {
3862
if (lang.startsWith('zh')) {
3963
return zh;
4064
}
65+
if (lang.startsWith('ru')) {
66+
return ru;
67+
}
68+
if (lang.startsWith('vi')) {
69+
return vi;
70+
}
4171
return en;
4272
}

src/layouts/MainLayout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const MainLayout: React.FC = () => {
4444
{
4545
to: '/proxy',
4646
icon: Network,
47-
label: t('nav.proxy', 'API Proxy'),
47+
label: t('nav.proxy'),
4848
},
4949
{
5050
to: '/settings',

0 commit comments

Comments
 (0)