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. +

+