Skip to content

Commit a4263bf

Browse files
liangweifengclaude
andcommitted
fix: dark mode text contrast + localized error messages
- Upgrade all dark:text-surface-600 → dark:text-surface-500 for WCAG AA compliance (9 components: Input, Select, HotkeyCapture, ResultPanel, HistoryPage, PrivacySettings, ToneRulesSettings) - Extract friendlyErrorMessage() to shared src/utils/friendlyError.ts - Dashboard: error items now show localized title instead of raw "错误" - History: error items show friendly localized title + detail instead of raw English technical messages like "No API key configured for siliconflow" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ad5eac3 commit a4263bf

9 files changed

Lines changed: 40 additions & 39 deletions

File tree

src/components/recording/ResultPanel.tsx

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,7 @@
11
import { useState } from 'react';
22
import { Button } from '../ui';
33
import { useTranslation } from '../../i18n';
4-
5-
function friendlyErrorMessage(error: string, t: (k: string) => string): { title: string; detail: string } {
6-
const lower = error.toLowerCase();
7-
if (lower.includes('no api key') || lower.includes('api key required'))
8-
return { title: t('recording.errorApiKey'), detail: t('recording.errorApiKeyDetail') };
9-
if (lower.includes('no stt model') || lower.includes('no llm model'))
10-
return { title: t('recording.errorNoModel'), detail: t('recording.errorNoModelDetail') };
11-
if (lower.includes('microphone') || lower.includes('mic'))
12-
return { title: t('recording.errorMic'), detail: error };
13-
if (lower.includes('no speech'))
14-
return { title: t('recording.errorNoSpeech'), detail: t('recording.errorNoSpeechDetail') };
15-
if (lower.includes('timeout') || lower.includes('timed out'))
16-
return { title: t('recording.errorTimeout'), detail: t('recording.errorTimeoutDetail') };
17-
if (lower.includes('pipeline busy'))
18-
return { title: t('recording.errorBusy'), detail: t('recording.errorBusyDetail') };
19-
// HTTP status code errors from API calls
20-
if (lower.includes('401') || lower.includes('403') || lower.includes('unauthorized') || lower.includes('forbidden') || lower.includes('invalid'))
21-
return { title: t('recording.errorAuth'), detail: t('recording.errorAuthDetail') };
22-
if (lower.includes('429') || lower.includes('rate limit'))
23-
return { title: t('recording.errorRateLimit'), detail: t('recording.errorRateLimitDetail') };
24-
if (/\b5\d\d\b/.test(error) || lower.includes('service unavailable') || lower.includes('bad gateway'))
25-
return { title: t('recording.errorServer'), detail: t('recording.errorServerDetail') };
26-
// Fallback: show technical error
27-
return { title: t('recording.error'), detail: error };
28-
}
4+
import { friendlyErrorMessage } from '../../utils/friendlyError';
295

