fix(triage): VGAP-03..07 replace hardcoded strings with i18n in 5 components

BulkActionBar: action labels, selected count, processing text
EmptyStates: search empty, not connected, configure token, open settings
IssueListHeader: title, open count, analyze/auto-fix labels, tooltips, filters
LabelManager: add label, filter, no match
AssigneeManager: assign, search, no match

All 28+ hardcoded strings now use t() with keys in en + fr common.json.
Updated BulkActionBar test expectations to match i18n keys.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Sondre Engebråten
2026-02-13 09:49:12 +01:00
co-authored by Claude Opus 4.6
parent 986ba6b6cf
commit bebaefe1f2
9 changed files with 135 additions and 81 deletions
@@ -1,4 +1,5 @@
import { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus, X, Check } from 'lucide-react';
import { Button } from '../../ui/button';
@@ -17,6 +18,7 @@ export function AssigneeManager({
onRemoveAssignee,
disabled,
}: AssigneeManagerProps) {
const { t } = useTranslation('common');
const [dropdownOpen, setDropdownOpen] = useState(false);
const [search, setSearch] = useState('');
const dropdownRef = useRef<HTMLDivElement>(null);
@@ -85,7 +87,7 @@ export function AssigneeManager({
aria-label="Assign"
>
<Plus className="h-3 w-3" />
Assign
{t('assignees.assign')}
</Button>
{/* Dropdown */}
@@ -95,7 +97,7 @@ export function AssigneeManager({
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search collaborators..."
placeholder={t('assignees.search')}
className="w-full px-2 py-1 text-xs border-b border-border bg-transparent focus:outline-none"
aria-label="Search collaborators"
/>
@@ -121,7 +123,7 @@ export function AssigneeManager({
})}
{filteredCollaborators.length === 0 && (
<div className="px-2 py-1.5 text-xs text-muted-foreground">
No matching collaborators
{t('assignees.noMatch')}
</div>
)}
</div>
@@ -14,14 +14,14 @@ interface BulkActionBarProps {
onDeselectAll?: () => void;
}
const BULK_ACTIONS: Array<{ action: BulkActionType; label: string }> = [
{ action: 'close', label: 'Close' },
{ action: 'reopen', label: 'Reopen' },
{ action: 'add-label', label: 'Add Label' },
{ action: 'remove-label', label: 'Remove Label' },
{ action: 'add-assignee', label: 'Assign' },
{ action: 'remove-assignee', label: 'Unassign' },
{ action: 'transition', label: 'Transition' },
const BULK_ACTIONS: Array<{ action: BulkActionType; labelKey: string }> = [
{ action: 'close', labelKey: 'bulk.actionClose' },
{ action: 'reopen', labelKey: 'bulk.actionReopen' },
{ action: 'add-label', labelKey: 'bulk.actionAddLabel' },
{ action: 'remove-label', labelKey: 'bulk.actionRemoveLabel' },
{ action: 'add-assignee', labelKey: 'bulk.actionAssign' },
{ action: 'remove-assignee', labelKey: 'bulk.actionUnassign' },
{ action: 'transition', labelKey: 'bulk.actionTransition' },
];
export function BulkActionBar({
@@ -60,7 +60,7 @@ export function BulkActionBar({
className="flex items-center gap-2 rounded-md border border-border bg-muted/50 px-3 py-2"
>
<span className="text-sm font-medium text-foreground">
{selectedCount} selected
{t('bulk.selected', { count: selectedCount })}
</span>
{onSelectAll && (
@@ -106,7 +106,7 @@ export function BulkActionBar({
</div>
) : (
<div className="flex items-center gap-1.5 ml-2">
{BULK_ACTIONS.map(({ action, label }) => (
{BULK_ACTIONS.map(({ action, labelKey }) => (
<button
key={action}
type="button"
@@ -114,7 +114,7 @@ export function BulkActionBar({
disabled={isOperating}
onClick={() => setPendingAction(action)}
>
{label}
{t(labelKey)}
</button>
))}
</div>
@@ -159,7 +159,7 @@ export function BulkActionBar({
{isOperating && progress && (
<span className="ml-auto text-xs text-muted-foreground">
Processing {progress.processedItems}/{progress.totalItems}...
{t('bulk.processing', { current: progress.processedItems, total: progress.totalItems })}
</span>
)}
</div>
@@ -1,36 +1,39 @@
import { useTranslation } from 'react-i18next';
import { Github, Settings2 } from 'lucide-react';
import { Button } from '../../ui/button';
import type { EmptyStateProps, NotConnectedStateProps } from '../types';
export function EmptyState({ searchQuery, icon: Icon = Github, message }: EmptyStateProps) {
const { t } = useTranslation('common');
return (
<div className="flex-1 flex flex-col items-center justify-center p-8 text-center">
<div className="w-12 h-12 rounded-full bg-muted/50 flex items-center justify-center mb-3">
<Icon className="h-6 w-6 text-muted-foreground" />
</div>
<p className="text-sm text-muted-foreground">
{searchQuery ? 'No issues match your search' : message}
{searchQuery ? t('issues.emptySearch') : message}
</p>
</div>
);
}
export function NotConnectedState({ error, onOpenSettings }: NotConnectedStateProps) {
const { t } = useTranslation('common');
return (
<div className="flex-1 flex flex-col items-center justify-center p-8 text-center">
<div className="w-16 h-16 rounded-full bg-muted/50 flex items-center justify-center mb-4">
<Github className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold text-foreground mb-2">
GitHub Not Connected
{t('issues.notConnected')}
</h3>
<p className="text-sm text-muted-foreground mb-4 max-w-md">
{error || 'Configure your GitHub token and repository in project settings to sync issues.'}
{error || t('issues.configureToken')}
</p>
{onOpenSettings && (
<Button onClick={onOpenSettings} variant="outline">
<Settings2 className="h-4 w-4 mr-2" />
Open Settings
{t('issues.openSettings')}
</Button>
)}
</div>
@@ -54,7 +54,7 @@ export function IssueListHeader({
</div>
<div>
<h2 className="text-lg font-semibold text-foreground">
GitHub Issues
{t('issues.title')}
</h2>
<p className="text-xs text-muted-foreground">
{repoFullName}
@@ -63,7 +63,7 @@ export function IssueListHeader({
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">
{openIssuesCount} open
{t('issues.openCount', { count: openIssuesCount })}
</Badge>
{onToggleTriageMode && (
<TooltipProvider>
@@ -117,11 +117,11 @@ export function IssueListHeader({
) : (
<Layers className="h-4 w-4 mr-2" />
)}
Analyze & Group Issues
{t('issues.analyzeGroup')}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
<p>Analyze up to 200 open issues, group similar ones, and review proposed batches before creating tasks.</p>
<p>{t('issues.analyzeGroupTooltip')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -140,7 +140,7 @@ export function IssueListHeader({
<Wand2 className="h-4 w-4 text-muted-foreground" />
)}
<Label htmlFor="auto-fix-toggle" className="text-sm cursor-pointer whitespace-nowrap">
Auto-Fix New
{t('issues.autoFixNew')}
</Label>
<Switch
id="auto-fix-toggle"
@@ -151,9 +151,9 @@ export function IssueListHeader({
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
<p>Automatically fix new issues as they come in.</p>
<p>{t('issues.autoFixTooltip')}</p>
{autoFixRunning && autoFixProcessing !== undefined && autoFixProcessing > 0 && (
<p className="mt-1 text-primary">Processing {autoFixProcessing} issue{autoFixProcessing > 1 ? 's' : ''}...</p>
<p className="mt-1 text-primary">{t('issues.autoFixProcessing', { count: autoFixProcessing })}</p>
)}
</TooltipContent>
</Tooltip>
@@ -167,7 +167,7 @@ export function IssueListHeader({
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search issues..."
placeholder={t('issues.searchPlaceholder')}
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-9"
@@ -179,9 +179,9 @@ export function IssueListHeader({
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="open">Open</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
<SelectItem value="all">All</SelectItem>
<SelectItem value="open">{t('issues.filterOpen')}</SelectItem>
<SelectItem value="closed">{t('issues.filterClosed')}</SelectItem>
<SelectItem value="all">{t('issues.filterAll')}</SelectItem>
</SelectContent>
</Select>
{onWorkflowFilterChange && (
@@ -1,4 +1,5 @@
import { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus, X, Check } from 'lucide-react';
import { Button } from '../../ui/button';
import { Badge } from '../../ui/badge';
@@ -22,6 +23,7 @@ export function LabelManager({
disabled,
isLoading,
}: LabelManagerProps) {
const { t } = useTranslation('common');
const [dropdownOpen, setDropdownOpen] = useState(false);
const [search, setSearch] = useState('');
const dropdownRef = useRef<HTMLDivElement>(null);
@@ -89,7 +91,7 @@ export function LabelManager({
aria-label="Add label"
>
<Plus className="h-3 w-3" />
Add Label
{t('labels.add')}
</Button>
{/* Dropdown */}
@@ -99,7 +101,7 @@ export function LabelManager({
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Filter labels..."
placeholder={t('labels.filter')}
className="w-full px-2 py-1 text-xs border-b border-border bg-transparent focus:outline-none"
aria-label="Filter labels"
/>
@@ -129,7 +131,7 @@ export function LabelManager({
})}
{filteredLabels.length === 0 && (
<div className="px-2 py-1.5 text-xs text-muted-foreground">
No matching labels
{t('labels.noMatch')}
</div>
)}
</div>
@@ -35,7 +35,7 @@ describe('BulkActionBar', () => {
isOperating={false}
/>,
);
expect(screen.getByText('5 selected')).toBeDefined();
expect(screen.getByText('bulk.selected')).toBeDefined();
});
it('Close button shows confirmation, confirm fires onBulkAction', () => {
@@ -48,7 +48,7 @@ describe('BulkActionBar', () => {
/>,
);
// Click Close — should show confirm prompt, NOT fire immediately
fireEvent.click(screen.getByText('Close'));
fireEvent.click(screen.getByText('bulk.actionClose'));
expect(onBulkAction).not.toHaveBeenCalled();
expect(screen.getByText('bulk.confirmMessage')).toBeDefined();
// Click Confirm — should fire onBulkAction
@@ -65,13 +65,13 @@ describe('BulkActionBar', () => {
isOperating={false}
/>,
);
fireEvent.click(screen.getByText('Close'));
fireEvent.click(screen.getByText('bulk.actionClose'));
expect(screen.getByText('bulk.confirmMessage')).toBeDefined();
// Click Cancel
fireEvent.click(screen.getByText('bulk.cancel'));
expect(onBulkAction).not.toHaveBeenCalled();
// Action buttons should be back
expect(screen.getByText('Close')).toBeDefined();
expect(screen.getByText('bulk.actionClose')).toBeDefined();
});
it('confirmation dialog has role=alert', () => {
@@ -82,7 +82,7 @@ describe('BulkActionBar', () => {
isOperating={false}
/>,
);
fireEvent.click(screen.getByText('Close'));
fireEvent.click(screen.getByText('bulk.actionClose'));
expect(screen.getByRole('alert')).toBeDefined();
});
@@ -142,7 +142,7 @@ describe('BulkActionBar', () => {
}}
/>,
);
expect(screen.getByText('Processing 2/5...')).toBeDefined();
expect(screen.getByText('bulk.processing')).toBeDefined();
});
it('renders Select All button when onSelectAll provided', () => {
@@ -441,7 +441,23 @@
"loadingMore": "Loading more...",
"scrollForMore": "Scroll for more",
"allLoaded": "All issues loaded",
"listLabel": "Issues"
"listLabel": "Issues",
"title": "GitHub Issues",
"openCount": "{{count}} open",
"searchPlaceholder": "Search issues...",
"filterOpen": "Open",
"filterClosed": "Closed",
"filterAll": "All",
"analyzeGroup": "Analyze & Group Issues",
"analyzeGroupTooltip": "Analyze up to 200 open issues, group similar ones, and review proposed batches before creating tasks.",
"autoFixNew": "Auto-Fix New",
"autoFixTooltip": "Automatically fix new issues as they come in.",
"autoFixProcessing": "Processing {{count}} issue(s)...",
"emptySearch": "No issues match your search",
"emptyList": "No issues found",
"notConnected": "GitHub Not Connected",
"configureToken": "Configure your GitHub token and repository in project settings to sync issues.",
"openSettings": "Open Settings"
},
"enrichment": {
"states": {
@@ -509,13 +525,16 @@
"add": "Add label",
"remove": "Remove label",
"search": "Search labels...",
"filter": "Filter labels...",
"noMatch": "No matching labels",
"limit": "Maximum of {{limit}} labels reached"
},
"assignees": {
"manage": "Manage assignees",
"assign": "Assign",
"unassign": "Unassign",
"search": "Search collaborators..."
"search": "Search collaborators...",
"noMatch": "No matching collaborators"
},
"bulk": {
"selectAll": "Select all",
@@ -526,6 +545,13 @@
"retryFailed": "Retry {{count}} failed",
"dismiss": "Dismiss",
"actions": "Bulk actions",
"actionClose": "Close",
"actionReopen": "Reopen",
"actionAddLabel": "Add Label",
"actionRemoveLabel": "Remove Label",
"actionAssign": "Assign",
"actionUnassign": "Unassign",
"actionTransition": "Transition",
"confirmMessage": "{{action}} {{count}} issues?",
"confirm": "Confirm",
"cancel": "Cancel"
@@ -441,7 +441,23 @@
"loadingMore": "Chargement...",
"scrollForMore": "Défiler pour plus",
"allLoaded": "Toutes les issues chargées",
"listLabel": "Issues"
"listLabel": "Issues",
"title": "Issues GitHub",
"openCount": "{{count}} ouvertes",
"searchPlaceholder": "Rechercher des issues...",
"filterOpen": "Ouvertes",
"filterClosed": "Fermées",
"filterAll": "Toutes",
"analyzeGroup": "Analyser et regrouper",
"analyzeGroupTooltip": "Analyser jusqu'à 200 issues ouvertes, regrouper les similaires et vérifier les lots proposés avant de créer des tâches.",
"autoFixNew": "Auto-fix nouvelles",
"autoFixTooltip": "Corriger automatiquement les nouvelles issues.",
"autoFixProcessing": "Traitement de {{count}} issue(s)...",
"emptySearch": "Aucune issue ne correspond à votre recherche",
"emptyList": "Aucune issue trouvée",
"notConnected": "GitHub non connecté",
"configureToken": "Configurez votre token GitHub et le dépôt dans les paramètres du projet pour synchroniser les issues.",
"openSettings": "Ouvrir les paramètres"
},
"enrichment": {
"states": {
@@ -509,13 +525,16 @@
"add": "Ajouter un label",
"remove": "Supprimer le label",
"search": "Rechercher des labels...",
"filter": "Filtrer les labels...",
"noMatch": "Aucun label correspondant",
"limit": "Maximum de {{limit}} labels atteint"
},
"assignees": {
"manage": "Gérer les assignés",
"assign": "Assigner",
"unassign": "Désassigner",
"search": "Rechercher des collaborateurs..."
"search": "Rechercher des collaborateurs...",
"noMatch": "Aucun collaborateur correspondant"
},
"bulk": {
"selectAll": "Tout sélectionner",
@@ -526,6 +545,13 @@
"retryFailed": "Réessayer {{count}} échoués",
"dismiss": "Fermer",
"actions": "Actions groupées",
"actionClose": "Fermer",
"actionReopen": "Rouvrir",
"actionAddLabel": "Ajouter un label",
"actionRemoveLabel": "Supprimer un label",
"actionAssign": "Assigner",
"actionUnassign": "Désassigner",
"actionTransition": "Transition",
"confirmMessage": "{{action}} {{count}} issues ?",
"confirm": "Confirmer",
"cancel": "Annuler"
+33 -38
View File
@@ -3,7 +3,7 @@
**Branch:** `terminal/enhancement-issues-tab`
**Created:** 2026-02-13
**Total Gaps:** 46 confirmed (from 9-agent triple-verified audit)
**Status:** 2 / 17 complete
**Status:** 7 / 17 complete
---
@@ -61,74 +61,69 @@ Each gap has: ID, description, status, files to modify, verification source, tes
## TIER 2 — i18n Hardcoded Strings
### VGAP-03: BulkActionBar.tsx hardcoded action labels (8 strings)
- **Status:** `PENDING`
- **Status:** `DONE`
- **Priority:** SHOULD-FIX
- **Scope:** Medium
- **Verified by:** i18n agent + Verifier-2 (CONFIRMED)
- **Doc ref:** CLAUDE.md > i18n required
- **Files to modify:** `renderer/components/github-issues/components/BulkActionBar.tsx`, `shared/i18n/locales/en/common.json`, `shared/i18n/locales/fr/common.json`
- **Problem:** `BULK_ACTIONS` constant (lines 17-25) has hardcoded labels: 'Close', 'Reopen', 'Add Label', 'Remove Label', 'Assign', 'Unassign', 'Transition'. Line 63 has hardcoded `{selectedCount} selected`. Line 162 has hardcoded `Processing X/Y...`
- **Fix:** Add `useTranslation` hook, add i18n keys `bulk.actions.close`, `bulk.actions.reopen`, etc. Replace hardcoded text with `t()` calls. Add keys to both en + fr.
- **Tests:** Verify translated keys render (existing tests may need i18n mock update)
- **Test status:** —
- **Files modified:** `renderer/components/github-issues/components/BulkActionBar.tsx`, `shared/i18n/locales/en/common.json`, `shared/i18n/locales/fr/common.json`
- **Fix:** Changed `BULK_ACTIONS` constant to use `labelKey` instead of `label`, rendering via `t(labelKey)`. Replaced `{selectedCount} selected` with `t('bulk.selected', { count })`. Replaced `Processing X/Y...` with `t('bulk.processing', { current, total })`. Updated test expectations to match i18n keys.
- **Tests:** 3860 pass, lint clean
- **Test status:** `PASS`
- **Depends on:** None
- **Commit:**
- **Commit:** VGAP-03..07
### VGAP-04: EmptyStates.tsx hardcoded strings (4 strings)
- **Status:** `PENDING`
- **Status:** `DONE`
- **Priority:** SHOULD-FIX
- **Scope:** Small
- **Verified by:** i18n agent + Verifier-2 (CONFIRMED)
- **Doc ref:** CLAUDE.md > i18n required
- **Files to modify:** `renderer/components/github-issues/components/EmptyStates.tsx`, `shared/i18n/locales/en/common.json`, `shared/i18n/locales/fr/common.json`
- **Problem:** Line 12: 'No issues match your search'. Line 25: 'GitHub Not Connected'. Line 28: 'Configure your GitHub token...'. Line 33: 'Open Settings'.
- **Fix:** Add `useTranslation` hook, add i18n keys `issues.emptySearch`, `issues.notConnected`, `issues.configureToken`, `issues.openSettings`. Replace with `t()`. Add to both en + fr.
- **Tests:** Verify translated keys render
- **Test status:** —
- **Files modified:** `renderer/components/github-issues/components/EmptyStates.tsx`, `shared/i18n/locales/en/common.json`, `shared/i18n/locales/fr/common.json`
- **Fix:** Added `useTranslation('common')` to both EmptyState and NotConnectedState. Replaced 4 hardcoded strings with `t('issues.emptySearch')`, `t('issues.notConnected')`, `t('issues.configureToken')`, `t('issues.openSettings')`.
- **Tests:** 3860 pass, lint clean
- **Test status:** `PASS`
- **Depends on:** None
- **Commit:**
- **Commit:** VGAP-03..07
### VGAP-05: IssueListHeader.tsx hardcoded strings (9+ strings)
- **Status:** `PENDING`
- **Status:** `DONE`
- **Priority:** SHOULD-FIX
- **Scope:** Medium
- **Verified by:** i18n agent + Verifier-2 (CONFIRMED)
- **Doc ref:** CLAUDE.md > i18n required
- **Files to modify:** `renderer/components/github-issues/components/IssueListHeader.tsx`, `shared/i18n/locales/en/common.json`, `shared/i18n/locales/fr/common.json`
- **Problem:** Line 57: 'GitHub Issues'. Line 66: '{N} open'. Line 120: 'Analyze & Group Issues'. Line 124: tooltip text. Line 143: 'Auto-Fix New'. Line 154: tooltip. Line 156: 'Processing...'. Line 170: 'Search issues...'. Lines 182-184: 'Open', 'Closed', 'All'.
- **Fix:** Add i18n keys for all strings. Use `t()` with interpolation for counts. Add to both en + fr.
- **Tests:** Verify translated keys render
- **Test status:** —
- **Files modified:** `renderer/components/github-issues/components/IssueListHeader.tsx`, `shared/i18n/locales/en/common.json`, `shared/i18n/locales/fr/common.json`
- **Fix:** Replaced 10 hardcoded strings with `t()` calls: title, openCount (with interpolation), analyzeGroup, analyzeGroupTooltip, autoFixNew, autoFixTooltip, autoFixProcessing (with interpolation), searchPlaceholder, filterOpen/Closed/All.
- **Tests:** 3860 pass, lint clean
- **Test status:** `PASS`
- **Depends on:** None
- **Commit:**
- **Commit:** VGAP-03..07
### VGAP-06: LabelManager.tsx hardcoded strings (3 strings)
- **Status:** `PENDING`
- **Status:** `DONE`
- **Priority:** SHOULD-FIX
- **Scope:** Small
- **Verified by:** i18n agent + Verifier-2 (CONFIRMED)
- **Doc ref:** CLAUDE.md > i18n required
- **Files to modify:** `renderer/components/github-issues/components/LabelManager.tsx`, `shared/i18n/locales/en/common.json`, `shared/i18n/locales/fr/common.json`
- **Problem:** Line 92: 'Add Label'. Line 102: 'Filter labels...'. Line 132: 'No matching labels'. Keys exist in common.json (`labels.add`, etc.) but component doesn't import or use `useTranslation`.
- **Fix:** Import `useTranslation`, replace hardcoded strings with `t('common:labels.add')`, `t('common:labels.filter')`, `t('common:labels.noMatch')`. Add any missing keys to both en + fr.
- **Tests:** Verify translated keys render
- **Test status:** —
- **Files modified:** `renderer/components/github-issues/components/LabelManager.tsx`, `shared/i18n/locales/en/common.json`, `shared/i18n/locales/fr/common.json`
- **Fix:** Added `useTranslation('common')`. Replaced `'Add Label'``t('labels.add')`, `'Filter labels...'``t('labels.filter')`, `'No matching labels'``t('labels.noMatch')`.
- **Tests:** 3860 pass, lint clean
- **Test status:** `PASS`
- **Depends on:** None
- **Commit:**
- **Commit:** VGAP-03..07
### VGAP-07: AssigneeManager.tsx hardcoded strings (3 strings)
- **Status:** `PENDING`
- **Status:** `DONE`
- **Priority:** SHOULD-FIX
- **Scope:** Small
- **Verified by:** i18n agent + Verifier-2 (CONFIRMED)
- **Doc ref:** CLAUDE.md > i18n required
- **Files to modify:** `renderer/components/github-issues/components/AssigneeManager.tsx`, `shared/i18n/locales/en/common.json`, `shared/i18n/locales/fr/common.json`
- **Problem:** Line 88: 'Assign'. Line 98: 'Search collaborators...'. Line 124: 'No matching collaborators'. Keys exist in common.json (`assignees.assign`, etc.) but component doesn't use `useTranslation`.
- **Fix:** Import `useTranslation`, replace hardcoded strings with `t()` calls using existing keys. Add any missing keys to both en + fr.
- **Tests:** Verify translated keys render
- **Test status:** —
- **Files modified:** `renderer/components/github-issues/components/AssigneeManager.tsx`, `shared/i18n/locales/en/common.json`, `shared/i18n/locales/fr/common.json`
- **Fix:** Added `useTranslation('common')`. Replaced `'Assign'``t('assignees.assign')`, `'Search collaborators...'``t('assignees.search')`, `'No matching collaborators'``t('assignees.noMatch')`.
- **Tests:** 3860 pass, lint clean
- **Test status:** `PASS`
- **Depends on:** None
- **Commit:**
- **Commit:** VGAP-03..07
---
@@ -289,10 +284,10 @@ Each gap has: ID, description, status, files to modify, verification source, tes
| Tier | Description | Total | Done | Remaining |
|------|-------------|-------|------|-----------|
| 1 | Critical Wiring | 2 | 2 | 0 |
| 2 | i18n Hardcoded Strings | 5 | 0 | 5 |
| 2 | i18n Hardcoded Strings | 5 | 5 | 0 |
| 3 | Accessibility Keyboard | 2 | 0 | 2 |
| 4 | IPC Consistency | 3 | 0 | 3 |
| 5 | Phase 3 Audit Gaps | 5 | 0 | 5 |
| **Total** | | **17** | **0** | **17** |
| **Total** | | **17** | **7** | **10** |
Note: VGAP-03 through VGAP-07 contain 28+ individual hardcoded strings grouped by component file. The 17 gap count represents work units (one per component/file), not individual string count.