From b79af10014a47571bcebd968b565ebf2a38effd1 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 19 Dec 2025 18:55:41 +0000 Subject: [PATCH] feat: enhance WebSocket support by adding X-Forwarded headers and related tests --- backend/internal/caddy/types.go | 6 + backend/internal/caddy/types_extra_test.go | 42 + docs/plans/current_spec.md | 3437 +---------------- .../prev_spec_i18n_language_selector_dec19.md | 3387 ++++++++++++++++ 4 files changed, 3538 insertions(+), 3334 deletions(-) create mode 100644 docs/plans/prev_spec_i18n_language_selector_dec19.md diff --git a/backend/internal/caddy/types.go b/backend/internal/caddy/types.go index 9f8b217a..392c569b 100644 --- a/backend/internal/caddy/types.go +++ b/backend/internal/caddy/types.go @@ -141,6 +141,12 @@ func ReverseProxyHandler(dial string, enableWS bool, application string) Handler if enableWS { setHeaders["Upgrade"] = []string{"{http.request.header.Upgrade}"} setHeaders["Connection"] = []string{"{http.request.header.Connection}"} + // Add X-Forwarded headers for WebSocket proxy awareness + // Required by many apps (e.g., SignalR, FileFlows) to properly handle + // WebSocket connections behind a reverse proxy + setHeaders["X-Forwarded-Proto"] = []string{"{http.request.scheme}"} + setHeaders["X-Forwarded-Host"] = []string{"{http.request.host}"} + setHeaders["X-Real-IP"] = []string{"{http.request.remote.host}"} } // Application-specific headers for proper client IP forwarding diff --git a/backend/internal/caddy/types_extra_test.go b/backend/internal/caddy/types_extra_test.go index 7d649b48..3a71d5e7 100644 --- a/backend/internal/caddy/types_extra_test.go +++ b/backend/internal/caddy/types_extra_test.go @@ -41,3 +41,45 @@ func TestReverseProxyHandler_PlexAndOthers(t *testing.T) { } } } + +func TestReverseProxyHandler_WebSocketHeaders(t *testing.T) { + // Test: WebSocket enabled should include X-Forwarded headers + h := ReverseProxyHandler("app:8080", true, "none") + require.Equal(t, "reverse_proxy", h["handler"]) + + hdrs, ok := h["headers"].(map[string]interface{}) + require.True(t, ok, "expected headers map when enableWS=true") + + req, ok := hdrs["request"].(map[string]interface{}) + require.True(t, ok, "expected request headers") + + set, ok := req["set"].(map[string][]string) + require.True(t, ok, "expected set headers") + + // Verify WebSocket passthrough headers + require.Contains(t, set, "Upgrade", "Upgrade header should be set for WebSocket") + require.Equal(t, []string{"{http.request.header.Upgrade}"}, set["Upgrade"]) + + require.Contains(t, set, "Connection", "Connection header should be set for WebSocket") + require.Equal(t, []string{"{http.request.header.Connection}"}, set["Connection"]) + + // Verify X-Forwarded headers for proxy awareness + require.Contains(t, set, "X-Forwarded-Proto", "X-Forwarded-Proto should be set for WebSocket") + require.Equal(t, []string{"{http.request.scheme}"}, set["X-Forwarded-Proto"]) + + require.Contains(t, set, "X-Forwarded-Host", "X-Forwarded-Host should be set for WebSocket") + require.Equal(t, []string{"{http.request.host}"}, set["X-Forwarded-Host"]) + + require.Contains(t, set, "X-Real-IP", "X-Real-IP should be set for WebSocket") + require.Equal(t, []string{"{http.request.remote.host}"}, set["X-Real-IP"]) +} + +func TestReverseProxyHandler_NoWebSocketNoForwardedHeaders(t *testing.T) { + // Test: WebSocket disabled with no application should NOT have X-Forwarded headers + h := ReverseProxyHandler("app:8080", false, "none") + require.Equal(t, "reverse_proxy", h["handler"]) + + // With enableWS=false and application="none", there should be no headers config + _, ok := h["headers"] + require.False(t, ok, "expected no headers when enableWS=false and application=none") +} diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md index ef18ad38..16b6e606 100644 --- a/docs/plans/current_spec.md +++ b/docs/plans/current_spec.md @@ -1,3387 +1,156 @@ -# Language Selector Bug - Root Cause Analysis & Implementation Plan +# Implementation Plan: WebSocket X-Forwarded Headers Fix -**Created:** 2025-12-19 -**Last Updated:** 2025-12-19 -**Status:** Implementation Ready - Production Approved -**Priority:** HIGH - User-impacting UX issue -**Estimated Timeline:** 3-4 weeks (15-20 business days) +## Overview + +**Issue**: When WebSocket support is enabled on a Proxy Host, Charon correctly adds `Upgrade` and `Connection` header passthrough, but does NOT add `X-Forwarded-*` headers. Many applications (like FileFlows using SignalR) require these headers to properly handle WebSocket connections behind a reverse proxy. + +**Solution**: Add `X-Forwarded-Proto`, `X-Forwarded-Host`, and `X-Real-IP` headers when `enableWS` is true. --- -## Executive Summary +## Files to Modify -**Bug**: Language selector changes state but UI remains in English across all pages. +### 1. `backend/internal/caddy/types.go` -**Root Cause**: Complete disconnect between i18n infrastructure and actual component implementation. While the language selection mechanism works correctly (state changes, localStorage updates, i18n.changeLanguage is called), NO components in the application are actually using the translation system to display text. All UI text is hardcoded in English. +**Location**: Lines 124-127 (WebSocket support block in `ReverseProxyHandler`) -**Impact**: The entire internationalization infrastructure (5 language files with 132+ translation keys each) is unused. The feature appears to work (dropdown changes, state updates) but has zero effect on the UI. +#### Current Code -**Implementation Approach**: Phased rollout with per-component validation, automated testing, and feature flag protection. Focus on high-visibility components first (Layout/Navigation β†’ ProxyHosts) to validate pattern before wider rollout. - ---- - -## Table of Contents - -1. [Translation Key Verification & Mapping](#translation-key-verification--mapping) -2. [File Inventory](#complete-file-inventory) -3. [Data Flow Analysis](#data-flow-analysis) -4. [Root Cause Summary](#root-cause-summary) -5. [Translation Key Naming Convention](#translation-key-naming-convention) -6. [Risk Assessment & Mitigation](#risk-assessment--mitigation) -7. [Implementation Plan (Revised Phases)](#implementation-plan---revised-phases) -8. [Detailed Timeline (3-4 Weeks)](#detailed-timeline-3-4-weeks) -9. [Code Review Checklist](#code-review-checklist) -10. [Testing Strategy (Expanded)](#testing-strategy-expanded) -11. [Success Metrics & Verification](#success-metrics--verification) -12. [Translation Maintenance Strategy](#translation-maintenance-strategy) -13. [Rollback Strategy & Feature Flags](#rollback-strategy--feature-flags) -14. [Code Patterns](#code-patterns) -15. [Conclusion](#conclusion) - ---- - -## Translation Key Verification & Mapping - -### Audit Summary - -**Total Keys in English Translation File:** 132 -**Languages Supported:** 5 (en, es, fr, de, zh) -**Key Categories:** 11 (common, navigation, dashboard, settings, proxyHosts, certificates, auth, errors, notifications, security, remoteServers) - -### Translation Key Mapping Table - -This table maps ALL hardcoded strings found in components to their corresponding translation keys. Status: βœ… Key Exists | ⚠️ Key Missing | πŸ“ Needs Addition - -#### Navigation & Layout (Layout.tsx) - -| Hardcoded String | Translation Key | Status | Notes | -|-----------------|-----------------|--------|-------| -| "Dashboard" | `navigation.dashboard` | βœ… | | -| "Proxy Hosts" | `navigation.proxyHosts` | βœ… | | -| "Remote Servers" | `navigation.remoteServers` | βœ… | | -| "Domains" | `navigation.domains` | βœ… | | -| "Certificates" | `navigation.certificates` | βœ… | | -| "Security" | `navigation.security` | βœ… | | -| "Access Lists" | `navigation.accessLists` | βœ… | | -| "CrowdSec" | `navigation.crowdsec` | βœ… | | -| "Rate Limiting" | `navigation.rateLimiting` | βœ… | | -| "WAF" | `navigation.waf` | βœ… | | -| "Uptime" | `navigation.uptime` | βœ… | | -| "Notifications" | `navigation.notifications` | βœ… | | -| "Users" | `navigation.users` | βœ… | | -| "Tasks" | `navigation.tasks` | βœ… | | -| "Settings" | `navigation.settings` | βœ… | | -| "Logout" | `auth.logout` | βœ… | | -| "Sign Out" | `auth.signOut` | βœ… | | - -#### Dashboard (Dashboard.tsx) - -| Hardcoded String | Translation Key | Status | Notes | -|-----------------|-----------------|--------|-------| -| "Dashboard" | `dashboard.title` | βœ… | | -| "Overview of your Charon reverse proxy" | `dashboard.description` | βœ… | | -| "Proxy Hosts" | `dashboard.proxyHosts` | βœ… | | -| "Remote Servers" | `dashboard.remoteServers` | βœ… | | -| "Certificates" | `dashboard.certificates` | βœ… | | -| "Access Lists" | `dashboard.accessLists` | βœ… | | -| "System Status" | `dashboard.systemStatus` | βœ… | | -| "Healthy" | `dashboard.healthy` | βœ… | | -| "X active" | `dashboard.activeHosts` | βœ… | Uses {{count}} placeholder | - -#### Common Buttons & Actions - -| Hardcoded String | Translation Key | Status | Notes | -|-----------------|-----------------|--------|-------| -| "Save" | `common.save` | βœ… | | -| "Cancel" | `common.cancel` | βœ… | | -| "Delete" | `common.delete` | βœ… | | -| "Edit" | `common.edit` | βœ… | | -| "Add" | `common.add` | βœ… | | -| "Create" | `common.create` | βœ… | | -| "Update" | `common.update` | βœ… | | -| "Close" | `common.close` | βœ… | | -| "Confirm" | `common.confirm` | βœ… | | -| "Back" | `common.back` | βœ… | | -| "Next" | `common.next` | βœ… | | -| "Loading..." | `common.loading` | βœ… | | -| "Enabled" | `common.enabled` | βœ… | | -| "Disabled" | `common.disabled` | βœ… | | -| "Search" | `common.search` | βœ… | | -| "Filter" | `common.filter` | βœ… | | - -#### ProxyHosts (ProxyHosts.tsx - Priority Component) - -| Hardcoded String | Translation Key | Status | Notes | -|-----------------|-----------------|--------|-------| -| "Proxy Hosts" | `proxyHosts.title` | βœ… | | -| "Manage your reverse proxy configurations" | `proxyHosts.description` | βœ… | | -| "Add Proxy Host" | `proxyHosts.addHost` | βœ… | | -| "Edit Proxy Host" | `proxyHosts.editHost` | βœ… | | -| "Delete Proxy Host" | `proxyHosts.deleteHost` | βœ… | | -| "Domain Names" | `proxyHosts.domainNames` | βœ… | | -| "Forward Host" | `proxyHosts.forwardHost` | βœ… | | -| "Forward Port" | `proxyHosts.forwardPort` | βœ… | | -| "SSL Enabled" | `proxyHosts.sslEnabled` | βœ… | | -| "Force SSL" | `proxyHosts.sslForced` | βœ… | | -| "Bulk Actions" | `proxyHosts.bulkActions` | ⚠️ | **NEEDS ADDITION** | -| "Apply ACL" | `proxyHosts.applyAcl` | ⚠️ | **NEEDS ADDITION** | -| "Export" | `proxyHosts.export` | ⚠️ | **NEEDS ADDITION** | - -#### Auth & Setup (Login.tsx, Setup.tsx) - -| Hardcoded String | Translation Key | Status | Notes | -|-----------------|-----------------|--------|-------| -| "Login" | `auth.login` | βœ… | | -| "Email" | `auth.email` | βœ… | | -| "Password" | `auth.password` | βœ… | | -| "Sign In" | `auth.signIn` | βœ… | | -| "Forgot Password?" | `auth.forgotPassword` | βœ… | | -| "Remember Me" | `auth.rememberMe` | βœ… | | - -#### Error Messages & Notifications - -| Hardcoded String | Translation Key | Status | Notes | -|-----------------|-----------------|--------|-------| -| "Changes saved successfully" | `notifications.saveSuccess` | βœ… | | -| "Failed to save changes" | `notifications.saveFailed` | βœ… | | -| "Deleted successfully" | `notifications.deleteSuccess` | βœ… | | -| "This field is required" | `errors.required` | βœ… | | -| "Invalid email address" | `errors.invalidEmail` | βœ… | | -| "Network error. Please check your connection." | `errors.networkError` | βœ… | | - -### Missing Keys to Add - -The following keys need to be added to all translation files (en, es, fr, de, zh): - -```json -{ - "proxyHosts": { - "bulkActions": "Bulk Actions", - "applyAcl": "Apply ACL", - "export": "Export", - "import": "Import", - "selectAll": "Select All", - "clearSelection": "Clear Selection", - "selectedCount": "{{count}} selected", - "confirmDelete": "Are you sure you want to delete {{count}} proxy host(s)?", - "confirmBulkUpdate": "Apply changes to {{count}} proxy host(s)?" - }, - "security": { - "title": "Security", - "description": "Configure security settings", - "headers": "Security Headers", - "waf": "Web Application Firewall", - "crowdsec": "CrowdSec Configuration", - "rateLimit": "Rate Limiting" - }, - "certificates": { - "requestCertificate": "Request Certificate", - "renewCertificate": "Renew Certificate", - "revokeCertificate": "Revoke Certificate", - "autoRenewal": "Auto Renewal", - "wildcardCert": "Wildcard Certificate" - }, - "remoteServers": { - "title": "Remote Servers", - "description": "Manage upstream servers", - "addServer": "Add Remote Server", - "editServer": "Edit Remote Server", - "testConnection": "Test Connection", - "connectionStatus": "Connection Status" - }, - "domains": { - "title": "Domains", - "description": "Manage your domains", - "addDomain": "Add Domain", - "verifyDomain": "Verify Domain", - "dnsSettings": "DNS Settings" - }, - "uptime": { - "title": "Uptime Monitoring", - "description": "Monitor service availability", - "addMonitor": "Add Monitor", - "responseTime": "Response Time", - "availability": "Availability" - }, - "tasks": { - "title": "Tasks", - "description": "View background tasks", - "running": "Running", - "completed": "Completed", - "failed": "Failed", - "scheduled": "Scheduled" - }, - "logs": { - "title": "Logs", - "description": "View system logs", - "downloadLogs": "Download Logs", - "clearLogs": "Clear Logs", - "filterByLevel": "Filter by Level" - }, - "smtp": { - "title": "Email Settings", - "description": "Configure SMTP for email notifications", - "testEmail": "Send Test Email", - "smtpHost": "SMTP Host", - "smtpPort": "SMTP Port" - }, - "backups": { - "title": "Backups", - "description": "Manage system backups", - "createBackup": "Create Backup", - "restoreBackup": "Restore Backup", - "downloadBackup": "Download Backup", - "scheduleBackup": "Schedule Backup" - }, - "common": { - "logout": "Logout", - "profile": "Profile", - "account": "Account", - "preferences": "Preferences", - "advanced": "Advanced", - "export": "Export", - "import": "Import", - "refresh": "Refresh", - "retry": "Retry", - "viewDetails": "View Details", - "copyToClipboard": "Copy to Clipboard", - "copied": "Copied!" - } -} +```go + // WebSocket support + if enableWS { + setHeaders["Upgrade"] = []string{"{http.request.header.Upgrade}"} + setHeaders["Connection"] = []string{"{http.request.header.Connection}"} + } ``` -### Key Coverage Analysis - -- **Existing Keys:** 132 -- **Missing Keys Identified:** 48 -- **Total Keys Needed:** 180 -- **Coverage Rate:** 73% (132/180) -- **Target Coverage:** 100% (all hardcoded strings mapped) - -**Action Required:** Add missing keys to all 5 language files before Phase 1 implementation begins. - ---- - -## Complete File Inventory - -### 1. Infrastructure Layer (βœ… Working Correctly) - -| File | Purpose | Status | -|------|---------|--------| -| \`frontend/src/i18n.ts\` | i18next configuration, language detection, localStorage integration | βœ… Functional | -| \`frontend/src/context/LanguageContext.tsx\` | React Context provider for language state management | βœ… Functional | -| \`frontend/src/context/LanguageContextValue.ts\` | TypeScript types for language context | βœ… Functional | -| \`frontend/src/hooks/useLanguage.ts\` | React hook for accessing language context | βœ… Functional | -| \`frontend/src/components/LanguageSelector.tsx\` | UI dropdown component for language selection | βœ… Functional | -| \`frontend/src/main.tsx\` | App wrapper with LanguageProvider | βœ… Properly wrapped | - -### 2. Translation Files (βœ… Complete but Unused) - -All translation files contain **132+ keys** organized into sections: -- \`common\`: 29 keys (save, cancel, delete, edit, etc.) -- \`navigation\`: 15 keys (dashboard, proxyHosts, security, etc.) -- \`dashboard\`: 13 keys (title, description, stats, etc.) -- \`proxyHosts\`: 25+ keys -- \`certificates\`: 20+ keys -- \`security\`: 30+ keys - -Files: -- \`frontend/src/locales/en/translation.json\` βœ… -- \`frontend/src/locales/es/translation.json\` βœ… -- \`frontend/src/locales/fr/translation.json\` βœ… -- \`frontend/src/locales/de/translation.json\` βœ… -- \`frontend/src/locales/zh/translation.json\` βœ… - -### 3. Application Layer (❌ Not Using Translations) - -#### Pages - ALL use hardcoded English text: -- \`Dashboard.tsx\` (177 lines) -- \`ProxyHosts.tsx\` (1023 lines) **LARGEST** -- \`SystemSettings.tsx\` (430 lines) -- \`RemoteServers.tsx\` (~500 lines) -- \`Domains.tsx\` (~400 lines) -- \`Certificates.tsx\` (~600 lines) -- \`Security.tsx\` (~500 lines) -- \`AccessLists.tsx\` (~700 lines) -- \`CrowdSecConfig.tsx\` (~600 lines) -- \`WafConfig.tsx\` (~400 lines) -- \`RateLimiting.tsx\` (~400 lines) -- \`Uptime.tsx\` (~500 lines) -- \`Notifications.tsx\` (~400 lines) -- \`UsersPage.tsx\` (~500 lines) -- \`SecurityHeaders.tsx\` (~800 lines) -- \`Login.tsx\` (~200 lines) -- \`Setup.tsx\` (~300 lines) -- \`Backups.tsx\` (~400 lines) -- \`Tasks.tsx\` (~300 lines) -- \`Logs.tsx\` (~400 lines) -- \`ImportCaddy.tsx\` (~200 lines) -- \`ImportCrowdSec.tsx\` (~200 lines) -- \`Account.tsx\` (~300 lines) -- \`SMTPSettings.tsx\` (~400 lines) - -#### Layout & Navigation: -- \`Layout.tsx\` (367 lines) - ALL navigation items hardcoded - ---- - -## Data Flow Analysis - -### Current Flow (Working but Ineffective) - -\`\`\` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ 1. USER INTERACTION β”‚ -β”‚ User clicks LanguageSelector β†’ selects "EspaΓ±ol" β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ 2. STATE UPDATE (LanguageSelector.tsx) β”‚ -β”‚ handleChange(e) β†’ setLanguage('es') β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ 3. CONTEXT UPDATE (LanguageContext.tsx) β”‚ -β”‚ - setLanguageState('es') β”‚ -β”‚ - localStorage.setItem('charon-language', 'es') β”‚ -β”‚ - i18n.changeLanguage('es') β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ 4. I18N LIBRARY UPDATE (i18n.ts) β”‚ -β”‚ - i18next loads es/translation.json β”‚ -β”‚ - Translation keys are available via i18n.t() β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ 5. COMPONENT RENDERING ❌ BROKEN HERE β”‚ -β”‚ Pages render hardcoded English: β”‚ -β”‚ -

Dashboard

β”‚ -β”‚ - β”‚ -β”‚ NO COMPONENT CALLS useTranslation() OR t() β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -\`\`\` - -### Expected Flow (After Fix) - -\`\`\` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ 5. COMPONENT RENDERING βœ… FIXED β”‚ -β”‚ Components use useTranslation: β”‚ -β”‚ const { t } = useTranslation() β”‚ -β”‚ return

{t('dashboard.title')}

β”‚ -β”‚ β”‚ -β”‚ Translation resolves: β”‚ -β”‚ - t('dashboard.title') β†’ "Panel de Control" (Spanish) β”‚ -β”‚ - t('common.save') β†’ "Guardar" β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -\`\`\` - ---- - -## Root Cause Summary - -### What Works βœ… -1. Language selection UI (LanguageSelector) -2. State management (LanguageContext) -3. localStorage persistence -4. i18next configuration -5. Translation files (complete with 132+ keys each) -6. React Context provider hierarchy - -### What's Broken ❌ -1. **ZERO components import \`useTranslation\` from react-i18next** - - Only found in: LanguageContext.tsx (infrastructure) and test files - - Not found in: Any page, layout, or UI component - -2. **ZERO components call \`t()\` function to get translations** - - All text is hardcoded in JSX - - Example: \`

Dashboard

\` instead of \`

{t('dashboard.title')}

\` - -3. **Navigation menu is entirely hardcoded** - - Layout.tsx has 20+ navigation items with English labels - ---- - -## Translation Key Naming Convention - -### Standard Structure - -All translation keys follow a hierarchical namespace structure: - -``` -{category}.{subcategory}.{descriptor} -``` - -### Category Guidelines - -| Category | Purpose | Example Keys | -|----------|---------|--------------| -| `common` | Shared UI elements used across multiple pages | `common.save`, `common.cancel` | -| `navigation` | Top-level navigation menu items | `navigation.dashboard`, `navigation.proxyHosts` | -| `{page}` | Page-specific content (dashboard, proxyHosts, etc.) | `proxyHosts.title`, `dashboard.description` | -| `errors` | Error messages and validation | `errors.required`, `errors.invalidEmail` | -| `notifications` | Toast/alert messages | `notifications.saveSuccess` | -| `auth` | Authentication and authorization | `auth.login`, `auth.logout` | - -### Naming Rules - -1. **Use camelCase** for all keys: `proxyHosts`, not `proxy-hosts` or `proxy_hosts` -2. **Be specific but concise**: `proxyHosts.addHost` not `proxyHosts.addNewProxyHost` -3. **Avoid abbreviations** unless universally understood: `smtp` (OK), `cfg` (avoid, use `config`) -4. **Group related keys** under same parent: `dashboard.activeHosts`, `dashboard.activeServers` - -### Special Patterns - -#### Pluralization - -Use ICU MessageFormat for plurals: - -```json -{ - "proxyHosts": { - "count": "{{count}} proxy host", - "count_plural": "{{count}} proxy hosts", - "selectedCount": "{{count}} selected", - "selectedCount_plural": "{{count}} selected" - } -} -``` - -**Usage:** -```tsx -t('proxyHosts.count', { count: 1 }) // "1 proxy host" -t('proxyHosts.count', { count: 5 }) // "5 proxy hosts" -``` - -#### Dynamic Interpolation - -Use `{{variableName}}` for dynamic content: - -```json -{ - "dashboard": { - "activeHosts": "{{count}} active", - "welcomeUser": "Welcome back, {{userName}}!", - "lastSync": "Last synced {{time}}" - } -} -``` - -**Usage:** -```tsx -t('dashboard.welcomeUser', { userName: 'Alice' }) // "Welcome back, Alice!" -``` - -#### Context Variants - -For gender or context-specific translations: - -```json -{ - "common": { - "delete": "Delete", - "delete_male": "Delete (m)", - "delete_female": "Delete (f)", - "save_short": "Save", - "save_long": "Save Changes" - } -} -``` - -**Usage:** -```tsx -t('common.delete', { context: 'male' }) // Uses delete_male -t('common.save', { context: 'short' }) // Uses save_short -``` - -#### Nested Keys - -Maximum 3 levels deep for maintainability: - -```json -{ - "security": { - "headers": { - "csp": "Content Security Policy", - "hsts": "HTTP Strict Transport Security" - }, - "waf": { - "enabled": "WAF Enabled", - "rulesets": "Active Rulesets" - } - } -} -``` - -**Usage:** -```tsx -t('security.headers.csp') // "Content Security Policy" -``` - -#### Boolean States - -Use consistent naming for on/off states: - -```json -{ - "common": { - "enabled": "Enabled", - "disabled": "Disabled", - "active": "Active", - "inactive": "Inactive", - "on": "On", - "off": "Off" - } -} -``` - -### Examples by Component Type - -#### Page Title & Description - -```json -{ - "proxyHosts": { - "title": "Proxy Hosts", - "description": "Manage your reverse proxy configurations" - } -} -``` - -#### Form Labels - -```json -{ - "proxyHosts": { - "domainNames": "Domain Names", - "forwardHost": "Forward Host", - "forwardPort": "Forward Port", - "sslEnabled": "SSL Enabled" - } -} -``` - -#### Button Actions - -```json -{ - "proxyHosts": { - "addHost": "Add Proxy Host", - "editHost": "Edit Proxy Host", - "deleteHost": "Delete Proxy Host", - "bulkActions": "Bulk Actions" - } -} -``` - -#### Table Columns - -```json -{ - "proxyHosts": { - "columnDomain": "Domain", - "columnTarget": "Target", - "columnStatus": "Status", - "columnActions": "Actions" - } -} -``` - -#### Confirmation Dialogs - -```json -{ - "proxyHosts": { - "confirmDelete": "Are you sure you want to delete {{domainName}}?", - "confirmBulkDelete": "Delete {{count}} proxy host(s)?", - "confirmDisable": "Disable this proxy host?" - } -} -``` - -### Anti-Patterns to Avoid - -❌ **Don't repeat category in key:** -```json -{ "proxyHosts": { "proxyHostsTitle": "..." } } // Wrong -{ "proxyHosts": { "title": "..." } } // Correct -``` - -❌ **Don't embed markup:** -```json -{ "common": { "warning": "Warning: ..." } } // Wrong -{ "common": { "warning": "Warning: ..." } } // Correct -``` - -❌ **Don't hardcode units:** -```json -{ "uptime": { "responseTime": "Response Time (ms)" } } // Wrong -{ "uptime": { "responseTime": "Response Time", "unitMs": "ms" } } // Correct -``` - -❌ **Don't use generic keys for specific content:** -```json -{ "common": { "text1": "...", "text2": "..." } } // Wrong -{ "proxyHosts": { "helpText": "...", "warningText": "..." } } // Correct +#### New Code + +```go + // WebSocket support + if enableWS { + setHeaders["Upgrade"] = []string{"{http.request.header.Upgrade}"} + setHeaders["Connection"] = []string{"{http.request.header.Connection}"} + // Add X-Forwarded headers for WebSocket proxy awareness + // Required by many apps (e.g., SignalR, FileFlows) to properly handle + // WebSocket connections behind a reverse proxy + setHeaders["X-Forwarded-Proto"] = []string{"{http.request.scheme}"} + setHeaders["X-Forwarded-Host"] = []string{"{http.request.host}"} + setHeaders["X-Real-IP"] = []string{"{http.request.remote.host}"} + } ``` --- -## Risk Assessment & Mitigation +### 2. `backend/internal/caddy/types_extra_test.go` -### Risk 1: State Management Re-render Performance +**Location**: End of file (add new test functions) -**Risk Level:** 🟑 MEDIUM +#### New Test Functions -**Description:** Adding `useTranslation()` hook to every component may cause unnecessary re-renders when language changes, especially in large components like ProxyHosts.tsx (1023 lines). +```go +func TestReverseProxyHandler_WebSocketHeaders(t *testing.T) { + // Test: WebSocket enabled should include X-Forwarded headers + h := ReverseProxyHandler("app:8080", true, "none") + require.Equal(t, "reverse_proxy", h["handler"]) -**Impact:** -- Language changes trigger re-render of all components using `useTranslation()` -- Potential UI lag or frozen state during language switch -- Memory pressure from simultaneous component updates + hdrs, ok := h["headers"].(map[string]interface{}) + require.True(t, ok, "expected headers map when enableWS=true") -**Mitigation Strategies:** -1. **Use React.memo for expensive components:** - ```tsx - export default React.memo(ProxyHosts) - ``` + req, ok := hdrs["request"].(map[string]interface{}) + require.True(t, ok, "expected request headers") -2. **Memoize translation calls in render-heavy components:** - ```tsx - const columns = useMemo(() => [ - { header: t('proxyHosts.domain'), ... }, - { header: t('proxyHosts.target'), ... } - ], [t, language]) - ``` + set, ok := req["set"].(map[string][]string) + require.True(t, ok, "expected set headers") -3. **Split large components into smaller, memoized subcomponents:** - ```tsx - const ProxyHostTable = React.memo(({ data }) => { ... }) - const ProxyHostForm = React.memo(({ onSave }) => { ... }) - ``` + // Verify WebSocket passthrough headers + require.Contains(t, set, "Upgrade", "Upgrade header should be set for WebSocket") + require.Equal(t, []string{"{http.request.header.Upgrade}"}, set["Upgrade"]) -4. **Add performance monitoring:** - ```tsx - useEffect(() => { - const start = performance.now() - return () => { - const duration = performance.now() - start - if (duration > 100) console.warn('Slow render:', duration) - } - }) - ``` + require.Contains(t, set, "Connection", "Connection header should be set for WebSocket") + require.Equal(t, []string{"{http.request.header.Connection}"}, set["Connection"]) -**Acceptance Criteria:** -- Language switch completes in < 500ms on Desktop -- Language switch completes in < 1000ms on Mobile -- No visible UI freezing during switch -- Memory usage increase < 10% after language switch + // Verify X-Forwarded headers for proxy awareness + require.Contains(t, set, "X-Forwarded-Proto", "X-Forwarded-Proto should be set for WebSocket") + require.Equal(t, []string{"{http.request.scheme}"}, set["X-Forwarded-Proto"]) ---- + require.Contains(t, set, "X-Forwarded-Host", "X-Forwarded-Host should be set for WebSocket") + require.Equal(t, []string{"{http.request.host}"}, set["X-Forwarded-Host"]) -### Risk 2: Third-Party Component i18n Support - -**Risk Level:** 🟠 HIGH - -**Description:** Some third-party UI components (DataTable, Dialog, DatePicker, etc.) may not properly support dynamic language changes or may have their own i18n systems. - -**Affected Components:** -- DataTable (pagination, sorting labels) -- Date/Time Pickers (month names, day names) -- Form validation libraries (error messages) -- Rich text editors -- File upload components - -**Mitigation Strategies:** -1. **Audit all third-party components** (Pre-Phase 1): - ```bash - grep -r "import.*from" frontend/src/components | grep -E "(table|date|form|picker|editor)" - ``` - -2. **Wrapper pattern for incompatible components:** - ```tsx - // Wrap DatePicker with localized props - const LocalizedDatePicker = ({ ...props }) => { - const { i18n } = useTranslation() - return ( - - ) - } - ``` - -3. **Replace components if necessary:** - - Document replacement decisions - - Ensure feature parity - - Test thoroughly - -4. **Configure third-party i18n integrations:** - ```tsx - // For libraries like react-datepicker - import { registerLocale, setDefaultLocale } from "react-datepicker"; - import es from 'date-fns/locale/es'; - registerLocale('es', es); - ``` - -**Action Items:** -- Create compatibility matrix (see below) -- Test each component with all 5 languages -- Document workarounds in component README - ---- - -### Risk 3: Date/Time/Number Formatting - -**Risk Level:** 🟑 MEDIUM - -**Description:** Dates, times, numbers, and currencies need locale-aware formatting. Hardcoded formats (MM/DD/YYYY) will not adapt to user locale. - -**Examples:** -- Dates: US (12/31/2025) vs EU (31/12/2025) -- Times: 12-hour (3:00 PM) vs 24-hour (15:00) -- Numbers: 1,234.56 (US) vs 1.234,56 (EU) -- Currencies: $1,234.56 vs 1 234,56 € - -**Mitigation Strategies:** -1. **Use Intl API for formatting:** - ```tsx - // Date formatting - const formatDate = (date: Date, locale: string) => { - return new Intl.DateTimeFormat(locale, { - year: 'numeric', - month: 'long', - day: 'numeric' - }).format(date) - } - - // Number formatting - const formatNumber = (num: number, locale: string) => { - return new Intl.NumberFormat(locale).format(num) - } - - // Currency formatting - const formatCurrency = (amount: number, locale: string, currency: string) => { - return new Intl.NumberFormat(locale, { - style: 'currency', - currency - }).format(amount) - } - ``` - -2. **Create formatting utilities:** - ```tsx - // frontend/src/utils/formatting.ts - import { useTranslation } from 'react-i18next' - - export const useFormatting = () => { - const { i18n } = useTranslation() - - return { - date: (date: Date) => formatDate(date, i18n.language), - time: (date: Date) => formatTime(date, i18n.language), - number: (num: number) => formatNumber(num, i18n.language), - relativeTime: (date: Date) => formatRelativeTime(date, i18n.language) - } - } - ``` - -3. **Use date-fns with locale support:** - ```tsx - import { format } from 'date-fns' - import { es, fr, de, zhCN } from 'date-fns/locale' - - const locales = { en: enUS, es, fr, de, zh: zhCN } - - format(new Date(), 'PPP', { locale: locales[language] }) - ``` - -**Acceptance Criteria:** -- All dates use `Intl.DateTimeFormat` or `date-fns` with locale -- All numbers use `Intl.NumberFormat` -- No hardcoded date/number formats in components - ---- - -### Risk 4: RTL (Right-to-Left) Language Support - -**Risk Level:** 🟑 MEDIUM - -**Description:** Future support for RTL languages (Arabic, Hebrew) will require layout and CSS adjustments. Current plan only includes LTR languages, but architecture should not prevent RTL addition. - -**Current Languages:** All LTR (English, Spanish, French, German, Chinese) -**Future Consideration:** Arabic (ar), Hebrew (he) - -**Mitigation Strategies:** -1. **Use logical CSS properties now:** - ```css - /* ❌ Avoid */ - margin-left: 16px; - padding-right: 8px; - - /* βœ… Use instead */ - margin-inline-start: 16px; - padding-inline-end: 8px; - ``` - -2. **Avoid absolute positioning where possible:** - ```css - /* ❌ Problematic for RTL */ - position: absolute; - left: 0; - - /* βœ… Use flexbox/grid */ - display: flex; - justify-content: flex-start; - ``` - -3. **Add `dir` attribute support to root:** - ```tsx - // main.tsx or App.tsx - useEffect(() => { - document.dir = i18n.dir(i18n.language) - }, [i18n.language]) - ``` - -4. **Test with RTL browser extension:** - - Install "Force RTL" browser extension - - Validate layout doesn't break - - Check icon alignment - -**Acceptance Criteria:** -- All CSS uses logical properties (inline-start/end) -- No hardcoded left/right positioning -- `dir` attribute infrastructure in place -- Layout tested with "Force RTL" tool - ---- - -### Risk 5: Translation File Drift - -**Risk Level:** 🟠 HIGH - -**Description:** Over time, translation files can become out of sync as developers add keys to English but forget to update other languages, leading to missing translations and fallback to English. - -**Impact:** -- Inconsistent user experience across languages -- Some text remains in English in non-English locales -- Hard to track which keys are missing - -**Mitigation Strategies:** -1. **Automated sync checking (CI/CD - see Maintenance Strategy section)** - -2. **Translation key generation script:** - ```bash - # scripts/sync-translations.sh - #!/bin/bash - node scripts/sync-translation-keys.js - ``` - - ```javascript - // scripts/sync-translation-keys.js - const fs = require('fs') - const path = require('path') - - const localesDir = path.join(__dirname, '../frontend/src/locales') - const enFile = path.join(localesDir, 'en/translation.json') - const enKeys = JSON.parse(fs.readFileSync(enFile, 'utf8')) - - const languages = ['es', 'fr', 'de', 'zh'] - - languages.forEach(lang => { - const langFile = path.join(localesDir, `${lang}/translation.json`) - const langKeys = JSON.parse(fs.readFileSync(langFile, 'utf8')) - const missingKeys = findMissingKeys(enKeys, langKeys) - - if (missingKeys.length > 0) { - console.error(`❌ Missing keys in ${lang}:`, missingKeys) - process.exit(1) - } - }) - ``` - -3. **Pull request template requirement:** - - Checklist item: "All translation files updated" - - Automated comment if keys don't match - -4. **Fallback chain with warnings:** - ```tsx - // i18n.ts - i18n.init({ - fallbackLng: 'en', - missingKeyHandler: (lngs, ns, key) => { - console.warn(`Missing translation key: ${key} for language: ${lngs[0]}`) - // Optionally report to error tracking - Sentry.captureMessage(`Missing i18n key: ${key}`) - } - }) - ``` - -**Acceptance Criteria:** -- CI fails if translation keys don't match -- Missing keys logged to console in development -- PR template includes translation checklist - ---- - -### Risk Mitigation Summary - -| Risk | Level | Primary Mitigation | Monitoring | -|------|-------|-------------------|------------| -| Re-render Performance | 🟑 Medium | React.memo, useMemo | Performance profiling in Phase 1 | -| Third-Party Components | 🟠 High | Audit + wrapper pattern | Manual QA per component | -| Date/Number Formatting | 🟑 Medium | Intl API utilities | Visual QA in all locales | -| RTL Support | 🟑 Medium | Logical CSS properties | RTL browser extension testing | -| Translation Drift | 🟠 High | CI checks + scripts | Automated on every PR | - ---- - -## Implementation Plan - Revised Phases - -**Strategy:** Validate pattern early with high-visibility components, then scale systematically. Each phase includes implementation, testing, code review, and bug fixes. - ---- - -### Phase 1: Layout & Navigation (Days 1-3) -**Objective:** Establish pattern with most visible user-facing component. Validates infrastructure and approach. - -**Files:** -- `frontend/src/components/Layout.tsx` (367 lines) -- `frontend/src/components/LanguageSelector.tsx` (already uses translations) - -**Tasks:** -- [ ] Day 1: Add missing translation keys (48 new keys) to all 5 language files -- [ ] Day 1: Update navigation array to use `t('navigation.*')` keys -- [ ] Day 1: Update logout/profile buttons -- [ ] Day 1: Update sidebar tooltips -- [ ] Day 2: Create Layout.test.tsx with language switching tests -- [ ] Day 2: Manual QA in all 5 languages -- [ ] Day 2: Code review and address feedback -- [ ] Day 3: Fix bugs, performance profiling, merge PR - -**Success Criteria:** -- Navigation menu switches languages instantly -- No console warnings for missing keys -- All 5 languages render correctly -- Performance: < 100ms to switch languages -- Code review approved - -**Example Changes:** -```tsx -// Before -const navigation: NavItem[] = [ - { name: 'Dashboard', path: '/', icon: 'πŸ“Š' }, - { name: 'Proxy Hosts', path: '/proxy-hosts', icon: '🌐' } -] - -// After -const { t } = useTranslation() -const navigation: NavItem[] = [ - { name: t('navigation.dashboard'), path: '/', icon: 'πŸ“Š' }, - { name: t('navigation.proxyHosts'), path: '/proxy-hosts', icon: '🌐' } -] -``` - ---- - -### Phase 2: ProxyHosts (Days 4-7) -**Objective:** Validate pattern on largest, most complex component. Proves approach scales to complex forms and tables. - -**Files:** -- `frontend/src/pages/ProxyHosts.tsx` (1023 lines) **HIGHEST COMPLEXITY** -- `frontend/src/components/ProxyHostForm.tsx` (if exists) - -**Tasks:** -- [ ] Day 4: Update PageShell title/description -- [ ] Day 4: Update all button text (Create, Edit, Delete, Bulk Apply) -- [ ] Day 5: Update DataTable column headers -- [ ] Day 5: Update form labels and placeholders -- [ ] Day 5: Update status badges (Enabled/Disabled, SSL indicators) -- [ ] Day 6: Update dialogs (confirmation, bulk update) -- [ ] Day 6: Update toast/notification messages -- [ ] Day 6: Add ProxyHosts.test.tsx -- [ ] Day 6: Manual QA with CRUD operations -- [ ] Day 7: Code review, bug fixes, performance check, merge PR - -**Success Criteria:** -- All UI text translates correctly -- Form validation messages localized -- Toast notifications in selected language -- No layout breaks in any language (especially German - longest strings) -- Performance: Page renders in < 200ms after language change -- Code review approved - -**Example Changes:** -```tsx -// Before - - -// After -const { t } = useTranslation() - -``` - ---- - -### Phase 3: Core Pages (Days 8-12) -**Objective:** Apply validated pattern to remaining core pages. Parallelizable work. - -**Files (in priority order):** -1. `SystemSettings.tsx` (430 lines) - Already imports LanguageSelector -2. `Security.tsx` (500 lines) -3. `AccessLists.tsx` (700 lines) -4. `Certificates.tsx` (600 lines) -5. `RemoteServers.tsx` (500 lines) -6. `Domains.tsx` (400 lines) - -**Tasks:** -- [ ] Day 8: SystemSettings, Security (2 files) -- [ ] Day 9: AccessLists, Certificates (2 files) -- [ ] Day 10: RemoteServers, Domains (2 files) -- [ ] Day 11: Add tests for all 6 files -- [ ] Day 11: Manual QA for all pages -- [ ] Day 12: Code review, bug fixes, merge PRs - -**Success Criteria:** -- All pages follow established pattern -- Tests pass for all components -- No regressions in functionality -- Code reviews approved - ---- - -### Phase 4: Dashboard & Supporting Pages (Days 13-15) -**Objective:** Complete main application pages and validate integration across full workflow. - -**Files:** -- `Dashboard.tsx` (177 lines) - **Integration validation** -- `CrowdSecConfig.tsx` (600 lines) -- `WafConfig.tsx` (400 lines) -- `RateLimiting.tsx` (400 lines) -- `Uptime.tsx` (500 lines) -- `Notifications.tsx` (400 lines) -- `UsersPage.tsx` (500 lines) -- `SecurityHeaders.tsx` (800 lines) - -**Tasks:** -- [ ] Day 13: Dashboard, CrowdSecConfig, WafConfig -- [ ] Day 14: RateLimiting, Uptime, Notifications, UsersPage -- [ ] Day 14: SecurityHeaders -- [ ] Day 15: Integration tests (full user workflow in each language) -- [ ] Day 15: Code review, bug fixes, merge PRs - -**Success Criteria:** -- Dashboard correctly aggregates translated content -- All stats and widgets display localized text -- Full workflow (create proxy β†’ configure SSL β†’ test) works in all languages -- Code reviews approved - ---- - -### Phase 5: Auth & Setup Pages (Days 16-17) -**Objective:** Critical user onboarding experience. Must be perfect. - -**Files:** -- `Login.tsx` (200 lines) -- `Setup.tsx` (300 lines) -- `Account.tsx` (300 lines) - -**Tasks:** -- [ ] Day 16: Login, Setup pages -- [ ] Day 16: Account page -- [ ] Day 16: Test authentication flows in all languages -- [ ] Day 17: QA first-time setup experience -- [ ] Day 17: Code review, bug fixes, merge PR - -**Success Criteria:** -- First-time users see setup in their browser's default language -- Login errors display in correct language -- Form validation messages localized -- Success/error toasts localized -- Code review approved - ---- - -### Phase 6: Utility Pages & Final Integration (Days 18-19) -**Objective:** Complete remaining pages and ensure consistency. - -**Files:** -- `Backups.tsx` (400 lines) -- `Tasks.tsx` (300 lines) -- `Logs.tsx` (400 lines) -- `ImportCaddy.tsx` (200 lines) -- `ImportCrowdSec.tsx` (200 lines) -- `SMTPSettings.tsx` (400 lines) - -**Tasks:** -- [ ] Day 18: All utility pages -- [ ] Day 18: Import pages -- [ ] Day 18: SMTP settings -- [ ] Day 19: Final integration QA -- [ ] Day 19: Code reviews, bug fixes, merge PRs - -**Success Criteria:** -- All pages translated -- Import workflows work in all languages -- No missing translation keys -- Code reviews approved - ---- - -### Phase 7: Comprehensive QA & Polish (Days 20-23) -**Objective:** Thorough testing, bug fixes, performance optimization, and production readiness. - -**Tasks:** - -#### Day 20: Automated Testing -- [ ] Run full test suite in all 5 languages -- [ ] Translation coverage tests (100% key coverage) -- [ ] Bundle size analysis (ensure no significant increase) -- [ ] Performance profiling (language switching speed) -- [ ] Accessibility testing (screen reader compatibility) - -#### Day 21: Manual QA - Core Workflows -- [ ] Test full user workflows in all 5 languages: - - [ ] First-time setup - - [ ] Login/logout - - [ ] Create/edit/delete proxy host - - [ ] Configure SSL certificate - - [ ] Apply access list - - [ ] Configure security settings - - [ ] View logs and tasks -- [ ] Test language switching mid-workflow (e.g., while editing form) -- [ ] Test WebSocket reconnection with language changes (logs page) -- [ ] Test browser back/forward with language changes - -#### Day 22: Edge Cases & Error Handling -- [ ] Backend API errors in all languages -- [ ] Network errors with WebSocket (logs page) -- [ ] Mid-edit language switches (forms preserve data) -- [ ] Rapid language switching (no race conditions) -- [ ] Browser locale detection on first visit -- [ ] LocalStorage corruption/missing (graceful fallback) - -#### Day 23: Final Polish & Documentation -- [ ] Fix all bugs found in QA -- [ ] Update user documentation with language switching instructions -- [ ] Create developer guide for adding new translations -- [ ] Final performance check -- [ ] Prepare release notes - -**Success Criteria:** -- All automated tests pass -- All manual QA workflows complete successfully -- No P0/P1 bugs remaining -- Performance meets targets (< 500ms language switch) -- Bundle size increase < 50KB -- Documentation updated - ---- - -## Detailed Timeline (3-4 Weeks) - -### Week 1: Foundation & Validation - -| Day | Phase | Tasks | Deliverables | -|-----|-------|-------|--------------| -| **Mon 1** | Phase 1 | Add missing keys, update Layout.tsx navigation | Navigation menu translations | -| **Tue 2** | Phase 1 | Tests, QA, code review | Layout PR ready | -| **Wed 3** | Phase 1 | Bug fixes, performance, merge | βœ… Layout complete | -| **Thu 4-5** | Phase 2 | ProxyHosts.tsx implementation | ProxyHosts translations | -| **Fri 5** | Phase 2 | ProxyHosts forms, tables | ProxyHosts UI complete | - -**Week 1 Milestones:** -- βœ… Navigation fully translated (most visible change) -- βœ… ProxyHosts 80% complete (validates complex component approach) -- βœ… Pattern established and documented - ---- - -### Week 2: Core Pages Rollout - -| Day | Phase | Tasks | Deliverables | -|-----|-------|-------|--------------| -| **Mon 6-7** | Phase 2 | ProxyHosts dialogs, toasts, tests, QA | βœ… ProxyHosts complete | -| **Tue 8** | Phase 3 | SystemSettings, Security | 2 pages complete | -| **Wed 9** | Phase 3 | AccessLists, Certificates | 2 pages complete | -| **Thu 10** | Phase 3 | RemoteServers, Domains | 2 pages complete | -| **Fri 11-12** | Phase 3 | Tests, QA, code reviews, bug fixes | βœ… 6 core pages complete | - -**Week 2 Milestones:** -- βœ… ProxyHosts complete (largest component done) -- βœ… 6 additional core pages translated -- βœ… All security-related pages functional - ---- - -### Week 3: Dashboard Integration & Auth - -| Day | Phase | Tasks | Deliverables | -|-----|-------|-------|--------------| -| **Mon 13** | Phase 4 | Dashboard, CrowdSec, WAF | Dashboard + 2 config pages | -| **Tue 14** | Phase 4 | Rate Limiting, Uptime, Notifications, Users, Headers | 5 pages complete | -| **Wed 15** | Phase 4 | Integration tests, QA, bug fixes | βœ… All main pages complete | -| **Thu 16** | Phase 5 | Login, Setup, Account | Auth flow complete | -| **Fri 17** | Phase 5 | Auth QA, bug fixes | βœ… Critical auth complete | - -**Week 3 Milestones:** -- βœ… Dashboard integrated (validates cross-page consistency) -- βœ… All security and monitoring pages complete -- βœ… Auth and setup flows fully translated - ---- - -### Week 4: Finalization & QA - -| Day | Phase | Tasks | Deliverables | -|-----|-------|-------|--------------| -| **Mon 18** | Phase 6 | Backups, Tasks, Logs, Import pages, SMTP | All utility pages | -| **Tue 19** | Phase 6 | Final integration, code reviews | βœ… All pages complete | -| **Wed 20** | Phase 7 | Automated testing, bundle analysis | Test results, metrics | -| **Thu 21** | Phase 7 | Manual QA - core workflows | QA report | -| **Fri 22** | Phase 7 | Edge case testing, bug fixes | Bug list, fixes | -| **Mon 23** | Phase 7 | Final polish, documentation | βœ… Production ready | - -**Week 4 Milestones:** -- βœ… 100% of pages translated -- βœ… All automated tests passing -- βœ… All manual QA complete -- βœ… Documentation updated -- βœ… Ready for production deployment - ---- - -### Buffer Time (Optional Week 5) - -**Purpose:** Handle unexpected delays, additional bugs, or extended QA - -| Day | Tasks | -|-----|-------| -| Mon 24 | Address any remaining P1 bugs | -| Tue 25 | Additional QA if needed | -| Wed 26 | Performance optimization | -| Thu 27 | Stakeholder review | -| Fri 28 | Final production prep | - ---- - -### Daily Stand-up Template - -**What was completed yesterday:** -- [Specific pages/components translated] -- [Tests added] -- [Bugs fixed] - -**What will be done today:** -- [Specific pages to translate] -- [Tests to add] -- [Code reviews to complete] - -**Blockers:** -- [Any issues blocking progress] -- [Missing information or dependencies] - -**QA Status:** -- [Pages ready for QA] -- [Bugs found] -- [Bugs fixed] - ---- - -## Code Review Checklist - -Use this checklist for EVERY pull request containing translation changes. - -### Pre-Review (Author Self-Check) - -- [ ] All hardcoded strings replaced with translation keys -- [ ] Translation keys added to ALL 5 language files (en, es, fr, de, zh) -- [ ] Keys follow naming convention (category.subcategory.descriptor) -- [ ] Dynamic content uses interpolation (`{{variableName}}`) -- [ ] Pluralization handled correctly (count, count_plural) -- [ ] Component imports `useTranslation` from 'react-i18next' -- [ ] Component calls `const { t } = useTranslation()` inside function body -- [ ] Tests added/updated for component -- [ ] Manual QA completed in at least 3 languages -- [ ] No console warnings for missing keys -- [ ] No layout breaks or text overflow in any language - -### Code Quality - -- [ ] **Import statement correct:** - ```tsx - import { useTranslation } from 'react-i18next' - ``` - -- [ ] **Hook placement correct (inside component):** - ```tsx - export default function MyComponent() { - const { t } = useTranslation() // βœ… Correct - // ... - } - ``` - -- [ ] **Translation keys valid (no typos, exist in files):** - ```tsx - t('proxyHosts.title') // βœ… Key exists - t('proxyhosts.titel') // ❌ Typo, wrong key - ``` - -- [ ] **Interpolation syntax correct:** - ```tsx - t('dashboard.activeHosts', { count: 5 }) // βœ… Correct - t('dashboard.activeHosts', { num: 5 }) // ❌ Variable name mismatch - ``` - -- [ ] **No string concatenation:** - ```tsx - // ❌ Wrong -

{t('common.total')}: {count}

- - // βœ… Correct -

{t('common.totalCount', { count })}

- ``` - -### Translation File Quality - -- [ ] **All 5 files updated (en, es, fr, de, zh)** -- [ ] **Keys in same order in all files** -- [ ] **No duplicate keys** -- [ ] **No missing commas or JSON syntax errors** -- [ ] **Interpolation placeholders match:** - ```json - // en - "activeHosts": "{{count}} active" - // es (same placeholder name) - "activeHosts": "{{count}} activos" - ``` - -- [ ] **Pluralization implemented if needed:** - ```json - "count": "{{count}} item", - "count_plural": "{{count}} items" - ``` - -### Performance - -- [ ] **Large components use React.memo:** - ```tsx - export default React.memo(ProxyHosts) - ``` - -- [ ] **Expensive translation calls memoized:** - ```tsx - const columns = useMemo(() => [ - { header: t('common.name'), ... } - ], [t]) - ``` - -- [ ] **No unnecessary re-renders on language change** -- [ ] **Bundle size increase documented (if > 5KB)** - -### Testing - -- [ ] **Unit tests added/updated:** - ```tsx - it('renders in Spanish', () => { - i18n.changeLanguage('es') - render() - expect(screen.getByText('Panel de Control')).toBeInTheDocument() - }) - ``` - -- [ ] **Translation key existence test:** - ```tsx - it('all keys exist in all languages', () => { - const enKeys = Object.keys(en) - languages.forEach(lang => { - expect(Object.keys(translations[lang])).toEqual(enKeys) - }) - }) - ``` - -- [ ] **Language switching test:** - ```tsx - it('updates when language changes', () => { - const { rerender } = render() - expect(screen.getByText('Dashboard')).toBeInTheDocument() - - i18n.changeLanguage('es') - rerender() - expect(screen.getByText('Panel de Control')).toBeInTheDocument() - }) - ``` - -### Accessibility - -- [ ] **ARIA labels translated:** - ```tsx - - ``` - -- [ ] **Form labels associated correctly:** - ```tsx - - - ``` - -- [ ] **Error messages accessible:** - ```tsx - {t('errors.required')} - ``` - -- [ ] **Screen reader tested (if available)** - -### UI/UX - -- [ ] **No text overflow in any language (especially German)** -- [ ] **Buttons and labels don't break layout** -- [ ] **Proper spacing maintained** -- [ ] **Text direction correct (all LTR for current languages)** -- [ ] **Font rendering acceptable for all languages** - -### Edge Cases - -- [ ] **Empty states translated:** - ```tsx - {items.length === 0 &&

{t('common.noData')}

} - ``` - -- [ ] **Error messages translated:** - ```tsx - catch (error) { - toast.error(t('errors.saveFailed')) - } - ``` - -- [ ] **Loading states translated:** - ```tsx - {loading && {t('common.loading')}} - ``` - -- [ ] **Confirmation dialogs translated:** - ```tsx - const confirmed = window.confirm(t('proxyHosts.confirmDelete', { domain })) - ``` - -### Documentation - -- [ ] **Translation keys documented (if new pattern)** -- [ ] **Component README updated (if applicable)** -- [ ] **PR description includes:** - - Pages/components updated - - New translation keys added - - Manual QA results (languages tested) - - Screenshots (if UI changes visible) - - Performance impact (if measurable) - -### Final Checks - -- [ ] **All tests pass locally** -- [ ] **CI/CD pipeline passes** -- [ ] **No console errors or warnings** -- [ ] **Translation sync check passes** -- [ ] **Manual QA completed in β‰₯3 languages:** - - [ ] English (en) - - [ ] Spanish (es) OR French (fr) - - [ ] German (de) OR Chinese (zh) - ---- - -## Testing Strategy (Expanded) - -### 1. Automated Unit Tests - -**Coverage Target:** 90%+ for translation-enabled components - -#### Translation Key Existence Tests - -```typescript -// frontend/src/__tests__/translation-coverage.test.ts -import { describe, it, expect } from 'vitest' -import enTranslations from '../locales/en/translation.json' -import esTranslations from '../locales/es/translation.json' -import frTranslations from '../locales/fr/translation.json' -import deTranslations from '../locales/de/translation.json' -import zhTranslations from '../locales/zh/translation.json' - -function flattenKeys(obj: any, prefix = ''): string[] { - return Object.keys(obj).reduce((acc: string[], key) => { - const fullKey = prefix ? `${prefix}.${key}` : key - if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { - return [...acc, ...flattenKeys(obj[key], fullKey)] - } - return [...acc, fullKey] - }, []) + require.Contains(t, set, "X-Real-IP", "X-Real-IP should be set for WebSocket") + require.Equal(t, []string{"{http.request.remote.host}"}, set["X-Real-IP"]) } -describe('Translation Coverage', () => { - const languages = [ - { name: 'Spanish', code: 'es', translations: esTranslations }, - { name: 'French', code: 'fr', translations: frTranslations }, - { name: 'German', code: 'de', translations: deTranslations }, - { name: 'Chinese', code: 'zh', translations: zhTranslations } - ] - - const enKeys = flattenKeys(enTranslations) - - languages.forEach(({ name, code, translations }) => { - it(`all English keys exist in ${name}`, () => { - const langKeys = flattenKeys(translations) - const missingKeys = enKeys.filter(key => !langKeys.includes(key)) - - expect(missingKeys).toEqual([]) - }) - - it(`no extra keys in ${name}`, () => { - const langKeys = flattenKeys(translations) - const extraKeys = langKeys.filter(key => !enKeys.includes(key)) - - expect(extraKeys).toEqual([]) - }) - }) - - it('all files have same number of keys', () => { - const counts = languages.map(({ translations }) => - flattenKeys(translations).length - ) - - counts.forEach(count => { - expect(count).toBe(enKeys.length) - }) - }) -}) -``` - -#### Component Translation Tests - -```typescript -// frontend/src/pages/__tests__/Dashboard.test.tsx -import { render, screen } from '@testing-library/react' -import { describe, it, expect, beforeEach } from 'vitest' -import Dashboard from '../Dashboard' -import i18n from '../../i18n' - -describe('Dashboard Translations', () => { - beforeEach(async () => { - await i18n.changeLanguage('en') - }) - - it('renders in English by default', () => { - render() - expect(screen.getByText('Dashboard')).toBeInTheDocument() - expect(screen.getByText(/overview of your/i)).toBeInTheDocument() - }) - - it('renders in Spanish', async () => { - await i18n.changeLanguage('es') - render() - expect(screen.getByText('Panel de Control')).toBeInTheDocument() - }) - - it('renders in French', async () => { - await i18n.changeLanguage('fr') - render() - expect(screen.getByText('Tableau de bord')).toBeInTheDocument() - }) - - it('renders in German', async () => { - await i18n.changeLanguage('de') - render() - expect(screen.getByText('Dashboard')).toBeInTheDocument() - }) - - it('renders in Chinese', async () => { - await i18n.changeLanguage('zh') - render() - expect(screen.getByText('δ»ͺ葨板')).toBeInTheDocument() - }) - - it('updates when language changes', async () => { - const { rerender } = render() - expect(screen.getByText('Dashboard')).toBeInTheDocument() - - await i18n.changeLanguage('es') - rerender() - - expect(screen.queryByText('Dashboard')).not.toBeInTheDocument() - expect(screen.getByText('Panel de Control')).toBeInTheDocument() - }) -}) -``` - -#### Dynamic Content Translation Tests - -```typescript -// frontend/src/pages/__tests__/ProxyHosts.test.tsx -it('handles plural translations correctly', async () => { - await i18n.changeLanguage('en') - render() - - // Mock data with 1 item - expect(screen.getByText('1 active')).toBeInTheDocument() - - // Mock data with 5 items - expect(screen.getByText('5 active')).toBeInTheDocument() -}) - -it('interpolates variables correctly', async () => { - await i18n.changeLanguage('en') - render() - - expect(screen.getByText(/delete example\.com/i)).toBeInTheDocument() -}) -``` - ---- - -### 2. Manual QA Testing - -#### Per-Component QA Checklist - -For each component/page: - -- [ ] **Visual Inspection:** - - [ ] Text renders correctly in all 5 languages - - [ ] No text overflow or truncation - - [ ] Buttons don't break layout - - [ ] Proper spacing maintained - -- [ ] **Functional Testing:** - - [ ] All buttons clickable - - [ ] Forms submit correctly - - [ ] Validation messages display - - [ ] Error/success toasts appear - - [ ] Dialogs open/close properly - -- [ ] **Language Switching:** - - [ ] Switch to each language from selector - - [ ] UI updates immediately - - [ ] No console errors - - [ ] Selection persists on reload - -- [ ] **Dynamic Content:** - - [ ] Numbers format correctly - - [ ] Dates display in proper format - - [ ] Plurals work correctly - - [ ] Variable interpolation works - -#### Language-Specific Testing - -**German Testing (Longest Strings):** -- Focus on buttons and labels that may overflow -- Check table headers don't wrap awkwardly -- Verify no horizontal scrolling triggered - -**Chinese Testing (Character Width):** -- Ensure proper font rendering -- Check spacing between characters -- Verify no character clipping - ---- - -### 3. Edge Case Testing - -#### Mid-Edit Language Changes - -**Test Scenario:** User is filling out a form, switches language mid-edit - -```typescript -// frontend/src/pages/__tests__/ProxyHosts.edge-cases.test.tsx -it('preserves form data when language changes', async () => { - render() - - // Fill form in English - const domainInput = screen.getByLabelText('Domain Names') - await userEvent.type(domainInput, 'example.com') - - // Switch to Spanish - await i18n.changeLanguage('es') - - // Verify data still there - expect(domainInput).toHaveValue('example.com') - - // Verify label changed - expect(screen.getByLabelText('Nombres de Dominio')).toBeInTheDocument() -}) -``` - -**Expected Behavior:** -- Form data preserved -- Labels update to new language -- Validation messages in new language -- No data loss - ---- - -#### Backend Error Handling - -**Test Scenario:** API returns error while UI is in non-English language - -```typescript -it('displays backend errors in current language', async () => { - await i18n.changeLanguage('es') - - // Mock API error - server.use( - http.post('/api/proxy-hosts', () => { - return HttpResponse.json( - { error: 'Invalid domain' }, - { status: 400 } - ) - }) - ) - - render() - const submitButton = screen.getByText('Guardar') - await userEvent.click(submitButton) - - // Error toast should be in Spanish - await waitFor(() => { - expect(screen.getByText(/error al guardar/i)).toBeInTheDocument() - }) -}) -``` - -**Expected Behavior:** -- API errors trigger translated error messages -- Toast notifications in current language -- Console logging in English (for debugging) - ---- - -#### WebSocket Reconnection - -**Test Scenario:** WebSocket disconnects/reconnects while viewing logs in non-English - -```typescript -it('handles WebSocket reconnection with translations', async () => { - await i18n.changeLanguage('fr') - render() - - // Verify initial state - expect(screen.getByText('ConnectΓ©')).toBeInTheDocument() - - // Simulate disconnect - mockWebSocket.close() - - await waitFor(() => { - expect(screen.getByText('DΓ©connectΓ©')).toBeInTheDocument() - }) - - // Simulate reconnect - mockWebSocket.open() - - await waitFor(() => { - expect(screen.getByText('ConnectΓ©')).toBeInTheDocument() - }) -}) -``` - -**Expected Behavior:** -- Connection status messages translated -- Log messages display correctly after reconnect -- No data loss during reconnection - ---- - -#### Rapid Language Switching - -**Test Scenario:** User rapidly clicks through all 5 languages - -```typescript -it('handles rapid language switching without errors', async () => { - const { rerender } = render() - - const languages = ['en', 'es', 'fr', 'de', 'zh'] - - for (const lang of languages) { - await i18n.changeLanguage(lang) - rerender() - - // Should not throw errors - expect(screen.getByRole('navigation')).toBeInTheDocument() - } - - // No console errors - expect(console.error).not.toHaveBeenCalled() -}) -``` - -**Expected Behavior:** -- No race conditions -- UI updates cleanly each time -- No console errors or warnings -- Final language selection persists - ---- - -### 4. Accessibility Testing - -#### Screen Reader Compatibility - -**Manual Test Steps:** -1. Enable screen reader (NVDA on Windows, VoiceOver on Mac) -2. Navigate application using keyboard only -3. Verify announcements in selected language - -**Test Checklist:** -- [ ] Page titles announced in correct language -- [ ] Button labels read correctly -- [ ] Form labels associated properly -- [ ] Error messages announced -- [ ] ARIA labels translated -- [ ] Live regions update with translations - -**Example Test:** -```typescript -it('has accessible labels in all languages', async () => { - await i18n.changeLanguage('es') - render() - - const closeButton = screen.getByRole('button', { name: 'Cerrar' }) - expect(closeButton).toHaveAttribute('aria-label', 'Cerrar') -}) -``` - ---- - -### 5. Performance Testing - -#### Bundle Size Analysis - -```bash -# Before implementation -npm run build -ls -lh dist/assets/*.js - -# After implementation -npm run build -ls -lh dist/assets/*.js - -# Calculate increase -``` - -**Acceptance Criteria:** -- Bundle size increase < 50KB (compressed) -- Translation files lazy-loaded per language -- Only active language loaded initially - -**Tool:** `webpack-bundle-analyzer` or `rollup-plugin-visualizer` - -```bash -npm run build -- --analyze -``` - ---- - -#### Language Switch Performance - -**Test Script:** -```typescript -// frontend/src/__tests__/performance.test.ts -it('switches language in under 500ms', async () => { - render() - - const start = performance.now() - await i18n.changeLanguage('es') - const duration = performance.now() - start - - expect(duration).toBeLessThan(500) -}) -``` - -**Manual Test:** -1. Open DevTools β†’ Performance -2. Start recording -3. Click language selector -4. Select different language -5. Stop recording -6. Analyze flame graph - -**Acceptance Criteria:** -- Desktop: < 500ms total switch time -- Mobile: < 1000ms total switch time -- No visible UI freezing -- No layout thrashing - ---- - -#### Memory Profiling - -**Test Procedure:** -1. Open DevTools β†’ Memory -2. Take heap snapshot (baseline) -3. Switch languages 10 times -4. Take another heap snapshot -5. Compare memory usage - -**Acceptance Criteria:** -- Memory increase < 10% after 10 switches -- No detached DOM nodes -- No event listener leaks - ---- - -### 6. Fallback Behavior Testing - -#### Missing Translation Keys - -**Test Scenario:** A translation key is missing in one language - -```typescript -// In es/translation.json, remove a key -// { -// "proxyHosts": { -// "title": "Proxy Hosts" -// // "description": "..." <- Missing -// } -// } - -it('falls back to English for missing keys', async () => { - await i18n.changeLanguage('es') - render() - - // Title should be in Spanish - expect(screen.getByText('Hosts de Proxy')).toBeInTheDocument() - - // Description falls back to English - expect(screen.getByText('Manage your reverse proxy configurations')).toBeInTheDocument() - - // Warning logged to console - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining('Missing translation key: proxyHosts.description') - ) -}) -``` - -**Expected Behavior:** -- Falls back to English gracefully -- Console warning in development -- Error reported to monitoring in production -- UI remains functional - ---- - -#### Corrupted LocalStorage - -**Test Scenario:** localStorage has invalid language value - -```typescript -it('handles corrupted language preference', () => { - localStorage.setItem('charon-language', 'invalid-lang') - - render() - - // Should fall back to browser default or 'en' - expect(i18n.language).toBe('en') -}) - -it('handles missing localStorage', () => { - // Simulate localStorage unavailable - const { localStorage: originalStorage } = window - Object.defineProperty(window, 'localStorage', { - get: () => { throw new Error('localStorage unavailable') } - }) - - render() - - // Should use browser language or default to 'en' - expect(i18n.language).toMatch(/en|es|fr|de|zh/) - - // Restore - Object.defineProperty(window, 'localStorage', { - get: () => originalStorage - }) -}) -``` - -**Expected Behavior:** -- Graceful fallback to browser default -- No application crashes -- Language can still be changed manually - ---- - -### 7. Regression Testing - -#### Existing Functionality - -**Test Checklist:** -- [ ] All existing unit tests still pass -- [ ] All existing integration tests still pass -- [ ] No broken API calls -- [ ] No broken WebSocket connections -- [ ] All forms submit correctly -- [ ] All CRUD operations work -- [ ] Authentication still works -- [ ] Authorization checks still work - -**Automated Regression Suite:** -```bash -npm run test:unit -npm run test:integration -npm run test:e2e -``` - ---- - -### 8. Cross-Browser Testing - -**Browsers to Test:** -- Chrome (latest) -- Firefox (latest) -- Safari (latest) -- Edge (latest) -- Mobile Safari (iOS) -- Mobile Chrome (Android) - -**Per-Browser Checklist:** -- [ ] Language selector works -- [ ] Translations render correctly -- [ ] localStorage persistence works -- [ ] No console errors -- [ ] Performance acceptable - ---- - -## Success Metrics & Verification - -### 1. Translation Coverage Metrics - -**Measurement Method:** Automated script - -```bash -# scripts/check-translation-coverage.sh -#!/bin/bash -set -e - -echo "Checking translation coverage..." - -# Run coverage test -npm run test:coverage:i18n - -# Check for hardcoded strings -echo "Searching for hardcoded strings..." -node scripts/find-hardcoded-strings.js - -echo "βœ… Translation coverage check complete" -``` - -```javascript -// scripts/find-hardcoded-strings.js -const fs = require('fs') -const path = require('path') -const glob = require('glob') - -const componentFiles = glob.sync('frontend/src/{pages,components}/**/*.tsx') -const violations = [] - -componentFiles.forEach(file => { - const content = fs.readFileSync(file, 'utf8') - - // Check for common hardcoded patterns - const patterns = [ - /]*>([A-Z][a-z]+\s?)+<\/button>/g, // - /title="([A-Z][a-z]+\s?)+"/g, // title="Dashboard" - /placeholder="([A-Z][a-z]+\s?)+"/g // placeholder="Enter name" - ] - - patterns.forEach(pattern => { - const matches = content.match(pattern) - if (matches) { - violations.push({ - file, - matches: matches.slice(0, 3) // First 3 matches - }) - } - }) -}) - -if (violations.length > 0) { - console.error('❌ Found hardcoded strings:') - violations.forEach(({ file, matches }) => { - console.error(` ${file}:`) - matches.forEach(m => console.error(` - ${m}`)) - }) - process.exit(1) -} else { - console.log('βœ… No hardcoded strings found') -} -``` - -**Acceptance Criteria:** -- βœ… 100% of components use `useTranslation` hook -- βœ… 0 hardcoded display strings (script finds none) -- βœ… All 132+ translation keys exist in all 5 languages -- βœ… No missing key warnings in console - -**Verification:** -```bash -npm run check:translations -``` - ---- - -### 2. Functional Verification - -**Measurement Method:** Manual QA + automated tests - -#### Language Switching Test -```typescript -// Automated test -it('language selection persists across sessions', () => { - render() - - // Select Spanish - selectLanguage('es') - expect(localStorage.getItem('charon-language')).toBe('es') - - // Reload page - window.location.reload() - - // Should still be Spanish - expect(i18n.language).toBe('es') - expect(screen.getByText('Panel de Control')).toBeInTheDocument() -}) -``` - -**Manual Verification:** -- [ ] Open app in incognito mode -- [ ] Select Spanish from language selector -- [ ] Verify UI switches to Spanish -- [ ] Close browser -- [ ] Reopen same URL -- [ ] Verify still in Spanish - -**Acceptance Criteria:** -- βœ… Language selector immediately updates UI (< 500ms) -- βœ… Selection persists in localStorage -- βœ… Persists across browser restarts -- βœ… Works in all 5 languages - -**Acceptable Miss Rate:** 0% (must work perfectly) - ---- - -### 3. Visual Regression Testing - -**Measurement Method:** Visual diff screenshots - -```javascript -// visual-regression.spec.ts (Playwright) -import { test, expect } from '@playwright/test' - -const languages = ['en', 'es', 'fr', 'de', 'zh'] -const pages = ['/', '/proxy-hosts', '/security', '/dashboard'] - -languages.forEach(lang => { - pages.forEach(page => { - test(`${page} in ${lang} matches snapshot`, async ({ page: pw }) => { - await pw.goto(`http://localhost:3000${page}`) - await pw.evaluate((language) => { - localStorage.setItem('charon-language', language) - window.location.reload() - }, lang) - - await pw.waitForLoadState('networkidle') - - // Take screenshot - await expect(pw).toHaveScreenshot(`${page.replace('/', '')}-${lang}.png`) - }) - }) -}) -``` - -**Acceptance Criteria:** -- βœ… No layout breaks in any language -- βœ… No text overflow (especially German) -- βœ… No horizontal scrollbars introduced -- βœ… Proper character rendering (especially Chinese) -- βœ… Button/label widths accommodate text - -**Acceptable Miss Rate:** < 5% pixel difference (accounting for font rendering) - ---- - -### 4. Performance Metrics - -**Measurement Method:** Performance API + Lighthouse - -#### Language Switch Speed - -```typescript -// performance.test.ts -it('language switch completes under 500ms', async () => { - render() - - const measurements = [] - - for (let i = 0; i < 10; i++) { - const start = performance.now() - await i18n.changeLanguage('es') - await waitFor(() => - expect(screen.getByText('Panel de Control')).toBeInTheDocument() - ) - const duration = performance.now() - start - measurements.push(duration) - - await i18n.changeLanguage('en') - } - - const avg = measurements.reduce((a, b) => a + b) / measurements.length - const max = Math.max(...measurements) - - console.log(`Avg: ${avg}ms, Max: ${max}ms`) - - expect(avg).toBeLessThan(500) - expect(max).toBeLessThan(1000) -}) -``` - -**Manual Measurement:** -1. Open DevTools β†’ Performance -2. Start recording -3. Click language selector β†’ Select Spanish -4. Stop recording -5. Measure time from click to UI update complete - -**Acceptance Criteria:** -- βœ… Average switch time < 500ms (desktop) -- βœ… Average switch time < 1000ms (mobile) -- βœ… 95th percentile < 800ms (desktop) -- βœ… No visible UI freezing - -**Target:** βœ… P50: 200ms, P95: 500ms, P99: 800ms - ---- - -#### Bundle Size Impact - -```bash -# Before implementation -npm run build -du -sh dist/assets/*.js - -# After implementation -npm run build -du -sh dist/assets/*.js -``` - -**Measurement:** -```bash -# Get bundle size report -npm run build -- --analyze - -# Check specific assets -ls -lh dist/assets/index-*.js -ls -lh dist/assets/translation-*.js -``` - -**Acceptance Criteria:** -- βœ… Main bundle increase < 20KB (gzipped) -- βœ… Translation files lazy-loaded per language -- βœ… Only active language loaded on page load -- βœ… Total bundle size < 500KB (gzipped) - -**Target:** -- Main bundle increase: < 15KB -- Per-language file: < 5KB each -- Total increase: < 40KB (all languages) - ---- - -### 5. Accessibility Metrics - -**Measurement Method:** axe-core automated testing + manual screen reader testing - -```typescript -// accessibility.test.ts -import { axe, toHaveNoViolations } from 'jest-axe' -expect.extend(toHaveNoViolations) - -it('has no accessibility violations in all languages', async () => { - const languages = ['en', 'es', 'fr', 'de', 'zh'] - - for (const lang of languages) { - await i18n.changeLanguage(lang) - const { container } = render() - const results = await axe(container) - - expect(results).toHaveNoViolations() - } -}) -``` - -**Manual Verification (Screen Reader):** -- [ ] Navigate with keyboard only (Tab, Enter, Space) -- [ ] Enable screen reader (NVDA/VoiceOver) -- [ ] Verify announcements in selected language -- [ ] Check ARIA labels translated -- [ ] Verify form labels associated correctly -- [ ] Test error announcements - -**Acceptance Criteria:** -- βœ… 0 critical accessibility violations -- βœ… All ARIA labels translated -- βœ… Screen reader announces in correct language -- βœ… Keyboard navigation works in all languages - -**Acceptable Miss Rate:** 0% (accessibility is non-negotiable) - ---- - -### 6. Error Rate Metrics - -**Measurement Method:** Error tracking + console monitoring - -```typescript -// Setup error tracking -i18n.init({ - fallbackLng: 'en', - missingKeyHandler: (lngs, ns, key, fallbackValue) => { - // Log to error tracking service - console.error(`Missing i18n key: ${key} for language: ${lngs[0]}`) - - // Report to Sentry/monitoring - if (process.env.NODE_ENV === 'production') { - reportError({ - type: 'missing_translation', - key, - language: lngs[0], - fallbackUsed: fallbackValue - }) - } - } -}) -``` - -**Monitoring Dashboard:** -- Missing translation key count (by language) -- Translation load failures -- Language switch errors -- LocalStorage read/write errors - -**Acceptance Criteria:** -- βœ… 0 missing translation keys in production -- βœ… < 0.1% translation load failure rate -- βœ… 0 language switch errors -- βœ… All errors gracefully handled with fallback - -**Acceptable Miss Rate:** < 0.1% (in production, due to edge cases) - ---- - -### 7. User Experience Metrics - -**Measurement Method:** User testing + analytics - -#### Task Completion Rate -**Test:** Ask 5 users to complete key workflows in their non-native language - -**Tasks:** -1. Change language to Spanish -2. Create a proxy host -3. Configure SSL -4. Apply access list -5. View logs - -**Measurement:** -- Task completion rate (% of users who succeed) -- Time to complete each task -- Number of errors/retries -- User satisfaction score (1-5) - -**Acceptance Criteria:** -- βœ… Task completion rate > 90% -- βœ… Time to complete similar to English (Β±20%) -- βœ… User satisfaction score β‰₯ 4/5 -- βœ… No blocker issues reported - ---- - -### 8. Regression Testing Metrics - -**Measurement Method:** Automated test suite - -```bash -# Run full regression suite -npm run test:unit -npm run test:integration -npm run test:e2e - -# Check for failures -echo "Exit code: $?" -``` - -**Acceptance Criteria:** -- βœ… 100% of existing unit tests pass -- βœ… 100% of existing integration tests pass -- βœ… 100% of existing e2e tests pass -- βœ… No new console errors introduced -- βœ… All API calls still work -- βœ… WebSocket connections still function - -**Acceptable Miss Rate:** 0% (no regressions allowed) - ---- - -## Translation Maintenance Strategy - -### 1. CI/CD Integration - -#### GitHub Actions Workflow - -```yaml -# .github/workflows/i18n-check.yml -name: Translation Coverage Check - -on: - pull_request: - paths: - - 'frontend/src/locales/**' - - 'frontend/src/**/*.tsx' - - 'frontend/src/**/*.ts' - -jobs: - check-translations: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Install dependencies - run: cd frontend && npm ci - - - name: Check translation key sync - run: npm run check:i18n:sync - - - name: Check for hardcoded strings - run: npm run check:i18n:hardcoded - - - name: Run translation tests - run: npm run test:i18n - - - name: Comment PR with results - if: always() - uses: actions/github-script@v6 - with: - script: | - const fs = require('fs') - const report = fs.readFileSync('i18n-report.txt', 'utf8') - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: `## Translation Check Results\n\n${report}` - }) -``` - -#### Pre-commit Hook - -```bash -# .git/hooks/pre-commit (or via husky) -#!/bin/bash - -echo "Running translation checks..." - -# Check if any translation files changed -TRANSLATION_FILES=$(git diff --cached --name-only | grep 'locales/') - -if [ -n "$TRANSLATION_FILES" ]; then - echo "Translation files changed. Running sync check..." - npm run check:i18n:sync || exit 1 -fi - -# Check if any component files changed -COMPONENT_FILES=$(git diff --cached --name-only | grep -E '\.(tsx|ts)$') - -if [ -n "$COMPONENT_FILES" ]; then - echo "Component files changed. Checking for hardcoded strings..." - npm run check:i18n:hardcoded || exit 1 -fi - -echo "βœ… Translation checks passed" -``` - ---- - -### 2. Developer Workflow - -#### Adding New Translation Keys - -**Step-by-Step Process:** - -1. **Add key to English first:** - ```json - // frontend/src/locales/en/translation.json - { - "proxyHosts": { - "newFeature": "New Feature Button" - } - } - ``` - -2. **Run sync script:** - ```bash - npm run i18n:sync - ``` - This automatically adds the key to all other language files with `[TODO]` marker: - ```json - // frontend/src/locales/es/translation.json - { - "proxyHosts": { - "newFeature": "[TODO] New Feature Button" - } - } - ``` - -3. **Use in component:** - ```tsx - - ``` - -4. **Create PR with translation TODO:** - - PR title includes `[i18n]` tag - - PR description lists untranslated keys - - Assign to translation team for review - -5. **Translation team updates:** - - Replace `[TODO]` with proper translations - - Test in UI - - Approve PR - ---- - -#### Sync Script Implementation - -```javascript -// scripts/sync-translation-keys.js -const fs = require('fs') -const path = require('path') - -const localesDir = path.join(__dirname, '../frontend/src/locales') -const languages = ['es', 'fr', 'de', 'zh'] - -// Read English as source of truth -const enFile = path.join(localesDir, 'en/translation.json') -const enKeys = JSON.parse(fs.readFileSync(enFile, 'utf8')) - -function flattenKeys(obj, prefix = '') { - return Object.keys(obj).reduce((acc, key) => { - const fullKey = prefix ? `${prefix}.${key}` : key - if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { - return { ...acc, ...flattenKeys(obj[key], fullKey) } - } - return { ...acc, [fullKey]: obj[key] } - }, {}) -} - -function unflattenKeys(flat) { - const result = {} - Object.keys(flat).forEach(key => { - const parts = key.split('.') - let current = result - parts.forEach((part, i) => { - if (i === parts.length - 1) { - current[part] = flat[key] - } else { - current[part] = current[part] || {} - current = current[part] - } - }) - }) - return result -} - -const flatEnKeys = flattenKeys(enKeys) - -languages.forEach(lang => { - const langFile = path.join(localesDir, `${lang}/translation.json`) - const langKeys = JSON.parse(fs.readFileSync(langFile, 'utf8')) - const flatLangKeys = flattenKeys(langKeys) - - let updated = false - - // Add missing keys with [TODO] marker - Object.keys(flatEnKeys).forEach(key => { - if (!flatLangKeys[key]) { - flatLangKeys[key] = `[TODO] ${flatEnKeys[key]}` - updated = true - console.log(`Added to ${lang}: ${key}`) - } - }) - - // Remove extra keys not in English - Object.keys(flatLangKeys).forEach(key => { - if (!flatEnKeys[key]) { - delete flatLangKeys[key] - updated = true - console.log(`Removed from ${lang}: ${key}`) - } - }) - - if (updated) { - const unflattened = unflattenKeys(flatLangKeys) - fs.writeFileSync( - langFile, - JSON.stringify(unflattened, null, 2) + '\n' - ) - console.log(`βœ… Updated ${lang}/translation.json`) - } else { - console.log(`βœ… ${lang}/translation.json is up to date`) - } -}) - -// Check for [TODO] markers -const allTodos = [] -languages.forEach(lang => { - const langFile = path.join(localesDir, `${lang}/translation.json`) - const content = fs.readFileSync(langFile, 'utf8') - const todos = content.match(/\[TODO\]/g) - if (todos) { - allTodos.push({ lang, count: todos.length }) - } -}) - -if (allTodos.length > 0) { - console.warn('\n⚠️ Pending translations:') - allTodos.forEach(({ lang, count }) => { - console.warn(` ${lang}: ${count} keys need translation`) - }) - process.exit(1) -} - -console.log('\nβœ… All translation files in sync!') -``` - -**NPM Scripts:** -```json -{ - "scripts": { - "i18n:sync": "node scripts/sync-translation-keys.js", - "i18n:check": "node scripts/check-translation-sync.js", - "check:i18n:sync": "npm run i18n:check", - "check:i18n:hardcoded": "node scripts/find-hardcoded-strings.js", - "test:i18n": "vitest run --testPathPattern=translation" - } +func TestReverseProxyHandler_NoWebSocketNoForwardedHeaders(t *testing.T) { + // Test: WebSocket disabled with no application should NOT have X-Forwarded headers + h := ReverseProxyHandler("app:8080", false, "none") + require.Equal(t, "reverse_proxy", h["handler"]) + + // With enableWS=false and application="none", there should be no headers config + _, ok := h["headers"] + require.False(t, ok, "expected no headers when enableWS=false and application=none") } ``` --- -### 3. Ownership Model +## Implementation Steps -**Translation Team Structure:** +1. **Modify `types.go`** + - Open [backend/internal/caddy/types.go](backend/internal/caddy/types.go#L124) + - Locate the WebSocket support block (lines 124-127) + - Add the three X-Forwarded header lines after the Connection header -| Role | Responsibility | Languages | -|------|----------------|-----------| -| **Lead Developer** | English source, review all PRs | en | -| **Spanish Translator** | Maintain Spanish translations | es | -| **French Translator** | Maintain French translations | fr | -| **German Translator** | Maintain German translations | de | -| **Chinese Translator** | Maintain Chinese translations | zh | -| **QA Team** | Verify translations in context | all | +2. **Add tests to `types_extra_test.go`** + - Open [backend/internal/caddy/types_extra_test.go](backend/internal/caddy/types_extra_test.go) + - Add `TestReverseProxyHandler_WebSocketHeaders` function + - Add `TestReverseProxyHandler_NoWebSocketNoForwardedHeaders` function -**Responsibility Matrix:** +3. **Run tests** + - Execute: `cd backend && go test ./internal/caddy/... -v -run "TestReverseProxy"` + - Verify all tests pass -| Task | Owner | Backup | -|------|-------|--------| -| Add new English keys | Feature developer | Lead developer | -| Translate to Spanish | Spanish translator | Community | -| Translate to French | French translator | Community | -| Translate to German | German translator | Community | -| Translate to Chinese | Chinese translator | Community | -| Review translations | QA team | Lead developer | -| Approve PR | Lead developer | Tech lead | - -**Escalation Path:** -1. Developer adds English key in feature PR -2. CI flags missing translations -3. PR merged with `[TODO]` markers -4. Translation issue created automatically -5. Assigned to translator for language -6. Translator updates and creates PR -7. QA verifies in UI -8. Lead developer merges +4. **Verify existing tests still pass** + - Execute: `cd backend && go test ./internal/caddy/... -v` + - Ensure no regressions --- -### 4. Community Contributions +## Test Verification Matrix -**Guidelines for contributors:** - -1. **New Feature PRs:** - - Must include English translation keys - - May include translations for other languages (optional) - - CI will flag missing translations - - OK to merge with `[TODO]` markers - -2. **Translation-Only PRs:** - - Must update ALL specified languages - - Must test in UI (screenshots required) - - Must follow naming conventions - - Must pass all CI checks - -3. **Documentation:** - ```markdown - ## Contributing Translations - - We welcome translation contributions! Please follow these steps: - - 1. Fork the repository - 2. Find keys marked `[TODO]` in `frontend/src/locales/{lang}/translation.json` - 3. Replace with accurate translations - 4. Test in UI by changing language - 5. Take screenshots - 6. Create PR with: - - Title: `[i18n] Update {language} translations` - - Description: List of keys updated - - Screenshots showing translations in UI - 7. Wait for review from language maintainer - - ### Translation Guidelines - - Keep formatting placeholders: `{{variable}}` - - Maintain similar length to English (Β±30%) - - Use formal tone - - Test pluralization if applicable - ``` +| Scenario | enableWS | application | Expected Headers | +|----------|----------|-------------|------------------| +| WebSocket only | `true` | `"none"` | Upgrade, Connection, X-Forwarded-Proto, X-Forwarded-Host, X-Real-IP | +| WebSocket + Plex | `true` | `"plex"` | Upgrade, Connection, X-Forwarded-Proto, X-Forwarded-Host, X-Real-IP, X-Plex-* | +| WebSocket + Jellyfin | `true` | `"jellyfin"` | Upgrade, Connection, X-Forwarded-Proto, X-Forwarded-Host, X-Real-IP | +| No WebSocket, no app | `false` | `"none"` | No headers config | +| No WebSocket + Plex | `false` | `"plex"` | X-Plex-*, X-Real-IP, X-Forwarded-Host | --- -### 5. Translation Quality Assurance +## Definition of Done Checklist -#### Review Checklist - -For each translation PR: - -- [ ] **Accuracy:** - - [ ] Translation conveys same meaning as English - - [ ] Technical terms translated appropriately - - [ ] No mistranslations or ambiguities - -- [ ] **Consistency:** - - [ ] Terms translated consistently across all keys - - [ ] Follows existing patterns in language file - - [ ] Matches style guide for language - -- [ ] **Formatting:** - - [ ] Placeholders preserved: `{{count}}`, `{{domain}}` - - [ ] Punctuation appropriate for language - - [ ] Capitalization follows language rules - -- [ ] **Length:** - - [ ] Fits in UI (no overflow) - - [ ] Not excessively longer than English - - [ ] Acceptable truncation if needed - -- [ ] **Context:** - - [ ] Makes sense in UI context - - [ ] Appropriate formality level - - [ ] Tested in actual application - -#### Automated Quality Checks - -```javascript -// scripts/check-translation-quality.js -const checks = { - placeholders: (en, translated) => { - const enPlaceholders = (en.match(/{{[^}]+}}/g) || []).sort() - const transPlaceholders = (translated.match(/{{[^}]+}}/g) || []).sort() - return JSON.stringify(enPlaceholders) === JSON.stringify(transPlaceholders) - }, - - length: (en, translated) => { - const ratio = translated.length / en.length - return ratio >= 0.5 && ratio <= 2.0 // Within 50-200% of English length - }, - - noTodoMarkers: (translated) => { - return !translated.includes('[TODO]') - }, - - noHtmlTags: (translated) => { - return !/<[^>]+>/.test(translated) - } -} - -// Run checks on all translations -languages.forEach(lang => { - // ... check each key against English -}) -``` +- [ ] `types.go` modified to add X-Forwarded headers in WebSocket block +- [ ] `TestReverseProxyHandler_WebSocketHeaders` test added and passing +- [ ] `TestReverseProxyHandler_NoWebSocketNoForwardedHeaders` test added and passing +- [ ] All existing tests in `backend/internal/caddy/` pass +- [ ] `go vet ./...` passes with no warnings +- [ ] Code follows project conventions (comments, naming) --- -## Rollback Strategy & Feature Flags +## Risk Assessment -### 1. Feature Flag Implementation +**Low Risk**: This change only adds headers when `enableWS=true`. It does not modify existing logic for application-specific headers (Plex, Jellyfin, etc.) which already set some of these headers. The WebSocket block is independent and executes before the application-specific switch statement. -**Approach:** Use environment variable + React Context to enable/disable i18n feature - -#### Implementation - -```typescript -// frontend/src/config/featureFlags.ts -export const featureFlags = { - enableI18n: import.meta.env.VITE_ENABLE_I18N === 'true' || false -} - -// Can be overridden at runtime for testing -if (typeof window !== 'undefined') { - (window as any).__FEATURE_FLAGS__ = featureFlags -} -``` - -```typescript -// frontend/src/context/FeatureFlagContext.tsx -import { createContext, useContext, ReactNode } from 'react' -import { featureFlags } from '../config/featureFlags' - -const FeatureFlagContext = createContext(featureFlags) - -export const useFeatureFlags = () => useContext(FeatureFlagContext) - -export function FeatureFlagProvider({ children }: { children: ReactNode }) { - return ( - - {children} - - ) -} -``` - -#### Usage in Components - -```tsx -// frontend/src/components/Layout.tsx -import { useTranslation } from 'react-i18next' -import { useFeatureFlags } from '../context/FeatureFlagContext' - -export default function Layout() { - const { t } = useTranslation() - const { enableI18n } = useFeatureFlags() - - const navigation = [ - { - name: enableI18n ? t('navigation.dashboard') : 'Dashboard', - path: '/', - icon: 'πŸ“Š' - }, - { - name: enableI18n ? t('navigation.proxyHosts') : 'Proxy Hosts', - path: '/proxy-hosts', - icon: '🌐' - } - ] - - return -} -``` - -#### Environment Configuration - -```bash -# .env.development -VITE_ENABLE_I18N=true - -# .env.production (initially false, enable after validation) -VITE_ENABLE_I18N=false - -# .env.staging (test with flag enabled) -VITE_ENABLE_I18N=true -``` +**Note on Header Overlap**: For applications like `"plex"` and `"jellyfin"` that already set `X-Real-IP` and `X-Forwarded-Host`, the WebSocket block will set them first, then the application block will overwrite with the same values. This is harmless as the values are identical. --- -### 2. Phased Rollout Plan +## Related Files Reference -**Strategy:** Gradual rollout with monitoring at each stage - -#### Stage 1: Internal Testing (Week 1) -- **Target:** Development team + QA team -- **Flag:** Enabled in development environment only -- **Monitoring:** - - Console errors - - Translation key misses - - Performance metrics - - User feedback from team - -**Go/No-Go Criteria:** -- βœ… No critical bugs -- βœ… All translation keys work -- βœ… Performance acceptable -- βœ… Team approves UX - -**Rollback Trigger:** -- Critical bug that blocks usage -- Performance degradation > 30% -- > 5% missing translation keys - ---- - -#### Stage 2: Beta Users (Week 2-3) -- **Target:** 10% of production users (opt-in beta program) -- **Flag:** Enabled via URL parameter or localStorage override -- **Monitoring:** - - Error rate per language - - Language switch frequency - - Task completion rate - - User feedback surveys - -**Enable Method:** -```typescript -// Allow beta users to enable via URL -const urlParams = new URLSearchParams(window.location.search) -if (urlParams.get('beta_i18n') === 'true') { - localStorage.setItem('beta_i18n_enabled', 'true') -} - -const betaEnabled = localStorage.getItem('beta_i18n_enabled') === 'true' - -export const featureFlags = { - enableI18n: import.meta.env.VITE_ENABLE_I18N === 'true' || betaEnabled -} -``` - -**Go/No-Go Criteria:** -- βœ… Error rate < 1% -- βœ… > 80% positive user feedback -- βœ… No P0/P1 bugs -- βœ… Performance within targets - -**Rollback Trigger:** -- Error rate > 5% -- < 60% positive feedback -- Critical bug discovered -- Performance degradation - ---- - -#### Stage 3: Gradual Rollout (Week 3-4) -- **Target:** Gradual increase to 100% of users -- **Flag:** Percentage-based rollout (10% β†’ 25% β†’ 50% β†’ 100%) -- **Monitoring:** - - Real-time error dashboards - - Performance metrics - - User support tickets - - Social media/reviews - -**Rollout Schedule:** -| Day | Percentage | Monitoring Period | -|-----|-----------|-------------------| -| 1 | 10% | 24 hours | -| 2 | 25% | 24 hours | -| 4 | 50% | 48 hours | -| 7 | 100% | Ongoing | - -**Implementation:** -```typescript -// Server-side or edge function -function getUserRolloutPercentage(userId: string): boolean { - const hash = simpleHash(userId) - const rolloutPercentage = getCurrentRolloutPercentage() // e.g., 50 - return (hash % 100) < rolloutPercentage -} - -// Client-side -const userId = getCurrentUserId() -const isInRollout = await checkRolloutStatus(userId) - -export const featureFlags = { - enableI18n: isInRollout || import.meta.env.VITE_ENABLE_I18N === 'true' -} -``` - -**Go/No-Go Criteria (Each Stage):** -- βœ… Error rate stable or decreasing -- βœ… No increase in support tickets -- βœ… Performance stable -- βœ… No critical bugs - -**Rollback Trigger:** -- Error rate spike > 10% -- Critical bug affecting > 1% users -- Support ticket surge -- Performance degradation > 20% - ---- - -### 3. Rollback Procedure - -**Scenario:** Critical issue discovered in production requiring immediate rollback - -#### Immediate Rollback (< 5 minutes) - -```bash -# 1. Disable feature flag via environment variable -# Update .env.production or config service -VITE_ENABLE_I18N=false - -# 2. Redeploy with flag disabled -npm run build -npm run deploy:production - -# OR use CDN config to inject flag override -# (if build artifacts are cached) - -# 3. Clear CDN cache if necessary -curl -X PURGE https://cdn.example.com/assets/* - -# 4. Monitor error rates drop -watch -n 5 'curl -s https://api.example.com/metrics/errors' -``` - -**Expected Result:** -- All users revert to hardcoded English strings -- Translation infrastructure remains in place -- No data loss or state corruption -- Immediate error rate drop - ---- - -#### Gradual Rollback - -**Use Case:** Non-critical issue but want to reduce exposure - -```typescript -// Reduce rollout percentage gradually -// 100% β†’ 50% β†’ 25% β†’ 10% β†’ 0% - -function getRolloutPercentage() { - // Fetch from config service or feature flag platform - return configService.get('i18n_rollout_percentage') -} - -// Decrease by 25% every hour until stable -``` - ---- - -#### Component-Specific Rollback - -**Use Case:** Issue isolated to specific component (e.g., ProxyHosts) - -```tsx -// Temporarily disable i18n for specific component -import { useFeatureFlags } from '../context/FeatureFlagContext' - -export default function ProxyHosts() { - const { t } = useTranslation() - const { enableI18n } = useFeatureFlags() - - // Force disable for this component only - const useI18n = enableI18n && !isProxyHostsBugActive() - - return ( - - {/* ... */} - - ) -} -``` - ---- - -### 4. Rollback Decision Matrix - -| Issue Severity | Impact | Action | Timeline | -|---------------|--------|--------|----------| -| **P0 - Critical** | > 10% users can't use app | Immediate full rollback | < 5 min | -| **P1 - High** | Core feature broken for > 5% | Gradual rollback to 0% | < 1 hour | -| **P2 - Medium** | Non-critical feature broken | Component-specific disable | < 4 hours | -| **P3 - Low** | Minor visual issue | Fix forward, no rollback | Next release | - -**Examples:** - -- **P0:** Language switch crashes app β†’ Immediate rollback -- **P1:** German translations cause layout breaks β†’ Gradual rollback -- **P2:** Toast messages not translated β†’ Disable toasts i18n only -- **P3:** Minor spacing issue in Chinese β†’ Fix in next sprint - ---- - -### 5. Monitoring & Alerting - -#### Key Metrics to Monitor - -```typescript -// Error tracking -const i18nMetrics = { - missingKeys: 0, // Translation key not found - loadFailures: 0, // Translation file failed to load - switchErrors: 0, // Error during language switch - renderErrors: 0, // Component render error after i18n - performanceSlow: 0 // Language switch > 1000ms -} - -// Alert thresholds -const alertThresholds = { - missingKeys: 10, // Alert if > 10 missing keys in 5 min - loadFailures: 5, // Alert if > 5 load failures in 5 min - switchErrors: 3, // Alert if > 3 switch errors in 5 min - renderErrors: 5, // Alert if > 5 render errors in 5 min - performanceSlow: 20 // Alert if > 20 slow switches in 5 min -} -``` - -#### Alert Configuration - -```yaml -# alerts.yml (for Prometheus/Grafana/Datadog) -alerts: - - name: i18n_missing_keys_high - condition: rate(i18n_missing_keys_total[5m]) > 10 - severity: warning - action: notify_team - - - name: i18n_load_failures_critical - condition: rate(i18n_load_failures_total[5m]) > 5 - severity: critical - action: page_oncall - - - name: i18n_switch_errors_high - condition: rate(i18n_switch_errors_total[5m]) > 3 - severity: warning - action: notify_team - - - name: i18n_performance_degraded - condition: histogram_quantile(0.95, i18n_switch_duration_seconds) > 1 - severity: warning - action: notify_team -``` - ---- - -### 6. Communication Plan - -#### Pre-Rollout Communication - -**To Users:** -- Feature announcement on blog/social media -- In-app banner: "New: Multi-language support coming soon!" -- Email to beta testers with opt-in link - -**To Team:** -- Slack announcement with rollout schedule -- On-call rotation briefing -- Runbook shared with ops team - -#### During Rollout - -**Status Updates:** -- Hourly updates in Slack #i18n-rollout channel -- Dashboard showing live metrics -- Go/no-go decision points documented - -**Example Update:** -``` -πŸš€ i18n Rollout Update - 2pm EST -Stage: 25% rollout (Day 2) -Status: βœ… GREEN -Metrics: -- Error rate: 0.3% (target <1%) βœ… -- Missing keys: 2 (target <10) βœ… -- Performance P95: 380ms (target <500ms) βœ… -- User feedback: 87% positive (target >80%) βœ… - -Next checkpoint: 5pm EST (50% rollout decision) -``` - -#### Rollback Communication - -**If rollback needed:** -``` -⚠️ i18n Rollback Initiated - 3pm EST -Reason: Critical bug in language switching (P0) -Action: Full rollback to 0% - ETA 5 minutes -Impact: Users see English only, no data loss -Follow-up: Root cause analysis tomorrow 10am -Incident Report: [link] -``` - ---- - -## Code Patterns - -### ❌ Anti-Pattern (Current) -```tsx -export default function ProxyHosts() { - return ( - - - - ) -} -``` - -### βœ… Correct Pattern (After Fix) -```tsx -import { useTranslation } from 'react-i18next' - -export default function ProxyHosts() { - const { t } = useTranslation() - return ( - - - - ) -} -``` - -### βœ… With Feature Flag (Transition) -```tsx -import { useTranslation } from 'react-i18next' -import { useFeatureFlags } from '../context/FeatureFlagContext' - -export default function ProxyHosts() { - const { t } = useTranslation() - const { enableI18n } = useFeatureFlags() - - return ( - - - - ) -} -``` - -### Dynamic Content Pattern -```tsx -// ❌ Wrong -

