// Enhanced Internationalization (i18n) utility for the AI Chat application class I18n { constructor() { this.currentLocale = 'en'; this.translations = {}; this.fallbackLocale = 'en'; this.loadedLocales = new Set(); this.cache = new Map(); this.observers = []; this.pluralRules = new Map(); this.dateTimeFormats = new Map(); this.numberFormats = new Map(); } // Load translations for a specific locale with caching async loadLocale(locale) { if (this.loadedLocales.has(locale)) { return this.translations[locale]; } try { const response = await fetch(`../locales/${locale}.json`); if (!response.ok) { throw new Error(`Failed to load locale ${locale}: ${response.status}`); } const translations = await response.json(); this.translations[locale] = translations; this.loadedLocales.add(locale); // Cache commonly used translations this.cacheTranslations(locale, translations); // Setup locale-specific formatters this.setupLocaleFormatters(locale); return translations; } catch (error) { console.warn(`Could not load locale ${locale}:`, error); if (locale !== this.fallbackLocale) { return await this.loadLocale(this.fallbackLocale); } throw error; } } // Cache frequently used translations cacheTranslations(locale, translations) { const commonKeys = [ 'app.title', 'chat.placeholder', 'chat.send', 'ui.loading', 'ui.error', 'ui.retry' ]; commonKeys.forEach(key => { const value = this.getNestedValue(translations, key); if (value !== undefined) { this.cache.set(`${locale}:${key}`, value); } }); } // Setup locale-specific formatters setupLocaleFormatters(locale) { try { // Date/time formatters this.dateTimeFormats.set(locale, { short: new Intl.DateTimeFormat(locale, { timeStyle: 'short' }), medium: new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' }), relative: new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }) }); // Number formatters this.numberFormats.set(locale, { decimal: new Intl.NumberFormat(locale), percent: new Intl.NumberFormat(locale, { style: 'percent' }), currency: new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD' }) }); // Plural rules this.pluralRules.set(locale, new Intl.PluralRules(locale)); } catch (error) { console.warn(`Failed to setup formatters for ${locale}:`, error); } } // Set the current locale with enhanced features async setLocale(locale) { const previousLocale = this.currentLocale; if (!this.translations[locale]) { await this.loadLocale(locale); } this.currentLocale = locale; // Update document language document.documentElement.lang = locale; // Update direction for RTL languages const rtlLanguages = ['ar', 'he', 'fa', 'ur']; document.documentElement.dir = rtlLanguages.includes(locale) ? 'rtl' : 'ltr'; // Store preference localStorage.setItem('i18n-locale', locale); // Notify observers this.notifyObservers(locale, previousLocale); // Trigger global locale change event document.dispatchEvent(new CustomEvent('localeChanged', { detail: { locale: this.currentLocale, previousLocale } })); } // Enhanced translation with context and pluralization t(key, params = {}) { const cacheKey = `${this.currentLocale}:${key}`; // Check cache first if (this.cache.has(cacheKey) && Object.keys(params).length === 0) { return this.cache.get(cacheKey); } let translation = this.getNestedValue( this.translations[this.currentLocale], key ); if (translation === undefined) { // Fallback to default locale translation = this.getNestedValue( this.translations[this.fallbackLocale], key ); if (translation === undefined) { console.warn(`Translation missing for key: ${key}`); return key; } } // Handle pluralization if (typeof translation === 'object' && params.count !== undefined) { translation = this.handlePluralization(translation, params.count); } // Handle interpolation const result = this.interpolate(translation, params); // Cache result if no parameters if (Object.keys(params).length === 0) { this.cache.set(cacheKey, result); } return result; } // Handle pluralization rules handlePluralization(translations, count) { const rules = this.pluralRules.get(this.currentLocale); if (!rules) return translations.other || ''; const rule = rules.select(count); return translations[rule] || translations.other || translations.one || ''; } // Enhanced interpolation with formatting interpolate(text, params) { if (typeof text !== 'string') return text; return text.replace(/\{\{(\w+)(?::(\w+))?\}\}/g, (match, key, format) => { const value = params[key]; if (value === undefined) return match; // Apply formatting if specified if (format) { return this.formatValue(value, format); } return value; }); } // Format values based on type formatValue(value, format) { const formatters = this.numberFormats.get(this.currentLocale); const dateFormatters = this.dateTimeFormats.get(this.currentLocale); switch (format) { case 'number': return formatters?.decimal.format(value) || value; case 'percent': return formatters?.percent.format(value) || value; case 'currency': return formatters?.currency.format(value) || value; case 'date': return dateFormatters?.medium.format(new Date(value)) || value; case 'time': return dateFormatters?.short.format(new Date(value)) || value; case 'relative': const now = Date.now(); const diff = Math.floor((value - now) / 1000); return dateFormatters?.relative.format(diff, 'second') || value; case 'uppercase': return String(value).toUpperCase(); case 'lowercase': return String(value).toLowerCase(); case 'capitalize': return String(value).charAt(0).toUpperCase() + String(value).slice(1); default: return value; } } // Get nested value from object using dot notation getNestedValue(obj, path) { return path.split('.').reduce((current, key) => { return current && current[key] !== undefined ? current[key] : undefined; }, obj); } // Add observer for locale changes addObserver(callback) { this.observers.push(callback); } // Remove observer removeObserver(callback) { const index = this.observers.indexOf(callback); if (index > -1) { this.observers.splice(index, 1); } } // Notify all observers of locale change notifyObservers(newLocale, oldLocale) { this.observers.forEach(callback => { try { callback(newLocale, oldLocale); } catch (error) { console.error('Error in i18n observer:', error); } }); } // Get current locale getCurrentLocale() { return this.currentLocale; } // Get available locales getAvailableLocales() { return Array.from(this.loadedLocales); } // Get all loaded translations (for debugging) getAllTranslations() { return this.translations; } // Initialize the i18n system with enhanced detection async init(locale = null) { try { // Detect locale in order of preference const detectedLocale = locale || localStorage.getItem('i18n-locale') || this.detectBrowserLocale() || this.fallbackLocale; await this.loadLocale(detectedLocale); await this.setLocale(detectedLocale); console.log(`i18n initialized with locale: ${detectedLocale}`); return true; } catch (error) { console.error('Failed to initialize i18n:', error); return false; } } // Detect browser locale detectBrowserLocale() { if (navigator.languages && navigator.languages.length) { // Check each preferred language for (const lang of navigator.languages) { const shortLang = lang.split('-')[0]; // Return first supported language const supportedLocales = ['en', 'es', 'fr', 'de', 'cs']; if (supportedLocales.includes(shortLang)) { return shortLang; } } } return navigator.language?.split('-')[0] || 'en'; } // Preload multiple locales async preloadLocales(locales) { const promises = locales.map(locale => this.loadLocale(locale)); await Promise.allSettled(promises); } // Clear cache clearCache() { this.cache.clear(); } // Get memory usage statistics getStats() { return { loadedLocales: this.loadedLocales.size, cachedTranslations: this.cache.size, observers: this.observers.length, currentLocale: this.currentLocale, memoryUsage: JSON.stringify(this.translations).length }; } } // Enhanced translation helper functions class TranslationHelpers { static createKeyExtractor(prefix = '') { return (key) => prefix ? `${prefix}.${key}` : key; } static createScopedTranslator(i18n, scope) { return (key, params) => i18n.t(`${scope}.${key}`, params); } static validateTranslations(translations, requiredKeys) { const missing = []; requiredKeys.forEach(key => { if (!this.hasKey(translations, key)) { missing.push(key); } }); return missing; } static hasKey(obj, path) { return path.split('.').reduce((current, key) => { return current && current[key] !== undefined ? current[key] : undefined; }, obj) !== undefined; } static flattenTranslations(obj, prefix = '') { const flattened = {}; for (const key in obj) { const newKey = prefix ? `${prefix}.${key}` : key; if (typeof obj[key] === 'object' && obj[key] !== null) { Object.assign(flattened, this.flattenTranslations(obj[key], newKey)); } else { flattened[newKey] = obj[key]; } } return flattened; } } // Create global instance const i18n = new I18n(); // Helper function for easy access with enhanced features function t(key, params = {}) { return i18n.t(key, params); } // Additional helper functions function tc(key, count, params = {}) { return i18n.t(key, { ...params, count }); } function td(key, date, params = {}) { return i18n.t(key, { ...params, date }); } function tn(key, number, params = {}) { return i18n.t(key, { ...params, number }); } // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = { I18n, i18n, t, tc, td, tn, TranslationHelpers }; } // Export to global scope for debugging window.i18nDebug = { i18n, TranslationHelpers, getStats: () => i18n.getStats(), clearCache: () => i18n.clearCache(), getAllTranslations: () => i18n.getAllTranslations() };