Skip to content

Commit 72f28b8

Browse files
feat(dashboard): replace hardcoded aria-labels with i18n t() calls — batch 2
Replace 37 hardcoded aria-label strings with t('aria.keyName') calls across 20 component files (shared, layout, modals, tour, mobile). Changes: - 20 components: add useT import + hook, replace hardcoded strings - i18n/context.tsx: useT fallback resolves keys from en catalog (prevents crashes in tests without I18nProvider) - touch-targets.test.ts: match new i18n pattern for New Session button - KeyboardShortcutsHelp: rename t→translate to avoid collision with setTimeout variable Verification: tsc clean, 1265/1265 tests pass, build green. Refs: #3229 — Daedalus 🏛️
1 parent 536ff01 commit 72f28b8

22 files changed

Lines changed: 110 additions & 59 deletions

dashboard/src/__tests__/touch-targets.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe('Mobile touch targets (issue #2350)', () => {
2121
const lines = src.split('\n');
2222
let found = false;
2323
for (let i = 0; i < lines.length; i++) {
24-
if (lines[i].includes('aria-label="New Session')) {
24+
if (lines[i].includes("aria.newSession") || lines[i].includes('aria-label="New Session')) {
2525
// Search within 5 lines in both directions for className with min-h
2626
for (let j = Math.max(0, i - 5); j <= Math.min(lines.length - 1, i + 5); j++) {
2727
if (lines[j].includes('className="') && lines[j].includes('min-h-[44px]')) {

dashboard/src/components/CreatePipelineModal.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
* components/CreatePipelineModal.tsx — Modal dialog for creating new pipelines.
33
*/
44

5-
import { useState, useEffect, useRef, useCallback } from 'react';
5+
import { useState, useEffect, useRef, useCallback } from 'react'
6+
import { useT } from '../i18n/context';
67
import { useFocusTrap } from '../hooks/useFocusTrap';
78
import { useNavigate } from 'react-router-dom';
89
import { X, Loader2, Plus, Trash2 } from 'lucide-react';
@@ -26,6 +27,7 @@ function makeStep(): StepRow {
2627
}
2728

2829
export default function CreatePipelineModal({ open, onClose }: CreatePipelineModalProps) {
30+
const t = useT();
2931
const navigate = useNavigate();
3032
const nameRef = useRef<HTMLInputElement>(null);
3133
const trapRef = useFocusTrap(open);
@@ -118,11 +120,11 @@ export default function CreatePipelineModal({ open, onClose }: CreatePipelineMod
118120
<div className="fixed inset-0 z-50 flex items-center justify-center">
119121
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={handleClose} />
120122

121-
<div ref={trapRef} role="dialog" aria-modal="true" aria-label="Create new pipeline" className="relative w-full max-w-2xl mx-4 bg-[var(--color-surface)] border border-[var(--color-void-lighter)] rounded-lg shadow-2xl max-h-[90vh] overflow-y-auto">
123+
<div ref={trapRef} role="dialog" aria-modal="true" aria-label={t('aria.createNewPipeline')} className="relative w-full max-w-2xl mx-4 bg-[var(--color-surface)] border border-[var(--color-void-lighter)] rounded-lg shadow-2xl max-h-[90vh] overflow-y-auto">
122124
{/* Header */}
123125
<div className="flex items-center justify-between px-4 sm:px-5 py-4 border-b border-[var(--color-void-lighter)]">
124126
<h2 className="text-sm font-semibold text-[var(--color-text-primary)]">New Pipeline</h2>
125-
<button aria-label="Close"
127+
<button aria-label={t('aria.close')}
126128
onClick={handleClose}
127129
className="min-h-[44px] min-w-[44px] flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors"
128130
>
@@ -142,7 +144,7 @@ export default function CreatePipelineModal({ open, onClose }: CreatePipelineMod
142144
value={pipelineName}
143145
onChange={(e) => setPipelineName(e.target.value)}
144146
placeholder="my-pipeline"
145-
aria-label="Pipeline Name"
147+
aria-label={t('aria.pipelineName')}
146148
className="w-full min-h-[44px] px-3 py-2.5 text-sm bg-[var(--color-void)] border border-[var(--color-void-lighter)] rounded text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] focus:outline-none focus:border-[var(--color-accent)]"
147149
/>
148150
</div>

dashboard/src/components/CreateSessionModal.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
* components/CreateSessionModal.tsx — Modal dialog for creating new sessions.
33
*/
44

5-
import { useState, useEffect, useRef, useCallback } from 'react';
5+
import { useState, useEffect, useRef, useCallback } from 'react'
6+
import { useT } from '../i18n/context';
67
import { useFocusTrap } from '../hooks/useFocusTrap';
78
import { useNavigate } from 'react-router-dom';
89
import { X, Loader2, Plus, Trash2 } from 'lucide-react';
@@ -21,6 +22,7 @@ function makeRow(): BatchRow {
2122
}
2223

2324
export default function CreateSessionModal({ open, onClose }: CreateSessionModalProps) {
25+
const t = useT();
2426
const navigate = useNavigate();
2527
const workDirRef = useRef<HTMLInputElement>(null);
2628
const abortRef = useRef<AbortController | null>(null);
@@ -192,7 +194,7 @@ export default function CreateSessionModal({ open, onClose }: CreateSessionModal
192194
/>
193195

194196
{/* Modal */}
195-
<div ref={trapRef} role="dialog" aria-modal="true" aria-label="Create new session" className={`relative w-full ${mode === 'batch' ? 'max-w-2xl' : 'max-w-md'} mx-4 bg-[var(--color-surface)] border border-[var(--color-void-lighter)] rounded-lg shadow-2xl max-h-[90vh] overflow-y-auto`}>
197+
<div ref={trapRef} role="dialog" aria-modal="true" aria-label={t('aria.createNewSession')} className={`relative w-full ${mode === 'batch' ? 'max-w-2xl' : 'max-w-md'} mx-4 bg-[var(--color-surface)] border border-[var(--color-void-lighter)] rounded-lg shadow-2xl max-h-[90vh] overflow-y-auto`}>
196198
{/* Header */}
197199
<div className="flex items-center justify-between px-4 sm:px-5 py-4 border-b border-[var(--color-void-lighter)]">
198200
<div className="flex items-center gap-4">
@@ -235,7 +237,7 @@ export default function CreateSessionModal({ open, onClose }: CreateSessionModal
235237
)}
236238
</div>
237239
</div>
238-
<button aria-label="Close"
240+
<button aria-label={t('aria.close')}
239241
onClick={handleClose}
240242
className="min-h-[44px] min-w-[44px] flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors"
241243
>
@@ -388,7 +390,7 @@ export default function CreateSessionModal({ open, onClose }: CreateSessionModal
388390
<button
389391
type="button"
390392
onClick={() => removeBatchRow(i)}
391-
aria-label="Remove row"
393+
aria-label={t('aria.removeRow')}
392394
disabled={batchRows.length <= 1}
393395
className="min-h-[44px] min-w-[44px] flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-error)] transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
394396
>

dashboard/src/components/KeyboardShortcutsHelp/index.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { useEffect, useState } from 'react';
1+
import { useEffect, useState } from 'react'
2+
import { useT } from '../../i18n/context';
23
import { X, Keyboard } from 'lucide-react';
34
import { SHORTCUTS } from '../../hooks/useKeyboardShortcuts';
45

@@ -9,13 +10,14 @@ export function KeyboardShortcutsHelp({
910
open: boolean;
1011
onClose: () => void;
1112
}) {
13+
const translate = useT();
1214
const [visible, setVisible] = useState(false);
1315

1416
useEffect(() => {
1517
if (open) setVisible(true);
1618
else {
17-
const t = setTimeout(() => setVisible(false), 200);
18-
return () => clearTimeout(t);
19+
const timer = setTimeout(() => setVisible(false), 200);
20+
return () => clearTimeout(timer);
1921
}
2022
}, [open]);
2123

@@ -29,7 +31,7 @@ export function KeyboardShortcutsHelp({
2931
onClick={onClose}
3032
role="dialog"
3133
aria-modal="true"
32-
aria-label="Keyboard shortcuts"
34+
aria-label={translate('aria.keyboardShortcuts')}
3335
>
3436
<div
3537
className="w-full max-w-md rounded-xl border border-[var(--color-void-lighter)]/60 bg-[var(--color-surface)] p-6 shadow-2xl"
@@ -43,7 +45,7 @@ export function KeyboardShortcutsHelp({
4345
<button
4446
onClick={onClose}
4547
className="rounded p-1 text-[var(--color-text-muted)] hover:bg-[var(--color-void-lighter)]/50 hover:text-[var(--color-text-primary)] transition-colors"
46-
aria-label="Close"
48+
aria-label={translate('aria.close')}
4749
>
4850
<X className="h-4 w-4" />
4951
</button>

dashboard/src/components/Layout.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { logger } from '../utils/logger';
44
*/
55

66
import { NavLink, Outlet } from 'react-router-dom';
7-
import { useEffect, useState } from 'react';
7+
import { useEffect, useState } from 'react'
8+
import { useT } from '../i18n/context';
89
import Breadcrumb from './shared/Breadcrumb';
910
import { ErrorBoundary } from './shared/ErrorBoundary';
1011
import { useTheme } from '../hooks/useTheme';
@@ -105,6 +106,7 @@ function isMobileSidebarViewport(): boolean {
105106
}
106107

107108
export default function Layout() {
109+
const t = useT();
108110
const sseConnected = useStore((s) => s.sseConnected);
109111
const setSseConnected = useStore((s) => s.setSseConnected);
110112
const sseError = useStore((s) => s.sseError);
@@ -406,7 +408,7 @@ export default function Layout() {
406408

407409
{/* ── Sidebar ─────────────────────────────────────────── */}
408410
<aside
409-
aria-label="Primary sidebar"
411+
aria-label={t('aria.primarySidebar')}
410412
className={`
411413
fixed inset-y-0 left-0 z-40 flex flex-col border-r border-white/5 bg-transparent backdrop-blur-xl
412414
transition-all duration-300 ease-in-out
@@ -428,15 +430,15 @@ export default function Layout() {
428430
tabIndex={hiddenMobileSidebarControlTabIndex}
429431
disabled={isMobileSidebarHidden}
430432
className="md:hidden inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-lg text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 dark:text-[var(--color-text-muted)] dark:hover:bg-void-lighter dark:hover:text-[var(--color-text-primary)]"
431-
aria-label="Close menu"
433+
aria-label={t('aria.closeMenu')}
432434
aria-hidden={isMobileSidebarHidden ? 'true' : undefined}
433435
>
434436
<X className="h-5 w-5" />
435437
</button>
436438
</div>
437439

438440
{/* Nav links */}
439-
<nav className="flex flex-col gap-4 px-3 py-6 flex-1 overflow-y-auto overflow-x-hidden" aria-label="Main navigation">
441+
<nav className="flex flex-col gap-4 px-3 py-6 flex-1 overflow-y-auto overflow-x-hidden" aria-label={t('aria.mainNavigation')}>
440442
{NAV_GROUPS.map((group) => (
441443
<div key={group.label} className="flex flex-col gap-1">
442444
{!isCollapsed && (
@@ -471,7 +473,7 @@ export default function Layout() {
471473
{/* Bottom section: Settings + toggle + logout */}
472474
<div className="border-t border-white/5 px-3 py-4 flex flex-col gap-2">
473475
{identityLabel && identityDetailLabel && !isCollapsed && (
474-
<div className="px-3 py-2" aria-label="Signed in user">
476+
<div className="px-3 py-2" aria-label={t('aria.signedInUser')}>
475477
<p className="truncate text-xs font-medium text-slate-700 dark:text-[var(--color-text-primary)]">{identityLabel}</p>
476478
<p className="truncate text-[11px] text-slate-500 dark:text-[var(--color-text-muted)]">
477479
{identityDetailLabel}
@@ -519,7 +521,7 @@ export default function Layout() {
519521
onClick={handleLogout}
520522
tabIndex={hiddenMobileSidebarControlTabIndex}
521523
className={`flex items-center gap-2.5 rounded-lg px-3 py-3 min-h-[44px] text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-[var(--color-text-muted)] dark:hover:bg-void-lighter dark:hover:text-[var(--color-text-primary)] transition-colors w-full ${isCollapsed ? 'justify-center' : ''}`}
522-
aria-label="Sign out"
524+
aria-label={t('aria.signOut')}
523525
>
524526
<LogOut className="h-4 w-4 shrink-0" />
525527
{!isCollapsed && <span className="truncate">Sign out</span>}
@@ -540,7 +542,7 @@ export default function Layout() {
540542
tabIndex={isMobileDrawerOpen ? -1 : undefined}
541543
aria-hidden={isMobileDrawerOpen ? 'true' : undefined}
542544
className="md:hidden inline-flex h-11 w-11 items-center justify-center rounded-lg text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-[var(--color-text-muted)] dark:hover:bg-void-lighter dark:hover:text-[var(--color-text-primary)] transition-colors"
543-
aria-label="Open menu"
545+
aria-label={t('aria.openMenu')}
544546
>
545547
<Menu className="h-5 w-5" />
546548
</button>
@@ -559,7 +561,7 @@ export default function Layout() {
559561
<button
560562
type="button"
561563
onClick={openNewSession}
562-
aria-label="New Session (⌘N)"
564+
aria-label={t('aria.newSessionCmd')}
563565
title="New Session (⌘N)"
564566
className="inline-flex h-11 w-11 items-center justify-center rounded-lg p-2.5 min-h-[44px] min-w-[44px] text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-[var(--color-text-muted)] dark:hover:bg-void-lighter dark:hover:text-[var(--color-text-primary)] transition-colors"
565567
>

dashboard/src/components/NewSessionDrawer.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
* Width: 480px desktop, full-width mobile.
55
*/
66

7-
import { useState, useCallback, useEffect, useRef } from 'react';
7+
import { useState, useCallback, useEffect, useRef } from 'react'
8+
import { useT } from '../i18n/context';
89
import { useFocusTrap } from '../hooks/useFocusTrap';
910
import { useNavigate } from 'react-router-dom';
1011
import { Loader2, Plus, X } from 'lucide-react';
@@ -22,6 +23,7 @@ const PERMISSION_MODES = [
2223
];
2324

2425
export function NewSessionDrawer() {
26+
const t = useT();
2527
const navigate = useNavigate();
2628
const addToast = useToastStore((t) => t.addToast);
2729
const { newSessionOpen, closeNewSession } = useDrawerStore();
@@ -126,7 +128,7 @@ export function NewSessionDrawer() {
126128
key="drawer-panel"
127129
role="dialog"
128130
aria-modal="true"
129-
aria-label="New Session"
131+
aria-label={t('aria.newSession')}
130132
ref={trapRef as React.Ref<HTMLDivElement>}
131133
initial={{ x: '100%' }}
132134
animate={{ x: 0 }}
@@ -143,7 +145,7 @@ export function NewSessionDrawer() {
143145
<button
144146
type="button"
145147
onClick={closeNewSession}
146-
aria-label="Close drawer"
148+
aria-label={t('aria.closeDrawer')}
147149
className="rounded-lg p-2 text-[var(--color-text-muted)] hover:bg-white/5 hover:text-[var(--color-text-primary)] transition-colors"
148150
>
149151
<X className="h-4 w-4" />

dashboard/src/components/SaveTemplateModal.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
* components/SaveTemplateModal.tsx — Modal dialog for saving a session as a template.
33
*/
44

5-
import { useState, useEffect, useRef, useCallback } from 'react';
5+
import { useState, useEffect, useRef, useCallback } from 'react'
6+
import { useT } from '../i18n/context';
67
import { useFocusTrap } from '../hooks/useFocusTrap';
78
import { X, Loader2 } from 'lucide-react';
89
import { createTemplate } from '../api/client';
@@ -15,6 +16,7 @@ interface SaveTemplateModalProps {
1516
}
1617

1718
export default function SaveTemplateModal({ open, onClose, sessionId }: SaveTemplateModalProps) {
19+
const t = useT();
1820
const abortRef = useRef<AbortController | null>(null);
1921
const trapRef = useFocusTrap(open);
2022

@@ -95,13 +97,13 @@ export default function SaveTemplateModal({ open, onClose, sessionId }: SaveTemp
9597
ref={trapRef}
9698
role="dialog"
9799
aria-modal="true"
98-
aria-label="Save session as template"
100+
aria-label={t('aria.saveAsTemplate')}
99101
className="relative w-full max-w-md mx-4 bg-[var(--color-surface)] border border-[var(--color-void-lighter)] rounded-lg shadow-2xl max-h-[90vh] overflow-y-auto"
100102
>
101103
{/* Header */}
102104
<div className="flex items-center justify-between px-4 sm:px-5 py-4 border-b border-[var(--color-void-lighter)]">
103105
<h2 className="text-sm font-semibold text-[var(--color-text-primary)]">Save as Template</h2>
104-
<button aria-label="Close"
106+
<button aria-label={t('aria.close')}
105107
onClick={handleClose}
106108
className="min-h-[44px] min-w-[44px] flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors"
107109
>

dashboard/src/components/TemplateModal.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
* components/TemplateModal.tsx — Modal dialog for creating and editing session templates.
33
*/
44

5-
import { useState, useEffect, useRef, useCallback } from 'react';
5+
import { useState, useEffect, useRef, useCallback } from 'react'
6+
import { useT } from '../i18n/context';
67
import { useFocusTrap } from '../hooks/useFocusTrap';
78
import { X, Loader2 } from 'lucide-react';
89
import { createTemplate, updateTemplate } from '../api/client';
@@ -28,6 +29,7 @@ interface TemplateModalProps {
2829
}
2930

3031
export default function TemplateModal({ open, onClose, template, onSaved }: TemplateModalProps) {
32+
const t = useT();
3133
const abortRef = useRef<AbortController | null>(null);
3234
const nameInputRef = useRef<HTMLInputElement>(null);
3335
const trapRef = useFocusTrap(open);
@@ -164,7 +166,7 @@ export default function TemplateModal({ open, onClose, template, onSaved }: Temp
164166
<h2 className="text-sm font-semibold text-[var(--color-text-primary)]">
165167
{isEditing ? 'Edit Template' : 'Create Template'}
166168
</h2>
167-
<button aria-label="Close"
169+
<button aria-label={t('aria.close')}
168170
onClick={handleClose}
169171
className="min-h-[44px] min-w-[44px] flex items-center justify-center text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors"
170172
>

dashboard/src/components/ToastContainer.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
* - Colors via CSS vars: success=emerald, warning=amber, error=red, info=slate
88
*/
99

10-
import { useCallback, useEffect, useRef, useState } from 'react';
10+
import { useCallback, useEffect, useRef, useState } from 'react'
11+
import { useT } from '../i18n/context';
1112
import { X, CheckCircle, AlertTriangle, Info, AlertCircle, Trash2, Undo } from 'lucide-react';
1213
import { useToastStore } from '../store/useToastStore';
1314
import type { ToastType } from '../store/useToastStore';
@@ -43,6 +44,7 @@ function ToastItem({
4344
description?: string;
4445
undoAction?: () => void;
4546
}) {
47+
const t = useT();
4648
const removeToast = useToastStore((s) => s.removeToast);
4749
const [progress, setProgress] = useState(100);
4850
const Icon = TYPE_ICONS[type];
@@ -131,7 +133,7 @@ function ToastItem({
131133
<button
132134
onClick={handleUndo}
133135
className="shrink-0 flex items-center gap-1 rounded px-2 py-1 text-xs font-medium opacity-80 hover:opacity-100 transition-opacity bg-current/10"
134-
aria-label="Undo"
136+
aria-label={t('aria.undo')}
135137
>
136138
<Undo className="h-3 w-3" />
137139
Undo
@@ -140,7 +142,7 @@ function ToastItem({
140142
<button
141143
onClick={() => removeToast(id)}
142144
className="shrink-0 rounded p-0.5 opacity-60 hover:opacity-100 transition-opacity"
143-
aria-label="Dismiss"
145+
aria-label={t('aria.dismiss')}
144146
>
145147
<X className="h-3.5 w-3.5" />
146148
</button>
@@ -150,22 +152,23 @@ function ToastItem({
150152

151153
export default function ToastContainer() {
152154
const toasts = useToastStore((s) => s.toasts);
155+
const t = useT();
153156
const removeToast = useToastStore((s) => s.removeToast);
154157

155158
if (toasts.length === 0) return null;
156159

157160
return (
158161
<div
159162
aria-live="polite"
160-
aria-label="Notifications"
163+
aria-label={t('aria.notifications')}
161164
className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm w-full pointer-events-none"
162165
>
163166
{toasts.length > 1 && (
164167
<div className="flex justify-end pointer-events-auto">
165168
<button
166169
onClick={() => toasts.forEach((t) => removeToast(t.id))}
167170
className="flex items-center gap-1 rounded px-2 py-1 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors"
168-
aria-label="Dismiss all notifications"
171+
aria-label={t('aria.dismissAll')}
169172
>
170173
<Trash2 className="h-3 w-3" />
171174
Clear all

0 commit comments

Comments
 (0)