You have {count} proxy hosts

- -// βœ… Correct -

{t('proxyHosts.count', { count })}

- -// In translation file: -{ "proxyHosts": { "count": "You have {{count}} proxy hosts" } } -``` - -### Error Handling Pattern -```tsx -// βœ… Correct -try { - await saveProxyHost(data) - toast.success(t('notifications.saveSuccess')) -} catch (error) { - toast.error(t('notifications.saveFailed')) - console.error('Save failed:', error) // Always log in English for debugging -} -``` - -### Form Validation Pattern -```tsx -// βœ… Correct -const schema = z.object({ - domain: z.string().min(1, t('errors.required')) -}) - -// Better: Use error key -const schema = z.object({ - domain: z.string().min(1, { message: 'errors.required' }) -}) - -// Then translate in form -{errors.domain && {t(errors.domain.message)}} -``` - -### Memoization Pattern (Performance) -```tsx -// βœ… For expensive computed values -const columns = useMemo(() => [ - { header: t('proxyHosts.domain'), accessorKey: 'domain_names' }, - { header: t('common.status'), accessorKey: 'enabled' } -], [t]) // Re-compute when t function changes (language switch) -``` - -### Conditional Translation Pattern -```tsx -// βœ… Correct - different keys for different states - - {proxy.enabled - ? t('common.enabled') - : t('common.disabled') - } - - -// βœ… Also correct - one key with context -{t('common.status', { context: proxy.enabled ? 'enabled' : 'disabled' })} -``` - ---- - -## Conclusion - -The language selector bug is a complete disconnect between infrastructure and implementation. The infrastructure works perfectlyβ€”the issue is that NO components use the translation system. All UI text is hardcoded in English. - -**Fix**: Systematically update every component to use `useTranslation` and replace hardcoded strings with translation keys. - -**Implementation Timeline:** 3-4 weeks (15-20 business days) -- Week 1: Layout + ProxyHosts (pattern validation) -- Week 2: Core pages (6-8 pages) -- Week 3: Dashboard + Auth (critical paths) -- Week 4: QA + Polish (comprehensive testing) -- Buffer: Week 5 (if needed) - -**Risk Level:** Low-Medium -- Infrastructure proven working -- Changes are display-only (no logic changes) -- Feature flag enables safe rollback -- Phased rollout reduces blast radius - -**User Impact:** High (positive) -- Enables full multilingual support for 5 languages -- Improves accessibility for non-English users -- Professional appearance and localization -- Competitive advantage for international users - -**Success Metrics:** -- 100% translation coverage (0 hardcoded strings) -- < 500ms language switch time -- 0% missing translation keys -- > 90% user task completion in all languages -- 0 critical bugs in production - -**Rollback Plan:** -- Feature flag for immediate disable -- Phased rollout with monitoring -- Clear rollback procedures -- 24/7 monitoring during rollout - -Once complete, the translation infrastructure will finally be utilized, and users will see the UI in their selected language. The phased approach with feature flags ensures we can roll back instantly if issues arise, while the comprehensive testing strategy ensures quality before wide release. - -**Next Steps:** -1. βœ… Approve this plan -2. Add missing 48 translation keys to all 5 language files -3. Begin Phase 1: Layout & Navigation (Day 1) -4. Proceed through phases systematically -5. Monitor metrics at each stage -6. Celebrate successful multilingual launch! πŸŽ‰ - ---- - -**Document Version:** 2.0 -**Last Updated:** 2025-12-19 -**Status:** βœ… Ready for Implementation -**Approvals Required:** Tech Lead, Product Manager, QA Lead +| File | Purpose | +|------|---------| +| [backend/internal/caddy/types.go](backend/internal/caddy/types.go) | Main implementation file | +| [backend/internal/caddy/types_test.go](backend/internal/caddy/types_test.go) | Basic handler tests | +| [backend/internal/caddy/types_extra_test.go](backend/internal/caddy/types_extra_test.go) | Extended handler tests | diff --git a/docs/plans/prev_spec_i18n_language_selector_dec19.md b/docs/plans/prev_spec_i18n_language_selector_dec19.md new file mode 100644 index 00000000..ef18ad38 --- /dev/null +++ b/docs/plans/prev_spec_i18n_language_selector_dec19.md @@ -0,0 +1,3387 @@ +# Language Selector Bug - Root Cause Analysis & Implementation Plan + +**Created:** 2025-12-19 +**Last Updated:** 2025-12-19 +**Status:** Implementation Ready - Production Approved +**Priority:** HIGH - User-impacting UX issue +**Estimated Timeline:** 3-4 weeks (15-20 business days) + +--- + +## Executive Summary + +**Bug**: Language selector changes state but UI remains in English across all pages. + +**Root Cause**: Complete disconnect between i18n infrastructure and actual component implementation. While the language selection mechanism works correctly (state changes, localStorage updates, i18n.changeLanguage is called), NO components in the application are actually using the translation system to display text. All UI text is hardcoded in English. + +**Impact**: The entire internationalization infrastructure (5 language files with 132+ translation keys each) is unused. The feature appears to work (dropdown changes, state updates) but has zero effect on the UI. + +**Implementation Approach**: Phased rollout with per-component validation, automated testing, and feature flag protection. Focus on high-visibility components first (Layout/Navigation β†’ ProxyHosts) to validate pattern before wider rollout. + +--- + +## Table of Contents + +1. [Translation Key Verification & Mapping](#translation-key-verification--mapping) +2. [File Inventory](#complete-file-inventory) +3. [Data Flow Analysis](#data-flow-analysis) +4. [Root Cause Summary](#root-cause-summary) +5. [Translation Key Naming Convention](#translation-key-naming-convention) +6. [Risk Assessment & Mitigation](#risk-assessment--mitigation) +7. [Implementation Plan (Revised Phases)](#implementation-plan---revised-phases) +8. [Detailed Timeline (3-4 Weeks)](#detailed-timeline-3-4-weeks) +9. [Code Review Checklist](#code-review-checklist) +10. [Testing Strategy (Expanded)](#testing-strategy-expanded) +11. [Success Metrics & Verification](#success-metrics--verification) +12. [Translation Maintenance Strategy](#translation-maintenance-strategy) +13. [Rollback Strategy & Feature Flags](#rollback-strategy--feature-flags) +14. [Code Patterns](#code-patterns) +15. [Conclusion](#conclusion) + +--- + +## Translation Key Verification & Mapping + +### Audit Summary + +**Total Keys in English Translation File:** 132 +**Languages Supported:** 5 (en, es, fr, de, zh) +**Key Categories:** 11 (common, navigation, dashboard, settings, proxyHosts, certificates, auth, errors, notifications, security, remoteServers) + +### Translation Key Mapping Table + +This table maps ALL hardcoded strings found in components to their corresponding translation keys. Status: βœ… Key Exists | ⚠️ Key Missing | πŸ“ Needs Addition + +#### Navigation & Layout (Layout.tsx) + +| Hardcoded String | Translation Key | Status | Notes | +|-----------------|-----------------|--------|-------| +| "Dashboard" | `navigation.dashboard` | βœ… | | +| "Proxy Hosts" | `navigation.proxyHosts` | βœ… | | +| "Remote Servers" | `navigation.remoteServers` | βœ… | | +| "Domains" | `navigation.domains` | βœ… | | +| "Certificates" | `navigation.certificates` | βœ… | | +| "Security" | `navigation.security` | βœ… | | +| "Access Lists" | `navigation.accessLists` | βœ… | | +| "CrowdSec" | `navigation.crowdsec` | βœ… | | +| "Rate Limiting" | `navigation.rateLimiting` | βœ… | | +| "WAF" | `navigation.waf` | βœ… | | +| "Uptime" | `navigation.uptime` | βœ… | | +| "Notifications" | `navigation.notifications` | βœ… | | +| "Users" | `navigation.users` | βœ… | | +| "Tasks" | `navigation.tasks` | βœ… | | +| "Settings" | `navigation.settings` | βœ… | | +| "Logout" | `auth.logout` | βœ… | | +| "Sign Out" | `auth.signOut` | βœ… | | + +#### Dashboard (Dashboard.tsx) + +| Hardcoded String | Translation Key | Status | Notes | +|-----------------|-----------------|--------|-------| +| "Dashboard" | `dashboard.title` | βœ… | | +| "Overview of your Charon reverse proxy" | `dashboard.description` | βœ… | | +| "Proxy Hosts" | `dashboard.proxyHosts` | βœ… | | +| "Remote Servers" | `dashboard.remoteServers` | βœ… | | +| "Certificates" | `dashboard.certificates` | βœ… | | +| "Access Lists" | `dashboard.accessLists` | βœ… | | +| "System Status" | `dashboard.systemStatus` | βœ… | | +| "Healthy" | `dashboard.healthy` | βœ… | | +| "X active" | `dashboard.activeHosts` | βœ… | Uses {{count}} placeholder | + +#### Common Buttons & Actions + +| Hardcoded String | Translation Key | Status | Notes | +|-----------------|-----------------|--------|-------| +| "Save" | `common.save` | βœ… | | +| "Cancel" | `common.cancel` | βœ… | | +| "Delete" | `common.delete` | βœ… | | +| "Edit" | `common.edit` | βœ… | | +| "Add" | `common.add` | βœ… | | +| "Create" | `common.create` | βœ… | | +| "Update" | `common.update` | βœ… | | +| "Close" | `common.close` | βœ… | | +| "Confirm" | `common.confirm` | βœ… | | +| "Back" | `common.back` | βœ… | | +| "Next" | `common.next` | βœ… | | +| "Loading..." | `common.loading` | βœ… | | +| "Enabled" | `common.enabled` | βœ… | | +| "Disabled" | `common.disabled` | βœ… | | +| "Search" | `common.search` | βœ… | | +| "Filter" | `common.filter` | βœ… | | + +#### ProxyHosts (ProxyHosts.tsx - Priority Component) + +| Hardcoded String | Translation Key | Status | Notes | +|-----------------|-----------------|--------|-------| +| "Proxy Hosts" | `proxyHosts.title` | βœ… | | +| "Manage your reverse proxy configurations" | `proxyHosts.description` | βœ… | | +| "Add Proxy Host" | `proxyHosts.addHost` | βœ… | | +| "Edit Proxy Host" | `proxyHosts.editHost` | βœ… | | +| "Delete Proxy Host" | `proxyHosts.deleteHost` | βœ… | | +| "Domain Names" | `proxyHosts.domainNames` | βœ… | | +| "Forward Host" | `proxyHosts.forwardHost` | βœ… | | +| "Forward Port" | `proxyHosts.forwardPort` | βœ… | | +| "SSL Enabled" | `proxyHosts.sslEnabled` | βœ… | | +| "Force SSL" | `proxyHosts.sslForced` | βœ… | | +| "Bulk Actions" | `proxyHosts.bulkActions` | ⚠️ | **NEEDS ADDITION** | +| "Apply ACL" | `proxyHosts.applyAcl` | ⚠️ | **NEEDS ADDITION** | +| "Export" | `proxyHosts.export` | ⚠️ | **NEEDS ADDITION** | + +#### Auth & Setup (Login.tsx, Setup.tsx) + +| Hardcoded String | Translation Key | Status | Notes | +|-----------------|-----------------|--------|-------| +| "Login" | `auth.login` | βœ… | | +| "Email" | `auth.email` | βœ… | | +| "Password" | `auth.password` | βœ… | | +| "Sign In" | `auth.signIn` | βœ… | | +| "Forgot Password?" | `auth.forgotPassword` | βœ… | | +| "Remember Me" | `auth.rememberMe` | βœ… | | + +#### Error Messages & Notifications + +| Hardcoded String | Translation Key | Status | Notes | +|-----------------|-----------------|--------|-------| +| "Changes saved successfully" | `notifications.saveSuccess` | βœ… | | +| "Failed to save changes" | `notifications.saveFailed` | βœ… | | +| "Deleted successfully" | `notifications.deleteSuccess` | βœ… | | +| "This field is required" | `errors.required` | βœ… | | +| "Invalid email address" | `errors.invalidEmail` | βœ… | | +| "Network error. Please check your connection." | `errors.networkError` | βœ… | | + +### Missing Keys to Add + +The following keys need to be added to all translation files (en, es, fr, de, zh): + +```json +{ + "proxyHosts": { + "bulkActions": "Bulk Actions", + "applyAcl": "Apply ACL", + "export": "Export", + "import": "Import", + "selectAll": "Select All", + "clearSelection": "Clear Selection", + "selectedCount": "{{count}} selected", + "confirmDelete": "Are you sure you want to delete {{count}} proxy host(s)?", + "confirmBulkUpdate": "Apply changes to {{count}} proxy host(s)?" + }, + "security": { + "title": "Security", + "description": "Configure security settings", + "headers": "Security Headers", + "waf": "Web Application Firewall", + "crowdsec": "CrowdSec Configuration", + "rateLimit": "Rate Limiting" + }, + "certificates": { + "requestCertificate": "Request Certificate", + "renewCertificate": "Renew Certificate", + "revokeCertificate": "Revoke Certificate", + "autoRenewal": "Auto Renewal", + "wildcardCert": "Wildcard Certificate" + }, + "remoteServers": { + "title": "Remote Servers", + "description": "Manage upstream servers", + "addServer": "Add Remote Server", + "editServer": "Edit Remote Server", + "testConnection": "Test Connection", + "connectionStatus": "Connection Status" + }, + "domains": { + "title": "Domains", + "description": "Manage your domains", + "addDomain": "Add Domain", + "verifyDomain": "Verify Domain", + "dnsSettings": "DNS Settings" + }, + "uptime": { + "title": "Uptime Monitoring", + "description": "Monitor service availability", + "addMonitor": "Add Monitor", + "responseTime": "Response Time", + "availability": "Availability" + }, + "tasks": { + "title": "Tasks", + "description": "View background tasks", + "running": "Running", + "completed": "Completed", + "failed": "Failed", + "scheduled": "Scheduled" + }, + "logs": { + "title": "Logs", + "description": "View system logs", + "downloadLogs": "Download Logs", + "clearLogs": "Clear Logs", + "filterByLevel": "Filter by Level" + }, + "smtp": { + "title": "Email Settings", + "description": "Configure SMTP for email notifications", + "testEmail": "Send Test Email", + "smtpHost": "SMTP Host", + "smtpPort": "SMTP Port" + }, + "backups": { + "title": "Backups", + "description": "Manage system backups", + "createBackup": "Create Backup", + "restoreBackup": "Restore Backup", + "downloadBackup": "Download Backup", + "scheduleBackup": "Schedule Backup" + }, + "common": { + "logout": "Logout", + "profile": "Profile", + "account": "Account", + "preferences": "Preferences", + "advanced": "Advanced", + "export": "Export", + "import": "Import", + "refresh": "Refresh", + "retry": "Retry", + "viewDetails": "View Details", + "copyToClipboard": "Copy to Clipboard", + "copied": "Copied!" + } +} +``` + +### Key Coverage Analysis + +- **Existing Keys:** 132 +- **Missing Keys Identified:** 48 +- **Total Keys Needed:** 180 +- **Coverage Rate:** 73% (132/180) +- **Target Coverage:** 100% (all hardcoded strings mapped) + +**Action Required:** Add missing keys to all 5 language files before Phase 1 implementation begins. + +--- + +## Complete File Inventory + +### 1. Infrastructure Layer (βœ… Working Correctly) + +| File | Purpose | Status | +|------|---------|--------| +| \`frontend/src/i18n.ts\` | i18next configuration, language detection, localStorage integration | βœ… Functional | +| \`frontend/src/context/LanguageContext.tsx\` | React Context provider for language state management | βœ… Functional | +| \`frontend/src/context/LanguageContextValue.ts\` | TypeScript types for language context | βœ… Functional | +| \`frontend/src/hooks/useLanguage.ts\` | React hook for accessing language context | βœ… Functional | +| \`frontend/src/components/LanguageSelector.tsx\` | UI dropdown component for language selection | βœ… Functional | +| \`frontend/src/main.tsx\` | App wrapper with LanguageProvider | βœ… Properly wrapped | + +### 2. Translation Files (βœ… Complete but Unused) + +All translation files contain **132+ keys** organized into sections: +- \`common\`: 29 keys (save, cancel, delete, edit, etc.) +- \`navigation\`: 15 keys (dashboard, proxyHosts, security, etc.) +- \`dashboard\`: 13 keys (title, description, stats, etc.) +- \`proxyHosts\`: 25+ keys +- \`certificates\`: 20+ keys +- \`security\`: 30+ keys + +Files: +- \`frontend/src/locales/en/translation.json\` βœ… +- \`frontend/src/locales/es/translation.json\` βœ… +- \`frontend/src/locales/fr/translation.json\` βœ… +- \`frontend/src/locales/de/translation.json\` βœ… +- \`frontend/src/locales/zh/translation.json\` βœ… + +### 3. Application Layer (❌ Not Using Translations) + +#### Pages - ALL use hardcoded English text: +- \`Dashboard.tsx\` (177 lines) +- \`ProxyHosts.tsx\` (1023 lines) **LARGEST** +- \`SystemSettings.tsx\` (430 lines) +- \`RemoteServers.tsx\` (~500 lines) +- \`Domains.tsx\` (~400 lines) +- \`Certificates.tsx\` (~600 lines) +- \`Security.tsx\` (~500 lines) +- \`AccessLists.tsx\` (~700 lines) +- \`CrowdSecConfig.tsx\` (~600 lines) +- \`WafConfig.tsx\` (~400 lines) +- \`RateLimiting.tsx\` (~400 lines) +- \`Uptime.tsx\` (~500 lines) +- \`Notifications.tsx\` (~400 lines) +- \`UsersPage.tsx\` (~500 lines) +- \`SecurityHeaders.tsx\` (~800 lines) +- \`Login.tsx\` (~200 lines) +- \`Setup.tsx\` (~300 lines) +- \`Backups.tsx\` (~400 lines) +- \`Tasks.tsx\` (~300 lines) +- \`Logs.tsx\` (~400 lines) +- \`ImportCaddy.tsx\` (~200 lines) +- \`ImportCrowdSec.tsx\` (~200 lines) +- \`Account.tsx\` (~300 lines) +- \`SMTPSettings.tsx\` (~400 lines) + +#### Layout & Navigation: +- \`Layout.tsx\` (367 lines) - ALL navigation items hardcoded + +--- + +## Data Flow Analysis + +### Current Flow (Working but Ineffective) + +\`\`\` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 1. USER INTERACTION β”‚ +β”‚ User clicks LanguageSelector β†’ selects "EspaΓ±ol" β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 2. STATE UPDATE (LanguageSelector.tsx) β”‚ +β”‚ handleChange(e) β†’ setLanguage('es') β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 3. CONTEXT UPDATE (LanguageContext.tsx) β”‚ +β”‚ - setLanguageState('es') β”‚ +β”‚ - localStorage.setItem('charon-language', 'es') β”‚ +β”‚ - i18n.changeLanguage('es') β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 4. I18N LIBRARY UPDATE (i18n.ts) β”‚ +β”‚ - i18next loads es/translation.json β”‚ +β”‚ - Translation keys are available via i18n.t() β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 5. COMPONENT RENDERING ❌ BROKEN HERE β”‚ +β”‚ Pages render hardcoded English: β”‚ +β”‚ -