306
interface ResultPanelProps {
317
rawText: string;
@@ -94,7 +70,7 @@ export function ResultPanel({ rawText, processedText, error }: ResultPanelProps)
9470

9571
{/* Stats footer */}
9672
{reduction > 0 && (
97-
<div className="px-4 pb-3 flex items-center gap-2 text-[11px] text-surface-400 dark:text-surface-600">
73+
<div className="px-4 pb-3 flex items-center gap-2 text-[11px] text-surface-400 dark:text-surface-500">
9874
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>
9975
{t('recording.cleaned', { chars: rawText.length - processedText.length, pct: reduction })}
10076
</div>

src/components/ui/HotkeyCapture.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,13 +274,13 @@ export function HotkeyCapture({ value, onChange, label, hint, usedKeys }: Hotkey
274274
) : value ? (
275275
<HotkeyBadges accel={value} />
276276
) : (
277-
<span className="text-sm text-surface-400 dark:text-surface-600">{t('common.clickToSet')}</span>
277+
<span className="text-sm text-surface-400 dark:text-surface-500">{t('common.clickToSet')}</span>
278278
)}
279279

280280
{!capturing && value && (
281281
<button
282282
onClick={(e) => { e.stopPropagation(); onChange(''); }}
283-
className="ml-2 text-surface-400 dark:text-surface-600 hover:text-surface-600 dark:hover:text-surface-400 flex-shrink-0"
283+
className="ml-2 text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-400 flex-shrink-0"
284284
tabIndex={-1}
285285
>
286286
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
@@ -289,7 +289,7 @@ export function HotkeyCapture({ value, onChange, label, hint, usedKeys }: Hotkey
289289
</button>
290290
)}
291291
</div>
292-
{hint && <p className="text-xs text-surface-400 dark:text-surface-600">{hint}</p>}
292+
{hint && <p className="text-xs text-surface-400 dark:text-surface-500">{hint}</p>}
293293
</div>
294294
);
295295
}

src/components/ui/Input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
1919
transition-colors ${error ? 'border-red-500/50' : ''} ${className}`}
2020
{...props}
2121
/>
22-
{hint && !error && <p className="text-xs text-surface-400 dark:text-surface-600">{hint}</p>}
22+
{hint && !error && <p className="text-xs text-surface-400 dark:text-surface-500">{hint}</p>}
2323
{error && <p className="text-xs text-red-500 dark:text-red-400">{error}</p>}
2424
</div>
2525
),

src/components/ui/Select.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export function Select({ label, hint, value, onChange, options, className = '',
113113
document.body,
114114
)}
115115

116-
{hint && <p className="text-xs text-surface-400 dark:text-surface-600">{hint}</p>}
116+
{hint && <p className="text-xs text-surface-400 dark:text-surface-500">{hint}</p>}
117117
</div>
118118
);
119119
}

src/pages/DashboardPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useMemo, useState } from 'react';
22
import { useConfigStore } from '../stores/configStore';
33
import { useTranslation } from '../i18n';
4+
import { friendlyErrorMessage } from '../utils/friendlyError';
45
import type { HistoryItem } from '../types/config';
56

67
const GITHUB_URL = 'https://github.com/WEIFENG2333/OpenType';
@@ -348,7 +349,7 @@ function StatCard({ icon, label, value, unit }: { icon: JSX.Element; label: stri
348349
/* ── Recent transcription item ── */
349350
function RecentItem({ item, expanded, isLast, onClick }: { item: HistoryItem; expanded: boolean; isLast: boolean; onClick: () => void }) {
350351
const { t } = useTranslation();
351-
const text = item.processedText || item.rawText || (item.error ? t('recording.error') : '');
352+
const text = item.processedText || item.rawText || (item.error ? friendlyErrorMessage(item.error, t).title : '');
352353
const ago = formatTimeAgo(item.timestamp, t);
353354
const dur = item.durationMs ? formatDuration(item.durationMs) : '';
354355

src/pages/HistoryPage.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useConfigStore } from '../stores/configStore';
33
import { PageHeader } from '../components/layout/PageHeader';
44
import { Button } from '../components/ui';
55
import { HistoryItem } from '../types/config';
6+
import { friendlyErrorMessage } from '../utils/friendlyError';
67
import { useTranslation } from '../i18n';
78

89
// ─── Audio helpers ───────────────────────────────────────────────────────────
@@ -280,8 +281,8 @@ export function HistoryPage() {
280281
<div className="flex-1 min-w-0">
281282
{item.error ? (
282283
<div>
283-
<p className="text-sm text-red-400">{t('recording.error')}</p>
284-
<p className="text-xs text-surface-500 dark:text-surface-400 line-clamp-1 mt-0.5">{item.error}</p>
284+
<p className="text-sm text-red-400">{friendlyErrorMessage(item.error, t).title}</p>
285+
<p className="text-xs text-surface-500 dark:text-surface-400 line-clamp-1 mt-0.5">{friendlyErrorMessage(item.error, t).detail}</p>
285286
</div>
286287
) : (
287288
<p className="text-sm text-surface-800 dark:text-surface-200 leading-relaxed line-clamp-2">
@@ -522,7 +523,7 @@ function DetailModal({ item, onClose, t, onDownloadAudio }: { item: HistoryItem;
522523
{item.rawText ? (
523524
<p className="text-sm text-surface-600 dark:text-surface-400 leading-relaxed whitespace-pre-wrap bg-surface-50 dark:bg-surface-900 rounded-lg p-3 border border-surface-200 dark:border-surface-800">{item.rawText}</p>
524525
) : (
525-
<p className="text-xs text-surface-400 italic">{isError ? item.error : t('history.noOutput')}</p>
526+
<p className="text-xs text-surface-400 italic">{isError ? friendlyErrorMessage(item.error!, t).detail : t('history.noOutput')}</p>
526527
)}
527528
</PipelineStep>
528529

@@ -540,7 +541,7 @@ function DetailModal({ item, onClose, t, onDownloadAudio }: { item: HistoryItem;
540541
) : isError ? (
541542
<div className="flex items-start gap-2 px-3 py-2.5 rounded-lg bg-red-500/5 border border-red-500/10">
542543
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-red-400 flex-shrink-0 mt-0.5"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
543-
<p className="text-xs text-red-400">{item.error}</p>
544+
<p className="text-xs text-red-400">{friendlyErrorMessage(item.error!, t).detail}</p>
544545
</div>
545546
) : (
546547
<p className="text-xs text-surface-400 italic">{t('history.noOutput')}</p>
@@ -643,7 +644,7 @@ function ContextSection({ title, enabled, hasData, t, children }: {
643644
}`}>
644645
<div className="flex items-center gap-2 mb-1">
645646
{isDisabled ? (
646-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-surface-300 dark:text-surface-600 flex-shrink-0"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
647+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-surface-300 dark:text-surface-500 flex-shrink-0"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
647648
) : hasData ? (
648649
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="text-green-500 flex-shrink-0"><polyline points="20 6 9 17 4 12"/></svg>
649650
) : (

src/pages/settings/PrivacySettings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function PrivacySettings() {
4343
<Button variant="secondary" size="sm" onClick={handleClearAll}>
4444
{t('settings.privacy.clearAll')}
4545
</Button>
46-
<p className="text-xs text-surface-400 dark:text-surface-600">
46+
<p className="text-xs text-surface-400 dark:text-surface-500">
4747
{t('settings.privacy.clearAllHint')}
4848
</p>
4949
</div>

src/pages/settings/ToneRulesSettings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function ToneRulesSettings() {
7878

7979
{/* Rules list */}
8080
{rules.length === 0 ? (
81-
<p className="text-xs text-surface-400 dark:text-surface-600 py-2">{t('settings.tones.noRules')}</p>
81+
<p className="text-xs text-surface-400 dark:text-surface-500 py-2">{t('settings.tones.noRules')}</p>
8282
) : (
8383
<div className="space-y-1.5">
8484
{rules.map((rule, i) => (

src/utils/friendlyError.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/** Map technical error strings to user-friendly localized messages */
2+
export function friendlyErrorMessage(error: string, t: (k: string) => string): { title: string; detail: string } {
3+
const lower = error.toLowerCase();
4+
if (lower.includes('no api key') || lower.includes('api key required'))
5+
return { title: t('recording.errorApiKey'), detail: t('recording.errorApiKeyDetail') };
6+
if (lower.includes('no stt model') || lower.includes('no llm model'))
7+
return { title: t('recording.errorNoModel'), detail: t('recording.errorNoModelDetail') };
8+
if (lower.includes('microphone') || lower.includes('mic'))
9+
return { title: t('recording.errorMic'), detail: error };
10+
if (lower.includes('no speech'))
11+
return { title: t('recording.errorNoSpeech'), detail: t('recording.errorNoSpeechDetail') };
12+
if (lower.includes('timeout') || lower.includes('timed out'))
13+
return { title: t('recording.errorTimeout'), detail: t('recording.errorTimeoutDetail') };
14+
if (lower.includes('pipeline busy'))
15+
return { title: t('recording.errorBusy'), detail: t('recording.errorBusyDetail') };
16+
if (lower.includes('401') || lower.includes('403') || lower.includes('unauthorized') || lower.includes('forbidden') || lower.includes('invalid'))
17+
return { title: t('recording.errorAuth'), detail: t('recording.errorAuthDetail') };
18+
if (lower.includes('429') || lower.includes('rate limit'))
19+
return { title: t('recording.errorRateLimit'), detail: t('recording.errorRateLimitDetail') };
20+
if (/\b5\d\d\b/.test(error) || lower.includes('service unavailable') || lower.includes('bad gateway'))
21+
return { title: t('recording.errorServer'), detail: t('recording.errorServerDetail') };
22+
return { title: t('recording.error'), detail: error };
23+
}

0 commit comments

Comments
 (0)