Files
orion/scripts/add_i18n_module_loading.py
Samir Boulahtit d7a0ff8818 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>
2026-02-01 21:02:56 +01:00

128 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""
Add I18n.loadModule() calls to JS files that use I18n.t().
This ensures module translations are loaded before use.
"""
import re
from pathlib import Path
from collections import defaultdict
PROJECT_ROOT = Path(__file__).parent.parent
MODULES_DIR = PROJECT_ROOT / "app" / "modules"
# Pattern to find I18n.t('module.xxx') calls and extract module name
I18N_PATTERN = re.compile(r"I18n\.t\(['\"](\w+)\.")
def find_modules_used(content: str) -> set[str]:
"""Find all modules referenced in I18n.t() calls."""
return set(I18N_PATTERN.findall(content))
def add_module_loading(js_file: Path):
"""Add I18n.loadModule() calls to a JS file."""
content = js_file.read_text(encoding="utf-8")
# Find modules used in this file
modules = find_modules_used(content)
if not modules:
return False
# Check if already has module loading
if "I18n.loadModule(" in content:
return False
# Find the init() method and add loading there
# Look for common patterns:
# 1. init() { ... }
# 2. async init() { ... }
init_patterns = [
# Pattern for "init() {" or "async init() {"
(r"((?:async\s+)?init\s*\(\s*\)\s*\{)", "init"),
# Pattern for "mounted() {" (Vue style)
(r"(mounted\s*\(\s*\)\s*\{)", "mounted"),
]
for pattern, method_name in init_patterns:
match = re.search(pattern, content)
if match:
# Generate loading code
load_calls = "\n".join(f" await I18n.loadModule('{m}');" for m in sorted(modules))
# If init is not async, we need to make it async
full_match = match.group(1)
if "async" not in full_match:
# Make init async
new_init = full_match.replace(f"{method_name}()", f"async {method_name}()")
content = content.replace(full_match, new_init)
full_match = new_init
# Add loading after the opening brace
insert_code = f"\n // Load i18n translations\n{load_calls}\n"
content = content.replace(full_match, full_match + insert_code)
js_file.write_text(content, encoding="utf-8")
return True
# If no init found, add at file level (for simpler scripts)
# This handles files that don't use Alpine components
if "function " in content or "const " in content:
# Find first function definition
func_match = re.search(r"^(function\s+\w+\s*\([^)]*\)\s*\{)", content, re.MULTILINE)
if func_match:
load_calls = "\n".join(f" await I18n.loadModule('{m}');" for m in sorted(modules))
# Make function async if needed
full_match = func_match.group(1)
if "async" not in full_match:
new_func = full_match.replace("function ", "async function ")
content = content.replace(full_match, new_func)
full_match = new_func
insert_code = f"\n // Load i18n translations\n{load_calls}\n"
content = content.replace(full_match, full_match + insert_code)
js_file.write_text(content, encoding="utf-8")
return True
return False
def main():
print("=" * 70)
print("Adding I18n.loadModule() to JS files")
print("=" * 70)
updated = 0
skipped = 0
# Find all JS files with I18n.t() calls
for js_file in MODULES_DIR.rglob("*.js"):
content = js_file.read_text(encoding="utf-8")
if "I18n.t(" not in content:
continue
modules = find_modules_used(content)
if not modules:
continue
rel_path = js_file.relative_to(PROJECT_ROOT)
if add_module_loading(js_file):
print(f" Updated: {rel_path} (modules: {', '.join(sorted(modules))})")
updated += 1
else:
print(f" Skipped: {rel_path} (already has loading or no init method)")
skipped += 1
print()
print(f"Updated: {updated} files")
print(f"Skipped: {skipped} files")
print("=" * 70)
if __name__ == "__main__":
main()