Dashboard

β”‚ +β”‚ - β”‚ +β”‚ NO COMPONENT CALLS useTranslation() OR t() β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +\`\`\` + +### Expected Flow (After Fix) + +\`\`\` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 5. COMPONENT RENDERING βœ… FIXED β”‚ +β”‚ Components use useTranslation: β”‚ +β”‚ const { t } = useTranslation() β”‚ +β”‚ return

{t('dashboard.title')}

β”‚ +β”‚ β”‚ +β”‚ Translation resolves: β”‚ +β”‚ - t('dashboard.title') β†’ "Panel de Control" (Spanish) β”‚ +β”‚ - t('common.save') β†’ "Guardar" β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +\`\`\` + +--- + +## Root Cause Summary + +### What Works βœ… +1. Language selection UI (LanguageSelector) +2. State management (LanguageContext) +3. localStorage persistence +4. i18next configuration +5. Translation files (complete with 132+ keys each) +6. React Context provider hierarchy + +### What's Broken ❌ +1. **ZERO components import \`useTranslation\` from react-i18next** + - Only found in: LanguageContext.tsx (infrastructure) and test files + - Not found in: Any page, layout, or UI component + +2. **ZERO components call \`t()\` function to get translations** + - All text is hardcoded in JSX + - Example: \`

Dashboard

\` instead of \`

{t('dashboard.title')}

\` + +3. **Navigation menu is entirely hardcoded** + - Layout.tsx has 20+ navigation items with English labels + +--- + +## Translation Key Naming Convention + +### Standard Structure + +All translation keys follow a hierarchical namespace structure: + +``` +{category}.{subcategory}.{descriptor} +``` + +### Category Guidelines + +| Category | Purpose | Example Keys | +|----------|---------|--------------| +| `common` | Shared UI elements used across multiple pages | `common.save`, `common.cancel` | +| `navigation` | Top-level navigation menu items | `navigation.dashboard`, `navigation.proxyHosts` | +| `{page}` | Page-specific content (dashboard, proxyHosts, etc.) | `proxyHosts.title`, `dashboard.description` | +| `errors` | Error messages and validation | `errors.required`, `errors.invalidEmail` | +| `notifications` | Toast/alert messages | `notifications.saveSuccess` | +| `auth` | Authentication and authorization | `auth.login`, `auth.logout` | + +### Naming Rules + +1. **Use camelCase** for all keys: `proxyHosts`, not `proxy-hosts` or `proxy_hosts` +2. **Be specific but concise**: `proxyHosts.addHost` not `proxyHosts.addNewProxyHost` +3. **Avoid abbreviations** unless universally understood: `smtp` (OK), `cfg` (avoid, use `config`) +4. **Group related keys** under same parent: `dashboard.activeHosts`, `dashboard.activeServers` + +### Special Patterns + +#### Pluralization + +Use ICU MessageFormat for plurals: + +```json +{ + "proxyHosts": { + "count": "{{count}} proxy host", + "count_plural": "{{count}} proxy hosts", + "selectedCount": "{{count}} selected", + "selectedCount_plural": "{{count}} selected" + } +} +``` + +**Usage:** +```tsx +t('proxyHosts.count', { count: 1 }) // "1 proxy host" +t('proxyHosts.count', { count: 5 }) // "5 proxy hosts" +``` + +#### Dynamic Interpolation + +Use `{{variableName}}` for dynamic content: + +```json +{ + "dashboard": { + "activeHosts": "{{count}} active", + "welcomeUser": "Welcome back, {{userName}}!", + "lastSync": "Last synced {{time}}" + } +} +``` + +**Usage:** +```tsx +t('dashboard.welcomeUser', { userName: 'Alice' }) // "Welcome back, Alice!" +``` + +#### Context Variants + +For gender or context-specific translations: + +```json +{ + "common": { + "delete": "Delete", + "delete_male": "Delete (m)", + "delete_female": "Delete (f)", + "save_short": "Save", + "save_long": "Save Changes" + } +} +``` + +**Usage:** +```tsx +t('common.delete', { context: 'male' }) // Uses delete_male +t('common.save', { context: 'short' }) // Uses save_short +``` + +#### Nested Keys + +Maximum 3 levels deep for maintainability: + +```json +{ + "security": { + "headers": { + "csp": "Content Security Policy", + "hsts": "HTTP Strict Transport Security" + }, + "waf": { + "enabled": "WAF Enabled", + "rulesets": "Active Rulesets" + } + } +} +``` + +**Usage:** +```tsx +t('security.headers.csp') // "Content Security Policy" +``` + +#### Boolean States + +Use consistent naming for on/off states: + +```json +{ + "common": { + "enabled": "Enabled", + "disabled": "Disabled", + "active": "Active", + "inactive": "Inactive", + "on": "On", + "off": "Off" + } +} +``` + +### Examples by Component Type + +#### Page Title & Description + +```json +{ + "proxyHosts": { + "title": "Proxy Hosts", + "description": "Manage your reverse proxy configurations" + } +} +``` + +#### Form Labels + +```json +{ + "proxyHosts": { + "domainNames": "Domain Names", + "forwardHost": "Forward Host", + "forwardPort": "Forward Port", + "sslEnabled": "SSL Enabled" + } +} +``` + +#### Button Actions + +```json +{ + "proxyHosts": { + "addHost": "Add Proxy Host", + "editHost": "Edit Proxy Host", + "deleteHost": "Delete Proxy Host", + "bulkActions": "Bulk Actions" + } +} +``` + +#### Table Columns + +```json +{ + "proxyHosts": { + "columnDomain": "Domain", + "columnTarget": "Target", + "columnStatus": "Status", + "columnActions": "Actions" + } +} +``` + +#### Confirmation Dialogs + +```json +{ + "proxyHosts": { + "confirmDelete": "Are you sure you want to delete {{domainName}}?", + "confirmBulkDelete": "Delete {{count}} proxy host(s)?", + "confirmDisable": "Disable this proxy host?" + } +} +``` + +### Anti-Patterns to Avoid + +❌ **Don't repeat category in key:** +```json +{ "proxyHosts": { "proxyHostsTitle": "..." } } // Wrong +{ "proxyHosts": { "title": "..." } } // Correct +``` + +❌ **Don't embed markup:** +```json +{ "common": { "warning": "Warning: ..." } } // Wrong +{ "common": { "warning": "Warning: ..." } } // Correct +``` + +❌ **Don't hardcode units:** +```json +{ "uptime": { "responseTime": "Response Time (ms)" } } // Wrong +{ "uptime": { "responseTime": "Response Time", "unitMs": "ms" } } // Correct +``` + +❌ **Don't use generic keys for specific content:** +```json +{ "common": { "text1": "...", "text2": "..." } } // Wrong +{ "proxyHosts": { "helpText": "...", "warningText": "..." } } // Correct +``` + +--- + +## Risk Assessment & Mitigation + +### Risk 1: State Management Re-render Performance + +**Risk Level:** 🟑 MEDIUM + +**Description:** Adding `useTranslation()` hook to every component may cause unnecessary re-renders when language changes, especially in large components like ProxyHosts.tsx (1023 lines). + +**Impact:** +- Language changes trigger re-render of all components using `useTranslation()` +- Potential UI lag or frozen state during language switch +- Memory pressure from simultaneous component updates + +**Mitigation Strategies:** +1. **Use React.memo for expensive components:** + ```tsx + export default React.memo(ProxyHosts) + ``` + +2. **Memoize translation calls in render-heavy components:** + ```tsx + const columns = useMemo(() => [ + { header: t('proxyHosts.domain'), ... }, + { header: t('proxyHosts.target'), ... } + ], [t, language]) + ``` + +3. **Split large components into smaller, memoized subcomponents:** + ```tsx + const ProxyHostTable = React.memo(({ data }) => { ... }) + const ProxyHostForm = React.memo(({ onSave }) => { ... }) + ``` + +4. **Add performance monitoring:** + ```tsx + useEffect(() => { + const start = performance.now() + return () => { + const duration = performance.now() - start + if (duration > 100) console.warn('Slow render:', duration) + } + }) + ``` + +**Acceptance Criteria:** +- Language switch completes in < 500ms on Desktop +- Language switch completes in < 1000ms on Mobile +- No visible UI freezing during switch +- Memory usage increase < 10% after language switch + +--- + +### Risk 2: Third-Party Component i18n Support + +**Risk Level:** 🟠 HIGH + +**Description:** Some third-party UI components (DataTable, Dialog, DatePicker, etc.) may not properly support dynamic language changes or may have their own i18n systems. + +**Affected Components:** +- DataTable (pagination, sorting labels) +- Date/Time Pickers (month names, day names) +- Form validation libraries (error messages) +- Rich text editors +- File upload components + +**Mitigation Strategies:** +1. **Audit all third-party components** (Pre-Phase 1): + ```bash + grep -r "import.*from" frontend/src/components | grep -E "(table|date|form|picker|editor)" + ``` + +2. **Wrapper pattern for incompatible components:** + ```tsx + // Wrap DatePicker with localized props + const LocalizedDatePicker = ({ ...props }) => { + const { i18n } = useTranslation() + return ( + + ) + } + ``` + +3. **Replace components if necessary:** + - Document replacement decisions + - Ensure feature parity + - Test thoroughly + +4. **Configure third-party i18n integrations:** + ```tsx + // For libraries like react-datepicker + import { registerLocale, setDefaultLocale } from "react-datepicker"; + import es from 'date-fns/locale/es'; + registerLocale('es', es); + ``` + +**Action Items:** +- Create compatibility matrix (see below) +- Test each component with all 5 languages +- Document workarounds in component README + +--- + +### Risk 3: Date/Time/Number Formatting + +**Risk Level:** 🟑 MEDIUM + +**Description:** Dates, times, numbers, and currencies need locale-aware formatting. Hardcoded formats (MM/DD/YYYY) will not adapt to user locale. + +**Examples:** +- Dates: US (12/31/2025) vs EU (31/12/2025) +- Times: 12-hour (3:00 PM) vs 24-hour (15:00) +- Numbers: 1,234.56 (US) vs 1.234,56 (EU) +- Currencies: $1,234.56 vs 1 234,56 € + +**Mitigation Strategies:** +1. **Use Intl API for formatting:** + ```tsx + // Date formatting + const formatDate = (date: Date, locale: string) => { + return new Intl.DateTimeFormat(locale, { + year: 'numeric', + month: 'long', + day: 'numeric' + }).format(date) + } + + // Number formatting + const formatNumber = (num: number, locale: string) => { + return new Intl.NumberFormat(locale).format(num) + } + + // Currency formatting + const formatCurrency = (amount: number, locale: string, currency: string) => { + return new Intl.NumberFormat(locale, { + style: 'currency', + currency + }).format(amount) + } + ``` + +2. **Create formatting utilities:** + ```tsx + // frontend/src/utils/formatting.ts + import { useTranslation } from 'react-i18next' + + export const useFormatting = () => { + const { i18n } = useTranslation() + + return { + date: (date: Date) => formatDate(date, i18n.language), + time: (date: Date) => formatTime(date, i18n.language), + number: (num: number) => formatNumber(num, i18n.language), + relativeTime: (date: Date) => formatRelativeTime(date, i18n.language) + } + } + ``` + +3. **Use date-fns with locale support:** + ```tsx + import { format } from 'date-fns' + import { es, fr, de, zhCN } from 'date-fns/locale' + + const locales = { en: enUS, es, fr, de, zh: zhCN } + + format(new Date(), 'PPP', { locale: locales[language] }) + ``` + +**Acceptance Criteria:** +- All dates use `Intl.DateTimeFormat` or `date-fns` with locale +- All numbers use `Intl.NumberFormat` +- No hardcoded date/number formats in components + +--- + +### Risk 4: RTL (Right-to-Left) Language Support + +**Risk Level:** 🟑 MEDIUM + +**Description:** Future support for RTL languages (Arabic, Hebrew) will require layout and CSS adjustments. Current plan only includes LTR languages, but architecture should not prevent RTL addition. + +**Current Languages:** All LTR (English, Spanish, French, German, Chinese) +**Future Consideration:** Arabic (ar), Hebrew (he) + +**Mitigation Strategies:** +1. **Use logical CSS properties now:** + ```css + /* ❌ Avoid */ + margin-left: 16px; + padding-right: 8px; + + /* βœ… Use instead */ + margin-inline-start: 16px; + padding-inline-end: 8px; + ``` + +2. **Avoid absolute positioning where possible:** + ```css + /* ❌ Problematic for RTL */ + position: absolute; + left: 0; + + /* βœ… Use flexbox/grid */ + display: flex; + justify-content: flex-start; + ``` + +3. **Add `dir` attribute support to root:** + ```tsx + // main.tsx or App.tsx + useEffect(() => { + document.dir = i18n.dir(i18n.language) + }, [i18n.language]) + ``` + +4. **Test with RTL browser extension:** + - Install "Force RTL" browser extension + - Validate layout doesn't break + - Check icon alignment + +**Acceptance Criteria:** +- All CSS uses logical properties (inline-start/end) +- No hardcoded left/right positioning +- `dir` attribute infrastructure in place +- Layout tested with "Force RTL" tool + +--- + +### Risk 5: Translation File Drift + +**Risk Level:** 🟠 HIGH + +**Description:** Over time, translation files can become out of sync as developers add keys to English but forget to update other languages, leading to missing translations and fallback to English. + +**Impact:** +- Inconsistent user experience across languages +- Some text remains in English in non-English locales +- Hard to track which keys are missing + +**Mitigation Strategies:** +1. **Automated sync checking (CI/CD - see Maintenance Strategy section)** + +2. **Translation key generation script:** + ```bash + # scripts/sync-translations.sh + #!/bin/bash + node scripts/sync-translation-keys.js + ``` + + ```javascript + // scripts/sync-translation-keys.js + const fs = require('fs') + const path = require('path') + + const localesDir = path.join(__dirname, '../frontend/src/locales') + const enFile = path.join(localesDir, 'en/translation.json') + const enKeys = JSON.parse(fs.readFileSync(enFile, 'utf8')) + + const languages = ['es', 'fr', 'de', 'zh'] + + languages.forEach(lang => { + const langFile = path.join(localesDir, `${lang}/translation.json`) + const langKeys = JSON.parse(fs.readFileSync(langFile, 'utf8')) + const missingKeys = findMissingKeys(enKeys, langKeys) + + if (missingKeys.length > 0) { + console.error(`❌ Missing keys in ${lang}:`, missingKeys) + process.exit(1) + } + }) + ``` + +3. **Pull request template requirement:** + - Checklist item: "All translation files updated" + - Automated comment if keys don't match + +4. **Fallback chain with warnings:** + ```tsx + // i18n.ts + i18n.init({ + fallbackLng: 'en', + missingKeyHandler: (lngs, ns, key) => { + console.warn(`Missing translation key: ${key} for language: ${lngs[0]}`) + // Optionally report to error tracking + Sentry.captureMessage(`Missing i18n key: ${key}`) + } + }) + ``` + +**Acceptance Criteria:** +- CI fails if translation keys don't match +- Missing keys logged to console in development +- PR template includes translation checklist + +--- + +### Risk Mitigation Summary + +| Risk | Level | Primary Mitigation | Monitoring | +|------|-------|-------------------|------------| +| Re-render Performance | 🟑 Medium | React.memo, useMemo | Performance profiling in Phase 1 | +| Third-Party Components | 🟠 High | Audit + wrapper pattern | Manual QA per component | +| Date/Number Formatting | 🟑 Medium | Intl API utilities | Visual QA in all locales | +| RTL Support | 🟑 Medium | Logical CSS properties | RTL browser extension testing | +| Translation Drift | 🟠 High | CI checks + scripts | Automated on every PR | + +--- + +## Implementation Plan - Revised Phases + +**Strategy:** Validate pattern early with high-visibility components, then scale systematically. Each phase includes implementation, testing, code review, and bug fixes. + +--- + +### Phase 1: Layout & Navigation (Days 1-3) +**Objective:** Establish pattern with most visible user-facing component. Validates infrastructure and approach. + +**Files:** +- `frontend/src/components/Layout.tsx` (367 lines) +- `frontend/src/components/LanguageSelector.tsx` (already uses translations) + +**Tasks:** +- [ ] Day 1: Add missing translation keys (48 new keys) to all 5 language files +- [ ] Day 1: Update navigation array to use `t('navigation.*')` keys +- [ ] Day 1: Update logout/profile buttons +- [ ] Day 1: Update sidebar tooltips +- [ ] Day 2: Create Layout.test.tsx with language switching tests +- [ ] Day 2: Manual QA in all 5 languages +- [ ] Day 2: Code review and address feedback +- [ ] Day 3: Fix bugs, performance profiling, merge PR + +**Success Criteria:** +- Navigation menu switches languages instantly +- No console warnings for missing keys +- All 5 languages render correctly +- Performance: < 100ms to switch languages +- Code review approved + +**Example Changes:** +```tsx +// Before +const navigation: NavItem[] = [ + { name: 'Dashboard', path: '/', icon: 'πŸ“Š' }, + { name: 'Proxy Hosts', path: '/proxy-hosts', icon: '🌐' } +] + +// After +const { t } = useTranslation() +const navigation: NavItem[] = [ + { name: t('navigation.dashboard'), path: '/', icon: 'πŸ“Š' }, + { name: t('navigation.proxyHosts'), path: '/proxy-hosts', icon: '🌐' } +] +``` + +--- + +### Phase 2: ProxyHosts (Days 4-7) +**Objective:** Validate pattern on largest, most complex component. Proves approach scales to complex forms and tables. + +**Files:** +- `frontend/src/pages/ProxyHosts.tsx` (1023 lines) **HIGHEST COMPLEXITY** +- `frontend/src/components/ProxyHostForm.tsx` (if exists) + +**Tasks:** +- [ ] Day 4: Update PageShell title/description +- [ ] Day 4: Update all button text (Create, Edit, Delete, Bulk Apply) +- [ ] Day 5: Update DataTable column headers +- [ ] Day 5: Update form labels and placeholders +- [ ] Day 5: Update status badges (Enabled/Disabled, SSL indicators) +- [ ] Day 6: Update dialogs (confirmation, bulk update) +- [ ] Day 6: Update toast/notification messages +- [ ] Day 6: Add ProxyHosts.test.tsx +- [ ] Day 6: Manual QA with CRUD operations +- [ ] Day 7: Code review, bug fixes, performance check, merge PR + +**Success Criteria:** +- All UI text translates correctly +- Form validation messages localized +- Toast notifications in selected language +- No layout breaks in any language (especially German - longest strings) +- Performance: Page renders in < 200ms after language change +- Code review approved + +**Example Changes:** +```tsx +// Before + + +// After +const { t } = useTranslation() + +``` + +--- + +### Phase 3: Core Pages (Days 8-12) +**Objective:** Apply validated pattern to remaining core pages. Parallelizable work. + +**Files (in priority order):** +1. `SystemSettings.tsx` (430 lines) - Already imports LanguageSelector +2. `Security.tsx` (500 lines) +3. `AccessLists.tsx` (700 lines) +4. `Certificates.tsx` (600 lines) +5. `RemoteServers.tsx` (500 lines) +6. `Domains.tsx` (400 lines) + +**Tasks:** +- [ ] Day 8: SystemSettings, Security (2 files) +- [ ] Day 9: AccessLists, Certificates (2 files) +- [ ] Day 10: RemoteServers, Domains (2 files) +- [ ] Day 11: Add tests for all 6 files +- [ ] Day 11: Manual QA for all pages +- [ ] Day 12: Code review, bug fixes, merge PRs + +**Success Criteria:** +- All pages follow established pattern +- Tests pass for all components +- No regressions in functionality +- Code reviews approved + +--- + +### Phase 4: Dashboard & Supporting Pages (Days 13-15) +**Objective:** Complete main application pages and validate integration across full workflow. + +**Files:** +- `Dashboard.tsx` (177 lines) - **Integration validation** +- `CrowdSecConfig.tsx` (600 lines) +- `WafConfig.tsx` (400 lines) +- `RateLimiting.tsx` (400 lines) +- `Uptime.tsx` (500 lines) +- `Notifications.tsx` (400 lines) +- `UsersPage.tsx` (500 lines) +- `SecurityHeaders.tsx` (800 lines) + +**Tasks:** +- [ ] Day 13: Dashboard, CrowdSecConfig, WafConfig +- [ ] Day 14: RateLimiting, Uptime, Notifications, UsersPage +- [ ] Day 14: SecurityHeaders +- [ ] Day 15: Integration tests (full user workflow in each language) +- [ ] Day 15: Code review, bug fixes, merge PRs + +**Success Criteria:** +- Dashboard correctly aggregates translated content +- All stats and widgets display localized text +- Full workflow (create proxy β†’ configure SSL β†’ test) works in all languages +- Code reviews approved + +--- + +### Phase 5: Auth & Setup Pages (Days 16-17) +**Objective:** Critical user onboarding experience. Must be perfect. + +**Files:** +- `Login.tsx` (200 lines) +- `Setup.tsx` (300 lines) +- `Account.tsx` (300 lines) + +**Tasks:** +- [ ] Day 16: Login, Setup pages +- [ ] Day 16: Account page +- [ ] Day 16: Test authentication flows in all languages +- [ ] Day 17: QA first-time setup experience +- [ ] Day 17: Code review, bug fixes, merge PR + +**Success Criteria:** +- First-time users see setup in their browser's default language +- Login errors display in correct language +- Form validation messages localized +- Success/error toasts localized +- Code review approved + +--- + +### Phase 6: Utility Pages & Final Integration (Days 18-19) +**Objective:** Complete remaining pages and ensure consistency. + +**Files:** +- `Backups.tsx` (400 lines) +- `Tasks.tsx` (300 lines) +- `Logs.tsx` (400 lines) +- `ImportCaddy.tsx` (200 lines) +- `ImportCrowdSec.tsx` (200 lines) +- `SMTPSettings.tsx` (400 lines) + +**Tasks:** +- [ ] Day 18: All utility pages +- [ ] Day 18: Import pages +- [ ] Day 18: SMTP settings +- [ ] Day 19: Final integration QA +- [ ] Day 19: Code reviews, bug fixes, merge PRs + +**Success Criteria:** +- All pages translated +- Import workflows work in all languages +- No missing translation keys +- Code reviews approved + +--- + +### Phase 7: Comprehensive QA & Polish (Days 20-23) +**Objective:** Thorough testing, bug fixes, performance optimization, and production readiness. + +**Tasks:** + +#### Day 20: Automated Testing +- [ ] Run full test suite in all 5 languages +- [ ] Translation coverage tests (100% key coverage) +- [ ] Bundle size analysis (ensure no significant increase) +- [ ] Performance profiling (language switching speed) +- [ ] Accessibility testing (screen reader compatibility) + +#### Day 21: Manual QA - Core Workflows +- [ ] Test full user workflows in all 5 languages: + - [ ] First-time setup + - [ ] Login/logout + - [ ] Create/edit/delete proxy host + - [ ] Configure SSL certificate + - [ ] Apply access list + - [ ] Configure security settings + - [ ] View logs and tasks +- [ ] Test language switching mid-workflow (e.g., while editing form) +- [ ] Test WebSocket reconnection with language changes (logs page) +- [ ] Test browser back/forward with language changes + +#### Day 22: Edge Cases & Error Handling +- [ ] Backend API errors in all languages +- [ ] Network errors with WebSocket (logs page) +- [ ] Mid-edit language switches (forms preserve data) +- [ ] Rapid language switching (no race conditions) +- [ ] Browser locale detection on first visit +- [ ] LocalStorage corruption/missing (graceful fallback) + +#### Day 23: Final Polish & Documentation +- [ ] Fix all bugs found in QA +- [ ] Update user documentation with language switching instructions +- [ ] Create developer guide for adding new translations +- [ ] Final performance check +- [ ] Prepare release notes + +**Success Criteria:** +- All automated tests pass +- All manual QA workflows complete successfully +- No P0/P1 bugs remaining +- Performance meets targets (< 500ms language switch) +- Bundle size increase < 50KB +- Documentation updated + +--- + +## Detailed Timeline (3-4 Weeks) + +### Week 1: Foundation & Validation + +| Day | Phase | Tasks | Deliverables | +|-----|-------|-------|--------------| +| **Mon 1** | Phase 1 | Add missing keys, update Layout.tsx navigation | Navigation menu translations | +| **Tue 2** | Phase 1 | Tests, QA, code review | Layout PR ready | +| **Wed 3** | Phase 1 | Bug fixes, performance, merge | βœ… Layout complete | +| **Thu 4-5** | Phase 2 | ProxyHosts.tsx implementation | ProxyHosts translations | +| **Fri 5** | Phase 2 | ProxyHosts forms, tables | ProxyHosts UI complete | + +**Week 1 Milestones:** +- βœ… Navigation fully translated (most visible change) +- βœ… ProxyHosts 80% complete (validates complex component approach) +- βœ… Pattern established and documented + +--- + +### Week 2: Core Pages Rollout + +| Day | Phase | Tasks | Deliverables | +|-----|-------|-------|--------------| +| **Mon 6-7** | Phase 2 | ProxyHosts dialogs, toasts, tests, QA | βœ… ProxyHosts complete | +| **Tue 8** | Phase 3 | SystemSettings, Security | 2 pages complete | +| **Wed 9** | Phase 3 | AccessLists, Certificates | 2 pages complete | +| **Thu 10** | Phase 3 | RemoteServers, Domains | 2 pages complete | +| **Fri 11-12** | Phase 3 | Tests, QA, code reviews, bug fixes | βœ… 6 core pages complete | + +**Week 2 Milestones:** +- βœ… ProxyHosts complete (largest component done) +- βœ… 6 additional core pages translated +- βœ… All security-related pages functional + +--- + +### Week 3: Dashboard Integration & Auth + +| Day | Phase | Tasks | Deliverables | +|-----|-------|-------|--------------| +| **Mon 13** | Phase 4 | Dashboard, CrowdSec, WAF | Dashboard + 2 config pages | +| **Tue 14** | Phase 4 | Rate Limiting, Uptime, Notifications, Users, Headers | 5 pages complete | +| **Wed 15** | Phase 4 | Integration tests, QA, bug fixes | βœ… All main pages complete | +| **Thu 16** | Phase 5 | Login, Setup, Account | Auth flow complete | +| **Fri 17** | Phase 5 | Auth QA, bug fixes | βœ… Critical auth complete | + +**Week 3 Milestones:** +- βœ… Dashboard integrated (validates cross-page consistency) +- βœ… All security and monitoring pages complete +- βœ… Auth and setup flows fully translated + +--- + +### Week 4: Finalization & QA + +| Day | Phase | Tasks | Deliverables | +|-----|-------|-------|--------------| +| **Mon 18** | Phase 6 | Backups, Tasks, Logs, Import pages, SMTP | All utility pages | +| **Tue 19** | Phase 6 | Final integration, code reviews | βœ… All pages complete | +| **Wed 20** | Phase 7 | Automated testing, bundle analysis | Test results, metrics | +| **Thu 21** | Phase 7 | Manual QA - core workflows | QA report | +| **Fri 22** | Phase 7 | Edge case testing, bug fixes | Bug list, fixes | +| **Mon 23** | Phase 7 | Final polish, documentation | βœ… Production ready | + +**Week 4 Milestones:** +- βœ… 100% of pages translated +- βœ… All automated tests passing +- βœ… All manual QA complete +- βœ… Documentation updated +- βœ… Ready for production deployment + +--- + +### Buffer Time (Optional Week 5) + +**Purpose:** Handle unexpected delays, additional bugs, or extended QA + +| Day | Tasks | +|-----|-------| +| Mon 24 | Address any remaining P1 bugs | +| Tue 25 | Additional QA if needed | +| Wed 26 | Performance optimization | +| Thu 27 | Stakeholder review | +| Fri 28 | Final production prep | + +--- + +### Daily Stand-up Template + +**What was completed yesterday:** +- [Specific pages/components translated] +- [Tests added] +- [Bugs fixed] + +**What will be done today:** +- [Specific pages to translate] +- [Tests to add] +- [Code reviews to complete] + +**Blockers:** +- [Any issues blocking progress] +- [Missing information or dependencies] + +**QA Status:** +- [Pages ready for QA] +- [Bugs found] +- [Bugs fixed] + +--- + +## Code Review Checklist + +Use this checklist for EVERY pull request containing translation changes. + +### Pre-Review (Author Self-Check) + +- [ ] All hardcoded strings replaced with translation keys +- [ ] Translation keys added to ALL 5 language files (en, es, fr, de, zh) +- [ ] Keys follow naming convention (category.subcategory.descriptor) +- [ ] Dynamic content uses interpolation (`{{variableName}}`) +- [ ] Pluralization handled correctly (count, count_plural) +- [ ] Component imports `useTranslation` from 'react-i18next' +- [ ] Component calls `const { t } = useTranslation()` inside function body +- [ ] Tests added/updated for component +- [ ] Manual QA completed in at least 3 languages +- [ ] No console warnings for missing keys +- [ ] No layout breaks or text overflow in any language + +### Code Quality + +- [ ] **Import statement correct:** + ```tsx + import { useTranslation } from 'react-i18next' + ``` + +- [ ] **Hook placement correct (inside component):** + ```tsx + export default function MyComponent() { + const { t } = useTranslation() // βœ… Correct + // ... + } + ``` + +- [ ] **Translation keys valid (no typos, exist in files):** + ```tsx + t('proxyHosts.title') // βœ… Key exists + t('proxyhosts.titel') // ❌ Typo, wrong key + ``` + +- [ ] **Interpolation syntax correct:** + ```tsx + t('dashboard.activeHosts', { count: 5 }) // βœ… Correct + t('dashboard.activeHosts', { num: 5 }) // ❌ Variable name mismatch + ``` + +- [ ] **No string concatenation:** + ```tsx + // ❌ Wrong +

