// Language management functionality class LanguageManager { constructor() { this.supportedLanguages = ['en', 'es', 'it', 'fr']; this.translations = {}; this.currentLanguage = this.getCurrentLanguageFromPage(); this.init(); } init() { this.loadTranslations(this.currentLanguage).then(() => { // Apply initial translations when translations are loaded this.translatePage(); this.updateUI(); }); this.setupEventListeners(); } getCurrentLanguageFromPage() { // Priority order: // 1. User's language from page data (if authenticated) // 2. Stored language in localStorage // 3. Browser language // Get language from page data (set by server for authenticated users) const bodyElement = document.body; if (bodyElement && bodyElement.dataset.currentLanguage) { const pageLanguage = bodyElement.dataset.currentLanguage; if (this.supportedLanguages.includes(pageLanguage)) { // Sync localStorage with user preference this.setStoredLanguage(pageLanguage); return pageLanguage; } } // Fallback to stored language or browser language return this.getStoredLanguage() || this.getBrowserLanguage(); } getStoredLanguage() { return localStorage.getItem('language'); } setStoredLanguage(language) { localStorage.setItem('language', language); } getBrowserLanguage() { // Safely get browser language with fallback if (!navigator.language) { return 'en'; } const browserLang = navigator.language.split('-')[0]; return this.supportedLanguages.includes(browserLang) ? browserLang : 'en'; } async loadTranslations(language) { if (this.translations[language]) { return this.translations[language]; } try { const response = await fetch(`/api/i18n/${language}`); if (response.ok) { this.translations[language] = await response.json(); return this.translations[language]; } } catch (error) { console.error(`Failed to load translations for ${language}:`, error); } // Fallback to English if loading fails if (language !== 'en') { return this.loadTranslations('en'); } return {}; } async setLanguage(language) { if (!this.supportedLanguages.includes(language)) { console.warn(`Unsupported language: ${language}`); return; } if (language === this.currentLanguage) { return; // No change needed } this.currentLanguage = language; this.setStoredLanguage(language); // Load translations await this.loadTranslations(language); // Update user preference via API if logged in try { const response = await fetch('/api/user/preferences', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ language: language }) }); if (!response.ok) { console.error('Failed to update language preference:', response.statusText); } } catch (error) { console.error('Failed to update language preference:', error); } // Update language attribute document.documentElement.lang = language; // Update body data attribute for consistency if (document.body) { document.body.dataset.currentLanguage = language; } // Dispatch language change event window.dispatchEvent(new CustomEvent('languageChanged', { detail: { language: language, translations: this.translations[language] || {} } })); // Update UI elements this.updateUI(); // Apply translations to the current page dynamically this.translatePage(); // Show a brief success message this.showLanguageChangeSuccess(language); } updateUI() { // Update language dropdown to show current selection const languageOptions = document.querySelectorAll('.language-option'); languageOptions.forEach(option => { const lang = option.dataset.lang; if (lang === this.currentLanguage) { option.classList.add('active'); } else { option.classList.remove('active'); } }); // Update language indicator in navbar const languageDisplay = document.getElementById('current-language-display'); if (languageDisplay) { languageDisplay.textContent = this.currentLanguage.toUpperCase(); } } setupEventListeners() { // Language selection buttons document.addEventListener('click', (e) => { if (e.target.classList.contains('language-option') || e.target.closest('.language-option')) { const button = e.target.classList.contains('language-option') ? e.target : e.target.closest('.language-option'); const language = button.dataset.lang; if (language && language !== this.currentLanguage) { this.setLanguage(language); } } }); // Listen for custom language change events window.addEventListener('setLanguage', (e) => { this.setLanguage(e.detail.language); }); } translate(key, params = {}) { const translations = this.translations[this.currentLanguage] || {}; // Handle specific key mappings for compatibility const keyMappings = { 'nav.dashboard': 'dashboard', 'actions.refresh': 'actions.refresh_scripts' // We'll add this to translations }; // Use mapped key if available const mappedKey = keyMappings[key] || key; // Try multiple formats for the key const keyVariants = [ mappedKey, // Mapped or original key mappedKey.replace(/\./g, '_'), // Underscore: app_title mappedKey.split('.').join('_'), // Same as above ]; for (const variant of keyVariants) { let value = this.lookupTranslation(translations, variant); if (value !== variant) { // Found a translation, apply parameters if any if (typeof value === 'string') { return this.replaceParameters(value, params); } return value; } } // Fallback to English if not current language if (this.currentLanguage !== 'en' && this.translations.en) { return this.translateWithLanguage('en', key, params); } // Special fallbacks for common missing translations if (key === 'actions.refresh') { return this.currentLanguage === 'es' ? 'Actualizar' : 'Refresh'; } return key; // Return key as fallback } translateWithLanguage(language, key, params = {}) { const translations = this.translations[language] || {}; // Try multiple formats for the key const keyVariants = [ key, // Original: app.title key.replace(/\./g, '_'), // Underscore: app_title ]; for (const variant of keyVariants) { let value = this.lookupTranslation(translations, variant); if (value !== variant) { if (typeof value === 'string') { return this.replaceParameters(value, params); } return value; } } return key; } lookupTranslation(translations, key) { // Support nested keys like 'user_level.admin' or flat keys like 'app_title' if (key.includes('.')) { const keys = key.split('.'); let value = translations; for (const k of keys) { if (typeof value === 'object' && value[k] !== undefined) { value = value[k]; } else { return key; // Not found } } return value; } else { // Direct lookup for flat keys return translations[key] !== undefined ? translations[key] : key; } } replaceParameters(value, params) { // Replace parameters in translation let result = String(value); for (const [param, replacement] of Object.entries(params)) { result = result.replace(new RegExp(`{${param}}`, 'g'), replacement); } return result; } getCurrentLanguage() { return this.currentLanguage; } getSupportedLanguages() { return [...this.supportedLanguages]; } // Dynamic translation helpers translateElement(element, key, params = {}) { const translation = this.translate(key, params); element.textContent = translation; } translateElements(selector, key, params = {}) { const elements = document.querySelectorAll(selector); elements.forEach(element => { this.translateElement(element, key, params); }); } // Translate all elements with data-translate attribute translatePage() { const elements = document.querySelectorAll('[data-translate]'); elements.forEach(element => { const key = element.dataset.translate; const params = element.dataset.translateParams ? JSON.parse(element.dataset.translateParams) : {}; this.translateElement(element, key, params); }); // Also update dynamic content like breadcrumbs, titles, etc. this.updateDynamicContent(); } updateDynamicContent() { // Update page title if it has translatable content const titleElement = document.querySelector('title'); if (titleElement && titleElement.dataset.translate) { const key = titleElement.dataset.translate; titleElement.textContent = this.translate(key); } // Update form labels and placeholders const labels = document.querySelectorAll('label[data-translate]'); labels.forEach(label => { const key = label.dataset.translate; label.textContent = this.translate(key); }); const inputs = document.querySelectorAll('input[data-translate-placeholder], textarea[data-translate-placeholder]'); inputs.forEach(input => { const key = input.dataset.translatePlaceholder; if (key) { input.placeholder = this.translate(key); } }); } showLanguageChangeSuccess(language) { // Get language display name const languageNames = { 'en': 'English', 'es': 'Español', 'it': 'Italiano', 'fr': 'Français' }; const displayName = languageNames[language] || language.toUpperCase(); // Create and show a toast notification this.showToast(`Language changed to ${displayName}`, 'success'); } showToast(message, type = 'info') { // Create toast element const toast = document.createElement('div'); toast.className = `toast align-items-center text-white bg-${type === 'success' ? 'success' : 'info'} border-0`; toast.setAttribute('role', 'alert'); toast.setAttribute('aria-live', 'assertive'); toast.setAttribute('aria-atomic', 'true'); toast.innerHTML = `
${message}
`; // Add to toast container or create one let toastContainer = document.querySelector('.toast-container'); if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3'; toastContainer.style.zIndex = '1055'; document.body.appendChild(toastContainer); } toastContainer.appendChild(toast); // Show toast using Bootstrap const bsToast = new bootstrap.Toast(toast, { delay: 3000 }); bsToast.show(); // Remove from DOM after hiding toast.addEventListener('hidden.bs.toast', () => { toast.remove(); }); } } // Language-specific formatting helpers class LanguageFormatter { constructor(languageManager) { this.languageManager = languageManager; } formatDate(date, options = {}) { const lang = this.languageManager.getCurrentLanguage(); const dateObj = typeof date === 'string' ? new Date(date) : date; const defaultOptions = { year: 'numeric', month: 'long', day: 'numeric' }; const formatOptions = { ...defaultOptions, ...options }; try { return dateObj.toLocaleDateString(this.getLocale(lang), formatOptions); } catch (error) { return dateObj.toLocaleDateString('en-US', formatOptions); } } formatTime(date, options = {}) { const lang = this.languageManager.getCurrentLanguage(); const dateObj = typeof date === 'string' ? new Date(date) : date; const defaultOptions = { hour: '2-digit', minute: '2-digit' }; const formatOptions = { ...defaultOptions, ...options }; try { return dateObj.toLocaleTimeString(this.getLocale(lang), formatOptions); } catch (error) { return dateObj.toLocaleTimeString('en-US', formatOptions); } } formatNumber(number, options = {}) { const lang = this.languageManager.getCurrentLanguage(); try { return number.toLocaleString(this.getLocale(lang), options); } catch (error) { return number.toLocaleString('en-US', options); } } getLocale(language) { const localeMap = { 'en': 'en-US', 'es': 'es-ES', 'it': 'it-IT', 'fr': 'fr-FR' }; return localeMap[language] || 'en-US'; } } // RTL (Right-to-Left) language support class RTLManager { constructor(languageManager) { this.languageManager = languageManager; this.rtlLanguages = ['ar', 'he', 'fa']; // Add RTL languages as needed this.init(); } init() { window.addEventListener('languageChanged', (e) => { this.updateDirection(e.detail.language); }); // Initial direction setup this.updateDirection(this.languageManager.getCurrentLanguage()); } updateDirection(language) { const isRTL = this.rtlLanguages.includes(language); document.documentElement.dir = isRTL ? 'rtl' : 'ltr'; // Update Bootstrap classes if needed if (isRTL) { document.body.classList.add('rtl'); } else { document.body.classList.remove('rtl'); } } } // Initialize language management when DOM is loaded document.addEventListener('DOMContentLoaded', function() { // Create global language manager instance window.languageManager = new LanguageManager(); window.languageFormatter = new LanguageFormatter(window.languageManager); window.rtlManager = new RTLManager(window.languageManager); // Initial page translation for dynamic elements window.addEventListener('languageChanged', () => { window.languageManager.translatePage(); }); }); // Export for use in other modules window.LanguageManager = { translate: (key, params) => window.languageManager ? window.languageManager.translate(key, params) : key, formatDate: (date, options) => window.languageFormatter ? window.languageFormatter.formatDate(date, options) : date, formatTime: (date, options) => window.languageFormatter ? window.languageFormatter.formatTime(date, options) : date, formatNumber: (number, options) => window.languageFormatter ? window.languageFormatter.formatNumber(number, options) : number };