504 lines
18 KiB
JavaScript
504 lines
18 KiB
JavaScript
// 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 = `
|
|
<div class="d-flex">
|
|
<div class="toast-body">
|
|
${message}
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
`;
|
|
|
|
// 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
|
|
}; |