{t('common.total')}: {count}

+ + // βœ… Correct +

{t('common.totalCount', { count })}

+ ``` + +### Translation File Quality + +- [ ] **All 5 files updated (en, es, fr, de, zh)** +- [ ] **Keys in same order in all files** +- [ ] **No duplicate keys** +- [ ] **No missing commas or JSON syntax errors** +- [ ] **Interpolation placeholders match:** + ```json + // en + "activeHosts": "{{count}} active" + // es (same placeholder name) + "activeHosts": "{{count}} activos" + ``` + +- [ ] **Pluralization implemented if needed:** + ```json + "count": "{{count}} item", + "count_plural": "{{count}} items" + ``` + +### Performance + +- [ ] **Large components use React.memo:** + ```tsx + export default React.memo(ProxyHosts) + ``` + +- [ ] **Expensive translation calls memoized:** + ```tsx + const columns = useMemo(() => [ + { header: t('common.name'), ... } + ], [t]) + ``` + +- [ ] **No unnecessary re-renders on language change** +- [ ] **Bundle size increase documented (if > 5KB)** + +### Testing + +- [ ] **Unit tests added/updated:** + ```tsx + it('renders in Spanish', () => { + i18n.changeLanguage('es') + render() + expect(screen.getByText('Panel de Control')).toBeInTheDocument() + }) + ``` + +- [ ] **Translation key existence test:** + ```tsx + it('all keys exist in all languages', () => { + const enKeys = Object.keys(en) + languages.forEach(lang => { + expect(Object.keys(translations[lang])).toEqual(enKeys) + }) + }) + ``` + +- [ ] **Language switching test:** + ```tsx + it('updates when language changes', () => { + const { rerender } = render() + expect(screen.getByText('Dashboard')).toBeInTheDocument() + + i18n.changeLanguage('es') + rerender() + expect(screen.getByText('Panel de Control')).toBeInTheDocument() + }) + ``` + +### Accessibility + +- [ ] **ARIA labels translated:** + ```tsx + + ``` + +- [ ] **Form labels associated correctly:** + ```tsx + + + ``` + +- [ ] **Error messages accessible:** + ```tsx + {t('errors.required')} + ``` + +- [ ] **Screen reader tested (if available)** + +### UI/UX + +- [ ] **No text overflow in any language (especially German)** +- [ ] **Buttons and labels don't break layout** +- [ ] **Proper spacing maintained** +- [ ] **Text direction correct (all LTR for current languages)** +- [ ] **Font rendering acceptable for all languages** + +### Edge Cases + +- [ ] **Empty states translated:** + ```tsx + {items.length === 0 &&

