/** * Internationalization (i18n) support for the application */ // Global translations object let currentTranslations = {}; let currentLanguage = 'sl'; /** * Initialize i18n system */ async function initI18n() { try { const response = await fetch('/api/language'); if (response.ok) { const data = await response.json(); currentTranslations = data.translations; currentLanguage = data.current_language; // Update language selector if it exists updateLanguageSelector(); // Translate the page translatePage(); // Signal that translations are ready window.dispatchEvent(new Event('i18nReady')); } } catch (error) { console.error('Failed to initialize i18n:', error); // Even on error, signal ready so page doesn't get stuck window.dispatchEvent(new Event('i18nReady')); } } /** * Get translation for a key with dot notation * @param {string} key - Translation key (e.g., 'general.save') * @param {object} params - Optional parameters for string interpolation * @returns {string} Translated text or key if not found */ function t(key, params = {}) { const keys = key.split('.'); let value = currentTranslations; for (const k of keys) { if (value && typeof value === 'object' && k in value) { value = value[k]; } else { console.warn(`Translation key not found: ${key}`); return key; // Return key if translation not found } } if (typeof value !== 'string') { console.warn(`Translation value is not a string: ${key}`); return key; } // Simple string interpolation let result = value; for (const [param, replacement] of Object.entries(params)) { result = result.replace(new RegExp(`\\{${param}\\}`, 'g'), replacement); } return result; } /** * Change application language * @param {string} language - Language code (e.g., 'sl', 'en') */ async function changeLanguage(language) { try { const response = await fetch(`/api/language/${language}`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); if (response.ok) { const data = await response.json(); currentTranslations = data.translations; currentLanguage = language; // Update UI updateLanguageSelector(); translatePage(); } else { throw new Error('Failed to change language'); } } catch (error) { console.error('Error changing language:', error); showNotification(t('messages.error'), 'error'); } } /** * Update language selector UI */ function updateLanguageSelector() { const selector = document.getElementById('languageSelector'); if (selector) { selector.value = currentLanguage; } // Update flag if exists const flag = document.getElementById('languageFlag'); if (flag) { flag.textContent = currentLanguage === 'sl' ? '๐Ÿ‡ธ๐Ÿ‡ฎ' : '๐Ÿ‡ฌ๐Ÿ‡ง'; } } /** * Translate all elements with data-i18n attribute */ function translatePage() { // Translate elements with data-i18n attribute const elements = document.querySelectorAll('[data-i18n]'); elements.forEach(element => { const key = element.getAttribute('data-i18n'); if (key) { element.textContent = t(key); } }); // Translate placeholders const placeholderElements = document.querySelectorAll('[data-i18n-placeholder]'); placeholderElements.forEach(element => { const key = element.getAttribute('data-i18n-placeholder'); if (key) { element.placeholder = t(key); } }); // Translate titles/tooltips const titleElements = document.querySelectorAll('[data-i18n-title]'); titleElements.forEach(element => { const key = element.getAttribute('data-i18n-title'); if (key) { element.title = t(key); } }); // Translate aria-labels const ariaElements = document.querySelectorAll('[data-i18n-aria]'); ariaElements.forEach(element => { const key = element.getAttribute('data-i18n-aria'); if (key) { element.setAttribute('aria-label', t(key)); } }); // Translate tooltips const tooltipElements = document.querySelectorAll('[data-i18n-tooltip]'); tooltipElements.forEach(element => { const key = element.getAttribute('data-i18n-tooltip'); if (key) { element.setAttribute('data-tooltip', t(key)); } }); } /** * Show notification message */ function showNotification(message, type = 'info') { // Create notification element if it doesn't exist let notification = document.getElementById('i18nNotification'); if (!notification) { notification = document.createElement('div'); notification.id = 'i18nNotification'; notification.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 8px; color: white; font-weight: bold; z-index: 10000; transition: all 0.3s ease; transform: translateX(100%); `; document.body.appendChild(notification); } // Set color based on type const colors = { success: '#28a745', error: '#dc3545', warning: '#ffc107', info: '#007bff' }; notification.style.backgroundColor = colors[type] || colors.info; notification.textContent = message; notification.style.transform = 'translateX(0)'; // Hide after 3 seconds setTimeout(() => { notification.style.transform = 'translateX(100%)'; }, 3000); } /** * Create language selector dropdown */ function createLanguageSelector(containerId) { const container = document.getElementById(containerId); if (!container) return; const selector = document.createElement('select'); selector.id = 'languageSelector'; selector.style.cssText = ` width: 100%; padding: 14px 18px; border: 2px solid #dee2e6; border-radius: 10px; background: white; color: #495057; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); margin-bottom: 12px; `; // Add options const languages = [ { code: 'sl', name: '๐Ÿ‡ธ๐Ÿ‡ฎ Slovenลกฤina', flag: '๐Ÿ‡ธ๐Ÿ‡ฎ' }, { code: 'en', name: '๐Ÿ‡ฌ๐Ÿ‡ง English', flag: '๐Ÿ‡ฌ๐Ÿ‡ง' } ]; languages.forEach(lang => { const option = document.createElement('option'); option.value = lang.code; option.textContent = lang.name; selector.appendChild(option); }); selector.value = currentLanguage; // Add event listeners selector.addEventListener('change', (e) => { changeLanguage(e.target.value); }); // Add hover effects to match nav-link styling selector.addEventListener('mouseenter', () => { selector.style.background = '#f8f9fa'; selector.style.borderColor = '#007bff'; selector.style.color = '#007bff'; selector.style.transform = 'translateY(-2px)'; selector.style.boxShadow = '0 5px 12px rgba(0, 123, 255, 0.15)'; }); selector.addEventListener('mouseleave', () => { selector.style.background = 'white'; selector.style.borderColor = '#dee2e6'; selector.style.color = '#495057'; selector.style.transform = 'translateY(0)'; selector.style.boxShadow = '0 3px 6px rgba(0, 0, 0, 0.1)'; }); container.appendChild(selector); } // Initialize when DOM is loaded document.addEventListener('DOMContentLoaded', initI18n); // Export functions for global use window.t = t; window.changeLanguage = changeLanguage; window.createLanguageSelector = createLanguageSelector; // Export i18n object with useful functions window.i18n = { t: t, updatePageTranslations: translatePage, changeLanguage: changeLanguage };