/** * DC-077: Internationalization (i18n) framework for DashCaddy * * Lightweight translation system for the dashboard frontend and API responses. * Supports multiple languages via JSON translation files loaded on demand. * * Languages are stored in /assets/i18n/{lang}.json * Default language is 'en' (English). * * Usage in frontend JS: * const { t, setLanguage, getLanguage } = window.DCI18n; * document.querySelector('.title').textContent = t('dashboard.title'); * * Usage in API responses: * const i18n = require('./i18n'); * const msg = i18n.t('error.container_not_found', req.lang || 'en'); */ const fs = require('fs'); const path = require('path'); // Built-in translations (loaded synchronously at startup) const TRANSLATIONS = { en: { 'dashboard.title': 'Dashboard', 'dashboard.services': 'Services', 'dashboard.containers': 'Containers', 'dashboard.health': 'Health', 'dashboard.settings': 'Settings', 'dashboard.backups': 'Backups', 'dashboard.monitoring': 'Monitoring', 'dashboard.security': 'Security', 'service.status.healthy': 'Healthy', 'service.status.degraded': 'Degraded', 'service.status.down': 'Down', 'service.status.unknown': 'Unknown', 'service.status.pending': 'Pending', 'action.start': 'Start', 'action.stop': 'Stop', 'action.restart': 'Restart', 'action.delete': 'Delete', 'action.update': 'Update', 'action.deploy': 'Deploy', 'action.save': 'Save', 'action.cancel': 'Cancel', 'action.confirm': 'Confirm', 'error.not_found': 'Resource not found', 'error.unauthorized': 'Unauthorized', 'error.forbidden': 'Forbidden', 'error.rate_limited': 'Too many requests', 'error.internal': 'Internal server error', 'error.container_not_found': 'Container not found', 'error.service_not_found': 'Service not found', 'error.invalid_input': 'Invalid input', 'error.docker_unreachable': 'Docker daemon is not reachable', 'error.disk_full': 'Disk space is critically low', }, es: { 'dashboard.title': 'Panel de control', 'dashboard.services': 'Servicios', 'dashboard.containers': 'Contenedores', 'dashboard.health': 'Salud', 'dashboard.settings': 'Configuración', 'dashboard.backups': 'Copias de seguridad', 'dashboard.monitoring': 'Monitoreo', 'dashboard.security': 'Seguridad', 'service.status.healthy': 'Saludable', 'service.status.degraded': 'Degradado', 'service.status.down': 'Caído', 'service.status.unknown': 'Desconocido', 'service.status.pending': 'Pendiente', 'action.start': 'Iniciar', 'action.stop': 'Detener', 'action.restart': 'Reiniciar', 'action.delete': 'Eliminar', 'action.update': 'Actualizar', 'action.deploy': 'Desplegar', 'action.save': 'Guardar', 'action.cancel': 'Cancelar', 'action.confirm': 'Confirmar', 'error.not_found': 'Recurso no encontrado', 'error.unauthorized': 'No autorizado', 'error.forbidden': 'Prohibido', 'error.rate_limited': 'Demasiadas solicitudes', 'error.internal': 'Error interno del servidor', 'error.container_not_found': 'Contenedor no encontrado', 'error.service_not_found': 'Servicio no encontrado', 'error.invalid_input': 'Entrada inválida', 'error.docker_unreachable': 'El demonio de Docker no es accesible', 'error.disk_full': 'Espacio en disco críticamente bajo', }, fr: { 'dashboard.title': 'Tableau de bord', 'dashboard.services': 'Services', 'dashboard.containers': 'Conteneurs', 'dashboard.health': 'Santé', 'dashboard.settings': 'Paramètres', 'dashboard.backups': 'Sauvegardes', 'dashboard.monitoring': 'Surveillance', 'dashboard.security': 'Sécurité', 'service.status.healthy': 'Sain', 'service.status.degraded': 'Dégradé', 'service.status.down': 'Hors ligne', 'service.status.unknown': 'Inconnu', 'service.status.pending': 'En attente', 'action.start': 'Démarrer', 'action.stop': 'Arrêter', 'action.restart': 'Redémarrer', 'action.delete': 'Supprimer', 'action.update': 'Mettre à jour', 'action.deploy': 'Déployer', 'action.save': 'Enregistrer', 'action.cancel': 'Annuler', 'action.confirm': 'Confirmer', 'error.not_found': 'Ressource introuvable', 'error.unauthorized': 'Non autorisé', 'error.forbidden': 'Interdit', 'error.rate_limited': 'Trop de requêtes', 'error.internal': 'Erreur interne du serveur', 'error.container_not_found': 'Conteneur introuvable', 'error.service_not_found': 'Service introuvable', 'error.invalid_input': 'Entrée invalide', 'error.docker_unreachable': 'Le démon Docker est injoignable', 'error.disk_full': 'Espace disque critique', }, de: { 'dashboard.title': 'Dashboard', 'dashboard.services': 'Dienste', 'dashboard.containers': 'Container', 'dashboard.health': 'Zustand', 'dashboard.settings': 'Einstellungen', 'dashboard.backups': 'Backups', 'dashboard.monitoring': 'Überwachung', 'dashboard.security': 'Sicherheit', 'service.status.healthy': 'Gesund', 'service.status.degraded': 'Beeinträchtigt', 'service.status.down': 'Ausgefallen', 'service.status.unknown': 'Unbekannt', 'service.status.pending': 'Ausstehend', 'action.start': 'Starten', 'action.stop': 'Stopp', 'action.restart': 'Neustart', 'action.delete': 'Löschen', 'action.update': 'Aktualisieren', 'action.deploy': 'Bereitstellen', 'action.save': 'Speichern', 'action.cancel': 'Abbrechen', 'action.confirm': 'Bestätigen', 'error.not_found': 'Ressource nicht gefunden', 'error.unauthorized': 'Nicht autorisiert', 'error.forbidden': 'Verboten', 'error.rate_limited': 'Zu viele Anfragen', 'error.internal': 'Interner Serverfehler', 'error.container_not_found': 'Container nicht gefunden', 'error.service_not_found': 'Dienst nicht gefunden', 'error.invalid_input': 'Ungültige Eingabe', 'error.docker_unreachable': 'Docker-Daemon ist nicht erreichbar', 'error.disk_full': 'Speicherplatz kritisch niedrig', }, ar: { 'dashboard.title': 'لوحة التحكم', 'dashboard.services': 'الخدمات', 'dashboard.containers': 'الحاويات', 'dashboard.health': 'الصحة', 'dashboard.settings': 'الإعدادات', 'dashboard.backups': 'النسخ الاحتياطية', 'dashboard.monitoring': 'المراقبة', 'dashboard.security': 'الأمان', 'service.status.healthy': 'سليم', 'service.status.degraded': 'متدهور', 'service.status.down': 'متوقف', 'service.status.unknown': 'غير معروف', 'service.status.pending': 'قيد الانتظار', 'action.start': 'تشغيل', 'action.stop': 'إيقاف', 'action.restart': 'إعادة تشغيل', 'action.delete': 'حذف', 'action.update': 'تحديث', 'action.deploy': 'نشر', 'action.save': 'حفظ', 'action.cancel': 'إلغاء', 'action.confirm': 'تأكيد', 'error.not_found': 'المورد غير موجود', 'error.unauthorized': 'غير مصرح', 'error.forbidden': 'محظور', 'error.rate_limited': 'طلبات كثيرة جداً', 'error.internal': 'خطأ داخلي في الخادم', 'error.container_not_found': 'الحاوية غير موجودة', 'error.service_not_found': 'الخدمة غير موجودة', 'error.invalid_input': 'إدخال غير صالح', 'error.docker_unreachable': 'لا يمكن الوصول إلى Docker', 'error.disk_full': 'مساحة القرص منخفضة بشكل حرج', }, }; const SUPPORTED_LANGUAGES = Object.keys(TRANSLATIONS); const DEFAULT_LANGUAGE = 'en'; /** * Translate a key to the specified language. * Falls back to English, then to the key itself if not found. */ function t(key, lang = DEFAULT_LANGUAGE) { const dict = TRANSLATIONS[lang] || TRANSLATIONS[DEFAULT_LANGUAGE]; return dict[key] || TRANSLATIONS[DEFAULT_LANGUAGE][key] || key; } /** * Get the list of supported languages */ function getSupportedLanguages() { return SUPPORTED_LANGUAGES; } /** * Check if a language is supported */ function isSupported(lang) { return SUPPORTED_LANGUAGES.includes(lang); } /** * Detect language from Accept-Language header */ function detectLanguage(acceptLanguage) { if (!acceptLanguage) return DEFAULT_LANGUAGE; const langs = acceptLanguage.split(',').map(l => { const [code, q] = l.trim().split(';q='); return { code: code.split('-')[0].toLowerCase(), q: q ? parseFloat(q) : 1 }; }).sort((a, b) => b.q - a.q); for (const { code } of langs) { if (isSupported(code)) return code; } return DEFAULT_LANGUAGE; } module.exports = { t, getSupportedLanguages, isSupported, detectLanguage, DEFAULT_LANGUAGE, TRANSLATIONS, };