Some checks failed
- Register Alpine magic $t() for reactive translations in templates - Dispatch i18n:ready event when translations load - Fix base.html to use current_language instead of storefront_language Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
209 lines
6.5 KiB
JavaScript
209 lines
6.5 KiB
JavaScript
/**
|
|
* 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)
|
|
* // <span x-text="$t('catalog.messages.product_created')"></span>
|
|
* // <span x-text="$t('common.welcome', {name: user})"></span>
|
|
*/
|
|
|
|
// 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;
|
|
}
|