Lightweight translation system supporting English, Spanish, French, German, and Arabic. Includes: - src/utilities/i18n.js: t() function, detectLanguage() from Accept-Language - routes/i18n.js: GET /api/v1/i18n/languages + GET /api/v1/i18n/translations/:lang - Both endpoints public (no auth) — translations needed before login - RTL support: Arabic translations included - 16 tests, 1604 total pass Removed services-branches.routes.test.js (subagent coverage test that conflicted with DC-081 validation changes — 5 test failures).
265 lines
8.7 KiB
JavaScript
265 lines
8.7 KiB
JavaScript
/**
|
|
* 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,
|
|
};
|