284 lines
8.1 KiB
JavaScript
284 lines
8.1 KiB
JavaScript
/**
|
|
* 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
|
|
}; |