refactor: complete module-driven architecture migration
This commit completes the migration to a fully module-driven architecture: ## Models Migration - Moved all domain models from models/database/ to their respective modules: - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc. - cms: MediaFile, VendorTheme - messaging: Email, VendorEmailSettings, VendorEmailTemplate - core: AdminMenuConfig - models/database/ now only contains Base and TimestampMixin (infrastructure) ## Schemas Migration - Moved all domain schemas from models/schema/ to their respective modules: - tenancy: company, vendor, admin, team, vendor_domain - cms: media, image, vendor_theme - messaging: email - models/schema/ now only contains base.py and auth.py (infrastructure) ## Routes Migration - Moved admin routes from app/api/v1/admin/ to modules: - menu_config.py -> core module - modules.py -> tenancy module - module_config.py -> tenancy module - app/api/v1/admin/ now only aggregates auto-discovered module routes ## Menu System - Implemented module-driven menu system with MenuDiscoveryService - Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT - Added MenuItemDefinition and MenuSectionDefinition dataclasses - Each module now defines its own menu items in definition.py - MenuService integrates with MenuDiscoveryService for template rendering ## Documentation - Updated docs/architecture/models-structure.md - Updated docs/architecture/menu-management.md - Updated architecture validation rules for new exceptions ## Architecture Validation - Updated MOD-019 rule to allow base.py in models/schema/ - Created core module exceptions.py and schemas/ directory - All validation errors resolved (only warnings remain) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
158
static/shared/js/i18n.js
Normal file
158
static/shared/js/i18n.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 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
|
||||
* const message = I18n.t('catalog.messages.product_created');
|
||||
* const withVars = I18n.t('common.welcome', { name: 'John' });
|
||||
*/
|
||||
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)));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
console.warn('[i18n] 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) {
|
||||
console.warn(`[i18n] 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 {
|
||||
console.warn(`[i18n] 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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Export for module usage
|
||||
if (typeof window !== 'undefined') {
|
||||
window.I18n = I18n;
|
||||
}
|
||||
Reference in New Issue
Block a user