diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 05fe9c4c..896eb05e 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -19,11 +19,14 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
+ "i18next": "^25.7.3",
+ "i18next-browser-languagedetector": "^8.2.0",
"lucide-react": "^0.561.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-hook-form": "^7.68.0",
"react-hot-toast": "^2.6.0",
+ "react-i18next": "^16.5.0",
"react-router-dom": "^7.11.0",
"tailwind-merge": "^3.4.0",
"tldts": "^7.0.19"
@@ -375,7 +378,6 @@
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
- "dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -5166,6 +5168,15 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
+ "node_modules/html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "license": "MIT",
+ "dependencies": {
+ "void-elements": "3.1.0"
+ }
+ },
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@@ -5192,6 +5203,46 @@
"node": ">= 14"
}
},
+ "node_modules/i18next": {
+ "version": "25.7.3",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz",
+ "integrity": "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4"
+ },
+ "peerDependencies": {
+ "typescript": "^5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/i18next-browser-languagedetector": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
+ "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -6401,6 +6452,33 @@
"react-dom": ">=16"
}
},
+ "node_modules/react-i18next": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.0.tgz",
+ "integrity": "sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.27.6",
+ "html-parse-stringify": "^3.0.1",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "peerDependencies": {
+ "i18next": ">= 25.6.2",
+ "react": ">= 16.8.0",
+ "typescript": "^5"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -6959,7 +7037,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -7084,6 +7162,15 @@
}
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/vite": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
@@ -7237,6 +7324,15 @@
}
}
},
+ "node_modules/void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 9cabac50..edc5abd6 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -38,11 +38,14 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
+ "i18next": "^25.7.3",
+ "i18next-browser-languagedetector": "^8.2.0",
"lucide-react": "^0.561.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-hook-form": "^7.68.0",
"react-hot-toast": "^2.6.0",
+ "react-i18next": "^16.5.0",
"react-router-dom": "^7.11.0",
"tailwind-merge": "^3.4.0",
"tldts": "^7.0.19"
diff --git a/frontend/src/components/LanguageSelector.tsx b/frontend/src/components/LanguageSelector.tsx
new file mode 100644
index 00000000..23626c4b
--- /dev/null
+++ b/frontend/src/components/LanguageSelector.tsx
@@ -0,0 +1,36 @@
+import { Globe } from 'lucide-react'
+import { useLanguage } from '../hooks/useLanguage'
+import { Language } from '../context/LanguageContextValue'
+
+const languageOptions: { code: Language; label: string; nativeLabel: string }[] = [
+ { code: 'en', label: 'English', nativeLabel: 'English' },
+ { code: 'es', label: 'Spanish', nativeLabel: 'Español' },
+ { code: 'fr', label: 'French', nativeLabel: 'Français' },
+ { code: 'de', label: 'German', nativeLabel: 'Deutsch' },
+ { code: 'zh', label: 'Chinese', nativeLabel: '中文' },
+]
+
+export function LanguageSelector() {
+ const { language, setLanguage } = useLanguage()
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setLanguage(e.target.value as Language)
+ }
+
+ return (
+
+
+
+
+ )
+}
diff --git a/frontend/src/context/LanguageContext.tsx b/frontend/src/context/LanguageContext.tsx
new file mode 100644
index 00000000..792c37c6
--- /dev/null
+++ b/frontend/src/context/LanguageContext.tsx
@@ -0,0 +1,30 @@
+import { ReactNode, useState, useEffect } from 'react'
+import { useTranslation } from 'react-i18next'
+import { LanguageContext, Language } from './LanguageContextValue'
+
+export function LanguageProvider({ children }: { children: ReactNode }) {
+ const { i18n } = useTranslation()
+ const [language, setLanguageState] = useState(() => {
+ const saved = localStorage.getItem('charon-language')
+ return (saved as Language) || 'en'
+ })
+
+ useEffect(() => {
+ i18n.changeLanguage(language)
+ }, [language, i18n])
+
+ const setLanguage = (lang: Language) => {
+ setLanguageState(lang)
+ localStorage.setItem('charon-language', lang)
+ i18n.changeLanguage(lang)
+ // Future: Update document direction for RTL languages (e.g., Arabic, Hebrew)
+ // Currently not used as we don't have RTL language translations yet
+ document.documentElement.dir = 'ltr'
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/frontend/src/context/LanguageContextValue.ts b/frontend/src/context/LanguageContextValue.ts
new file mode 100644
index 00000000..2af92d23
--- /dev/null
+++ b/frontend/src/context/LanguageContextValue.ts
@@ -0,0 +1,10 @@
+import { createContext } from 'react'
+
+export type Language = 'en' | 'es' | 'fr' | 'de' | 'zh'
+
+export interface LanguageContextType {
+ language: Language
+ setLanguage: (lang: Language) => void
+}
+
+export const LanguageContext = createContext(undefined)
diff --git a/frontend/src/hooks/useLanguage.ts b/frontend/src/hooks/useLanguage.ts
new file mode 100644
index 00000000..18cd8921
--- /dev/null
+++ b/frontend/src/hooks/useLanguage.ts
@@ -0,0 +1,10 @@
+import { useContext } from 'react'
+import { LanguageContext, LanguageContextType } from '../context/LanguageContextValue'
+
+export function useLanguage(): LanguageContextType {
+ const context = useContext(LanguageContext)
+ if (!context) {
+ throw new Error('useLanguage must be used within a LanguageProvider')
+ }
+ return context
+}
diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts
new file mode 100644
index 00000000..02243897
--- /dev/null
+++ b/frontend/src/i18n.ts
@@ -0,0 +1,36 @@
+import i18n from 'i18next'
+import { initReactI18next } from 'react-i18next'
+import LanguageDetector from 'i18next-browser-languagedetector'
+
+import enTranslation from './locales/en/translation.json'
+import esTranslation from './locales/es/translation.json'
+import frTranslation from './locales/fr/translation.json'
+import deTranslation from './locales/de/translation.json'
+import zhTranslation from './locales/zh/translation.json'
+
+const resources = {
+ en: { translation: enTranslation },
+ es: { translation: esTranslation },
+ fr: { translation: frTranslation },
+ de: { translation: deTranslation },
+ zh: { translation: zhTranslation },
+}
+
+i18n
+ .use(LanguageDetector) // Detect user language
+ .use(initReactI18next) // Pass i18n instance to react-i18next
+ .init({
+ resources,
+ fallbackLng: 'en', // Fallback to English if translation not found
+ debug: false, // Set to true for debugging
+ interpolation: {
+ escapeValue: false, // React already escapes values
+ },
+ detection: {
+ order: ['localStorage', 'navigator'], // Check localStorage first, then browser language
+ caches: ['localStorage'], // Cache language selection in localStorage
+ lookupLocalStorage: 'charon-language', // Key for storing language in localStorage
+ },
+ })
+
+export default i18n
diff --git a/frontend/src/locales/de/translation.json b/frontend/src/locales/de/translation.json
new file mode 100644
index 00000000..6a603a12
--- /dev/null
+++ b/frontend/src/locales/de/translation.json
@@ -0,0 +1,131 @@
+{
+ "common": {
+ "save": "Speichern",
+ "cancel": "Abbrechen",
+ "delete": "Löschen",
+ "edit": "Bearbeiten",
+ "add": "Hinzufügen",
+ "create": "Erstellen",
+ "update": "Aktualisieren",
+ "close": "Schließen",
+ "confirm": "Bestätigen",
+ "back": "Zurück",
+ "next": "Weiter",
+ "loading": "Laden...",
+ "error": "Fehler",
+ "success": "Erfolg",
+ "warning": "Warnung",
+ "info": "Information",
+ "yes": "Ja",
+ "no": "Nein",
+ "enabled": "Aktiviert",
+ "disabled": "Deaktiviert",
+ "name": "Name",
+ "description": "Beschreibung",
+ "actions": "Aktionen",
+ "status": "Status",
+ "search": "Suchen",
+ "filter": "Filtern",
+ "settings": "Einstellungen",
+ "language": "Sprache"
+ },
+ "navigation": {
+ "dashboard": "Dashboard",
+ "proxyHosts": "Proxy-Hosts",
+ "remoteServers": "Remote-Server",
+ "domains": "Domänen",
+ "certificates": "Zertifikate",
+ "security": "Sicherheit",
+ "accessLists": "Zugriffslisten",
+ "crowdsec": "CrowdSec",
+ "rateLimiting": "Ratenbegrenzung",
+ "waf": "WAF",
+ "uptime": "Verfügbarkeit",
+ "notifications": "Benachrichtigungen",
+ "users": "Benutzer",
+ "tasks": "Aufgaben",
+ "settings": "Einstellungen"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "description": "Übersicht Ihres Charon-Reverse-Proxys",
+ "proxyHosts": "Proxy-Hosts",
+ "remoteServers": "Remote-Server",
+ "certificates": "Zertifikate",
+ "accessLists": "Zugriffslisten",
+ "systemStatus": "Systemstatus",
+ "healthy": "Gesund",
+ "unhealthy": "Ungesund",
+ "pendingCertificates": "Ausstehende Zertifikate",
+ "allCertificatesValid": "Alle Zertifikate gültig",
+ "activeHosts": "{{count}} aktiv",
+ "activeServers": "{{count}} aktiv",
+ "activeLists": "{{count}} aktiv",
+ "validCerts": "{{count}} gültig"
+ },
+ "settings": {
+ "title": "Einstellungen",
+ "description": "Konfigurieren Sie Ihre Charon-Instanz",
+ "system": "System",
+ "smtp": "E-Mail (SMTP)",
+ "account": "Konto",
+ "language": "Sprache",
+ "languageDescription": "Wählen Sie Ihre bevorzugte Sprache",
+ "theme": "Design",
+ "themeDescription": "Wählen Sie helles oder dunkles Design"
+ },
+ "proxyHosts": {
+ "title": "Proxy-Hosts",
+ "description": "Verwalten Sie Ihre Reverse-Proxy-Konfigurationen",
+ "addHost": "Proxy-Host hinzufügen",
+ "editHost": "Proxy-Host bearbeiten",
+ "deleteHost": "Proxy-Host löschen",
+ "domainNames": "Domänennamen",
+ "forwardHost": "Weiterleitungs-Host",
+ "forwardPort": "Weiterleitungs-Port",
+ "sslEnabled": "SSL aktiviert",
+ "sslForced": "SSL erzwingen"
+ },
+ "certificates": {
+ "title": "Zertifikate",
+ "description": "SSL-Zertifikate verwalten",
+ "addCertificate": "Zertifikat hinzufügen",
+ "domain": "Domäne",
+ "status": "Status",
+ "expiresAt": "Läuft ab am",
+ "valid": "Gültig",
+ "pending": "Ausstehend",
+ "expired": "Abgelaufen"
+ },
+ "auth": {
+ "login": "Anmelden",
+ "logout": "Abmelden",
+ "email": "E-Mail",
+ "password": "Passwort",
+ "username": "Benutzername",
+ "signIn": "Anmelden",
+ "signOut": "Abmelden",
+ "forgotPassword": "Passwort vergessen?",
+ "rememberMe": "Angemeldet bleiben"
+ },
+ "errors": {
+ "required": "Dieses Feld ist erforderlich",
+ "invalidEmail": "Ungültige E-Mail-Adresse",
+ "passwordTooShort": "Das Passwort muss mindestens 8 Zeichen lang sein",
+ "genericError": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
+ "networkError": "Netzwerkfehler. Bitte überprüfen Sie Ihre Verbindung.",
+ "unauthorized": "Nicht autorisiert. Bitte melden Sie sich erneut an.",
+ "notFound": "Ressource nicht gefunden",
+ "serverError": "Serverfehler. Bitte versuchen Sie es später erneut."
+ },
+ "notifications": {
+ "saveSuccess": "Änderungen erfolgreich gespeichert",
+ "deleteSuccess": "Erfolgreich gelöscht",
+ "createSuccess": "Erfolgreich erstellt",
+ "updateSuccess": "Erfolgreich aktualisiert",
+ "saveFailed": "Fehler beim Speichern der Änderungen",
+ "deleteFailed": "Fehler beim Löschen",
+ "createFailed": "Fehler beim Erstellen",
+ "updateFailed": "Fehler beim Aktualisieren"
+ }
+}
diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json
new file mode 100644
index 00000000..fd0318ee
--- /dev/null
+++ b/frontend/src/locales/en/translation.json
@@ -0,0 +1,131 @@
+{
+ "common": {
+ "save": "Save",
+ "cancel": "Cancel",
+ "delete": "Delete",
+ "edit": "Edit",
+ "add": "Add",
+ "create": "Create",
+ "update": "Update",
+ "close": "Close",
+ "confirm": "Confirm",
+ "back": "Back",
+ "next": "Next",
+ "loading": "Loading...",
+ "error": "Error",
+ "success": "Success",
+ "warning": "Warning",
+ "info": "Info",
+ "yes": "Yes",
+ "no": "No",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "name": "Name",
+ "description": "Description",
+ "actions": "Actions",
+ "status": "Status",
+ "search": "Search",
+ "filter": "Filter",
+ "settings": "Settings",
+ "language": "Language"
+ },
+ "navigation": {
+ "dashboard": "Dashboard",
+ "proxyHosts": "Proxy Hosts",
+ "remoteServers": "Remote Servers",
+ "domains": "Domains",
+ "certificates": "Certificates",
+ "security": "Security",
+ "accessLists": "Access Lists",
+ "crowdsec": "CrowdSec",
+ "rateLimiting": "Rate Limiting",
+ "waf": "WAF",
+ "uptime": "Uptime",
+ "notifications": "Notifications",
+ "users": "Users",
+ "tasks": "Tasks",
+ "settings": "Settings"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "description": "Overview of your Charon reverse proxy",
+ "proxyHosts": "Proxy Hosts",
+ "remoteServers": "Remote Servers",
+ "certificates": "Certificates",
+ "accessLists": "Access Lists",
+ "systemStatus": "System Status",
+ "healthy": "Healthy",
+ "unhealthy": "Unhealthy",
+ "pendingCertificates": "Pending certificates",
+ "allCertificatesValid": "All certificates valid",
+ "activeHosts": "{{count}} active",
+ "activeServers": "{{count}} active",
+ "activeLists": "{{count}} active",
+ "validCerts": "{{count}} valid"
+ },
+ "settings": {
+ "title": "Settings",
+ "description": "Configure your Charon instance",
+ "system": "System",
+ "smtp": "Email (SMTP)",
+ "account": "Account",
+ "language": "Language",
+ "languageDescription": "Select your preferred language",
+ "theme": "Theme",
+ "themeDescription": "Choose light or dark theme"
+ },
+ "proxyHosts": {
+ "title": "Proxy Hosts",
+ "description": "Manage your reverse proxy configurations",
+ "addHost": "Add Proxy Host",
+ "editHost": "Edit Proxy Host",
+ "deleteHost": "Delete Proxy Host",
+ "domainNames": "Domain Names",
+ "forwardHost": "Forward Host",
+ "forwardPort": "Forward Port",
+ "sslEnabled": "SSL Enabled",
+ "sslForced": "Force SSL"
+ },
+ "certificates": {
+ "title": "Certificates",
+ "description": "Manage SSL certificates",
+ "addCertificate": "Add Certificate",
+ "domain": "Domain",
+ "status": "Status",
+ "expiresAt": "Expires At",
+ "valid": "Valid",
+ "pending": "Pending",
+ "expired": "Expired"
+ },
+ "auth": {
+ "login": "Login",
+ "logout": "Logout",
+ "email": "Email",
+ "password": "Password",
+ "username": "Username",
+ "signIn": "Sign In",
+ "signOut": "Sign Out",
+ "forgotPassword": "Forgot Password?",
+ "rememberMe": "Remember Me"
+ },
+ "errors": {
+ "required": "This field is required",
+ "invalidEmail": "Invalid email address",
+ "passwordTooShort": "Password must be at least 8 characters",
+ "genericError": "An error occurred. Please try again.",
+ "networkError": "Network error. Please check your connection.",
+ "unauthorized": "Unauthorized. Please login again.",
+ "notFound": "Resource not found",
+ "serverError": "Server error. Please try again later."
+ },
+ "notifications": {
+ "saveSuccess": "Changes saved successfully",
+ "deleteSuccess": "Deleted successfully",
+ "createSuccess": "Created successfully",
+ "updateSuccess": "Updated successfully",
+ "saveFailed": "Failed to save changes",
+ "deleteFailed": "Failed to delete",
+ "createFailed": "Failed to create",
+ "updateFailed": "Failed to update"
+ }
+}
diff --git a/frontend/src/locales/es/translation.json b/frontend/src/locales/es/translation.json
new file mode 100644
index 00000000..7e234695
--- /dev/null
+++ b/frontend/src/locales/es/translation.json
@@ -0,0 +1,131 @@
+{
+ "common": {
+ "save": "Guardar",
+ "cancel": "Cancelar",
+ "delete": "Eliminar",
+ "edit": "Editar",
+ "add": "Añadir",
+ "create": "Crear",
+ "update": "Actualizar",
+ "close": "Cerrar",
+ "confirm": "Confirmar",
+ "back": "Atrás",
+ "next": "Siguiente",
+ "loading": "Cargando...",
+ "error": "Error",
+ "success": "Éxito",
+ "warning": "Advertencia",
+ "info": "Información",
+ "yes": "Sí",
+ "no": "No",
+ "enabled": "Habilitado",
+ "disabled": "Deshabilitado",
+ "name": "Nombre",
+ "description": "Descripción",
+ "actions": "Acciones",
+ "status": "Estado",
+ "search": "Buscar",
+ "filter": "Filtrar",
+ "settings": "Configuración",
+ "language": "Idioma"
+ },
+ "navigation": {
+ "dashboard": "Panel de Control",
+ "proxyHosts": "Hosts Proxy",
+ "remoteServers": "Servidores Remotos",
+ "domains": "Dominios",
+ "certificates": "Certificados",
+ "security": "Seguridad",
+ "accessLists": "Listas de Acceso",
+ "crowdsec": "CrowdSec",
+ "rateLimiting": "Limitación de Tasa",
+ "waf": "WAF",
+ "uptime": "Tiempo de Actividad",
+ "notifications": "Notificaciones",
+ "users": "Usuarios",
+ "tasks": "Tareas",
+ "settings": "Configuración"
+ },
+ "dashboard": {
+ "title": "Panel de Control",
+ "description": "Resumen de tu proxy inverso Charon",
+ "proxyHosts": "Hosts Proxy",
+ "remoteServers": "Servidores Remotos",
+ "certificates": "Certificados",
+ "accessLists": "Listas de Acceso",
+ "systemStatus": "Estado del Sistema",
+ "healthy": "Saludable",
+ "unhealthy": "No Saludable",
+ "pendingCertificates": "Certificados pendientes",
+ "allCertificatesValid": "Todos los certificados válidos",
+ "activeHosts": "{{count}} activo",
+ "activeServers": "{{count}} activo",
+ "activeLists": "{{count}} activo",
+ "validCerts": "{{count}} válido"
+ },
+ "settings": {
+ "title": "Configuración",
+ "description": "Configura tu instancia de Charon",
+ "system": "Sistema",
+ "smtp": "Correo Electrónico (SMTP)",
+ "account": "Cuenta",
+ "language": "Idioma",
+ "languageDescription": "Selecciona tu idioma preferido",
+ "theme": "Tema",
+ "themeDescription": "Elige tema claro u oscuro"
+ },
+ "proxyHosts": {
+ "title": "Hosts Proxy",
+ "description": "Gestiona tus configuraciones de proxy inverso",
+ "addHost": "Añadir Host Proxy",
+ "editHost": "Editar Host Proxy",
+ "deleteHost": "Eliminar Host Proxy",
+ "domainNames": "Nombres de Dominio",
+ "forwardHost": "Host de Reenvío",
+ "forwardPort": "Puerto de Reenvío",
+ "sslEnabled": "SSL Habilitado",
+ "sslForced": "Forzar SSL"
+ },
+ "certificates": {
+ "title": "Certificados",
+ "description": "Gestiona certificados SSL",
+ "addCertificate": "Añadir Certificado",
+ "domain": "Dominio",
+ "status": "Estado",
+ "expiresAt": "Expira el",
+ "valid": "Válido",
+ "pending": "Pendiente",
+ "expired": "Expirado"
+ },
+ "auth": {
+ "login": "Iniciar Sesión",
+ "logout": "Cerrar Sesión",
+ "email": "Correo Electrónico",
+ "password": "Contraseña",
+ "username": "Nombre de Usuario",
+ "signIn": "Iniciar Sesión",
+ "signOut": "Cerrar Sesión",
+ "forgotPassword": "¿Olvidaste tu Contraseña?",
+ "rememberMe": "Recuérdame"
+ },
+ "errors": {
+ "required": "Este campo es obligatorio",
+ "invalidEmail": "Dirección de correo electrónico inválida",
+ "passwordTooShort": "La contraseña debe tener al menos 8 caracteres",
+ "genericError": "Ocurrió un error. Por favor, inténtalo de nuevo.",
+ "networkError": "Error de red. Por favor, verifica tu conexión.",
+ "unauthorized": "No autorizado. Por favor, inicia sesión de nuevo.",
+ "notFound": "Recurso no encontrado",
+ "serverError": "Error del servidor. Por favor, inténtalo más tarde."
+ },
+ "notifications": {
+ "saveSuccess": "Cambios guardados exitosamente",
+ "deleteSuccess": "Eliminado exitosamente",
+ "createSuccess": "Creado exitosamente",
+ "updateSuccess": "Actualizado exitosamente",
+ "saveFailed": "Error al guardar cambios",
+ "deleteFailed": "Error al eliminar",
+ "createFailed": "Error al crear",
+ "updateFailed": "Error al actualizar"
+ }
+}
diff --git a/frontend/src/locales/fr/translation.json b/frontend/src/locales/fr/translation.json
new file mode 100644
index 00000000..b47956b4
--- /dev/null
+++ b/frontend/src/locales/fr/translation.json
@@ -0,0 +1,131 @@
+{
+ "common": {
+ "save": "Enregistrer",
+ "cancel": "Annuler",
+ "delete": "Supprimer",
+ "edit": "Modifier",
+ "add": "Ajouter",
+ "create": "Créer",
+ "update": "Mettre à jour",
+ "close": "Fermer",
+ "confirm": "Confirmer",
+ "back": "Retour",
+ "next": "Suivant",
+ "loading": "Chargement...",
+ "error": "Erreur",
+ "success": "Succès",
+ "warning": "Avertissement",
+ "info": "Information",
+ "yes": "Oui",
+ "no": "Non",
+ "enabled": "Activé",
+ "disabled": "Désactivé",
+ "name": "Nom",
+ "description": "Description",
+ "actions": "Actions",
+ "status": "Statut",
+ "search": "Rechercher",
+ "filter": "Filtrer",
+ "settings": "Paramètres",
+ "language": "Langue"
+ },
+ "navigation": {
+ "dashboard": "Tableau de bord",
+ "proxyHosts": "Hôtes Proxy",
+ "remoteServers": "Serveurs Distants",
+ "domains": "Domaines",
+ "certificates": "Certificats",
+ "security": "Sécurité",
+ "accessLists": "Listes d'Accès",
+ "crowdsec": "CrowdSec",
+ "rateLimiting": "Limitation de Débit",
+ "waf": "WAF",
+ "uptime": "Disponibilité",
+ "notifications": "Notifications",
+ "users": "Utilisateurs",
+ "tasks": "Tâches",
+ "settings": "Paramètres"
+ },
+ "dashboard": {
+ "title": "Tableau de bord",
+ "description": "Vue d'ensemble de votre proxy inverse Charon",
+ "proxyHosts": "Hôtes Proxy",
+ "remoteServers": "Serveurs Distants",
+ "certificates": "Certificats",
+ "accessLists": "Listes d'Accès",
+ "systemStatus": "État du Système",
+ "healthy": "En bonne santé",
+ "unhealthy": "Pas en bonne santé",
+ "pendingCertificates": "Certificats en attente",
+ "allCertificatesValid": "Tous les certificats sont valides",
+ "activeHosts": "{{count}} actif",
+ "activeServers": "{{count}} actif",
+ "activeLists": "{{count}} actif",
+ "validCerts": "{{count}} valide"
+ },
+ "settings": {
+ "title": "Paramètres",
+ "description": "Configurez votre instance Charon",
+ "system": "Système",
+ "smtp": "Email (SMTP)",
+ "account": "Compte",
+ "language": "Langue",
+ "languageDescription": "Sélectionnez votre langue préférée",
+ "theme": "Thème",
+ "themeDescription": "Choisissez le thème clair ou sombre"
+ },
+ "proxyHosts": {
+ "title": "Hôtes Proxy",
+ "description": "Gérez vos configurations de proxy inverse",
+ "addHost": "Ajouter un Hôte Proxy",
+ "editHost": "Modifier l'Hôte Proxy",
+ "deleteHost": "Supprimer l'Hôte Proxy",
+ "domainNames": "Noms de Domaine",
+ "forwardHost": "Hôte de Transfert",
+ "forwardPort": "Port de Transfert",
+ "sslEnabled": "SSL Activé",
+ "sslForced": "Forcer SSL"
+ },
+ "certificates": {
+ "title": "Certificats",
+ "description": "Gérer les certificats SSL",
+ "addCertificate": "Ajouter un Certificat",
+ "domain": "Domaine",
+ "status": "Statut",
+ "expiresAt": "Expire le",
+ "valid": "Valide",
+ "pending": "En attente",
+ "expired": "Expiré"
+ },
+ "auth": {
+ "login": "Connexion",
+ "logout": "Déconnexion",
+ "email": "Email",
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur",
+ "signIn": "Se connecter",
+ "signOut": "Se déconnecter",
+ "forgotPassword": "Mot de passe oublié?",
+ "rememberMe": "Se souvenir de moi"
+ },
+ "errors": {
+ "required": "Ce champ est obligatoire",
+ "invalidEmail": "Adresse email invalide",
+ "passwordTooShort": "Le mot de passe doit contenir au moins 8 caractères",
+ "genericError": "Une erreur s'est produite. Veuillez réessayer.",
+ "networkError": "Erreur réseau. Veuillez vérifier votre connexion.",
+ "unauthorized": "Non autorisé. Veuillez vous reconnecter.",
+ "notFound": "Ressource non trouvée",
+ "serverError": "Erreur serveur. Veuillez réessayer plus tard."
+ },
+ "notifications": {
+ "saveSuccess": "Modifications enregistrées avec succès",
+ "deleteSuccess": "Supprimé avec succès",
+ "createSuccess": "Créé avec succès",
+ "updateSuccess": "Mis à jour avec succès",
+ "saveFailed": "Échec de l'enregistrement des modifications",
+ "deleteFailed": "Échec de la suppression",
+ "createFailed": "Échec de la création",
+ "updateFailed": "Échec de la mise à jour"
+ }
+}
diff --git a/frontend/src/locales/zh/translation.json b/frontend/src/locales/zh/translation.json
new file mode 100644
index 00000000..413ae943
--- /dev/null
+++ b/frontend/src/locales/zh/translation.json
@@ -0,0 +1,131 @@
+{
+ "common": {
+ "save": "保存",
+ "cancel": "取消",
+ "delete": "删除",
+ "edit": "编辑",
+ "add": "添加",
+ "create": "创建",
+ "update": "更新",
+ "close": "关闭",
+ "confirm": "确认",
+ "back": "返回",
+ "next": "下一步",
+ "loading": "加载中...",
+ "error": "错误",
+ "success": "成功",
+ "warning": "警告",
+ "info": "信息",
+ "yes": "是",
+ "no": "否",
+ "enabled": "已启用",
+ "disabled": "已禁用",
+ "name": "名称",
+ "description": "描述",
+ "actions": "操作",
+ "status": "状态",
+ "search": "搜索",
+ "filter": "筛选",
+ "settings": "设置",
+ "language": "语言"
+ },
+ "navigation": {
+ "dashboard": "仪表板",
+ "proxyHosts": "代理主机",
+ "remoteServers": "远程服务器",
+ "domains": "域名",
+ "certificates": "证书",
+ "security": "安全",
+ "accessLists": "访问列表",
+ "crowdsec": "CrowdSec",
+ "rateLimiting": "速率限制",
+ "waf": "WAF",
+ "uptime": "正常运行时间",
+ "notifications": "通知",
+ "users": "用户",
+ "tasks": "任务",
+ "settings": "设置"
+ },
+ "dashboard": {
+ "title": "仪表板",
+ "description": "Charon反向代理概览",
+ "proxyHosts": "代理主机",
+ "remoteServers": "远程服务器",
+ "certificates": "证书",
+ "accessLists": "访问列表",
+ "systemStatus": "系统状态",
+ "healthy": "健康",
+ "unhealthy": "不健康",
+ "pendingCertificates": "待处理证书",
+ "allCertificatesValid": "所有证书有效",
+ "activeHosts": "{{count}} 个活动",
+ "activeServers": "{{count}} 个活动",
+ "activeLists": "{{count}} 个活动",
+ "validCerts": "{{count}} 个有效"
+ },
+ "settings": {
+ "title": "设置",
+ "description": "配置您的Charon实例",
+ "system": "系统",
+ "smtp": "电子邮件 (SMTP)",
+ "account": "账户",
+ "language": "语言",
+ "languageDescription": "选择您的首选语言",
+ "theme": "主题",
+ "themeDescription": "选择浅色或深色主题"
+ },
+ "proxyHosts": {
+ "title": "代理主机",
+ "description": "管理您的反向代理配置",
+ "addHost": "添加代理主机",
+ "editHost": "编辑代理主机",
+ "deleteHost": "删除代理主机",
+ "domainNames": "域名",
+ "forwardHost": "转发主机",
+ "forwardPort": "转发端口",
+ "sslEnabled": "已启用SSL",
+ "sslForced": "强制SSL"
+ },
+ "certificates": {
+ "title": "证书",
+ "description": "管理SSL证书",
+ "addCertificate": "添加证书",
+ "domain": "域名",
+ "status": "状态",
+ "expiresAt": "过期时间",
+ "valid": "有效",
+ "pending": "待处理",
+ "expired": "已过期"
+ },
+ "auth": {
+ "login": "登录",
+ "logout": "注销",
+ "email": "电子邮件",
+ "password": "密码",
+ "username": "用户名",
+ "signIn": "登录",
+ "signOut": "注销",
+ "forgotPassword": "忘记密码?",
+ "rememberMe": "记住我"
+ },
+ "errors": {
+ "required": "此字段为必填项",
+ "invalidEmail": "无效的电子邮件地址",
+ "passwordTooShort": "密码必须至少8个字符",
+ "genericError": "发生错误。请重试。",
+ "networkError": "网络错误。请检查您的连接。",
+ "unauthorized": "未授权。请重新登录。",
+ "notFound": "未找到资源",
+ "serverError": "服务器错误。请稍后再试。"
+ },
+ "notifications": {
+ "saveSuccess": "更改已成功保存",
+ "deleteSuccess": "删除成功",
+ "createSuccess": "创建成功",
+ "updateSuccess": "更新成功",
+ "saveFailed": "保存更改失败",
+ "deleteFailed": "删除失败",
+ "createFailed": "创建失败",
+ "updateFailed": "更新失败"
+ }
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 76861983..f45797e0 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -3,6 +3,8 @@ import ReactDOM from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App.tsx'
import { ThemeProvider } from './context/ThemeContext'
+import { LanguageProvider } from './context/LanguageContext'
+import './i18n'
import './index.css'
// Global query client with optimized defaults for performance
@@ -22,7 +24,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
-
+
+
+
,
diff --git a/frontend/src/pages/SystemSettings.tsx b/frontend/src/pages/SystemSettings.tsx
index 3c53a9ca..5b7ec6eb 100644
--- a/frontend/src/pages/SystemSettings.tsx
+++ b/frontend/src/pages/SystemSettings.tsx
@@ -17,6 +17,7 @@ import client from '../api/client'
import { Server, RefreshCw, Save, Activity, Info, ExternalLink } from 'lucide-react'
import { ConfigReloadOverlay } from '../components/LoadingStates'
import { WebSocketStatusCard } from '../components/WebSocketStatusCard'
+import { LanguageSelector } from '../components/LanguageSelector'
interface HealthResponse {
status: string
@@ -284,6 +285,14 @@ export default function SystemSettings() {
Control how domain links open in the Proxy Hosts list.
+
+
+
+
+
+ Select your preferred language. Changes take effect immediately.
+
+