{t('common.noData')}

} + ``` + +- [ ] **Error messages translated:** + ```tsx + catch (error) { + toast.error(t('errors.saveFailed')) + } + ``` + +- [ ] **Loading states translated:** + ```tsx + {loading && {t('common.loading')}} + ``` + +- [ ] **Confirmation dialogs translated:** + ```tsx + const confirmed = window.confirm(t('proxyHosts.confirmDelete', { domain })) + ``` + +### Documentation + +- [ ] **Translation keys documented (if new pattern)** +- [ ] **Component README updated (if applicable)** +- [ ] **PR description includes:** + - Pages/components updated + - New translation keys added + - Manual QA results (languages tested) + - Screenshots (if UI changes visible) + - Performance impact (if measurable) + +### Final Checks + +- [ ] **All tests pass locally** +- [ ] **CI/CD pipeline passes** +- [ ] **No console errors or warnings** +- [ ] **Translation sync check passes** +- [ ] **Manual QA completed in β‰₯3 languages:** + - [ ] English (en) + - [ ] Spanish (es) OR French (fr) + - [ ] German (de) OR Chinese (zh) + +--- + +## Testing Strategy (Expanded) + +### 1. Automated Unit Tests + +**Coverage Target:** 90%+ for translation-enabled components + +#### Translation Key Existence Tests + +```typescript +// frontend/src/__tests__/translation-coverage.test.ts +import { describe, it, expect } from 'vitest' +import enTranslations from '../locales/en/translation.json' +import esTranslations from '../locales/es/translation.json' +import frTranslations from '../locales/fr/translation.json' +import deTranslations from '../locales/de/translation.json' +import zhTranslations from '../locales/zh/translation.json' + +function flattenKeys(obj: any, prefix = ''): string[] { + return Object.keys(obj).reduce((acc: string[], key) => { + const fullKey = prefix ? `${prefix}.${key}` : key + if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { + return [...acc, ...flattenKeys(obj[key], fullKey)] + } + return [...acc, fullKey] + }, []) +} + +describe('Translation Coverage', () => { + const languages = [ + { name: 'Spanish', code: 'es', translations: esTranslations }, + { name: 'French', code: 'fr', translations: frTranslations }, + { name: 'German', code: 'de', translations: deTranslations }, + { name: 'Chinese', code: 'zh', translations: zhTranslations } + ] + + const enKeys = flattenKeys(enTranslations) + + languages.forEach(({ name, code, translations }) => { + it(`all English keys exist in ${name}`, () => { + const langKeys = flattenKeys(translations) + const missingKeys = enKeys.filter(key => !langKeys.includes(key)) + + expect(missingKeys).toEqual([]) + }) + + it(`no extra keys in ${name}`, () => { + const langKeys = flattenKeys(translations) + const extraKeys = langKeys.filter(key => !enKeys.includes(key)) + + expect(extraKeys).toEqual([]) + }) + }) + + it('all files have same number of keys', () => { + const counts = languages.map(({ translations }) => + flattenKeys(translations).length + ) + + counts.forEach(count => { + expect(count).toBe(enKeys.length) + }) + }) +}) +``` + +#### Component Translation Tests + +```typescript +// frontend/src/pages/__tests__/Dashboard.test.tsx +import { render, screen } from '@testing-library/react' +import { describe, it, expect, beforeEach } from 'vitest' +import Dashboard from '../Dashboard' +import i18n from '../../i18n' + +describe('Dashboard Translations', () => { + beforeEach(async () => { + await i18n.changeLanguage('en') + }) + + it('renders in English by default', () => { + render() + expect(screen.getByText('Dashboard')).toBeInTheDocument() + expect(screen.getByText(/overview of your/i)).toBeInTheDocument() + }) + + it('renders in Spanish', async () => { + await i18n.changeLanguage('es') + render() + expect(screen.getByText('Panel de Control')).toBeInTheDocument() + }) + + it('renders in French', async () => { + await i18n.changeLanguage('fr') + render() + expect(screen.getByText('Tableau de bord')).toBeInTheDocument() + }) + + it('renders in German', async () => { + await i18n.changeLanguage('de') + render() + expect(screen.getByText('Dashboard')).toBeInTheDocument() + }) + + it('renders in Chinese', async () => { + await i18n.changeLanguage('zh') + render() + expect(screen.getByText('δ»ͺ葨板')).toBeInTheDocument() + }) + + it('updates when language changes', async () => { + const { rerender } = render() + expect(screen.getByText('Dashboard')).toBeInTheDocument() + + await i18n.changeLanguage('es') + rerender() + + expect(screen.queryByText('Dashboard')).not.toBeInTheDocument() + expect(screen.getByText('Panel de Control')).toBeInTheDocument() + }) +}) +``` + +#### Dynamic Content Translation Tests + +```typescript +// frontend/src/pages/__tests__/ProxyHosts.test.tsx +it('handles plural translations correctly', async () => { + await i18n.changeLanguage('en') + render() + + // Mock data with 1 item + expect(screen.getByText('1 active')).toBeInTheDocument() + + // Mock data with 5 items + expect(screen.getByText('5 active')).toBeInTheDocument() +}) + +it('interpolates variables correctly', async () => { + await i18n.changeLanguage('en') + render() + + expect(screen.getByText(/delete example\.com/i)).toBeInTheDocument() +}) +``` + +--- + +### 2. Manual QA Testing + +#### Per-Component QA Checklist + +For each component/page: + +- [ ] **Visual Inspection:** + - [ ] Text renders correctly in all 5 languages + - [ ] No text overflow or truncation + - [ ] Buttons don't break layout + - [ ] Proper spacing maintained + +- [ ] **Functional Testing:** + - [ ] All buttons clickable + - [ ] Forms submit correctly + - [ ] Validation messages display + - [ ] Error/success toasts appear + - [ ] Dialogs open/close properly + +- [ ] **Language Switching:** + - [ ] Switch to each language from selector + - [ ] UI updates immediately + - [ ] No console errors + - [ ] Selection persists on reload + +- [ ] **Dynamic Content:** + - [ ] Numbers format correctly + - [ ] Dates display in proper format + - [ ] Plurals work correctly + - [ ] Variable interpolation works + +#### Language-Specific Testing + +**German Testing (Longest Strings):** +- Focus on buttons and labels that may overflow +- Check table headers don't wrap awkwardly +- Verify no horizontal scrolling triggered + +**Chinese Testing (Character Width):** +- Ensure proper font rendering +- Check spacing between characters +- Verify no character clipping + +--- + +### 3. Edge Case Testing + +#### Mid-Edit Language Changes + +**Test Scenario:** User is filling out a form, switches language mid-edit + +```typescript +// frontend/src/pages/__tests__/ProxyHosts.edge-cases.test.tsx +it('preserves form data when language changes', async () => { + render() + + // Fill form in English + const domainInput = screen.getByLabelText('Domain Names') + await userEvent.type(domainInput, 'example.com') + + // Switch to Spanish + await i18n.changeLanguage('es') + + // Verify data still there + expect(domainInput).toHaveValue('example.com') + + // Verify label changed + expect(screen.getByLabelText('Nombres de Dominio')).toBeInTheDocument() +}) +``` + +**Expected Behavior:** +- Form data preserved +- Labels update to new language +- Validation messages in new language +- No data loss + +--- + +#### Backend Error Handling + +**Test Scenario:** API returns error while UI is in non-English language + +```typescript +it('displays backend errors in current language', async () => { + await i18n.changeLanguage('es') + + // Mock API error + server.use( + http.post('/api/proxy-hosts', () => { + return HttpResponse.json( + { error: 'Invalid domain' }, + { status: 400 } + ) + }) + ) + + render() + const submitButton = screen.getByText('Guardar') + await userEvent.click(submitButton) + + // Error toast should be in Spanish + await waitFor(() => { + expect(screen.getByText(/error al guardar/i)).toBeInTheDocument() + }) +}) +``` + +**Expected Behavior:** +- API errors trigger translated error messages +- Toast notifications in current language +- Console logging in English (for debugging) + +--- + +#### WebSocket Reconnection + +**Test Scenario:** WebSocket disconnects/reconnects while viewing logs in non-English + +```typescript +it('handles WebSocket reconnection with translations', async () => { + await i18n.changeLanguage('fr') + render() + + // Verify initial state + expect(screen.getByText('ConnectΓ©')).toBeInTheDocument() + + // Simulate disconnect + mockWebSocket.close() + + await waitFor(() => { + expect(screen.getByText('DΓ©connectΓ©')).toBeInTheDocument() + }) + + // Simulate reconnect + mockWebSocket.open() + + await waitFor(() => { + expect(screen.getByText('ConnectΓ©')).toBeInTheDocument() + }) +}) +``` + +**Expected Behavior:** +- Connection status messages translated +- Log messages display correctly after reconnect +- No data loss during reconnection + +--- + +#### Rapid Language Switching + +**Test Scenario:** User rapidly clicks through all 5 languages + +```typescript +it('handles rapid language switching without errors', async () => { + const { rerender } = render() + + const languages = ['en', 'es', 'fr', 'de', 'zh'] + + for (const lang of languages) { + await i18n.changeLanguage(lang) + rerender() + + // Should not throw errors + expect(screen.getByRole('navigation')).toBeInTheDocument() + } + + // No console errors + expect(console.error).not.toHaveBeenCalled() +}) +``` + +**Expected Behavior:** +- No race conditions +- UI updates cleanly each time +- No console errors or warnings +- Final language selection persists + +--- + +### 4. Accessibility Testing + +#### Screen Reader Compatibility + +**Manual Test Steps:** +1. Enable screen reader (NVDA on Windows, VoiceOver on Mac) +2. Navigate application using keyboard only +3. Verify announcements in selected language + +**Test Checklist:** +- [ ] Page titles announced in correct language +- [ ] Button labels read correctly +- [ ] Form labels associated properly +- [ ] Error messages announced +- [ ] ARIA labels translated +- [ ] Live regions update with translations + +**Example Test:** +```typescript +it('has accessible labels in all languages', async () => { + await i18n.changeLanguage('es') + render() + + const closeButton = screen.getByRole('button', { name: 'Cerrar' }) + expect(closeButton).toHaveAttribute('aria-label', 'Cerrar') +}) +``` + +--- + +### 5. Performance Testing + +#### Bundle Size Analysis + +```bash +# Before implementation +npm run build +ls -lh dist/assets/*.js + +# After implementation +npm run build +ls -lh dist/assets/*.js + +# Calculate increase +``` + +**Acceptance Criteria:** +- Bundle size increase < 50KB (compressed) +- Translation files lazy-loaded per language +- Only active language loaded initially + +**Tool:** `webpack-bundle-analyzer` or `rollup-plugin-visualizer` + +```bash +npm run build -- --analyze +``` + +--- + +#### Language Switch Performance + +**Test Script:** +```typescript +// frontend/src/__tests__/performance.test.ts +it('switches language in under 500ms', async () => { + render() + + const start = performance.now() + await i18n.changeLanguage('es') + const duration = performance.now() - start + + expect(duration).toBeLessThan(500) +}) +``` + +**Manual Test:** +1. Open DevTools β†’ Performance +2. Start recording +3. Click language selector +4. Select different language +5. Stop recording +6. Analyze flame graph + +**Acceptance Criteria:** +- Desktop: < 500ms total switch time +- Mobile: < 1000ms total switch time +- No visible UI freezing +- No layout thrashing + +--- + +#### Memory Profiling + +**Test Procedure:** +1. Open DevTools β†’ Memory +2. Take heap snapshot (baseline) +3. Switch languages 10 times +4. Take another heap snapshot +5. Compare memory usage + +**Acceptance Criteria:** +- Memory increase < 10% after 10 switches +- No detached DOM nodes +- No event listener leaks + +--- + +### 6. Fallback Behavior Testing + +#### Missing Translation Keys + +**Test Scenario:** A translation key is missing in one language + +```typescript +// In es/translation.json, remove a key +// { +// "proxyHosts": { +// "title": "Proxy Hosts" +// // "description": "..." <- Missing +// } +// } + +it('falls back to English for missing keys', async () => { + await i18n.changeLanguage('es') + render() + + // Title should be in Spanish + expect(screen.getByText('Hosts de Proxy')).toBeInTheDocument() + + // Description falls back to English + expect(screen.getByText('Manage your reverse proxy configurations')).toBeInTheDocument() + + // Warning logged to console + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining('Missing translation key: proxyHosts.description') + ) +}) +``` + +**Expected Behavior:** +- Falls back to English gracefully +- Console warning in development +- Error reported to monitoring in production +- UI remains functional + +--- + +#### Corrupted LocalStorage + +**Test Scenario:** localStorage has invalid language value + +```typescript +it('handles corrupted language preference', () => { + localStorage.setItem('charon-language', 'invalid-lang') + + render() + + // Should fall back to browser default or 'en' + expect(i18n.language).toBe('en') +}) + +it('handles missing localStorage', () => { + // Simulate localStorage unavailable + const { localStorage: originalStorage } = window + Object.defineProperty(window, 'localStorage', { + get: () => { throw new Error('localStorage unavailable') } + }) + + render() + + // Should use browser language or default to 'en' + expect(i18n.language).toMatch(/en|es|fr|de|zh/) + + // Restore + Object.defineProperty(window, 'localStorage', { + get: () => originalStorage + }) +}) +``` + +**Expected Behavior:** +- Graceful fallback to browser default +- No application crashes +- Language can still be changed manually + +--- + +### 7. Regression Testing + +#### Existing Functionality + +**Test Checklist:** +- [ ] All existing unit tests still pass +- [ ] All existing integration tests still pass +- [ ] No broken API calls +- [ ] No broken WebSocket connections +- [ ] All forms submit correctly +- [ ] All CRUD operations work +- [ ] Authentication still works +- [ ] Authorization checks still work + +**Automated Regression Suite:** +```bash +npm run test:unit +npm run test:integration +npm run test:e2e +``` + +--- + +### 8. Cross-Browser Testing + +**Browsers to Test:** +- Chrome (latest) +- Firefox (latest) +- Safari (latest) +- Edge (latest) +- Mobile Safari (iOS) +- Mobile Chrome (Android) + +**Per-Browser Checklist:** +- [ ] Language selector works +- [ ] Translations render correctly +- [ ] localStorage persistence works +- [ ] No console errors +- [ ] Performance acceptable + +--- + +## Success Metrics & Verification + +### 1. Translation Coverage Metrics + +**Measurement Method:** Automated script + +```bash +# scripts/check-translation-coverage.sh +#!/bin/bash +set -e + +echo "Checking translation coverage..." + +# Run coverage test +npm run test:coverage:i18n + +# Check for hardcoded strings +echo "Searching for hardcoded strings..." +node scripts/find-hardcoded-strings.js + +echo "βœ… Translation coverage check complete" +``` + +```javascript +// scripts/find-hardcoded-strings.js +const fs = require('fs') +const path = require('path') +const glob = require('glob') + +const componentFiles = glob.sync('frontend/src/{pages,components}/**/*.tsx') +const violations = [] + +componentFiles.forEach(file => { + const content = fs.readFileSync(file, 'utf8') + + // Check for common hardcoded patterns + const patterns = [ + /]*>([A-Z][a-z]+\s?)+<\/button>/g, // + /title="([A-Z][a-z]+\s?)+"/g, // title="Dashboard" + /placeholder="([A-Z][a-z]+\s?)+"/g // placeholder="Enter name" + ] + + patterns.forEach(pattern => { + const matches = content.match(pattern) + if (matches) { + violations.push({ + file, + matches: matches.slice(0, 3) // First 3 matches + }) + } + }) +}) + +if (violations.length > 0) { + console.error('❌ Found hardcoded strings:') + violations.forEach(({ file, matches }) => { + console.error(` ${file}:`) + matches.forEach(m => console.error(` - ${m}`)) + }) + process.exit(1) +} else { + console.log('βœ… No hardcoded strings found') +} +``` + +**Acceptance Criteria:** +- βœ… 100% of components use `useTranslation` hook +- βœ… 0 hardcoded display strings (script finds none) +- βœ… All 132+ translation keys exist in all 5 languages +- βœ… No missing key warnings in console + +**Verification:** +```bash +npm run check:translations +``` + +--- + +### 2. Functional Verification + +**Measurement Method:** Manual QA + automated tests + +#### Language Switching Test +```typescript +// Automated test +it('language selection persists across sessions', () => { + render() + + // Select Spanish + selectLanguage('es') + expect(localStorage.getItem('charon-language')).toBe('es') + + // Reload page + window.location.reload() + + // Should still be Spanish + expect(i18n.language).toBe('es') + expect(screen.getByText('Panel de Control')).toBeInTheDocument() +}) +``` + +**Manual Verification:** +- [ ] Open app in incognito mode +- [ ] Select Spanish from language selector +- [ ] Verify UI switches to Spanish +- [ ] Close browser +- [ ] Reopen same URL +- [ ] Verify still in Spanish + +**Acceptance Criteria:** +- βœ… Language selector immediately updates UI (< 500ms) +- βœ… Selection persists in localStorage +- βœ… Persists across browser restarts +- βœ… Works in all 5 languages + +**Acceptable Miss Rate:** 0% (must work perfectly) + +--- + +### 3. Visual Regression Testing + +**Measurement Method:** Visual diff screenshots + +```javascript +// visual-regression.spec.ts (Playwright) +import { test, expect } from '@playwright/test' + +const languages = ['en', 'es', 'fr', 'de', 'zh'] +const pages = ['/', '/proxy-hosts', '/security', '/dashboard'] + +languages.forEach(lang => { + pages.forEach(page => { + test(`${page} in ${lang} matches snapshot`, async ({ page: pw }) => { + await pw.goto(`http://localhost:3000${page}`) + await pw.evaluate((language) => { + localStorage.setItem('charon-language', language) + window.location.reload() + }, lang) + + await pw.waitForLoadState('networkidle') + + // Take screenshot + await expect(pw).toHaveScreenshot(`${page.replace('/', '')}-${lang}.png`) + }) + }) +}) +``` + +**Acceptance Criteria:** +- βœ… No layout breaks in any language +- βœ… No text overflow (especially German) +- βœ… No horizontal scrollbars introduced +- βœ… Proper character rendering (especially Chinese) +- βœ… Button/label widths accommodate text + +**Acceptable Miss Rate:** < 5% pixel difference (accounting for font rendering) + +--- + +### 4. Performance Metrics + +**Measurement Method:** Performance API + Lighthouse + +#### Language Switch Speed + +```typescript +// performance.test.ts +it('language switch completes under 500ms', async () => { + render() + + const measurements = [] + + for (let i = 0; i < 10; i++) { + const start = performance.now() + await i18n.changeLanguage('es') + await waitFor(() => + expect(screen.getByText('Panel de Control')).toBeInTheDocument() + ) + const duration = performance.now() - start + measurements.push(duration) + + await i18n.changeLanguage('en') + } + + const avg = measurements.reduce((a, b) => a + b) / measurements.length + const max = Math.max(...measurements) + + console.log(`Avg: ${avg}ms, Max: ${max}ms`) + + expect(avg).toBeLessThan(500) + expect(max).toBeLessThan(1000) +}) +``` + +**Manual Measurement:** +1. Open DevTools β†’ Performance +2. Start recording +3. Click language selector β†’ Select Spanish +4. Stop recording +5. Measure time from click to UI update complete + +**Acceptance Criteria:** +- βœ… Average switch time < 500ms (desktop) +- βœ… Average switch time < 1000ms (mobile) +- βœ… 95th percentile < 800ms (desktop) +- βœ… No visible UI freezing + +**Target:** βœ… P50: 200ms, P95: 500ms, P99: 800ms + +--- + +#### Bundle Size Impact + +```bash +# Before implementation +npm run build +du -sh dist/assets/*.js + +# After implementation +npm run build +du -sh dist/assets/*.js +``` + +**Measurement:** +```bash +# Get bundle size report +npm run build -- --analyze + +# Check specific assets +ls -lh dist/assets/index-*.js +ls -lh dist/assets/translation-*.js +``` + +**Acceptance Criteria:** +- βœ… Main bundle increase < 20KB (gzipped) +- βœ… Translation files lazy-loaded per language +- βœ… Only active language loaded on page load +- βœ… Total bundle size < 500KB (gzipped) + +**Target:** +- Main bundle increase: < 15KB +- Per-language file: < 5KB each +- Total increase: < 40KB (all languages) + +--- + +### 5. Accessibility Metrics + +**Measurement Method:** axe-core automated testing + manual screen reader testing + +```typescript +// accessibility.test.ts +import { axe, toHaveNoViolations } from 'jest-axe' +expect.extend(toHaveNoViolations) + +it('has no accessibility violations in all languages', async () => { + const languages = ['en', 'es', 'fr', 'de', 'zh'] + + for (const lang of languages) { + await i18n.changeLanguage(lang) + const { container } = render() + const results = await axe(container) + + expect(results).toHaveNoViolations() + } +}) +``` + +**Manual Verification (Screen Reader):** +- [ ] Navigate with keyboard only (Tab, Enter, Space) +- [ ] Enable screen reader (NVDA/VoiceOver) +- [ ] Verify announcements in selected language +- [ ] Check ARIA labels translated +- [ ] Verify form labels associated correctly +- [ ] Test error announcements + +**Acceptance Criteria:** +- βœ… 0 critical accessibility violations +- βœ… All ARIA labels translated +- βœ… Screen reader announces in correct language +- βœ… Keyboard navigation works in all languages + +**Acceptable Miss Rate:** 0% (accessibility is non-negotiable) + +--- + +### 6. Error Rate Metrics + +**Measurement Method:** Error tracking + console monitoring + +```typescript +// Setup error tracking +i18n.init({ + fallbackLng: 'en', + missingKeyHandler: (lngs, ns, key, fallbackValue) => { + // Log to error tracking service + console.error(`Missing i18n key: ${key} for language: ${lngs[0]}`) + + // Report to Sentry/monitoring + if (process.env.NODE_ENV === 'production') { + reportError({ + type: 'missing_translation', + key, + language: lngs[0], + fallbackUsed: fallbackValue + }) + } + } +}) +``` + +**Monitoring Dashboard:** +- Missing translation key count (by language) +- Translation load failures +- Language switch errors +- LocalStorage read/write errors + +**Acceptance Criteria:** +- βœ… 0 missing translation keys in production +- βœ… < 0.1% translation load failure rate +- βœ… 0 language switch errors +- βœ… All errors gracefully handled with fallback + +**Acceptable Miss Rate:** < 0.1% (in production, due to edge cases) + +--- + +### 7. User Experience Metrics + +**Measurement Method:** User testing + analytics + +#### Task Completion Rate +**Test:** Ask 5 users to complete key workflows in their non-native language + +**Tasks:** +1. Change language to Spanish +2. Create a proxy host +3. Configure SSL +4. Apply access list +5. View logs + +**Measurement:** +- Task completion rate (% of users who succeed) +- Time to complete each task +- Number of errors/retries +- User satisfaction score (1-5) + +**Acceptance Criteria:** +- βœ… Task completion rate > 90% +- βœ… Time to complete similar to English (Β±20%) +- βœ… User satisfaction score β‰₯ 4/5 +- βœ… No blocker issues reported + +--- + +### 8. Regression Testing Metrics + +**Measurement Method:** Automated test suite + +```bash +# Run full regression suite +npm run test:unit +npm run test:integration +npm run test:e2e + +# Check for failures +echo "Exit code: $?" +``` + +**Acceptance Criteria:** +- βœ… 100% of existing unit tests pass +- βœ… 100% of existing integration tests pass +- βœ… 100% of existing e2e tests pass +- βœ… No new console errors introduced +- βœ… All API calls still work +- βœ… WebSocket connections still function + +**Acceptable Miss Rate:** 0% (no regressions allowed) + +--- + +## Translation Maintenance Strategy + +### 1. CI/CD Integration + +#### GitHub Actions Workflow + +```yaml +# .github/workflows/i18n-check.yml +name: Translation Coverage Check + +on: + pull_request: + paths: + - 'frontend/src/locales/**' + - 'frontend/src/**/*.tsx' + - 'frontend/src/**/*.ts' + +jobs: + check-translations: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Install dependencies + run: cd frontend && npm ci + + - name: Check translation key sync + run: npm run check:i18n:sync + + - name: Check for hardcoded strings + run: npm run check:i18n:hardcoded + + - name: Run translation tests + run: npm run test:i18n + + - name: Comment PR with results + if: always() + uses: actions/github-script@v6 + with: + script: | + const fs = require('fs') + const report = fs.readFileSync('i18n-report.txt', 'utf8') + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## Translation Check Results\n\n${report}` + }) +``` + +#### Pre-commit Hook + +```bash +# .git/hooks/pre-commit (or via husky) +#!/bin/bash + +echo "Running translation checks..." + +# Check if any translation files changed +TRANSLATION_FILES=$(git diff --cached --name-only | grep 'locales/') + +if [ -n "$TRANSLATION_FILES" ]; then + echo "Translation files changed. Running sync check..." + npm run check:i18n:sync || exit 1 +fi + +# Check if any component files changed +COMPONENT_FILES=$(git diff --cached --name-only | grep -E '\.(tsx|ts)$') + +if [ -n "$COMPONENT_FILES" ]; then + echo "Component files changed. Checking for hardcoded strings..." + npm run check:i18n:hardcoded || exit 1 +fi + +echo "βœ… Translation checks passed" +``` + +--- + +### 2. Developer Workflow + +#### Adding New Translation Keys + +**Step-by-Step Process:** + +1. **Add key to English first:** + ```json + // frontend/src/locales/en/translation.json + { + "proxyHosts": { + "newFeature": "New Feature Button" + } + } + ``` + +2. **Run sync script:** + ```bash + npm run i18n:sync + ``` + This automatically adds the key to all other language files with `[TODO]` marker: + ```json + // frontend/src/locales/es/translation.json + { + "proxyHosts": { + "newFeature": "[TODO] New Feature Button" + } + } + ``` + +3. **Use in component:** + ```tsx + + ``` + +4. **Create PR with translation TODO:** + - PR title includes `[i18n]` tag + - PR description lists untranslated keys + - Assign to translation team for review + +5. **Translation team updates:** + - Replace `[TODO]` with proper translations + - Test in UI + - Approve PR + +--- + +#### Sync Script Implementation + +```javascript +// scripts/sync-translation-keys.js +const fs = require('fs') +const path = require('path') + +const localesDir = path.join(__dirname, '../frontend/src/locales') +const languages = ['es', 'fr', 'de', 'zh'] + +// Read English as source of truth +const enFile = path.join(localesDir, 'en/translation.json') +const enKeys = JSON.parse(fs.readFileSync(enFile, 'utf8')) + +function flattenKeys(obj, prefix = '') { + return Object.keys(obj).reduce((acc, key) => { + const fullKey = prefix ? `${prefix}.${key}` : key + if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { + return { ...acc, ...flattenKeys(obj[key], fullKey) } + } + return { ...acc, [fullKey]: obj[key] } + }, {}) +} + +function unflattenKeys(flat) { + const result = {} + Object.keys(flat).forEach(key => { + const parts = key.split('.') + let current = result + parts.forEach((part, i) => { + if (i === parts.length - 1) { + current[part] = flat[key] + } else { + current[part] = current[part] || {} + current = current[part] + } + }) + }) + return result +} + +const flatEnKeys = flattenKeys(enKeys) + +languages.forEach(lang => { + const langFile = path.join(localesDir, `${lang}/translation.json`) + const langKeys = JSON.parse(fs.readFileSync(langFile, 'utf8')) + const flatLangKeys = flattenKeys(langKeys) + + let updated = false + + // Add missing keys with [TODO] marker + Object.keys(flatEnKeys).forEach(key => { + if (!flatLangKeys[key]) { + flatLangKeys[key] = `[TODO] ${flatEnKeys[key]}` + updated = true + console.log(`Added to ${lang}: ${key}`) + } + }) + + // Remove extra keys not in English + Object.keys(flatLangKeys).forEach(key => { + if (!flatEnKeys[key]) { + delete flatLangKeys[key] + updated = true + console.log(`Removed from ${lang}: ${key}`) + } + }) + + if (updated) { + const unflattened = unflattenKeys(flatLangKeys) + fs.writeFileSync( + langFile, + JSON.stringify(unflattened, null, 2) + '\n' + ) + console.log(`βœ… Updated ${lang}/translation.json`) + } else { + console.log(`βœ… ${lang}/translation.json is up to date`) + } +}) + +// Check for [TODO] markers +const allTodos = [] +languages.forEach(lang => { + const langFile = path.join(localesDir, `${lang}/translation.json`) + const content = fs.readFileSync(langFile, 'utf8') + const todos = content.match(/\[TODO\]/g) + if (todos) { + allTodos.push({ lang, count: todos.length }) + } +}) + +if (allTodos.length > 0) { + console.warn('\n⚠️ Pending translations:') + allTodos.forEach(({ lang, count }) => { + console.warn(` ${lang}: ${count} keys need translation`) + }) + process.exit(1) +} + +console.log('\nβœ… All translation files in sync!') +``` + +**NPM Scripts:** +```json +{ + "scripts": { + "i18n:sync": "node scripts/sync-translation-keys.js", + "i18n:check": "node scripts/check-translation-sync.js", + "check:i18n:sync": "npm run i18n:check", + "check:i18n:hardcoded": "node scripts/find-hardcoded-strings.js", + "test:i18n": "vitest run --testPathPattern=translation" + } +} +``` + +--- + +### 3. Ownership Model + +**Translation Team Structure:** + +| Role | Responsibility | Languages | +|------|----------------|-----------| +| **Lead Developer** | English source, review all PRs | en | +| **Spanish Translator** | Maintain Spanish translations | es | +| **French Translator** | Maintain French translations | fr | +| **German Translator** | Maintain German translations | de | +| **Chinese Translator** | Maintain Chinese translations | zh | +| **QA Team** | Verify translations in context | all | + +**Responsibility Matrix:** + +| Task | Owner | Backup | +|------|-------|--------| +| Add new English keys | Feature developer | Lead developer | +| Translate to Spanish | Spanish translator | Community | +| Translate to French | French translator | Community | +| Translate to German | German translator | Community | +| Translate to Chinese | Chinese translator | Community | +| Review translations | QA team | Lead developer | +| Approve PR | Lead developer | Tech lead | + +**Escalation Path:** +1. Developer adds English key in feature PR +2. CI flags missing translations +3. PR merged with `[TODO]` markers +4. Translation issue created automatically +5. Assigned to translator for language +6. Translator updates and creates PR +7. QA verifies in UI +8. Lead developer merges + +--- + +### 4. Community Contributions + +**Guidelines for contributors:** + +1. **New Feature PRs:** + - Must include English translation keys + - May include translations for other languages (optional) + - CI will flag missing translations + - OK to merge with `[TODO]` markers + +2. **Translation-Only PRs:** + - Must update ALL specified languages + - Must test in UI (screenshots required) + - Must follow naming conventions + - Must pass all CI checks + +3. **Documentation:** + ```markdown + ## Contributing Translations + + We welcome translation contributions! Please follow these steps: + + 1. Fork the repository + 2. Find keys marked `[TODO]` in `frontend/src/locales/{lang}/translation.json` + 3. Replace with accurate translations + 4. Test in UI by changing language + 5. Take screenshots + 6. Create PR with: + - Title: `[i18n] Update {language} translations` + - Description: List of keys updated + - Screenshots showing translations in UI + 7. Wait for review from language maintainer + + ### Translation Guidelines + - Keep formatting placeholders: `{{variable}}` + - Maintain similar length to English (Β±30%) + - Use formal tone + - Test pluralization if applicable + ``` + +--- + +### 5. Translation Quality Assurance + +#### Review Checklist + +For each translation PR: + +- [ ] **Accuracy:** + - [ ] Translation conveys same meaning as English + - [ ] Technical terms translated appropriately + - [ ] No mistranslations or ambiguities + +- [ ] **Consistency:** + - [ ] Terms translated consistently across all keys + - [ ] Follows existing patterns in language file + - [ ] Matches style guide for language + +- [ ] **Formatting:** + - [ ] Placeholders preserved: `{{count}}`, `{{domain}}` + - [ ] Punctuation appropriate for language + - [ ] Capitalization follows language rules + +- [ ] **Length:** + - [ ] Fits in UI (no overflow) + - [ ] Not excessively longer than English + - [ ] Acceptable truncation if needed + +- [ ] **Context:** + - [ ] Makes sense in UI context + - [ ] Appropriate formality level + - [ ] Tested in actual application + +#### Automated Quality Checks + +```javascript +// scripts/check-translation-quality.js +const checks = { + placeholders: (en, translated) => { + const enPlaceholders = (en.match(/{{[^}]+}}/g) || []).sort() + const transPlaceholders = (translated.match(/{{[^}]+}}/g) || []).sort() + return JSON.stringify(enPlaceholders) === JSON.stringify(transPlaceholders) + }, + + length: (en, translated) => { + const ratio = translated.length / en.length + return ratio >= 0.5 && ratio <= 2.0 // Within 50-200% of English length + }, + + noTodoMarkers: (translated) => { + return !translated.includes('[TODO]') + }, + + noHtmlTags: (translated) => { + return !/<[^>]+>/.test(translated) + } +} + +// Run checks on all translations +languages.forEach(lang => { + // ... check each key against English +}) +``` + +--- + +## Rollback Strategy & Feature Flags + +### 1. Feature Flag Implementation + +**Approach:** Use environment variable + React Context to enable/disable i18n feature + +#### Implementation + +```typescript +// frontend/src/config/featureFlags.ts +export const featureFlags = { + enableI18n: import.meta.env.VITE_ENABLE_I18N === 'true' || false +} + +// Can be overridden at runtime for testing +if (typeof window !== 'undefined') { + (window as any).__FEATURE_FLAGS__ = featureFlags +} +``` + +```typescript +// frontend/src/context/FeatureFlagContext.tsx +import { createContext, useContext, ReactNode } from 'react' +import { featureFlags } from '../config/featureFlags' + +const FeatureFlagContext = createContext(featureFlags) + +export const useFeatureFlags = () => useContext(FeatureFlagContext) + +export function FeatureFlagProvider({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} +``` + +#### Usage in Components + +```tsx +// frontend/src/components/Layout.tsx +import { useTranslation } from 'react-i18next' +import { useFeatureFlags } from '../context/FeatureFlagContext' + +export default function Layout() { + const { t } = useTranslation() + const { enableI18n } = useFeatureFlags() + + const navigation = [ + { + name: enableI18n ? t('navigation.dashboard') : 'Dashboard', + path: '/', + icon: 'πŸ“Š' + }, + { + name: enableI18n ? t('navigation.proxyHosts') : 'Proxy Hosts', + path: '/proxy-hosts', + icon: '🌐' + } + ] + + return +} +``` + +#### Environment Configuration + +```bash +# .env.development +VITE_ENABLE_I18N=true + +# .env.production (initially false, enable after validation) +VITE_ENABLE_I18N=false + +# .env.staging (test with flag enabled) +VITE_ENABLE_I18N=true +``` + +--- + +### 2. Phased Rollout Plan + +**Strategy:** Gradual rollout with monitoring at each stage + +#### Stage 1: Internal Testing (Week 1) +- **Target:** Development team + QA team +- **Flag:** Enabled in development environment only +- **Monitoring:** + - Console errors + - Translation key misses + - Performance metrics + - User feedback from team + +**Go/No-Go Criteria:** +- βœ… No critical bugs +- βœ… All translation keys work +- βœ… Performance acceptable +- βœ… Team approves UX + +**Rollback Trigger:** +- Critical bug that blocks usage +- Performance degradation > 30% +- > 5% missing translation keys + +--- + +#### Stage 2: Beta Users (Week 2-3) +- **Target:** 10% of production users (opt-in beta program) +- **Flag:** Enabled via URL parameter or localStorage override +- **Monitoring:** + - Error rate per language + - Language switch frequency + - Task completion rate + - User feedback surveys + +**Enable Method:** +```typescript +// Allow beta users to enable via URL +const urlParams = new URLSearchParams(window.location.search) +if (urlParams.get('beta_i18n') === 'true') { + localStorage.setItem('beta_i18n_enabled', 'true') +} + +const betaEnabled = localStorage.getItem('beta_i18n_enabled') === 'true' + +export const featureFlags = { + enableI18n: import.meta.env.VITE_ENABLE_I18N === 'true' || betaEnabled +} +``` + +**Go/No-Go Criteria:** +- βœ… Error rate < 1% +- βœ… > 80% positive user feedback +- βœ… No P0/P1 bugs +- βœ… Performance within targets + +**Rollback Trigger:** +- Error rate > 5% +- < 60% positive feedback +- Critical bug discovered +- Performance degradation + +--- + +#### Stage 3: Gradual Rollout (Week 3-4) +- **Target:** Gradual increase to 100% of users +- **Flag:** Percentage-based rollout (10% β†’ 25% β†’ 50% β†’ 100%) +- **Monitoring:** + - Real-time error dashboards + - Performance metrics + - User support tickets + - Social media/reviews + +**Rollout Schedule:** +| Day | Percentage | Monitoring Period | +|-----|-----------|-------------------| +| 1 | 10% | 24 hours | +| 2 | 25% | 24 hours | +| 4 | 50% | 48 hours | +| 7 | 100% | Ongoing | + +**Implementation:** +```typescript +// Server-side or edge function +function getUserRolloutPercentage(userId: string): boolean { + const hash = simpleHash(userId) + const rolloutPercentage = getCurrentRolloutPercentage() // e.g., 50 + return (hash % 100) < rolloutPercentage +} + +// Client-side +const userId = getCurrentUserId() +const isInRollout = await checkRolloutStatus(userId) + +export const featureFlags = { + enableI18n: isInRollout || import.meta.env.VITE_ENABLE_I18N === 'true' +} +``` + +**Go/No-Go Criteria (Each Stage):** +- βœ… Error rate stable or decreasing +- βœ… No increase in support tickets +- βœ… Performance stable +- βœ… No critical bugs + +**Rollback Trigger:** +- Error rate spike > 10% +- Critical bug affecting > 1% users +- Support ticket surge +- Performance degradation > 20% + +--- + +### 3. Rollback Procedure + +**Scenario:** Critical issue discovered in production requiring immediate rollback + +#### Immediate Rollback (< 5 minutes) + +```bash +# 1. Disable feature flag via environment variable +# Update .env.production or config service +VITE_ENABLE_I18N=false + +# 2. Redeploy with flag disabled +npm run build +npm run deploy:production + +# OR use CDN config to inject flag override +# (if build artifacts are cached) + +# 3. Clear CDN cache if necessary +curl -X PURGE https://cdn.example.com/assets/* + +# 4. Monitor error rates drop +watch -n 5 'curl -s https://api.example.com/metrics/errors' +``` + +**Expected Result:** +- All users revert to hardcoded English strings +- Translation infrastructure remains in place +- No data loss or state corruption +- Immediate error rate drop + +--- + +#### Gradual Rollback + +**Use Case:** Non-critical issue but want to reduce exposure + +```typescript +// Reduce rollout percentage gradually +// 100% β†’ 50% β†’ 25% β†’ 10% β†’ 0% + +function getRolloutPercentage() { + // Fetch from config service or feature flag platform + return configService.get('i18n_rollout_percentage') +} + +// Decrease by 25% every hour until stable +``` + +--- + +#### Component-Specific Rollback + +**Use Case:** Issue isolated to specific component (e.g., ProxyHosts) + +```tsx +// Temporarily disable i18n for specific component +import { useFeatureFlags } from '../context/FeatureFlagContext' + +export default function ProxyHosts() { + const { t } = useTranslation() + const { enableI18n } = useFeatureFlags() + + // Force disable for this component only + const useI18n = enableI18n && !isProxyHostsBugActive() + + return ( + + {/* ... */} + + ) +} +``` + +--- + +### 4. Rollback Decision Matrix + +| Issue Severity | Impact | Action | Timeline | +|---------------|--------|--------|----------| +| **P0 - Critical** | > 10% users can't use app | Immediate full rollback | < 5 min | +| **P1 - High** | Core feature broken for > 5% | Gradual rollback to 0% | < 1 hour | +| **P2 - Medium** | Non-critical feature broken | Component-specific disable | < 4 hours | +| **P3 - Low** | Minor visual issue | Fix forward, no rollback | Next release | + +**Examples:** + +- **P0:** Language switch crashes app β†’ Immediate rollback +- **P1:** German translations cause layout breaks β†’ Gradual rollback +- **P2:** Toast messages not translated β†’ Disable toasts i18n only +- **P3:** Minor spacing issue in Chinese β†’ Fix in next sprint + +--- + +### 5. Monitoring & Alerting + +#### Key Metrics to Monitor + +```typescript +// Error tracking +const i18nMetrics = { + missingKeys: 0, // Translation key not found + loadFailures: 0, // Translation file failed to load + switchErrors: 0, // Error during language switch + renderErrors: 0, // Component render error after i18n + performanceSlow: 0 // Language switch > 1000ms +} + +// Alert thresholds +const alertThresholds = { + missingKeys: 10, // Alert if > 10 missing keys in 5 min + loadFailures: 5, // Alert if > 5 load failures in 5 min + switchErrors: 3, // Alert if > 3 switch errors in 5 min + renderErrors: 5, // Alert if > 5 render errors in 5 min + performanceSlow: 20 // Alert if > 20 slow switches in 5 min +} +``` + +#### Alert Configuration + +```yaml +# alerts.yml (for Prometheus/Grafana/Datadog) +alerts: + - name: i18n_missing_keys_high + condition: rate(i18n_missing_keys_total[5m]) > 10 + severity: warning + action: notify_team + + - name: i18n_load_failures_critical + condition: rate(i18n_load_failures_total[5m]) > 5 + severity: critical + action: page_oncall + + - name: i18n_switch_errors_high + condition: rate(i18n_switch_errors_total[5m]) > 3 + severity: warning + action: notify_team + + - name: i18n_performance_degraded + condition: histogram_quantile(0.95, i18n_switch_duration_seconds) > 1 + severity: warning + action: notify_team +``` + +--- + +### 6. Communication Plan + +#### Pre-Rollout Communication + +**To Users:** +- Feature announcement on blog/social media +- In-app banner: "New: Multi-language support coming soon!" +- Email to beta testers with opt-in link + +**To Team:** +- Slack announcement with rollout schedule +- On-call rotation briefing +- Runbook shared with ops team + +#### During Rollout + +**Status Updates:** +- Hourly updates in Slack #i18n-rollout channel +- Dashboard showing live metrics +- Go/no-go decision points documented + +**Example Update:** +``` +πŸš€ i18n Rollout Update - 2pm EST +Stage: 25% rollout (Day 2) +Status: βœ… GREEN +Metrics: +- Error rate: 0.3% (target <1%) βœ… +- Missing keys: 2 (target <10) βœ… +- Performance P95: 380ms (target <500ms) βœ… +- User feedback: 87% positive (target >80%) βœ… + +Next checkpoint: 5pm EST (50% rollout decision) +``` + +#### Rollback Communication + +**If rollback needed:** +``` +⚠️ i18n Rollback Initiated - 3pm EST +Reason: Critical bug in language switching (P0) +Action: Full rollback to 0% - ETA 5 minutes +Impact: Users see English only, no data loss +Follow-up: Root cause analysis tomorrow 10am +Incident Report: [link] +``` + +--- + +## Code Patterns + +### ❌ Anti-Pattern (Current) +```tsx +export default function ProxyHosts() { + return ( + + + + ) +} +``` + +### βœ… Correct Pattern (After Fix) +```tsx +import { useTranslation } from 'react-i18next' + +export default function ProxyHosts() { + const { t } = useTranslation() + return ( + + + + ) +} +``` + +### βœ… With Feature Flag (Transition) +```tsx +import { useTranslation } from 'react-i18next' +import { useFeatureFlags } from '../context/FeatureFlagContext' + +export default function ProxyHosts() { + const { t } = useTranslation() + const { enableI18n } = useFeatureFlags() + + return ( + + + + ) +} +``` + +### Dynamic Content Pattern +```tsx +// ❌ Wrong +

