/** * JavaScript i18n Support * * Loads translations from module locale files for use in JavaScript. * Provides the same translation interface as the Python/Jinja2 system. * * Usage: * // Initialize with language and modules to preload * await I18n.init('en', ['catalog', 'orders']); * * // Or load modules later * await I18n.loadModule('inventory'); * * // Translate (in JS) * const message = I18n.t('catalog.messages.product_created'); * const withVars = I18n.t('common.welcome', { name: 'John' }); * * // Translate (in Alpine templates — reactive, updates when translations load) * // * // */ // Create logger for i18n module (with silent fallback if LogConfig not yet loaded) const i18nLog = window.LogConfig ? window.LogConfig.createLogger('I18N') : { warn: () => {}, // Silent fallback - i18n loads early before LogConfig error: () => {}, info: () => {}, debug: () => {} }; const I18n = { _translations: {}, _language: 'en', _loaded: new Set(), _loading: new Map(), // Track in-progress loads /** * Initialize with language (call once on page load) * @param {string} language - Language code (en, fr, de, lb) * @param {string[]} modules - Optional array of modules to preload */ async init(language = 'en', modules = []) { this._language = language; // Load shared translations first await this.loadShared(); // Preload any specified modules if (modules && modules.length > 0) { await Promise.all(modules.map(m => this.loadModule(m))); } this._notifyReady(); }, /** * Load shared/common translations from static/locales */ async loadShared() { if (this._loaded.has('shared')) return; try { const response = await fetch(`/static/locales/${this._language}.json`); if (response.ok) { const data = await response.json(); this._translations = { ...this._translations, ...data }; this._loaded.add('shared'); } } catch (e) { i18nLog.warn('Failed to load shared translations:', e); } }, /** * Load module-specific translations * @param {string} module - Module name (e.g., 'catalog', 'orders') */ async loadModule(module) { if (this._loaded.has(module)) return; try { const response = await fetch(`/static/modules/${module}/locales/${this._language}.json`); if (response.ok) { const data = await response.json(); // Namespace under module code (matching Python i18n behavior) this._translations[module] = data; this._loaded.add(module); } } catch (e) { i18nLog.warn(`Failed to load ${module} translations:`, e); } }, /** * Get translation by key path * @param {string} key - Dot-notation key (e.g., 'catalog.messages.product_created') * @param {object} vars - Variables for interpolation * @returns {string} Translated string or key if not found */ t(key, vars = {}) { const keys = key.split('.'); let value = this._translations; for (const k of keys) { if (value && typeof value === 'object' && k in value) { value = value[k]; } else { i18nLog.warn(`Missing translation: ${key}`); return key; } } if (typeof value !== 'string') return key; // Interpolate variables: {name} -> value return value.replace(/\{(\w+)\}/g, (match, name) => { return vars[name] !== undefined ? vars[name] : match; }); }, /** * Check if a translation key exists * @param {string} key - Dot-notation key * @returns {boolean} True if key exists */ has(key) { const keys = key.split('.'); let value = this._translations; for (const k of keys) { if (value && typeof value === 'object' && k in value) { value = value[k]; } else { return false; } } return typeof value === 'string'; }, /** * Get current language * @returns {string} Current language code */ getLanguage() { return this._language; }, /** * Change language (reloads all loaded modules) * @param {string} language - New language code */ async setLanguage(language) { if (language === this._language) return; try { const loadedModules = [...this._loaded]; this._language = language; this._translations = {}; this._loaded.clear(); // Reload all previously loaded modules for (const module of loadedModules) { if (module === 'shared') { await this.loadShared(); } else { await this.loadModule(module); } } this._notifyReady(); } catch (e) { i18nLog.error('Failed to change language:', e); } }, /** * Notify Alpine (and other listeners) that translations are ready. * Bumps the Alpine store version so reactive $t() bindings re-evaluate. */ _notifyReady() { this._ready = true; // Bump Alpine store version if available (triggers $t() re-evaluation) if (typeof Alpine !== 'undefined' && Alpine.store) { const store = Alpine.store('i18n'); if (store) store._v++; } document.dispatchEvent(new CustomEvent('i18n:ready')); } }; // Register Alpine magic $t() — reactive wrapper around I18n.t() // Works across all frontends (merchant, admin, store, storefront) document.addEventListener('alpine:init', () => { // Store with a reactive version counter Alpine.store('i18n', { _v: 0 }); // $t('key', {vars}) — use in x-text, x-html, or any Alpine expression // Before translations load, returns the key (which matches fallback text); // after _notifyReady() bumps _v, Alpine re-evaluates with loaded translations. Alpine.magic('t', (el) => { return (key, vars) => { void Alpine.store('i18n')._v; // reactive dependency return I18n.t(key, vars); }; }); }); // Export for module usage if (typeof window !== 'undefined') { window.I18n = I18n; }