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 = [
- /