diff --git a/apps/frontend/src/renderer/components/AddFeatureDialog.tsx b/apps/frontend/src/renderer/components/AddFeatureDialog.tsx index d29e2b97..d139298b 100644 --- a/apps/frontend/src/renderer/components/AddFeatureDialog.tsx +++ b/apps/frontend/src/renderer/components/AddFeatureDialog.tsx @@ -208,6 +208,7 @@ export function AddFeatureDialog({ value={title} onChange={(e) => setTitle(e.target.value)} disabled={isSaving} + aria-required="true" /> @@ -223,6 +224,7 @@ export function AddFeatureDialog({ onChange={(e) => setDescription(e.target.value)} rows={3} disabled={isSaving} + aria-required="true" /> @@ -253,7 +255,7 @@ export function AddFeatureDialog({ onValueChange={setPhaseId} disabled={isSaving} > - + @@ -338,7 +340,7 @@ export function AddFeatureDialog({ {/* Error */} {error && ( -
+
{error}
diff --git a/apps/frontend/src/renderer/components/AddProjectModal.tsx b/apps/frontend/src/renderer/components/AddProjectModal.tsx index fa8db82c..852f3feb 100644 --- a/apps/frontend/src/renderer/components/AddProjectModal.tsx +++ b/apps/frontend/src/renderer/components/AddProjectModal.tsx @@ -167,6 +167,7 @@ export function AddProjectModal({ open, onOpenChange, onProjectAdded }: AddProje 'bg-card hover:bg-accent hover:border-accent transition-all duration-200', 'text-left group' )} + aria-label={t('addProject.openExistingAriaLabel')} >
@@ -188,6 +189,7 @@ export function AddProjectModal({ open, onOpenChange, onProjectAdded }: AddProje 'bg-card hover:bg-accent hover:border-accent transition-all duration-200', 'text-left group' )} + aria-label={t('addProject.createNewAriaLabel')} >
@@ -203,7 +205,7 @@ export function AddProjectModal({ open, onOpenChange, onProjectAdded }: AddProje
{error && ( -
+
{error}
)} @@ -272,7 +274,7 @@ export function AddProjectModal({ open, onOpenChange, onProjectAdded }: AddProje
{error && ( -
+
{error}
)} diff --git a/apps/frontend/src/renderer/components/ClaudeCodeStatusBadge.tsx b/apps/frontend/src/renderer/components/ClaudeCodeStatusBadge.tsx index 06744006..726982fa 100644 --- a/apps/frontend/src/renderer/components/ClaudeCodeStatusBadge.tsx +++ b/apps/frontend/src/renderer/components/ClaudeCodeStatusBadge.tsx @@ -294,9 +294,10 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps) size="sm" className="w-full text-xs text-muted-foreground gap-1" onClick={() => window.electronAPI?.openExternal?.('https://claude.ai/code')} + aria-label={t('navigation:claudeCode.learnMoreAriaLabel', 'Learn more about Claude Code (opens in new window)')} > {t('navigation:claudeCode.learnMore', 'Learn more about Claude Code')} - +
diff --git a/apps/frontend/src/renderer/components/CompetitorAnalysisViewer.tsx b/apps/frontend/src/renderer/components/CompetitorAnalysisViewer.tsx index c71043d7..53f47767 100644 --- a/apps/frontend/src/renderer/components/CompetitorAnalysisViewer.tsx +++ b/apps/frontend/src/renderer/components/CompetitorAnalysisViewer.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import { TrendingUp, ExternalLink, AlertCircle } from 'lucide-react'; import { Dialog, @@ -21,6 +22,8 @@ export function CompetitorAnalysisViewer({ open, onOpenChange, }: CompetitorAnalysisViewerProps) { + const { t } = useTranslation('common'); + if (!analysis) return null; return ( @@ -66,9 +69,11 @@ export function CompetitorAnalysisViewer({ target="_blank" rel="noopener noreferrer" className="text-primary hover:underline flex items-center gap-1 text-sm ml-4" + aria-label={t('accessibility.visitExternalLink', { name: competitor.name })} > - +
diff --git a/apps/frontend/src/renderer/components/FileExplorerPanel.tsx b/apps/frontend/src/renderer/components/FileExplorerPanel.tsx index e7c0b980..5c598d84 100644 --- a/apps/frontend/src/renderer/components/FileExplorerPanel.tsx +++ b/apps/frontend/src/renderer/components/FileExplorerPanel.tsx @@ -1,4 +1,5 @@ import { motion, AnimatePresence } from 'motion/react'; +import { useTranslation } from 'react-i18next'; import { X, FolderTree, RefreshCw } from 'lucide-react'; import { Button } from './ui/button'; import { ScrollArea } from './ui/scroll-area'; @@ -34,6 +35,7 @@ const contentVariants = { }; export function FileExplorerPanel({ projectPath }: FileExplorerPanelProps) { + const { t } = useTranslation('common'); const { isOpen, close, clearCache, loadDirectory } = useFileExplorerStore(); const handleRefresh = () => { @@ -80,17 +82,18 @@ export function FileExplorerPanel({ projectPath }: FileExplorerPanelProps) { size="icon" className="h-6 w-6" onClick={handleRefresh} - title="Refresh" + aria-label={t('buttons.refresh')} > - +
diff --git a/apps/frontend/src/renderer/components/FileTreeItem.tsx b/apps/frontend/src/renderer/components/FileTreeItem.tsx index e0af5b94..d6273b5a 100644 --- a/apps/frontend/src/renderer/components/FileTreeItem.tsx +++ b/apps/frontend/src/renderer/components/FileTreeItem.tsx @@ -1,4 +1,5 @@ -import { useState, useRef, useEffect, type DragEvent } from 'react'; +import { useState, useRef, useEffect, type DragEvent, type KeyboardEvent } from 'react'; +import { useTranslation } from 'react-i18next'; import { ChevronRight, ChevronDown, Folder, File, FileCode, FileJson, FileText, FileImage, Loader2 } from 'lucide-react'; import { cn } from '../lib/utils'; import type { FileNode } from '../../shared/types'; @@ -70,6 +71,7 @@ export function FileTreeItem({ isLoading, onToggle, }: FileTreeItemProps) { + const { t } = useTranslation('common'); const [isDragging, setIsDragging] = useState(false); const dragImageRef = useRef(null); @@ -98,6 +100,16 @@ export function FileTreeItem({ } }; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + if (node.isDirectory) { + onToggle(); + } + } + }; + const handleDragStart = (e: DragEvent) => { e.stopPropagation(); setIsDragging(true); @@ -147,37 +159,47 @@ export function FileTreeItem({ return (
{/* Expand/collapse chevron for directories */} {node.isDirectory ? ( ) : ( - +
) : ( -
+
{/* Personal account */} {githubUsername && ( diff --git a/apps/frontend/src/renderer/components/Insights.tsx b/apps/frontend/src/renderer/components/Insights.tsx index 72e01a9a..3f3a9b5f 100644 --- a/apps/frontend/src/renderer/components/Insights.tsx +++ b/apps/frontend/src/renderer/components/Insights.tsx @@ -1,4 +1,5 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { MessageSquare, Send, @@ -15,7 +16,7 @@ import { PanelLeftClose, PanelLeft } from 'lucide-react'; -import ReactMarkdown from 'react-markdown'; +import ReactMarkdown, { type Components } from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Button } from './ui/button'; import { Textarea } from './ui/textarea'; @@ -46,42 +47,40 @@ import { TASK_COMPLEXITY_COLORS } from '../../shared/constants'; -// Safe link renderer for ReactMarkdown to prevent phishing and ensure external links open safely -const SafeLink = ({ href, children, ...props }: React.AnchorHTMLAttributes) => { - // Validate URL - only allow http, https, and relative links - const isValidUrl = href && ( - href.startsWith('http://') || - href.startsWith('https://') || - href.startsWith('/') || - href.startsWith('#') - ); +// createSafeLink - factory function that creates a SafeLink component with i18n support +const createSafeLink = (opensInNewWindowText: string) => { + return function SafeLink({ href, children, ...props }: React.AnchorHTMLAttributes) { + // Validate URL - only allow http, https, and relative links + const isValidUrl = href && ( + href.startsWith('http://') || + href.startsWith('https://') || + href.startsWith('/') || + href.startsWith('#') + ); - if (!isValidUrl) { - // For invalid or potentially malicious URLs, render as plain text - return {children}; - } + if (!isValidUrl) { + // For invalid or potentially malicious URLs, render as plain text + return {children}; + } - // External links get security attributes - const isExternal = href?.startsWith('http://') || href?.startsWith('https://'); + // External links get security attributes and accessibility indicator + const isExternal = href?.startsWith('http://') || href?.startsWith('https://'); - return ( - - {children} - - ); -}; - -// Markdown components with safe link rendering -const markdownComponents = { - a: SafeLink, + return ( + + {children} + {isExternal && {opensInNewWindowText}} + + ); + }; }; interface InsightsProps { @@ -89,6 +88,7 @@ interface InsightsProps { } export function Insights({ projectId }: InsightsProps) { + const { t } = useTranslation('common'); const session = useInsightsStore((state) => state.session); const sessions = useInsightsStore((state) => state.sessions); const status = useInsightsStore((state) => state.status); @@ -96,6 +96,11 @@ export function Insights({ projectId }: InsightsProps) { const currentTool = useInsightsStore((state) => state.currentTool); const isLoadingSessions = useInsightsStore((state) => state.isLoadingSessions); + // Create markdown components with translated accessibility text + const markdownComponents = useMemo(() => ({ + a: createSafeLink(t('accessibility.opensInNewWindow')), + }), [t]); + const [inputValue, setInputValue] = useState(''); const [creatingTask, setCreatingTask] = useState(null); const [taskCreated, setTaskCreated] = useState>(new Set()); @@ -295,6 +300,7 @@ export function Insights({ projectId }: InsightsProps) { handleCreateTask(message)} isCreatingTask={creatingTask === message.id} taskCreated={taskCreated.has(message.id)} @@ -387,6 +393,7 @@ export function Insights({ projectId }: InsightsProps) { interface MessageBubbleProps { message: InsightsChatMessage; + markdownComponents: Components; onCreateTask: () => void; isCreatingTask: boolean; taskCreated: boolean; @@ -394,6 +401,7 @@ interface MessageBubbleProps { function MessageBubble({ message, + markdownComponents, onCreateTask, isCreatingTask, taskCreated diff --git a/apps/frontend/src/renderer/components/KanbanBoard.tsx b/apps/frontend/src/renderer/components/KanbanBoard.tsx index 6541eede..05563d67 100644 --- a/apps/frontend/src/renderer/components/KanbanBoard.tsx +++ b/apps/frontend/src/renderer/components/KanbanBoard.tsx @@ -219,6 +219,7 @@ const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskCli size="icon" className="h-7 w-7 hover:bg-primary/10 hover:text-primary transition-colors" onClick={onAddClick} + aria-label={t('kanban.addTaskAriaLabel')} > @@ -229,7 +230,7 @@ const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskCli size="icon" className="h-7 w-7 hover:bg-muted-foreground/10 hover:text-muted-foreground transition-colors" onClick={onArchiveAll} - title={t('tooltips.archiveAllDone')} + aria-label={t('tooltips.archiveAllDone')} > diff --git a/apps/frontend/src/renderer/components/ProjectTabBar.tsx b/apps/frontend/src/renderer/components/ProjectTabBar.tsx index a3a9bc14..7836b8c7 100644 --- a/apps/frontend/src/renderer/components/ProjectTabBar.tsx +++ b/apps/frontend/src/renderer/components/ProjectTabBar.tsx @@ -1,4 +1,5 @@ import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { Plus } from 'lucide-react'; import { cn } from '../lib/utils'; import { Button } from './ui/button'; @@ -26,6 +27,8 @@ export function ProjectTabBar({ className, onSettingsClick }: ProjectTabBarProps) { + const { t } = useTranslation('common'); + // Keyboard shortcuts for tab navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -115,7 +118,7 @@ export function ProjectTabBar({ size="icon" className="h-8 w-8" onClick={onAddProject} - title="Add Project" + aria-label={t('projectTab.addProjectAriaLabel')} > diff --git a/apps/frontend/src/renderer/components/RateLimitModal.tsx b/apps/frontend/src/renderer/components/RateLimitModal.tsx index a3640165..b19c842a 100644 --- a/apps/frontend/src/renderer/components/RateLimitModal.tsx +++ b/apps/frontend/src/renderer/components/RateLimitModal.tsx @@ -375,9 +375,11 @@ export function RateLimitModal() { size="sm" className="gap-2" onClick={handleUpgrade} + aria-label={t('accessibility.upgradeSubscriptionAriaLabel')} > - +
diff --git a/apps/frontend/src/renderer/components/Sidebar.tsx b/apps/frontend/src/renderer/components/Sidebar.tsx index ce405852..5114b549 100644 --- a/apps/frontend/src/renderer/components/Sidebar.tsx +++ b/apps/frontend/src/renderer/components/Sidebar.tsx @@ -276,6 +276,7 @@ export function Sidebar({ key={item.id} onClick={() => handleNavClick(item.id)} disabled={!selectedProjectId} + aria-keyshortcuts={item.shortcut} className={cn( 'flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-all duration-200', 'hover:bg-accent hover:text-accent-foreground', @@ -354,6 +355,7 @@ export function Sidebar({ variant="ghost" size="icon" onClick={() => window.open('https://github.com/AndyMik90/Auto-Claude/issues', '_blank')} + aria-label={t('tooltips.help')} > diff --git a/apps/frontend/src/renderer/components/SortableProjectTab.tsx b/apps/frontend/src/renderer/components/SortableProjectTab.tsx index e6a9d6fd..d57cf129 100644 --- a/apps/frontend/src/renderer/components/SortableProjectTab.tsx +++ b/apps/frontend/src/renderer/components/SortableProjectTab.tsx @@ -160,7 +160,7 @@ export function SortableProjectTab({ isActive && 'opacity-100' )} onClick={onClose} - aria-label={t('projectTab.closeTab')} + aria-label={t('projectTab.closeTabAriaLabel')} > diff --git a/apps/frontend/src/renderer/components/TaskCreationWizard.tsx b/apps/frontend/src/renderer/components/TaskCreationWizard.tsx index e2c4d6f5..4bbed28a 100644 --- a/apps/frontend/src/renderer/components/TaskCreationWizard.tsx +++ b/apps/frontend/src/renderer/components/TaskCreationWizard.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback, useRef, useMemo, type ClipboardEvent, type DragEvent } from 'react'; +import { useTranslation } from 'react-i18next'; import { Loader2, ChevronDown, ChevronUp, Image as ImageIcon, X, RotateCcw, FolderTree, GitBranch } from 'lucide-react'; import { Dialog, @@ -59,6 +60,7 @@ export function TaskCreationWizard({ open, onOpenChange }: TaskCreationWizardProps) { + const { t } = useTranslation('tasks'); // Get selected agent profile from settings const { settings } = useSettingsStore(); const selectedProfile = DEFAULT_AGENT_PROFILES.find( @@ -797,6 +799,8 @@ export function TaskCreationWizard({ onDrop={handleTextareaDrop} rows={5} disabled={isCreating} + aria-required="true" + aria-describedby="description-help" className={cn( "resize-y min-h-[120px] max-h-[400px] relative bg-transparent", // Visual feedback when dragging over textarea @@ -815,7 +819,7 @@ export function TaskCreationWizard({ /> )} -

+

Files and images can be copy/pasted or dragged & dropped into the description.

@@ -852,6 +856,7 @@ export function TaskCreationWizard({ e.stopPropagation(); setImages(prev => prev.filter(img => img.id !== image.id)); }} + aria-label={t('images.removeImageAriaLabel', { filename: image.filename })} > @@ -915,6 +920,8 @@ export function TaskCreationWizard({ 'w-full justify-between py-2 px-3 rounded-md hover:bg-muted/50' )} disabled={isCreating} + aria-expanded={showAdvanced} + aria-controls="advanced-options-section" > Classification (optional) {showAdvanced ? ( @@ -926,7 +933,7 @@ export function TaskCreationWizard({ {/* Advanced Options */} {showAdvanced && ( -
+
{/* Category */}
@@ -1058,6 +1065,8 @@ export function TaskCreationWizard({ 'w-full justify-between py-2 px-3 rounded-md hover:bg-muted/50' )} disabled={isCreating} + aria-expanded={showGitOptions} + aria-controls="git-options-section" > @@ -1077,7 +1086,7 @@ export function TaskCreationWizard({ {/* Git Options */} {showGitOptions && ( -
+