/** * Frontend internationalization (i18n) service * Provides translation support for client-side JavaScript */ class I18nService { constructor() { this.translations = new Map(); this.defaultLocale = 'en'; this.availableLocales = ['en', 'es-MX']; this.currentLocale = this.getStoredLocale() || this.detectBrowserLocale(); } /** * Load translation data from the server */ async loadTranslations() { for (const locale of this.availableLocales) { try { const response = await fetch(`/api/i18n/${locale}`); if (response.ok) { const translations = await response.json(); this.translations.set(locale, translations); console.log(`Loaded translations for locale: ${locale}`); } } catch (error) { console.error(`Failed to load translations for locale ${locale}:`, error); } } // Trigger translation update after loading this.updatePageTranslations(); // Create language selector after translations are loaded this.createLanguageSelector('language-selector-container'); } /** * Get a translated string by key path */ t(keyPath, locale = null, params = null) { const targetLocale = locale || this.currentLocale; const translations = this.translations.get(targetLocale) || this.translations.get(this.defaultLocale); if (!translations) { console.warn(`No translations found for locale: ${targetLocale}`); return keyPath; } const keys = keyPath.split('.'); let value = translations; for (const key of keys) { value = value?.[key]; if (value === undefined) { // Fallback to default locale if key not found if (targetLocale !== this.defaultLocale) { return this.t(keyPath, this.defaultLocale, params); } console.warn(`Translation key not found: ${keyPath} for locale: ${targetLocale}`); return keyPath; } } if (typeof value !== 'string') { console.warn(`Translation value is not a string for key: ${keyPath}`); return keyPath; } // Replace parameters if provided if (params) { return this.replaceParams(value, params); } return value; } /** * Replace parameters in translation strings */ replaceParams(text, params) { return text.replace(/\{\{(\w+)\}\}/g, (match, key) => { return params[key] || match; }); } /** * Set the current locale and update the UI */ async setLocale(locale) { if (!this.availableLocales.includes(locale)) { console.warn(`Unsupported locale: ${locale}`); return; } this.currentLocale = locale; this.storeLocale(locale); // Update HTML lang attribute document.documentElement.lang = locale; // Update page translations this.updatePageTranslations(); // Trigger custom event for components to update window.dispatchEvent(new CustomEvent('localeChanged', { detail: { locale } })); } /** * Get current locale */ getLocale() { return this.currentLocale; } /** * Get available locales */ getAvailableLocales() { return [...this.availableLocales]; } /** * Get locale display name */ getLocaleDisplayName(locale) { const displayNames = { 'en': 'English', 'es-MX': 'Español (México)' }; return displayNames[locale] || locale; } /** * Detect browser's preferred locale */ detectBrowserLocale() { const browserLang = navigator.language || navigator.languages?.[0]; if (!browserLang) { return this.defaultLocale; } // Check for exact match first if (this.availableLocales.includes(browserLang)) { return browserLang; } // Check for language match (e.g., "es" matches "es-MX") const languageCode = browserLang.split('-')[0]; const matchingLocale = this.availableLocales.find(locale => locale.startsWith(languageCode) ); return matchingLocale || this.defaultLocale; } /** * Store locale preference in localStorage */ storeLocale(locale) { try { localStorage.setItem('preferredLocale', locale); } catch (error) { console.warn('Failed to store locale preference:', error); } } /** * Get stored locale preference from localStorage */ getStoredLocale() { try { return localStorage.getItem('preferredLocale'); } catch (error) { console.warn('Failed to get stored locale preference:', error); return null; } } /** * Update page translations using data-i18n attributes */ updatePageTranslations() { // Update elements with data-i18n attribute document.querySelectorAll('[data-i18n]').forEach(element => { const key = element.getAttribute('data-i18n'); const translation = this.t(key); if (element.tagName === 'INPUT' && (element.type === 'text' || element.type === 'password')) { element.placeholder = translation; } else if (element.hasAttribute('title')) { element.title = translation; } else { element.textContent = translation; } }); // Update elements with data-i18n-placeholder attribute document.querySelectorAll('[data-i18n-placeholder]').forEach(element => { const key = element.getAttribute('data-i18n-placeholder'); const translation = this.t(key); element.placeholder = translation; }); // Update elements with data-i18n-html attribute (for HTML content) document.querySelectorAll('[data-i18n-html]').forEach(element => { const key = element.getAttribute('data-i18n-html'); const translation = this.t(key); element.innerHTML = translation; }); // Update page title if it has translation const titleElement = document.querySelector('title[data-i18n]'); if (titleElement) { const key = titleElement.getAttribute('data-i18n'); document.title = this.t(key); } // Update meta description if it has translation const metaDescription = document.querySelector('meta[name="description"][data-i18n]'); if (metaDescription) { const key = metaDescription.getAttribute('data-i18n'); metaDescription.content = this.t(key); } } /** * Create language selector dropdown */ createLanguageSelector(containerId) { const container = document.getElementById(containerId); if (!container) { console.warn(`Container with id "${containerId}" not found`); return; } // Clear container first in case it's being recreated container.innerHTML = ''; const select = document.createElement('select'); select.id = 'language-selector'; select.className = 'language-selector'; // Use fallback title if translations not loaded yet const titleText = this.translations.size > 0 ? this.t('common.selectLanguage') : 'Select language'; select.title = titleText; // Add options for each available locale this.availableLocales.forEach(locale => { const option = document.createElement('option'); option.value = locale; option.textContent = this.getLocaleDisplayName(locale); option.selected = locale === this.currentLocale; select.appendChild(option); }); // Add change event listener select.addEventListener('change', (event) => { this.setLocale(event.target.value); }); container.appendChild(select); } } // Create global instance window.i18n = new I18nService(); // Helper function for easier access window.t = function(keyPath, locale = null, params = null) { return window.i18n.t(keyPath, locale, params); }; // Auto-initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { window.i18n.loadTranslations(); }); } else { window.i18n.loadTranslations(); }