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

+35 -59
View File
@@ -257,16 +257,14 @@ print(response.choices[0].message.content)`;
return (
<div className="container mx-auto max-w-4xl space-y-5 p-6">
<div>
<h2 className="text-3xl font-bold tracking-tight">{t('proxy.title', 'API Proxy')}</h2>
<p className="text-muted-foreground mt-1">
{t('proxy.description', 'Manage the local API proxy service.')}
</p>
<h2 className="text-3xl font-bold tracking-tight">{t('proxy.title')}</h2>
<p className="text-muted-foreground mt-1">{t('proxy.description')}</p>
{/* Local Access Info Banner */}
{proxyConfig?.enabled && (
<div className="mt-4 flex flex-col gap-2 rounded-md border border-blue-100 bg-blue-50 p-3 text-sm text-blue-900 dark:border-blue-900/50 dark:bg-blue-950/30 dark:text-blue-200">
<div className="flex items-center gap-2">
<div className="font-semibold">{t('proxy.config.local_access', 'Local Access:')}</div>
<div className="font-semibold">{t('proxy.config.local_access')}</div>
<code className="rounded bg-blue-100 px-1.5 py-0.5 font-mono select-all dark:bg-blue-900/50">
http://{selectedIp || 'localhost'}:{proxyConfig.port}/v1
</code>
@@ -274,7 +272,7 @@ print(response.choices[0].message.content)`;
{localIps && localIps.length > 1 && (
<Select value={selectedIp} onValueChange={setSelectedIp}>
<SelectTrigger className="ml-2 h-7 w-auto min-w-[180px] text-xs">
<SelectValue placeholder="Select IP" />
<SelectValue placeholder={t('proxy.config.select_ip')} />
</SelectTrigger>
<SelectContent>
{localIps.map((ip) => (
@@ -288,10 +286,7 @@ print(response.choices[0].message.content)`;
</div>
{!proxyConfig.api_key && (
<div className="flex items-center gap-2 text-xs font-medium text-amber-600 dark:text-amber-400">
{t(
'proxy.config.no_token_warning',
'⚠️ No API Key set. Service is open to the public network!',
)}
{t('proxy.config.no_token_warning')}
</div>
)}
</div>
@@ -303,19 +298,15 @@ print(response.choices[0].message.content)`;
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>{t('proxy.service.title', 'Service Status')}</CardTitle>
<CardDescription>
{t('proxy.service.description', 'Control the local API proxy server.')}
</CardDescription>
<CardTitle>{t('proxy.service.title')}</CardTitle>
<CardDescription>{t('proxy.service.description')}</CardDescription>
</div>
<div className="flex items-center gap-2">
<div
className={`h-3 w-3 rounded-full ${proxyConfig.enabled ? 'animate-pulse bg-green-500' : 'bg-gray-400'}`}
></div>
<span className="text-sm font-medium">
{proxyConfig.enabled
? t('proxy.service.running', 'Running')
: t('proxy.service.stopped', 'Stopped')}
{proxyConfig.enabled ? t('proxy.service.running') : t('proxy.service.stopped')}
</span>
</div>
</div>
@@ -336,16 +327,14 @@ print(response.choices[0].message.content)`;
}
}}
>
{proxyConfig.enabled
? t('proxy.service.stop', 'Stop Service')
: t('proxy.service.start', 'Start Service')}
{proxyConfig.enabled ? t('proxy.service.stop') : t('proxy.service.start')}
</Button>
</div>
{/* Port & Timeout Configuration */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="gateway-port">{t('proxy.config.port', 'Listen Port')}</Label>
<Label htmlFor="gateway-port">{t('proxy.config.port')}</Label>
<Input
id="gateway-port"
type="number"
@@ -357,9 +346,7 @@ print(response.choices[0].message.content)`;
/>
</div>
<div className="space-y-2">
<Label htmlFor="gateway-timeout">
{t('proxy.config.timeout', 'Request Timeout')}
</Label>
<Label htmlFor="gateway-timeout">{t('proxy.config.timeout')}</Label>
<Input
id="gateway-timeout"
type="number"
@@ -376,7 +363,7 @@ print(response.choices[0].message.content)`;
{/* API Key */}
<div className="space-y-2">
<Label>{t('proxy.config.api_key', 'API Key')}</Label>
<Label>{t('proxy.config.api_key')}</Label>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
@@ -405,28 +392,21 @@ print(response.choices[0].message.content)`;
onClick={() => navigator.clipboard.writeText(proxyConfig.api_key || '')}
>
<Copy size={14} className="mr-1" />
{t('proxy.copy', 'Copy')}
{t('proxy.copy')}
</Button>
<Button variant="outline" size="sm" onClick={() => setIsRegenerateDialogOpen(true)}>
{t('proxy.regenerate', 'Regenerate')}
{t('proxy.regenerate')}
</Button>
</div>
<Dialog open={isRegenerateDialogOpen} onOpenChange={setIsRegenerateDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{t('proxy.regenerateConfirm.title', 'Regenerate API Key?')}
</DialogTitle>
<DialogDescription>
{t(
'proxy.regenerateConfirm.description',
'This will invalidate the current API key immediately. Any applications using the old key will stop working.',
)}
</DialogDescription>
<DialogTitle>{t('proxy.regenerateConfirm.title')}</DialogTitle>
<DialogDescription>{t('proxy.regenerateConfirm.description')}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setIsRegenerateDialogOpen(false)}>
{t('proxy.regenerateConfirm.cancel', 'Cancel')}
{t('proxy.regenerateConfirm.cancel')}
</Button>
<Button
variant="destructive"
@@ -437,7 +417,7 @@ print(response.choices[0].message.content)`;
setIsRegenerateDialogOpen(false);
}}
>
{t('proxy.regenerateConfirm.confirm', 'Regenerate')}
{t('proxy.regenerateConfirm.confirm')}
</Button>
</DialogFooter>
</DialogContent>
@@ -447,10 +427,8 @@ print(response.choices[0].message.content)`;
{/* Auto Start Toggle */}
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1">
<Label>{t('proxy.config.auto_start', 'Auto Start with App')}</Label>
<p className="text-xs text-gray-500">
{t('proxy.config.auto_start_desc', 'Start proxy service when application launches')}
</p>
<Label>{t('proxy.config.auto_start')}</Label>
<p className="text-xs text-gray-500">{t('proxy.config.auto_start_desc')}</p>
</div>
<Switch
checked={proxyConfig.auto_start}
@@ -465,10 +443,8 @@ print(response.choices[0].message.content)`;
{/* Model Mapping Card */}
<Card>
<CardHeader>
<CardTitle>{t('proxy.mapping.title', 'Model Mapping')}</CardTitle>
<CardDescription>
{t('proxy.mapping.description', 'Map Claude models to Gemini models for routing.')}
</CardDescription>
<CardTitle>{t('proxy.mapping.title')}</CardTitle>
<CardDescription>{t('proxy.mapping.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
@@ -481,7 +457,7 @@ print(response.choices[0].message.content)`;
</h3>
</div>
<p className="mb-3 text-xs text-gray-600 dark:text-gray-400">
{t('proxy.mapping.maps_to', 'Maps to')}
{t('proxy.mapping.maps_to')}
</p>
<Select
value={resolveAnthropicMappingValue(
@@ -518,7 +494,7 @@ print(response.choices[0].message.content)`;
</h3>
</div>
<p className="mb-3 text-xs text-gray-600 dark:text-gray-400">
{t('proxy.mapping.maps_to', 'Maps to')}
{t('proxy.mapping.maps_to')}
</p>
<Select
value={resolveAnthropicMappingValue(
@@ -558,7 +534,7 @@ print(response.choices[0].message.content)`;
})
}
>
{t('proxy.mapping.restore', 'Restore Defaults')}
{t('proxy.mapping.restore')}
</Button>
</div>
</CardContent>
@@ -569,11 +545,9 @@ print(response.choices[0].message.content)`;
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Code size={20} />
{t('proxy.examples.title', 'Usage Examples')}
{t('proxy.examples.title')}
</CardTitle>
<CardDescription>
{t('proxy.examples.description', 'Example commands to call the local API proxy.')}
</CardDescription>
<CardDescription>{t('proxy.examples.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Protocol Selector Cards */}
@@ -588,7 +562,7 @@ print(response.choices[0].message.content)`;
className={`h-2 w-2 rounded-full ${selectedProtocol === 'openai' ? 'animate-pulse bg-blue-500' : 'bg-blue-400'}`}
></div>
<span className="text-sm font-bold text-blue-700 dark:text-blue-400">
OpenAI Protocol
{t('settings.examples.openai_protocol')}
</span>
</div>
<div className="mb-2 rounded border border-blue-200/50 bg-white/60 px-3 py-2 dark:border-blue-700/30 dark:bg-gray-800/40">
@@ -597,7 +571,7 @@ print(response.choices[0].message.content)`;
</code>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400">
💡 Cursor, Windsurf, NextChat
{t('settings.examples.openai_tools')}
</p>
</div>
@@ -611,7 +585,7 @@ print(response.choices[0].message.content)`;
className={`h-2 w-2 rounded-full ${selectedProtocol === 'anthropic' ? 'animate-pulse bg-purple-500' : 'bg-purple-400'}`}
></div>
<span className="text-sm font-bold text-purple-700 dark:text-purple-400">
Anthropic Protocol
{t('settings.examples.anthropic_protocol')}
</span>
</div>
<div className="mb-2 rounded border border-purple-200/50 bg-white/60 px-3 py-2 dark:border-purple-700/30 dark:bg-gray-800/40">
@@ -619,7 +593,9 @@ print(response.choices[0].message.content)`;
POST /v1/messages
</code>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400">💡 Claude Code CLI</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
{t('settings.examples.anthropic_tools')}
</p>
</div>
</div>
@@ -649,7 +625,7 @@ print(response.choices[0].message.content)`;
className="flex items-center gap-1 text-xs text-blue-600 hover:text-blue-700"
>
{copied === 'curl' ? <CheckCircle size={14} /> : <Copy size={14} />}
{copied === 'curl' ? 'Copied!' : 'Copy'}
{copied === 'curl' ? t('proxy.copied') : t('proxy.copy')}
</button>
</div>
<pre className="overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs whitespace-pre-wrap text-gray-100">
@@ -669,7 +645,7 @@ print(response.choices[0].message.content)`;
className="flex items-center gap-1 text-xs text-blue-600 hover:text-blue-700"
>
{copied === 'python' ? <CheckCircle size={14} /> : <Copy size={14} />}
{copied === 'python' ? 'Copied!' : 'Copy'}
{copied === 'python' ? t('proxy.copied') : t('proxy.copy')}
</button>
</div>
<pre className="overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs whitespace-pre-wrap text-gray-100">
+33 -79
View File
@@ -84,9 +84,9 @@ function SettingsPage() {
<Tabs defaultValue="general" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="general">{t('settings.general', 'General')}</TabsTrigger>
<TabsTrigger value="models">{t('settings.models', 'Models')}</TabsTrigger>
<TabsTrigger value="proxy">{t('settings.proxy_tab', 'Proxy')}</TabsTrigger>
<TabsTrigger value="general">{t('settings.general')}</TabsTrigger>
<TabsTrigger value="models">{t('settings.models')}</TabsTrigger>
<TabsTrigger value="proxy">{t('settings.proxy_tab')}</TabsTrigger>
</TabsList>
{/* --- GENERAL TAB --- */}
@@ -130,6 +130,7 @@ function SettingsPage() {
<SelectItem value="en">{t('settings.language.english')}</SelectItem>
<SelectItem value="zh-CN">{t('settings.language.chinese')}</SelectItem>
<SelectItem value="ru">{t('settings.language.russian')}</SelectItem>
<SelectItem value="vi">{t('settings.language.vietnamese')}</SelectItem>
</SelectContent>
</Select>
</div>
@@ -139,22 +140,15 @@ function SettingsPage() {
{/* Account Settings Card */}
<Card>
<CardHeader>
<CardTitle>{t('settings.account.title', 'Account Settings')}</CardTitle>
<CardDescription>
{t('settings.account.description', 'Configure automatic account refresh and sync.')}
</CardDescription>
<CardTitle>{t('settings.account.title')}</CardTitle>
<CardDescription>{t('settings.account.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Auto Refresh Quota */}
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1">
<Label>{t('settings.account.auto_refresh', 'Auto Refresh Quota')}</Label>
<p className="text-xs text-gray-500">
{t(
'settings.account.auto_refresh_desc',
'Periodically refresh quota info for all accounts',
)}
</p>
<Label>{t('settings.account.auto_refresh')}</Label>
<p className="text-xs text-gray-500">{t('settings.account.auto_refresh_desc')}</p>
</div>
<Switch
checked={config?.auto_refresh || false}
@@ -169,13 +163,8 @@ function SettingsPage() {
{/* Auto Sync Account */}
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1">
<Label>{t('settings.account.auto_sync', 'Auto Sync Current Account')}</Label>
<p className="text-xs text-gray-500">
{t(
'settings.account.auto_sync_desc',
'Periodically sync active account information',
)}
</p>
<Label>{t('settings.account.auto_sync')}</Label>
<p className="text-xs text-gray-500">{t('settings.account.auto_sync_desc')}</p>
</div>
<Switch
checked={config?.auto_sync || false}
@@ -192,23 +181,15 @@ function SettingsPage() {
{isAutoStartSupported && (
<Card>
<CardHeader>
<CardTitle>{t('settings.startup.title', 'Startup')}</CardTitle>
<CardDescription>
{t(
'settings.startup.description',
'Control application launch behavior at system startup.',
)}
</CardDescription>
<CardTitle>{t('settings.startup.title')}</CardTitle>
<CardDescription>{t('settings.startup.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1">
<Label>{t('settings.startup.auto_startup', 'Start with system')}</Label>
<Label>{t('settings.startup.auto_startup')}</Label>
<p className="text-xs text-gray-500">
{t(
'settings.startup.auto_startup_desc',
'Launch at sign-in and keep the app in the system tray',
)}
{t('settings.startup.auto_startup_desc')}
</p>
</div>
<Switch
@@ -222,10 +203,7 @@ function SettingsPage() {
</div>
{isMac && (
<p className="text-muted-foreground text-xs">
{t(
'settings.startup.macos_hint',
'macOS requires a signed app for Login Items to work. If auto-start fails, please sign the app or enable it manually in System Settings.',
)}
{t('settings.startup.macos_hint')}
</p>
)}
</CardContent>
@@ -240,10 +218,10 @@ function SettingsPage() {
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="text-muted-foreground">{t('settings.version')}</div>
<div className="font-medium">{appVersion || 'Unknown'}</div>
<div className="font-medium">{appVersion || t('common.unknown')}</div>
<div className="text-muted-foreground">{t('settings.platform')}</div>
<div className="font-medium capitalize">{platform || 'Unknown'}</div>
<div className="font-medium capitalize">{platform || t('common.unknown')}</div>
<div className="text-muted-foreground">{t('settings.license')}</div>
<div className="font-medium">CC BY-NC-SA 4.0</div>
@@ -254,7 +232,7 @@ function SettingsPage() {
className="flex items-center gap-2 font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
<FolderOpen className="h-4 w-4" />
<span>{t('settings.openLogDir', 'Open')}</span>
<span>{t('settings.openLogDir')}</span>
</button>
</div>
</CardContent>
@@ -263,23 +241,15 @@ function SettingsPage() {
{/* Privacy & Error Reporting Card */}
<Card>
<CardHeader>
<CardTitle>{t('settings.privacy.title', 'Privacy')}</CardTitle>
<CardDescription>
{t(
'settings.privacy.description',
'Control how your data is used to improve the application.',
)}
</CardDescription>
<CardTitle>{t('settings.privacy.title')}</CardTitle>
<CardDescription>{t('settings.privacy.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1">
<Label>{t('settings.privacy.error_reporting', 'Error Reporting')}</Label>
<Label>{t('settings.privacy.error_reporting')}</Label>
<p className="text-xs text-gray-500">
{t(
'settings.privacy.error_reporting_desc',
'Send anonymous error reports to help us improve the app. No personal data is collected.',
)}
{t('settings.privacy.error_reporting_desc')}
</p>
</div>
<Switch
@@ -291,35 +261,22 @@ function SettingsPage() {
}}
/>
</div>
<p className="text-muted-foreground text-xs">
{t(
'settings.privacy.restart_note',
'Changes to error reporting will take effect after restarting the application.',
)}
</p>
<p className="text-muted-foreground text-xs">{t('settings.privacy.restart_note')}</p>
</CardContent>
</Card>
{/* Notifications Card */}
<Card>
<CardHeader>
<CardTitle>{t('settings.notifications.title', 'Notifications')}</CardTitle>
<CardDescription>
{t(
'settings.notifications.description',
'Configure desktop alerts for account events.',
)}
</CardDescription>
<CardTitle>{t('settings.notifications.title')}</CardTitle>
<CardDescription>{t('settings.notifications.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1">
<Label>{t('settings.notifications.quotaAlert', 'Low Quota Alerts')}</Label>
<Label>{t('settings.notifications.quotaAlert')}</Label>
<p className="text-xs text-gray-500">
{t(
'settings.notifications.quotaAlertDesc',
'Get notified when a model quota drops below the set threshold',
)}
{t('settings.notifications.quotaAlertDesc')}
</p>
</div>
<Switch
@@ -328,10 +285,10 @@ function SettingsPage() {
if (config) {
try {
await saveConfig({ ...config, quota_alert_enabled: checked });
} catch (err) {
} catch {
toast({
title: t('common.error'),
description: 'Failed to save notification settings',
description: t('settings.notifications.saveFailed'),
variant: 'destructive',
});
}
@@ -341,12 +298,9 @@ function SettingsPage() {
</div>
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1">
<Label>{t('settings.notifications.quotaThreshold', 'Alert Threshold')}</Label>
<Label>{t('settings.notifications.quotaThreshold')}</Label>
<p className="text-xs text-gray-500">
{t(
'settings.notifications.quotaThresholdDesc',
'Percentage below which to trigger an alert',
)}
{t('settings.notifications.quotaThresholdDesc')}
</p>
</div>
<div className="flex items-center gap-2">
@@ -363,10 +317,10 @@ function SettingsPage() {
if (config) {
try {
await saveConfig({ ...config, quota_alert_threshold: parsed });
} catch (err) {
} catch {
toast({
title: t('common.error'),
description: 'Failed to save threshold setting',
description: t('settings.notifications.thresholdSaveFailed'),
variant: 'destructive',
});
}