SIDEL_ScriptsManager/app/static/js/language-manager.js

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
};