You have {count} proxy hosts

+ +// βœ… Correct +

{t('proxyHosts.count', { count })}

+ +// In translation file: +{ "proxyHosts": { "count": "You have {{count}} proxy hosts" } } +``` + +### Error Handling Pattern +```tsx +// βœ… Correct +try { + await saveProxyHost(data) + toast.success(t('notifications.saveSuccess')) +} catch (error) { + toast.error(t('notifications.saveFailed')) + console.error('Save failed:', error) // Always log in English for debugging +} +``` + +### Form Validation Pattern +```tsx +// βœ… Correct +const schema = z.object({ + domain: z.string().min(1, t('errors.required')) +}) + +// Better: Use error key +const schema = z.object({ + domain: z.string().min(1, { message: 'errors.required' }) +}) + +// Then translate in form +{errors.domain && {t(errors.domain.message)}} +``` + +### Memoization Pattern (Performance) +```tsx +// βœ… For expensive computed values +const columns = useMemo(() => [ + { header: t('proxyHosts.domain'), accessorKey: 'domain_names' }, + { header: t('common.status'), accessorKey: 'enabled' } +], [t]) // Re-compute when t function changes (language switch) +``` + +### Conditional Translation Pattern +```tsx +// βœ… Correct - different keys for different states + + {proxy.enabled + ? t('common.enabled') + : t('common.disabled') + } + + +// βœ… Also correct - one key with context +{t('common.status', { context: proxy.enabled ? 'enabled' : 'disabled' })} +``` + +--- + +## Conclusion + +The language selector bug is a complete disconnect between infrastructure and implementation. The infrastructure works perfectlyβ€”the issue is that NO components use the translation system. All UI text is hardcoded in English. + +**Fix**: Systematically update every component to use `useTranslation` and replace hardcoded strings with translation keys. + +**Implementation Timeline:** 3-4 weeks (15-20 business days) +- Week 1: Layout + ProxyHosts (pattern validation) +- Week 2: Core pages (6-8 pages) +- Week 3: Dashboard + Auth (critical paths) +- Week 4: QA + Polish (comprehensive testing) +- Buffer: Week 5 (if needed) + +**Risk Level:** Low-Medium +- Infrastructure proven working +- Changes are display-only (no logic changes) +- Feature flag enables safe rollback +- Phased rollout reduces blast radius + +**User Impact:** High (positive) +- Enables full multilingual support for 5 languages +- Improves accessibility for non-English users +- Professional appearance and localization +- Competitive advantage for international users + +**Success Metrics:** +- 100% translation coverage (0 hardcoded strings) +- < 500ms language switch time +- 0% missing translation keys +- > 90% user task completion in all languages +- 0 critical bugs in production + +**Rollback Plan:** +- Feature flag for immediate disable +- Phased rollout with monitoring +- Clear rollback procedures +- 24/7 monitoring during rollout + +Once complete, the translation infrastructure will finally be utilized, and users will see the UI in their selected language. The phased approach with feature flags ensures we can roll back instantly if issues arise, while the comprehensive testing strategy ensures quality before wide release. + +**Next Steps:** +1. βœ… Approve this plan +2. Add missing 48 translation keys to all 5 language files +3. Begin Phase 1: Layout & Navigation (Day 1) +4. Proceed through phases systematically +5. Monitor metrics at each stage +6. Celebrate successful multilingual launch! πŸŽ‰ + +--- + +**Document Version:** 2.0 +**Last Updated:** 2025-12-19 +**Status:** βœ… Ready for Implementation +**Approvals Required:** Tech Lead, Product Manager, QA Lead