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>
197 lines
6.5 KiB
Python
197 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Migrate hardcoded toast messages in JS files to use I18n.t().
|
|
Extracts messages, creates translation keys, and updates both JS and locale files.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from collections import defaultdict
|
|
|
|
PROJECT_ROOT = Path(__file__).parent.parent
|
|
MODULES_DIR = PROJECT_ROOT / "app" / "modules"
|
|
|
|
# Languages to update
|
|
LANGUAGES = ["en", "fr", "de", "lb"]
|
|
|
|
# Pattern to match Utils.showToast('message', 'type') or Utils.showToast("message", "type")
|
|
TOAST_PATTERN = re.compile(r"Utils\.showToast\(['\"]([^'\"]+)['\"],\s*['\"](\w+)['\"]\)")
|
|
|
|
# Map module names to their message namespace
|
|
MODULE_MESSAGE_NS = {
|
|
"catalog": "catalog",
|
|
"orders": "orders",
|
|
"customers": "customers",
|
|
"inventory": "inventory",
|
|
"marketplace": "marketplace",
|
|
"tenancy": "tenancy",
|
|
"core": "core",
|
|
"messaging": "messaging",
|
|
"billing": "billing",
|
|
"cms": "cms",
|
|
"checkout": "checkout",
|
|
"cart": "cart",
|
|
"dev_tools": "dev_tools",
|
|
"monitoring": "monitoring",
|
|
"analytics": "analytics",
|
|
}
|
|
|
|
|
|
def message_to_key(message: str) -> str:
|
|
"""Convert a message string to a translation key."""
|
|
# Remove special characters and convert to snake_case
|
|
key = message.lower()
|
|
key = re.sub(r'[^\w\s]', '', key)
|
|
key = re.sub(r'\s+', '_', key)
|
|
# Truncate if too long
|
|
if len(key) > 40:
|
|
key = key[:40].rstrip('_')
|
|
return key
|
|
|
|
|
|
def find_js_files_with_toasts() -> dict[str, list[Path]]:
|
|
"""Find all JS files with toast messages, grouped by module."""
|
|
files_by_module = defaultdict(list)
|
|
|
|
for module_dir in sorted(MODULES_DIR.iterdir()):
|
|
if not module_dir.is_dir():
|
|
continue
|
|
module_name = module_dir.name
|
|
|
|
# Find all JS files in this module
|
|
for js_file in module_dir.rglob("*.js"):
|
|
content = js_file.read_text(encoding="utf-8")
|
|
if "Utils.showToast(" in content:
|
|
files_by_module[module_name].append(js_file)
|
|
|
|
return dict(files_by_module)
|
|
|
|
|
|
def extract_messages_from_file(js_file: Path) -> list[tuple[str, str]]:
|
|
"""Extract all toast messages from a JS file."""
|
|
content = js_file.read_text(encoding="utf-8")
|
|
return TOAST_PATTERN.findall(content)
|
|
|
|
|
|
def load_locale_file(module_path: Path, lang: str) -> dict:
|
|
"""Load a module's locale file."""
|
|
locale_file = module_path / "locales" / f"{lang}.json"
|
|
if locale_file.exists():
|
|
with open(locale_file, encoding="utf-8") as f:
|
|
return json.load(f)
|
|
return {}
|
|
|
|
|
|
def save_locale_file(module_path: Path, lang: str, data: dict):
|
|
"""Save a module's locale file."""
|
|
locale_file = module_path / "locales" / f"{lang}.json"
|
|
locale_file.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(locale_file, "w", encoding="utf-8") as f:
|
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
f.write("\n")
|
|
|
|
|
|
def update_js_file(js_file: Path, module_name: str, message_keys: dict[str, str]):
|
|
"""Update a JS file to use I18n.t() for toast messages."""
|
|
content = js_file.read_text(encoding="utf-8")
|
|
original = content
|
|
|
|
for message, key in message_keys.items():
|
|
# Replace both single and double quoted versions
|
|
full_key = f"{module_name}.messages.{key}"
|
|
|
|
# Pattern to match the exact message in Utils.showToast
|
|
for quote in ["'", '"']:
|
|
old = f"Utils.showToast({quote}{message}{quote},"
|
|
new = f"Utils.showToast(I18n.t('{full_key}'),"
|
|
content = content.replace(old, new)
|
|
|
|
if content != original:
|
|
js_file.write_text(content, encoding="utf-8")
|
|
return True
|
|
return False
|
|
|
|
|
|
def process_module(module_name: str, js_files: list[Path]) -> dict[str, str]:
|
|
"""Process all JS files for a module and return message->key mapping."""
|
|
all_messages = {}
|
|
|
|
# Extract all unique messages
|
|
for js_file in js_files:
|
|
messages = extract_messages_from_file(js_file)
|
|
for message, msg_type in messages:
|
|
if message not in all_messages:
|
|
all_messages[message] = message_to_key(message)
|
|
|
|
return all_messages
|
|
|
|
|
|
def main():
|
|
print("=" * 70)
|
|
print("JS i18n Migration Script")
|
|
print("=" * 70)
|
|
|
|
# Find all JS files with toasts
|
|
files_by_module = find_js_files_with_toasts()
|
|
|
|
total_files = sum(len(files) for files in files_by_module.values())
|
|
print(f"Found {total_files} JS files with toast messages across {len(files_by_module)} modules")
|
|
print()
|
|
|
|
for module_name, js_files in sorted(files_by_module.items()):
|
|
print(f"\n{'='*70}")
|
|
print(f"Module: {module_name} ({len(js_files)} files)")
|
|
print("=" * 70)
|
|
|
|
module_path = MODULES_DIR / module_name
|
|
|
|
# Process all JS files and get message mappings
|
|
message_keys = process_module(module_name, js_files)
|
|
|
|
if not message_keys:
|
|
print(" No messages found")
|
|
continue
|
|
|
|
print(f" Found {len(message_keys)} unique messages:")
|
|
for msg, key in sorted(message_keys.items(), key=lambda x: x[1]):
|
|
print(f" {key}: {msg[:50]}{'...' if len(msg) > 50 else ''}")
|
|
|
|
# Update locale files for all languages
|
|
print(f"\n Updating locale files...")
|
|
for lang in LANGUAGES:
|
|
locale_data = load_locale_file(module_path, lang)
|
|
|
|
# Add messages section if not exists
|
|
if "messages" not in locale_data:
|
|
locale_data["messages"] = {}
|
|
|
|
# Add each message (only add if not already present)
|
|
for message, key in message_keys.items():
|
|
if key not in locale_data["messages"]:
|
|
# For English, use the original message
|
|
# For other languages, we'll use the English as placeholder
|
|
locale_data["messages"][key] = message
|
|
|
|
save_locale_file(module_path, lang, locale_data)
|
|
print(f" Updated: {lang}.json")
|
|
|
|
# Update JS files
|
|
print(f"\n Updating JS files...")
|
|
for js_file in js_files:
|
|
if update_js_file(js_file, module_name, message_keys):
|
|
rel_path = js_file.relative_to(PROJECT_ROOT)
|
|
print(f" Updated: {rel_path}")
|
|
|
|
print("\n" + "=" * 70)
|
|
print("Migration complete!")
|
|
print("\nNext steps:")
|
|
print("1. Review the generated message keys in locale files")
|
|
print("2. Translate non-English messages (currently using English as placeholder)")
|
|
print("3. Test the application to verify toast messages work")
|
|
print("=" * 70)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|