Skip to content

Commit 7d24f8b

Browse files
feat(dashboard): extract 152+ hardcoded aria-labels to i18n (batch 3)
- Replace hardcoded aria-label strings with t('aria.keyName') across 70 files - Add useT mock to test setup with English catalog resolution + param substitution - Add missing catalog keys: aria.searchPipelines, pipelines.sortBy - Fix PipelinesPage: use searchPipelines instead of searchSessions - Fix test assertions: update static analysis patterns for i18n keys - Update OverviewPage test mock to resolve from catalog - TSC clean, 128 test files pass, build green Refs: #3229
1 parent 536ff01 commit 7d24f8b

75 files changed

Lines changed: 376 additions & 173 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dashboard/src/__tests__/AuditPage.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ describe('AuditPage', () => {
226226
expect(screen.getByText('Page 1 of 2')).toBeDefined();
227227
});
228228

229-
await act(async () => { fireEvent.click(screen.getByLabelText('Next page')); });
229+
await act(async () => { fireEvent.click(screen.getByLabelText('Go to next page')); });
230230

231231
await waitFor(() => {
232232
expect(mockFetchAuditLogs).toHaveBeenLastCalledWith(expect.objectContaining({

dashboard/src/__tests__/LiveTerminal.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const src = readFileSync(resolve(__dirname, '../components/session/LiveTerminal.
1313

1414
describe('LiveTerminal — streaming failure UX (issue #2347)', () => {
1515
it('failure detail banner has accessible retry button', () => {
16-
expect(src).toContain('aria-label="Retry terminal connection"');
16+
expect(src).toContain('aria-label={t("aria.retryTerminal")}');
1717
});
1818

1919
it('failure banner mentions transcript and metrics fallback', () => {

dashboard/src/__tests__/OverviewPage.test.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,20 @@ vi.mock('../store/useStore', () => ({
2929
useStore: vi.fn((sel: (s: Record<string, unknown>) => unknown) => sel({ sseError: null })),
3030
}));
3131

32-
// Mock i18n
33-
vi.mock('../i18n/context', () => ({
34-
useT: () => (key: string) => key,
35-
}));
32+
// Mock i18n — resolves keys from the English catalog
33+
vi.mock('../i18n/context', async () => {
34+
const { en } = await import('../i18n/en');
35+
const catalog: Record<string, string> = {};
36+
const flatten = (obj: any, prefix: string) => {
37+
for (const [k, v] of Object.entries(obj)) {
38+
const key = prefix ? prefix + '.' + k : k;
39+
if (typeof v === 'string') catalog[key] = v;
40+
else if (typeof v === 'object' && v !== null) flatten(v, key);
41+
}
42+
};
43+
flatten(en, '');
44+
return { useT: () => (key: string) => catalog[key] || key };
45+
});
3646

3747
// Mock child components
3848
vi.mock('../components/overview/HomeStatusPanel', () => ({
@@ -58,7 +68,7 @@ describe('OverviewPage (CCMeter redesign)', () => {
5868

5969
it('renders page title', () => {
6070
render(<OverviewPage />);
61-
expect(screen.getByText('overview.title')).not.toBeNull();
71+
expect(screen.getByText('Overview')).not.toBeNull();
6272
});
6373

6474
it('renders New Session button', () => {

dashboard/src/__tests__/TerminalPassthrough.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('TerminalPassthrough', () => {
2020

2121
describe('streaming failure UX (issue #2347)', () => {
2222
it('failure detail banner has accessible retry button', () => {
23-
expect(src).toContain('aria-label="Retry terminal connection"');
23+
expect(src).toContain('aria.retryTerminal');
2424
});
2525

2626
it('failure banner mentions transcript and metrics fallback', () => {

dashboard/src/__tests__/i18n-integration.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
*/
44

55
import { describe, it, expect, vi, beforeEach } from 'vitest';
6+
// Needs real useT — override global mock
7+
vi.unmock("../i18n/context");
8+
69
vi.mock('../utils/logger', () => ({
710
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
811
}));

dashboard/src/__tests__/setup.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,42 @@ vi.mock('../../api/resilient-websocket', () => ({
6464
}),
6565
}));
6666

67+
68+
// Safe useT mock: resolves i18n keys using the English catalog.
69+
// Returns a stable function reference to avoid re-render loops.
70+
vi.mock('../i18n/context', async (importOriginal) => {
71+
const actual = await importOriginal<typeof import('../i18n/context')>();
72+
let enObj: any = {};
73+
try {
74+
const mod = await vi.importActual<typeof import('../i18n/en')>('../i18n/en');
75+
enObj = (mod as any).en || (mod as any).default || mod;
76+
} catch {
77+
// Fallback: empty catalog
78+
}
79+
80+
const catalog: Record<string, string> = {};
81+
const flatten = (obj: any, prefix: string) => {
82+
for (const [k, v] of Object.entries(obj)) {
83+
const key = prefix ? prefix + '.' + k : k;
84+
if (typeof v === 'string') catalog[key] = v;
85+
else if (typeof v === 'object' && v !== null) flatten(v, key);
86+
}
87+
};
88+
flatten(enObj, '');
89+
90+
const stableT = (key: string, params?: Record<string, string | number>): string => {
91+
let val = catalog[key] || key;
92+
if (params) {
93+
Object.entries(params).forEach(([k, v]) => {
94+
val = val.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
95+
});
96+
}
97+
return val;
98+
};
99+
100+
return {
101+
...actual,
102+
useT: () => stableT,
103+
I18nProvider: actual.I18nProvider,
104+
};
105+
});

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.newSessionCmd')) {
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/ConfirmDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export function ConfirmDialog({
103103
<button
104104
type="button"
105105
onClick={onCancel}
106-
className="flex-1 min-h-[44px] px-3 py-2 text-xs font-medium rounded bg-[var(--color-void)] border border-[var(--color-void-lighter)] text-[var(--color-text-primary)] hover:text-[var(--color-text-primary)] hover:border-[#333] transition-colors"
106+
className="flex-1 min-h-[44px] px-3 py-2 text-xs font-medium rounded bg-[var(--color-void)] border border-[var(--color-void-lighter)] text-[var(--color-text-primary)] hover:text-[var(--color-text-primary)] hover:border-muted transition-colors"
107107
>
108108
{cancelLabel}
109109
</button>

dashboard/src/components/CreatePipelineModal.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useFocusTrap } from '../hooks/useFocusTrap';
77
import { useNavigate } from 'react-router-dom';
88
import { X, Loader2, Plus, Trash2 } from 'lucide-react';
99
import { createPipeline } from '../api/client';
10+
import { useT } from '../i18n/context';
1011

1112
interface CreatePipelineModalProps {
1213
open: boolean;
@@ -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: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useNavigate } from 'react-router-dom';
88
import { X, Loader2, Plus, Trash2 } from 'lucide-react';
99
import { createSession, batchCreateSessions, getTemplates } from '../api/client';
1010
import type { SessionTemplate } from '../types';
11+
import { useT } from '../i18n/context';
1112

1213
interface CreateSessionModalProps {
1314
open: boolean;
@@ -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
>

0 commit comments

Comments
 (0)