// 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 = `