docs: add project documentation — changelog, QA tracker, audit reports, and guides
Add comprehensive project documentation including changelog, QA tracker, code quality audit, implementation guide, K6 load testing guide, frontend exploration notes, and file mapping reference. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
648
FILE_MAPPING_GUIDE.md
Normal file
648
FILE_MAPPING_GUIDE.md
Normal file
@@ -0,0 +1,648 @@
|
||||
# GoodGo Frontend: File-by-File i18n & A11y Implementation Guide
|
||||
|
||||
## 📋 Complete File Mapping
|
||||
|
||||
### PHASE 1: INFRASTRUCTURE SETUP
|
||||
|
||||
#### 1. `middleware.ts` (CRITICAL)
|
||||
**Current:** Auth routing only
|
||||
**Changes:**
|
||||
- Add locale detection from URL pathname
|
||||
- Add cookie-based locale storage
|
||||
- Redirect `/en/*` and `/vi/*` paths appropriately
|
||||
- Add Accept-Language header fallback
|
||||
|
||||
**Pseudo-code:**
|
||||
```typescript
|
||||
export function middleware(request: NextRequest) {
|
||||
const pathname = request.nextUrl.pathname;
|
||||
|
||||
// Extract locale from URL: /en/*, /vi/*, or default
|
||||
const locale = extractLocale(pathname) || getPreferredLocale(request);
|
||||
|
||||
// ... existing auth logic ...
|
||||
|
||||
// If no locale prefix, add it
|
||||
if (!locale) {
|
||||
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
|
||||
}
|
||||
|
||||
// Set locale cookie for client-side
|
||||
response.cookies.set('goodgo_locale', locale);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. `app/layout.tsx` (CRITICAL)
|
||||
**Current:** Thai providers, hardcoded Vietnamese metadata
|
||||
**Changes:**
|
||||
- Change `lang="vi"` to dynamic locale
|
||||
- Update metadata to be i18n-aware
|
||||
- Wrap with `NextIntlClientProvider` from next-intl
|
||||
- Keep existing providers (ThemeProvider, QueryProvider, AuthProvider)
|
||||
|
||||
**Key changes:**
|
||||
```typescript
|
||||
import { getLocale, getTranslations } from 'next-intl/server';
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const locale = getLocale();
|
||||
const t = getTranslations();
|
||||
|
||||
return {
|
||||
title: t('common.site_name'),
|
||||
description: t('common.site_description'),
|
||||
openGraph: {
|
||||
locale: locale === 'en' ? 'en_US' : 'vi_VN',
|
||||
...
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. `i18n/config.ts` (NEW)
|
||||
**Create new file** with i18n configuration:
|
||||
```typescript
|
||||
export const locales = ['en', 'vi'] as const;
|
||||
export const defaultLocale = 'vi';
|
||||
|
||||
export const timeZone = 'Asia/Ho_Chi_Minh';
|
||||
```
|
||||
|
||||
#### 4. `public/locales/en.json` (NEW - LARGE FILE)
|
||||
**Structure:**
|
||||
```json
|
||||
{
|
||||
"common": {
|
||||
"site_name": "GoodGo",
|
||||
"site_description": "Smart real estate platform in Vietnam",
|
||||
"home": "Home",
|
||||
"search": "Search",
|
||||
"dashboard": "Dashboard",
|
||||
"logout": "Logout",
|
||||
"loading": "Loading...",
|
||||
"error": "An error occurred",
|
||||
"try_again": "Try again"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"phone": "Phone number",
|
||||
"password": "Password",
|
||||
"confirm_password": "Confirm password",
|
||||
"no_account": "Don't have an account?",
|
||||
"sign_up": "Sign up",
|
||||
"sign_in": "Sign in",
|
||||
"oauth_failed": "Social login failed. Please try again.",
|
||||
"access_denied": "You denied access. Please try again.",
|
||||
"invalid_request": "Invalid login request. Please try again.",
|
||||
"server_error": "Server error. Please try again later.",
|
||||
"show_password": "Show",
|
||||
"hide_password": "Hide"
|
||||
},
|
||||
"property": {
|
||||
"apartment": "Apartment",
|
||||
"house": "House",
|
||||
"villa": "Villa",
|
||||
"land": "Land",
|
||||
"office": "Office",
|
||||
"shophouse": "Shophouse",
|
||||
"price": "Price",
|
||||
"area": "Area",
|
||||
"bedrooms": "Bedrooms",
|
||||
"bathrooms": "Bathrooms",
|
||||
"direction": "Direction"
|
||||
},
|
||||
"transaction": {
|
||||
"sale": "Sale",
|
||||
"rent": "Rent",
|
||||
"buy": "Buy",
|
||||
"lease": "Lease"
|
||||
},
|
||||
"direction": {
|
||||
"north": "North",
|
||||
"south": "South",
|
||||
"east": "East",
|
||||
"west": "West",
|
||||
"northeast": "Northeast",
|
||||
"northwest": "Northwest",
|
||||
"southeast": "Southeast",
|
||||
"southwest": "Southwest"
|
||||
},
|
||||
"status": {
|
||||
"draft": "Draft",
|
||||
"pending_review": "Pending review",
|
||||
"active": "Active",
|
||||
"reserved": "Reserved",
|
||||
"sold": "Sold",
|
||||
"rented": "Rented",
|
||||
"expired": "Expired",
|
||||
"rejected": "Rejected"
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"min_length": "Minimum {count} characters",
|
||||
"invalid_phone": "Invalid phone number",
|
||||
"invalid_email": "Invalid email",
|
||||
"passwords_match": "Passwords must match"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"search": "Search",
|
||||
"create_listing": "Create listing",
|
||||
"my_listings": "My listings",
|
||||
"analytics": "Analytics",
|
||||
"valuation": "Valuation",
|
||||
"profile": "Profile",
|
||||
"subscription": "Subscription",
|
||||
"payments": "Payments",
|
||||
"admin_dashboard": "Admin",
|
||||
"admin_users": "Users",
|
||||
"admin_kyc": "KYC",
|
||||
"admin_moderation": "Moderation"
|
||||
},
|
||||
"landing": {
|
||||
"hero_title": "Find your perfect real estate",
|
||||
"hero_subtitle": "Smart real estate platform in Vietnam",
|
||||
"search_placeholder": "Enter area, project, or keyword...",
|
||||
"featured_listings": "Featured listings",
|
||||
"districts": "Popular districts",
|
||||
"stats_title": "GoodGo in numbers",
|
||||
"stats_listings": "Listings",
|
||||
"stats_users": "Users",
|
||||
"stats_transactions": "Successful transactions",
|
||||
"stats_cities": "Cities",
|
||||
"cta_title": "Have a property to list?",
|
||||
"cta_subtitle": "List for free today, reach thousands of potential buyers",
|
||||
"cta_register": "Sign up free",
|
||||
"cta_search": "Search now"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. `public/locales/vi.json` (NEW - LARGE FILE)
|
||||
Same structure as en.json but with Vietnamese translations.
|
||||
|
||||
---
|
||||
|
||||
### PHASE 2: CORE COMPONENT UPDATES
|
||||
|
||||
#### Files Requiring Translation Hook Integration
|
||||
|
||||
##### Layout Files
|
||||
|
||||
1. **`app/(public)/layout.tsx`**
|
||||
```typescript
|
||||
// Current: Hardcoded nav items
|
||||
const navItems = [
|
||||
{ href: '/', label: 'Trang chủ' },
|
||||
{ href: '/search', label: 'Tìm kiếm' },
|
||||
];
|
||||
|
||||
// After i18n:
|
||||
export default function PublicLayout() {
|
||||
const t = useTranslations('navigation');
|
||||
const navItems = [
|
||||
{ href: '/', label: t('home') },
|
||||
{ href: '/search', label: t('search') },
|
||||
];
|
||||
|
||||
// Also update footer content
|
||||
}
|
||||
```
|
||||
|
||||
2. **`app/(dashboard)/layout.tsx`**
|
||||
```typescript
|
||||
// Update all 8 nav items to use t('navigation.item_name')
|
||||
// Update theme toggle aria-label
|
||||
const toggleLabel = theme === 'light'
|
||||
? t('common.toggle_dark_mode')
|
||||
: t('common.toggle_light_mode');
|
||||
```
|
||||
|
||||
3. **`app/(auth)/layout.tsx`**
|
||||
Update any error messages or labels to use translations.
|
||||
|
||||
##### Page Files
|
||||
|
||||
1. **`app/(public)/page.tsx` (LARGE FILE)**
|
||||
**Areas to update:**
|
||||
- Hero section (title, subtitle)
|
||||
- Search form (placeholder)
|
||||
- Property type badges
|
||||
- Price ranges
|
||||
- City options
|
||||
- Section headings
|
||||
- Stats labels
|
||||
- CTA buttons
|
||||
|
||||
```typescript
|
||||
export default function LandingPage() {
|
||||
const t = useTranslations();
|
||||
|
||||
const PROPERTY_TYPES_LABELS = PROPERTY_TYPES.map(pt => ({
|
||||
value: pt.value,
|
||||
label: t(`property.${pt.value.toLowerCase()}`),
|
||||
}));
|
||||
|
||||
// Update all hardcoded strings:
|
||||
// Hero title: t('landing.hero_title')
|
||||
// Hero subtitle: t('landing.hero_subtitle')
|
||||
// Search placeholder: t('landing.search_placeholder')
|
||||
// etc.
|
||||
}
|
||||
```
|
||||
|
||||
2. **`app/(auth)/login/page.tsx`**
|
||||
```typescript
|
||||
// Update all form labels to use t()
|
||||
const phoneLabel = t('auth.phone');
|
||||
const passwordLabel = t('auth.password');
|
||||
const loginButton = t('auth.sign_in');
|
||||
|
||||
// OAuth error messages - move to translations
|
||||
const OAUTH_ERROR_MESSAGES = {
|
||||
oauth_failed: t('auth.oauth_failed'),
|
||||
access_denied: t('auth.access_denied'),
|
||||
// ... etc
|
||||
};
|
||||
```
|
||||
|
||||
3. **`app/(auth)/register/page.tsx`**
|
||||
Same pattern as login page.
|
||||
|
||||
4. **`app/(dashboard)/dashboard/page.tsx`** and all other dashboard pages
|
||||
Update section titles, empty states, button labels.
|
||||
|
||||
##### Search & Listing Pages
|
||||
|
||||
1. **`app/(public)/search/page.tsx`**
|
||||
Update search results headings, empty states, filter labels.
|
||||
|
||||
2. **`app/(public)/listings/[id]/page.tsx`**
|
||||
Update property detail labels.
|
||||
|
||||
3. **`app/(dashboard)/listings/page.tsx`**
|
||||
Update table headers, status labels, action labels.
|
||||
|
||||
4. **`app/(dashboard)/listings/new/page.tsx`**
|
||||
Uses listing-form-steps component (see below).
|
||||
|
||||
---
|
||||
|
||||
#### Component Files
|
||||
|
||||
##### Critical Components (Do First)
|
||||
|
||||
1. **`components/search/filter-bar.tsx` (HIGH PRIORITY)**
|
||||
```typescript
|
||||
// Current: Hardcoded arrays
|
||||
const CITIES = ['Hồ Chí Minh', 'Hà Nội', 'Đà Nẵng', ...];
|
||||
const PRICE_RANGES = [
|
||||
{ label: 'Dưới 1 tỷ', ... },
|
||||
{ label: '1 - 3 tỷ', ... },
|
||||
];
|
||||
|
||||
// After i18n:
|
||||
const CITIES = t('locations.cities').split(',');
|
||||
const PRICE_RANGES = [
|
||||
{ label: t('search.price_under_1b'), ... },
|
||||
{ label: t('search.price_1_3b'), ... },
|
||||
];
|
||||
```
|
||||
|
||||
2. **`components/listings/listing-form-steps.tsx` (HIGH PRIORITY - LARGE FILE)**
|
||||
This multi-step form has many labels to translate:
|
||||
- Step 1: Transaction type, property type, title, description
|
||||
- Step 2: Address fields, location
|
||||
- Step 3: Area, rooms, bathrooms, direction, year built, etc.
|
||||
- Step 4: Pricing
|
||||
|
||||
All field labels should use `t('form.field_name')` pattern.
|
||||
|
||||
3. **`components/auth/oauth-buttons.tsx`**
|
||||
```typescript
|
||||
// Update button text
|
||||
<Button>
|
||||
{t('auth.google')} // Currently hardcoded "Google"
|
||||
</Button>
|
||||
<Button>
|
||||
{t('auth.zalo')} // Currently hardcoded "Zalo"
|
||||
</Button>
|
||||
```
|
||||
|
||||
##### Medium Priority Components
|
||||
|
||||
1. **`components/search/property-card.tsx`**
|
||||
```typescript
|
||||
// Update PROPERTY_TYPE_LABELS to use translations
|
||||
const t = useTranslations();
|
||||
const PROPERTY_TYPE_LABELS = {
|
||||
APARTMENT: t('property.apartment'),
|
||||
HOUSE: t('property.house'),
|
||||
// ... etc
|
||||
};
|
||||
|
||||
// Update aria-labels to use translations
|
||||
aria-label={t('property.card_label', {
|
||||
title: listing.property.title,
|
||||
type: propertyTypeLabel,
|
||||
price: formatPrice(listing.priceVND)
|
||||
})}
|
||||
```
|
||||
|
||||
2. **`components/listings/listing-status-badge.tsx`**
|
||||
```typescript
|
||||
// Update status labels
|
||||
const LISTING_STATUSES = {
|
||||
DRAFT: { label: t('status.draft'), variant: 'secondary' },
|
||||
ACTIVE: { label: t('status.active'), variant: 'success' },
|
||||
// ... etc
|
||||
};
|
||||
```
|
||||
|
||||
3. **`components/valuation/valuation-form.tsx`**
|
||||
Update form labels and buttons.
|
||||
|
||||
4. **`components/listings/image-upload.tsx`**
|
||||
Update button text and error messages.
|
||||
|
||||
5. **All `components/ui/*.tsx` files with text**
|
||||
- Button: any default text
|
||||
- Dialog: Close button aria-label
|
||||
- Input: placeholder attrs if hardcoded
|
||||
- Label: any default text
|
||||
- Others: similar
|
||||
|
||||
---
|
||||
|
||||
### PHASE 3: VALIDATION & ERROR MESSAGES
|
||||
|
||||
#### 1. `lib/validations/auth.ts`
|
||||
```typescript
|
||||
// Current:
|
||||
const loginSchema = z.object({
|
||||
phone: z.string().min(1, 'Vui lòng nhập số điện thoại'),
|
||||
password: z.string().min(1, 'Vui lòng nhập mật khẩu'),
|
||||
});
|
||||
|
||||
// After i18n - move to message files and use in component:
|
||||
// In component:
|
||||
const t = useTranslations('validation');
|
||||
const schema = z.object({
|
||||
phone: z.string().min(1, t('required')),
|
||||
password: z.string().min(1, t('required')),
|
||||
});
|
||||
```
|
||||
|
||||
#### 2. `lib/validations/listings.ts` (LARGE FILE)
|
||||
Update all Zod validation error messages:
|
||||
- "Vui lòng chọn loại giao dịch" → `t('validation.transaction_required')`
|
||||
- "Tiêu đề tối thiểu 5 ký tự" → `t('validation.title_min_length')`
|
||||
- All other validation messages
|
||||
|
||||
#### 3. `lib/validations/valuation.ts`
|
||||
Similar pattern to listings.
|
||||
|
||||
---
|
||||
|
||||
### PHASE 4: UTILITY UPDATES
|
||||
|
||||
#### 1. `lib/utils.ts`
|
||||
No changes (already minimal).
|
||||
|
||||
#### 2. `lib/auth-store.ts`
|
||||
```typescript
|
||||
// Check if any error messages are hardcoded
|
||||
// If so, move to i18n and pass locale context
|
||||
```
|
||||
|
||||
#### 3. `lib/api-client.ts`
|
||||
Check if error messages from API need i18n wrapping.
|
||||
|
||||
#### 4. All `lib/*-api.ts` files
|
||||
Update error message handling if needed.
|
||||
|
||||
---
|
||||
|
||||
### PHASE 5: ACCESSIBILITY UPDATES
|
||||
|
||||
#### 1. `components/ui/dialog.tsx` (CRITICAL A11y)
|
||||
**Add focus management:**
|
||||
```typescript
|
||||
// Add focus trap
|
||||
// Save initial focus element
|
||||
// On mount: move focus to dialog
|
||||
// On close: restore focus to initial element
|
||||
// On Escape key: close dialog
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
function Dialog() {
|
||||
const initialFocusRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Set initial focus
|
||||
const firstButton = dialogRef.current?.querySelector('button');
|
||||
firstButton?.focus();
|
||||
|
||||
// Trap focus
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Tab') {
|
||||
// Prevent focus from leaving dialog
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
onClose?.();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
// Restore focus
|
||||
(initialFocusRef.current as HTMLElement | null)?.focus();
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. `components/ui/input.tsx`
|
||||
Add aria-describedby for error messages:
|
||||
```typescript
|
||||
export function Input({ error, ...props }) {
|
||||
const errorId = `${props.id}-error`;
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
{...props}
|
||||
aria-invalid={!!error}
|
||||
aria-describedby={error ? errorId : undefined}
|
||||
/>
|
||||
{error && <p id={errorId} role="alert">{error}</p>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. `components/ui/button.tsx`
|
||||
Ensure all buttons have visible focus indicator (already in CSS likely).
|
||||
Add aria-busy for loading state if used:
|
||||
```typescript
|
||||
export function Button({ disabled, isLoading, ...props }) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
disabled={disabled || isLoading}
|
||||
aria-busy={isLoading}
|
||||
>
|
||||
{/* content */}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Form Components
|
||||
Update all forms to use aria-describedby for error messages:
|
||||
- `app/(auth)/login/page.tsx` — Already has role="alert" ✓ but could use aria-describedby
|
||||
- `app/(auth)/register/page.tsx` — Same
|
||||
- `components/listings/listing-form-steps.tsx` — Add aria-describedby
|
||||
- `components/search/filter-bar.tsx` — Ensure accessible labels
|
||||
|
||||
#### 5. All Icon-Only Buttons
|
||||
Find all buttons with only icons and add aria-label:
|
||||
```typescript
|
||||
// Search in components for: <Button> with only SVG children
|
||||
// Add aria-label={t('...')}
|
||||
|
||||
// Examples:
|
||||
<Button aria-label={t('common.close')}>
|
||||
<X />
|
||||
</Button>
|
||||
|
||||
<Button aria-label={t('common.toggle_dark_mode')}>
|
||||
<Moon />
|
||||
</Button>
|
||||
```
|
||||
|
||||
#### 6. Loading Spinners
|
||||
Add aria-busy and aria-label:
|
||||
```typescript
|
||||
// In app/(public)/page.tsx and similar:
|
||||
<div aria-busy={loadingFeatured} aria-label={t('common.loading')}>
|
||||
<div className="h-8 w-8 animate-spin rounded-full..." />
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 7. `components/listings/image-gallery.tsx`
|
||||
Add keyboard navigation (arrow keys):
|
||||
```typescript
|
||||
// Add keyboard event handler for arrow keys
|
||||
// Left/Right arrows to navigate images
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### PHASE 6: TEST SETUP UPDATES
|
||||
|
||||
#### 1. `vitest.setup.ts`
|
||||
```typescript
|
||||
// Mock next-intl for tests
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: () => (key) => key, // Return key as-is for testing
|
||||
getTranslations: async () => (key) => key,
|
||||
}));
|
||||
|
||||
// Or provide full mock messages
|
||||
const mockMessages = {
|
||||
common: { home: 'Home', search: 'Search' },
|
||||
auth: { login: 'Login', register: 'Register' },
|
||||
// ... etc
|
||||
};
|
||||
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: (namespace) => (key) => mockMessages[namespace]?.[key] || key,
|
||||
}));
|
||||
```
|
||||
|
||||
#### 2. `vitest.config.ts`
|
||||
May need to add path aliases or test environment setup.
|
||||
|
||||
#### 3. Update all test files in `__tests__/` folders
|
||||
- Add locale prop to component renders
|
||||
- Test both English and Vietnamese if applicable
|
||||
- Mock i18n translations
|
||||
|
||||
---
|
||||
|
||||
## 📊 Summary: Files by Update Complexity
|
||||
|
||||
### Trivial (5 min each)
|
||||
- `app/robots.ts`
|
||||
- `app/sitemap.ts`
|
||||
- `components/ui/badge.tsx`
|
||||
- `components/ui/card.tsx`
|
||||
- `components/ui/tabs.tsx`
|
||||
|
||||
### Simple (15-30 min each)
|
||||
- `app/(admin)/*.tsx` files (3 files)
|
||||
- `app/(dashboard)/analytics/page.tsx`
|
||||
- `app/(dashboard)/profile/page.tsx`
|
||||
- `app/(dashboard)/subscription/page.tsx`
|
||||
- `app/(dashboard)/payments/page.tsx`
|
||||
- `components/ui/*.tsx` (8 files)
|
||||
- `components/auth/oauth-buttons.tsx`
|
||||
- `components/listings/listing-status-badge.tsx`
|
||||
|
||||
### Medium (30-60 min each)
|
||||
- `app/(public)/layout.tsx`
|
||||
- `app/(auth)/login/page.tsx`
|
||||
- `app/(auth)/register/page.tsx`
|
||||
- `app/(dashboard)/layout.tsx`
|
||||
- `app/(dashboard)/dashboard/page.tsx`
|
||||
- `app/(public)/search/page.tsx`
|
||||
- `components/search/property-card.tsx`
|
||||
- `components/search/filter-bar.tsx`
|
||||
- `components/listings/image-upload.tsx`
|
||||
- `components/valuation/*.tsx` (3 files)
|
||||
|
||||
### Complex (1-2 hours each)
|
||||
- `app/(public)/page.tsx` (landing page - many sections)
|
||||
- `components/listings/listing-form-steps.tsx` (multi-step form)
|
||||
- `components/map/listing-map.tsx` (if has labels)
|
||||
- `components/charts/*.tsx` (3 files - chart labels)
|
||||
|
||||
### Critical Infrastructure
|
||||
- `middleware.ts` (30-45 min)
|
||||
- `app/layout.tsx` (30 min)
|
||||
- `lib/validations/*.ts` (3 files - 45 min)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Validation Checklist
|
||||
|
||||
Before considering i18n + A11y complete:
|
||||
|
||||
### i18n Verification
|
||||
- [ ] Both `/en/*` and `/vi/*` routes work
|
||||
- [ ] All text from messages files, not hardcoded
|
||||
- [ ] Metadata changes with locale
|
||||
- [ ] Cookies/headers work for locale selection
|
||||
- [ ] Validation messages use i18n
|
||||
- [ ] All enums use translations
|
||||
- [ ] Tests mock i18n correctly
|
||||
|
||||
### A11y Verification
|
||||
- [ ] Focus trap works in dialogs
|
||||
- [ ] Focus indicator visible on all inputs
|
||||
- [ ] Form errors linked with aria-describedby
|
||||
- [ ] Icon buttons all have aria-labels
|
||||
- [ ] Color contrast >= 4.5:1 for text (AA standard)
|
||||
- [ ] Keyboard navigation works everywhere
|
||||
- [ ] Screen reader testing (NVDA/JAWS)
|
||||
- [ ] Loading spinners have aria-busy
|
||||
- [ ] All tables have proper headers
|
||||
|
||||
---
|
||||
|
||||
**Generated:** April 9, 2026
|
||||
**Confidence:** High
|
||||
**Total Estimated Files to Update:** 50-60 files
|
||||
Reference in New Issue
Block a user