From 3bf07df8a2434e0821812662dffea93e4ef4bfe2 Mon Sep 17 00:00:00 2001 From: iamkunal9 Date: Fri, 24 Jul 2026 03:26:34 +0530 Subject: [PATCH 1/3] feat: add system-aware dark mode --- src/App.tsx | 38 ++ src/i18n/ja.ts | 4 + src/i18n/resources.ts | 8 + src/i18n/traditional.ts | 4 + src/main.tsx | 9 +- src/styles.css | 891 ++++++++++++++++++++++------------------ src/theme.tsx | 89 ++++ 7 files changed, 643 insertions(+), 400 deletions(-) create mode 100644 src/theme.tsx diff --git a/src/App.tsx b/src/App.tsx index d4230b8..3b9cf09 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,12 +13,16 @@ import { Languages, LogIn, MessageCircle, + Monitor, + Moon, Network, ServerCog, Settings, + Sun, } from 'lucide-react'; import appLogo from './assets/logo.jpg'; import { CoreRuntimeProvider, useCoreRuntime } from './coreRuntime'; +import { useTheme, type ThemeMode } from './theme'; import { ConfigPanelPage } from './pages/ConfigPanel'; import { ApiAccessPage } from './pages/ApiAccessPage'; import { AuthFileManagementPage } from './pages/AuthFileManagementPage'; @@ -89,6 +93,16 @@ const pages = [ }, ] as const; +const themeOptions: ReadonlyArray<{ + value: ThemeMode; + labelKey: 'app.theme.system' | 'app.theme.light' | 'app.theme.dark'; + icon: typeof Monitor; +}> = [ + { value: 'system', labelKey: 'app.theme.system', icon: Monitor }, + { value: 'light', labelKey: 'app.theme.light', icon: Sun }, + { value: 'dark', labelKey: 'app.theme.dark', icon: Moon }, +]; + type PageId = (typeof pages)[number]['id']; type WindowsCloseAction = 'exit' | 'minimize-to-tray'; @@ -107,6 +121,7 @@ function App() { function AppContent() { const { locale, setLocale, t } = useI18n(); + const { mode, setMode } = useTheme(); const [active, setActive] = useState('kernel'); const [languageMenuOpen, setLanguageMenuOpen] = useState(false); const [windowsClosePrompt, setWindowsClosePrompt] = useState(null); @@ -263,6 +278,29 @@ function AppContent() {
+
+ {t('app.theme')} +
+ {themeOptions.map((option) => { + const Icon = option.icon; + const selected = option.value === mode; + return ( + + ); + })} +
+
diff --git a/src/i18n/index.tsx b/src/i18n/index.tsx index 9e86c94..44959b1 100644 --- a/src/i18n/index.tsx +++ b/src/i18n/index.tsx @@ -1,6 +1,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { en, ja, zhCN, zhTW, type MessageKey, type MessageVariables } from './resources'; +import { localizeRuntimeText } from './runtimeText'; export type AppLocale = 'zh-CN' | 'zh-TW' | 'ja' | 'en'; @@ -56,13 +57,14 @@ function interpolate(template: string, variables?: MessageVariables): string { } export function translate(locale: AppLocale, key: MessageKey, variables?: MessageVariables): string { - return interpolate(resources[locale][key] ?? zhCN[key], variables); + return interpolate(resources[locale][key] ?? en[key] ?? key, variables); } type I18nContextValue = { locale: AppLocale; setLocale: (locale: AppLocale) => void; t: (key: MessageKey, variables?: MessageVariables) => string; + localizeText: (value: string | null | undefined) => string; formatNumber: (value: number, options?: Intl.NumberFormatOptions) => string; formatDate: (value: Date | number | string, options?: Intl.DateTimeFormatOptions) => string; }; @@ -94,6 +96,10 @@ export function I18nProvider({ children }: { children: ReactNode }) { (key: MessageKey, variables?: MessageVariables) => translate(locale, key, variables), [locale], ); + const localizeText = useCallback( + (value: string | null | undefined) => localizeRuntimeText(locale, value), + [locale], + ); const formatNumber = useCallback( (value: number, options?: Intl.NumberFormatOptions) => new Intl.NumberFormat(locale, options).format(value), @@ -108,8 +114,8 @@ export function I18nProvider({ children }: { children: ReactNode }) { ); const context = useMemo( - () => ({ locale, setLocale, t, formatNumber, formatDate }), - [formatDate, formatNumber, locale, setLocale, t], + () => ({ locale, setLocale, t, localizeText, formatNumber, formatDate }), + [formatDate, formatNumber, localizeText, locale, setLocale, t], ); return {children}; diff --git a/src/i18n/runtimeText.ts b/src/i18n/runtimeText.ts new file mode 100644 index 0000000..c217ce3 --- /dev/null +++ b/src/i18n/runtimeText.ts @@ -0,0 +1,65 @@ +export type RuntimeLocale = 'zh-CN' | 'zh-TW' | 'ja' | 'en'; + +const HAN_RE = /[㐀-鿿]/u; + +const localizedMessages: Record, Record> = { + en: { + '未安装 CPA 内核,请先安装最新版': 'CPA core is not installed. Install the latest version first.', + 'CPA 内核正在运行': 'CPA core is running', + 'CPA 内核已安装,当前未运行': 'CPA core is installed but not running', + 'CPA 内核已经在运行': 'CPA core is already running', + '等待内核启动': 'Waiting for the CPA core to start', + '使用记录采集中': 'Collecting usage records', + }, + 'zh-TW': { + '未安装 CPA 内核,请先安装最新版': '尚未安裝 CPA 核心,請先安裝最新版本。', + 'CPA 内核正在运行': 'CPA 核心正在執行', + 'CPA 内核已安装,当前未运行': 'CPA 核心已安裝,目前未執行', + 'CPA 内核已经在运行': 'CPA 核心已在執行', + '等待内核启动': '等待 CPA 核心啟動', + '使用记录采集中': '正在收集使用記錄', + }, + ja: { + '未安装 CPA 内核,请先安装最新版': 'CPA コアがインストールされていません。最新版を先にインストールしてください。', + 'CPA 内核正在运行': 'CPA コアは実行中です', + 'CPA 内核已安装,当前未运行': 'CPA コアはインストール済みですが、現在は停止しています', + 'CPA 内核已经在运行': 'CPA コアはすでに実行中です', + '等待内核启动': 'CPA コアの起動を待機中', + '使用记录采集中': '使用記録を収集中', + }, +}; + +const genericFailure: Record, string> = { + en: 'The operation failed.', + 'zh-TW': '操作失敗。', + ja: '操作に失敗しました。', +}; + +function localizePortError(locale: Exclude, text: string): string | null { + const match = text.match(/^端口\s+(\d+)\s+已被其他程序占用/); + if (!match) return null; + if (locale === 'en') return `Port ${match[1]} is already in use. Choose another port and try again.`; + if (locale === 'ja') return `ポート ${match[1]} はすでに使用されています。別のポートを選択して再試行してください。`; + return `連接埠 ${match[1]} 已被其他程式使用。請選擇其他連接埠後重試。`; +} + +function localizeInstallMessage(locale: Exclude, text: string): string | null { + const completed = text.match(/^(.+)\s+安装完成$/); + if (completed) { + if (locale === 'en') return `${completed[1]} installation completed.`; + if (locale === 'ja') return `${completed[1]} のインストールが完了しました。`; + return `${completed[1]} 安裝完成。`; + } + return null; +} + +export function localizeRuntimeText(locale: RuntimeLocale, text: string | null | undefined): string { + if (!text || locale === 'zh-CN' || !HAN_RE.test(text)) return text ?? ''; + + const exact = localizedMessages[locale][text]; + if (exact) return exact; + + return localizePortError(locale, text) + ?? localizeInstallMessage(locale, text) + ?? genericFailure[locale]; +} diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index df8bbb7..124058e 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -236,7 +236,7 @@ function AgentModelPicker({ onChange, onRefresh, }: AgentModelPickerProps) { - const { t } = useI18n(); + const { t, localizeText } = useI18n(); const [open, setOpen] = useState(false); const [search, setSearch] = useState(''); const [activeIndex, setActiveIndex] = useState(0); @@ -414,7 +414,7 @@ function AgentModelPicker({ {loading && models.length === 0 ? (
{t('agents.model.fetching')}
) : error && models.length === 0 ? ( -
{t('agents.model.loadFailed')}{error}
+
{t('agents.model.loadFailed')}{localizeText(error)}
) : choices.length === 0 ? (
{search.trim() ? t('agents.model.noMatch') : t('agents.model.unavailable')} @@ -452,7 +452,7 @@ function AgentModelPicker({ } export function AgentsPage() { - const { t } = useI18n(); + const { t, localizeText } = useI18n(); const [selected, setSelected] = useState(readSelectedAgentClient); const [statuses, setStatuses] = useState([]); const [models, setModels] = useState([]); @@ -696,8 +696,8 @@ export function AgentsPage() {
- {error ?
{error}
: null} - {!error && notice ?
{notice}
: null} + {error ?
{localizeText(error)}
: null} + {!error && notice ?
{localizeText(notice)}
: null}
@@ -739,9 +739,9 @@ export function AgentsPage() {
- {activeStatus?.error ?
{activeStatus.error}
: null} + {activeStatus?.error ?
{localizeText(activeStatus.error)}
: null} {activeStatus?.warnings.length && !activeStatus.error ? ( -
{activeStatus.warnings.join(';')}
+
{activeStatus.warnings.map((warning) => localizeText(warning)).join(' · ')}
) : null}
diff --git a/src/pages/ApiAccessPage.tsx b/src/pages/ApiAccessPage.tsx index a8fd35d..29ed00d 100644 --- a/src/pages/ApiAccessPage.tsx +++ b/src/pages/ApiAccessPage.tsx @@ -540,7 +540,7 @@ const providerIdentityMatches = (row: ProviderRow, record: Record('codex-api-key'); const [filter, setFilter] = useState(''); @@ -581,7 +581,7 @@ export function ApiAccessPage() { return next; }); if (failures.length > 0) { - setError(t('apiAccess.error.partialLoad', { errors: failures.join('; ') })); + setError(t('apiAccess.error.partialLoad', { errors: failures.map((failure) => localizeText(failure)).join('; ') })); } } catch (requestError) { setError(String(requestError)); @@ -813,8 +813,8 @@ export function ApiAccessPage() {
- {error ?
{error}
: null} - {notice ?
{notice}
: null} + {error ?
{localizeText(error)}
: null} + {notice ?
{localizeText(notice)}
: null}
- {modelError && !modelDiscoveryOpen ? {modelError} : null} + {modelError && !modelDiscoveryOpen ? {localizeText(modelError)} : null}
diff --git a/src/pages/AuthFileManagementPage.tsx b/src/pages/AuthFileManagementPage.tsx index a2e0c7a..0315a61 100644 --- a/src/pages/AuthFileManagementPage.tsx +++ b/src/pages/AuthFileManagementPage.tsx @@ -91,7 +91,7 @@ const statusText = (file: AuthFile) => { }; function AuthFileQuotaSummary({ quota }: { quota: QuotaState }) { - const { locale, t } = useI18n(); + const { locale, t, localizeText } = useI18n(); if (quota.status === 'loading') { return (
@@ -102,9 +102,9 @@ function AuthFileQuotaSummary({ quota }: { quota: QuotaState }) { } if (quota.status === 'error') { return ( -
+
{t('authFiles.quota.failed')} - {quota.error ? {quota.error} : null} + {quota.error ? {localizeText(quota.error)} : null}
); } @@ -134,7 +134,7 @@ function AuthFileQuotaSummary({ quota }: { quota: QuotaState }) { } export function AuthFileManagementPage() { - const { t } = useI18n(); + const { t, localizeText } = useI18n(); const [files, setFiles] = useState([]); const [filter, setFilter] = useState(''); const [providerFilter, setProviderFilter] = useState('all'); @@ -335,7 +335,7 @@ export function AuthFileManagementPage() { try { await loadFiles(); if (uploaded > 0) setNotice(t('authFiles.uploaded', { count: uploaded })); - if (failures.length > 0) setError(t('authFiles.uploadFailed', { count: failures.length, errors: failures.join('; ') })); + if (failures.length > 0) setError(t('authFiles.uploadFailed', { count: failures.length, errors: failures.map((failure) => localizeText(failure)).join('; ') })); } catch (requestError) { setError(String(requestError)); } finally { @@ -456,8 +456,8 @@ export function AuthFileManagementPage() {
- {error ?
{error}
: null} - {notice ?
{notice}
: null} + {error ?
{localizeText(error)}
: null} + {notice ?
{localizeText(notice)}
: null}
diff --git a/src/pages/ConfigPanel.tsx b/src/pages/ConfigPanel.tsx index 01ad6cd..4aaafea 100644 --- a/src/pages/ConfigPanel.tsx +++ b/src/pages/ConfigPanel.tsx @@ -38,7 +38,7 @@ const ROUTING_OPTIONS = [ ] as const; export function ConfigPanelPage() { - const { t } = useI18n(); + const { t, localizeText } = useI18n(); const [settings, setSettings] = useState(null); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(''); @@ -267,7 +267,7 @@ export function ConfigPanelPage() {
diff --git a/src/pages/Kernel.tsx b/src/pages/Kernel.tsx index 5ee75c8..d42b5c9 100644 --- a/src/pages/Kernel.tsx +++ b/src/pages/Kernel.tsx @@ -106,7 +106,7 @@ function requestLatestCore() { } export function KernelPage() { - const { t } = useI18n(); + const { t, localizeText } = useI18n(); const { status: coreStatus, statusError, @@ -211,7 +211,7 @@ export function KernelPage() { }) .catch((error) => { if (!disposed) { - setMessage(t('kernel.error.progressListener', { error: String(error) })); + setMessage(t('kernel.error.progressListener', { error: localizeText(String(error)) })); setMessageType('error'); } }); @@ -259,7 +259,7 @@ export function KernelPage() { .catch((error) => { if (disposed) return; console.warn('自动检查软件更新失败', error); - setAppUpdateError(String(error)); + setAppUpdateError(localizeText(String(error))); }) .finally(() => { if (!disposed) setCheckingAppUpdate(false); @@ -342,7 +342,7 @@ export function KernelPage() { showProcessNotice(messages?.success ?? t('kernel.notice.actionSuccess', { action: actionLabel }), 'success'); return true; } catch (error) { - const errorMessage = String(error); + const errorMessage = localizeText(String(error)); await refreshStatus(); showProcessNotice( messages?.failure @@ -365,7 +365,7 @@ export function KernelPage() { savedAllowLanRef.current = settings.allowLan; setSettingsError(''); } catch (error) { - setSettingsError(String(error)); + setSettingsError(localizeText(String(error))); } finally { setSettingsLoaded(true); } @@ -407,9 +407,9 @@ export function KernelPage() { } setAllowLanAccess(savedAllowLanRef.current); setCustomPort(String(savedPortRef.current)); - setSettingsError(String(error)); + setSettingsError(localizeText(String(error))); if (restartAfterSave) { - showProcessNotice(t('kernel.notice.networkSaveFailed', { error: String(error) }), 'error'); + showProcessNotice(t('kernel.notice.networkSaveFailed', { error: localizeText(String(error)) }), 'error'); } } }; @@ -445,7 +445,7 @@ export function KernelPage() { setPlatformError(''); } catch (error) { setPlatform(null); - setPlatformError(String(error)); + setPlatformError(localizeText(String(error))); } }; @@ -456,7 +456,7 @@ export function KernelPage() { setBundledCoreError(''); } catch (error) { setBundledCore(null); - setBundledCoreError(String(error)); + setBundledCoreError(localizeText(String(error))); } }; @@ -471,7 +471,7 @@ export function KernelPage() { setLatest(result); } catch (error) { setLatest(null); - setLatestError(String(error)); + setLatestError(localizeText(String(error))); } finally { setCheckingLatest(false); } @@ -482,7 +482,7 @@ export function KernelPage() { const task = await invoke('get_core_install_task'); applyInstallTask(task, false); } catch (error) { - setMessage(t('kernel.error.installTask', { error: String(error) })); + setMessage(t('kernel.error.installTask', { error: localizeText(String(error)) })); setMessageType('error'); } }; @@ -520,7 +520,7 @@ export function KernelPage() { }); await Promise.all([refreshStatus(), loadBundledCore()]); } catch (error) { - const errorMessage = String(error); + const errorMessage = localizeText(String(error)); setMessage(errorMessage); setMessageType(errorMessage.includes('取消') ? 'info' : 'error'); setProgress((current) => ({ @@ -562,7 +562,7 @@ export function KernelPage() { setMessageType('success'); await Promise.all([refreshStatus(), loadBundledCore()]); } catch (error) { - const errorMessage = String(error); + const errorMessage = localizeText(String(error)); setMessage(errorMessage); setMessageType('error'); setProgress((current) => ({ @@ -593,7 +593,7 @@ export function KernelPage() { await invoke('cancel_core_install'); } catch (error) { setCancellingInstall(false); - setMessage(String(error)); + setMessage(localizeText(String(error))); setMessageType('error'); } }; @@ -633,7 +633,7 @@ export function KernelPage() { setCurrentAppVersion(displayAppVersion(info.currentVersion)); setAppUpdate(info); } catch (error) { - setAppUpdateError(String(error)); + setAppUpdateError(localizeText(String(error))); } finally { setCheckingAppUpdate(false); } @@ -643,7 +643,7 @@ export function KernelPage() { try { await invoke('open_external_url', { url: appUpdate?.releaseUrl || APP_RELEASE_URL }); } catch (error) { - setAppUpdateError(t('kernel.error.openUpdate', { error: String(error) })); + setAppUpdateError(t('kernel.error.openUpdate', { error: localizeText(String(error)) })); } }; @@ -708,7 +708,7 @@ export function KernelPage() { : progress?.phase ? localizeInstallPhase(progress.phase, t) : t('kernel.install.inProgress') : offlineInstallRequired ? t('kernel.install.githubFailed') - : message || updateStateLabel; + : localizeText(message) || updateStateLabel; const versionStatusTone: MessageType = installTaskRunning ? 'info' : offlineInstallRequired @@ -736,7 +736,7 @@ export function KernelPage() { : t('kernel.install.titleFailed'); const installDialogMessage = cancellingInstall ? t('kernel.install.waitingStop') - : progress?.message || (installTaskRunning ? message || t('kernel.install.taskRunning') : ''); + : localizeText(progress?.message) || (installTaskRunning ? localizeText(message) || t('kernel.install.taskRunning') : ''); const installDialogAction = installTaskRunning ? cancellingInstall ? t('kernel.install.cancellingShort') @@ -1127,7 +1127,7 @@ export function KernelPage() {
{processNotice.tone === 'success' ? (
) : null} diff --git a/src/pages/ManagementPages.tsx b/src/pages/ManagementPages.tsx index d3878cd..a7c91a5 100644 --- a/src/pages/ManagementPages.tsx +++ b/src/pages/ManagementPages.tsx @@ -60,7 +60,7 @@ const OAUTH_SUCCESS_RESET_MS = 5000; const OAUTH_POLL_INTERVAL_MS = 3000; export function OAuthLoginPage() { - const { t } = useI18n(); + const { t, localizeText } = useI18n(); const [states, setStates] = useState>>({}); const [notice, setNotice] = useState<{ message: string; @@ -166,7 +166,7 @@ export function OAuthLoginPage() { showNotice( t('oauth.loginFailed', { provider: providerLabel(provider), - detail: result.error ? `: ${result.error}` : '', + detail: result.error ? `: ${localizeText(result.error)}` : '', }), 'error', ); @@ -188,7 +188,7 @@ export function OAuthLoginPage() { OAUTH_POLL_INTERVAL_MS, ); }, - [clearPollingTimer, completeProviderAuth, showNotice, t, updateProviderState], + [clearPollingTimer, completeProviderAuth, localizeText, showNotice, t, updateProviderState], ); useEffect(() => { @@ -241,7 +241,7 @@ export function OAuthLoginPage() { if (!result.opened) { showNotice( result.openError - ? t('oauth.openFailedDetail', { error: result.openError }) + ? t('oauth.openFailedDetail', { error: localizeText(result.openError) }) : t('oauth.openFailed'), 'info', ); @@ -327,7 +327,7 @@ export function OAuthLoginPage() { {notice ? (
- {notice.message} + {localizeText(notice.message)}
) : null} @@ -394,13 +394,13 @@ export function OAuthLoginPage() {
{t('oauth.callbackSubmitted')}
) : null} {state.callbackStatus === 'error' ? ( -
{t('oauth.callbackFailed', { detail: state.callbackError ? `: ${state.callbackError}` : '' })}
+
{t('oauth.callbackFailed', { detail: state.callbackError ? `: ${localizeText(state.callbackError)}` : '' })}
) : null}
) : null} {state.status === 'error' && state.error ? ( -
{state.error}
+
{localizeText(state.error)}
) : null}
diff --git a/src/pages/QuotaPage.tsx b/src/pages/QuotaPage.tsx index 2f7717d..06687ad 100644 --- a/src/pages/QuotaPage.tsx +++ b/src/pages/QuotaPage.tsx @@ -40,7 +40,7 @@ const providerOrder: QuotaProvider[] = ['claude', 'antigravity', 'codex', 'xai', const REFRESH_CONCURRENCY = 4; export function QuotaPage() { - const { locale, t } = useI18n(); + const { locale, t, localizeText } = useI18n(); const [files, setFiles] = useState([]); const quotas = useQuotaCache(); const [loading, setLoading] = useState(true); @@ -169,7 +169,7 @@ export function QuotaPage() { - {error ?
{error}
: null} + {error ?
{localizeText(error)}
: null} {loading ? (
{t('quota.loadingFiles')}
) : grouped.length === 0 ? ( @@ -189,7 +189,7 @@ export function QuotaPage() { } export function QuotaCard({ file, quota, onRefresh, onReset }: { file: AuthFile; quota: QuotaState; onRefresh: () => void; onReset?: () => void }) { - const { locale, t } = useI18n(); + const { locale, t, localizeText } = useI18n(); const provider = providerForFile(file); const name = fileName(file); const disabled = readBoolean(file, 'disabled'); @@ -198,7 +198,7 @@ export function QuotaCard({ file, quota, onRefresh, onReset }: { file: AuthFile;
{name}{provider ? providerMeta[provider].label : t('quota.unknownProvider')}{quota.plan ? ` · ${quota.plan}` : ''}
{onReset && (quota.resetCredits ?? 0) > 0 ? : null}
{quota.status === 'idle' ?
{disabled ? t('quota.fileDisabled') : t('quota.notFetched')}
: null} {quota.status === 'loading' ?
{t('quota.querying')}
: null} - {quota.status === 'error' ?
{quota.error}
: null} + {quota.status === 'error' ?
{localizeText(quota.error)}
: null} {quota.status === 'success' && provider === 'codex' ?
{t('quota.resetCredits')} {quota.resetCredits ?? '—'}{t('quota.earliestExpiry')} {formatQuotaTimestamp(quota.resetCreditsEarliestExpiry, locale)}
: null} {quota.status === 'success' ?
{quota.rows.map((row, index) =>
{row.label}{row.remainingPercent === null ? '—' : t('quota.remaining', { percent: Math.round(row.remainingPercent) })}
{row.detail ?? ''}{row.reset ? `${row.detail ? ' · ' : ''}${row.reset}` : ''}
)}
: null} diff --git a/src/pages/ThinkingAliasesPage.tsx b/src/pages/ThinkingAliasesPage.tsx index 8f5ad8a..b9c1d1d 100644 --- a/src/pages/ThinkingAliasesPage.tsx +++ b/src/pages/ThinkingAliasesPage.tsx @@ -64,7 +64,7 @@ const thinkingAliasSourceDetail = (source: ThinkingAliasSource) => ( ); export function ThinkingAliasesPage() { - const { t } = useI18n(); + const { t, localizeText } = useI18n(); const [entries, setEntries] = useState([]); const [sources, setSources] = useState([]); const [selectedSourceId, setSelectedSourceId] = useState(''); @@ -256,8 +256,8 @@ export function ThinkingAliasesPage() {
- {error ?
{error}
: null} - {!error && notice ?
{notice}
: null} + {error ?
{localizeText(error)}
: null} + {!error && notice ?
{localizeText(notice)}
: null}
diff --git a/src/pages/UsageRecordsPage.tsx b/src/pages/UsageRecordsPage.tsx index f078286..0aed02a 100644 --- a/src/pages/UsageRecordsPage.tsx +++ b/src/pages/UsageRecordsPage.tsx @@ -157,7 +157,7 @@ const formatTime = (value: string) => { const filterOptions = (items: UsageCategory[]) => items.filter((item) => item.key && item.label); export function UsageRecordsPage() { - const { t } = useI18n(); + const { t, localizeText } = useI18n(); const [activeTab, setActiveTab] = useState(loadTab); const [range, setRange] = useState(loadRange); const [customStart, setCustomStart] = useState(''); @@ -297,7 +297,7 @@ export function UsageRecordsPage() { Local Usage

{t('usage.title')}

-
+
{status?.state === 'collecting' ? t('usage.collector.collecting') : status?.state === 'error' ? t('usage.collector.error') : t('usage.collector.waiting')} @@ -306,7 +306,7 @@ export function UsageRecordsPage() {
- {error ?
{error}
: null} + {error ?
{localizeText(error)}
: null}
From 6abccb2112e5bf1c65d2afdc7417b417d82e6f1a Mon Sep 17 00:00:00 2001 From: iamkunal9 Date: Fri, 24 Jul 2026 17:16:54 +0530 Subject: [PATCH 3/3] fix: use locale-aware translation fallbacks --- src/i18n/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/i18n/index.tsx b/src/i18n/index.tsx index 44959b1..62d826f 100644 --- a/src/i18n/index.tsx +++ b/src/i18n/index.tsx @@ -56,8 +56,15 @@ function interpolate(template: string, variables?: MessageVariables): string { ); } +function fallbackResource(locale: AppLocale): Record { + if (locale === 'zh-TW') return zhTW; + if (locale === 'zh-CN') return zhCN; + return en; +} + export function translate(locale: AppLocale, key: MessageKey, variables?: MessageVariables): string { - return interpolate(resources[locale][key] ?? en[key] ?? key, variables); + const message = resources[locale][key] ?? fallbackResource(locale)[key] ?? key; + return interpolate(message, variables); } type I18nContextValue = {