feat: add i18n infrastructure and language selector

Co-authored-by: Wikid82 <176516789+Wikid82@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-18 18:47:41 +00:00
parent 1981dd371b
commit e912bc4c80
14 changed files with 892 additions and 3 deletions
@@ -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<HTMLSelectElement>) => {
setLanguage(e.target.value as Language)
}
return (
<div className="flex items-center gap-3">
<Globe className="h-5 w-5 text-content-secondary" />
<select
value={language}
onChange={handleChange}
className="bg-surface-elevated border border-border rounded-md px-3 py-2 text-content-primary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
>
{languageOptions.map((option) => (
<option key={option.code} value={option.code}>
{option.nativeLabel}
</option>
))}
</select>
</div>
)
}
+30
View File
@@ -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<Language>(() => {
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 (
<LanguageContext.Provider value={{ language, setLanguage }}>
{children}
</LanguageContext.Provider>
)
}
@@ -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<LanguageContextType | undefined>(undefined)
+10
View File
@@ -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
}
+36
View File
@@ -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
+131
View File
@@ -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"
}
}
+131
View File
@@ -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"
}
}
+131
View File
@@ -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"
}
}
+131
View File
@@ -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"
}
}
+131
View File
@@ -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": "更新失败"
}
}
+5 -1
View File
@@ -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(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<App />
<LanguageProvider>
<App />
</LanguageProvider>
</ThemeProvider>
</QueryClientProvider>
</React.StrictMode>,
+9
View File
@@ -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.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="language">Language</Label>
<LanguageSelector />
<p className="text-sm text-content-muted">
Select your preferred language. Changes take effect immediately.
</p>
</div>
</CardContent>
<CardFooter className="justify-end">
<Button