feat: add module-specific locale support for i18n

Enhance the self-contained module architecture with locale/translation support:

ModuleDefinition changes:
- Add locales_path attribute for module-specific translations
- Add get_locales_dir() helper method
- Include locales in validate_structure() check

i18n module changes (app/utils/i18n.py):
- Add get_module_locale_dirs() to discover module locales
- Update load_translations() to merge module translations with core
- Module translations namespaced under module code (e.g., cms.title)
- Add _deep_merge() helper for nested dictionary merging
- Add _load_json_file() helper for cleaner JSON loading

CMS module locales:
- Add app/modules/cms/locales/ with translations for all 4 languages
- en.json, fr.json, de.json, lb.json with CMS-specific strings
- Covers: pages, page editing, SEO, navigation, publishing, homepage
  sections, media library, themes, actions, and messages

Usage in templates:
  {{ _("cms.title") }}           -> "Content Management" (en)
  {{ _("cms.pages.create") }}    -> "Créer une page" (fr)
  {{ _("cms.publishing.draft") }} -> "Entwurf" (de)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 21:44:28 +01:00
parent 2ce19e66b1
commit 8ff9c39845
7 changed files with 628 additions and 18 deletions

View File

@@ -61,6 +61,7 @@ class ModuleDefinition:
schemas_path: Path to schemas subpackage (e.g., "app.modules.billing.schemas")
templates_path: Path to templates directory (relative to module)
exceptions_path: Path to exceptions module (e.g., "app.modules.billing.exceptions")
locales_path: Path to locales directory (relative to module, e.g., "locales")
is_self_contained: Whether module uses self-contained structure
Example (traditional thin wrapper):
@@ -91,6 +92,7 @@ class ModuleDefinition:
schemas_path="app.modules.cms.schemas",
templates_path="templates",
exceptions_path="app.modules.cms.exceptions",
locales_path="locales",
)
"""
@@ -120,6 +122,7 @@ class ModuleDefinition:
schemas_path: str | None = None
templates_path: str | None = None # Relative to module directory
exceptions_path: str | None = None
locales_path: str | None = None # Relative to module directory, e.g., "locales"
def get_menu_items(self, frontend_type: FrontendType) -> list[str]:
"""Get menu item IDs for a specific frontend type."""
@@ -176,6 +179,17 @@ class ModuleDefinition:
return None
return self.get_module_dir() / self.templates_path
def get_locales_dir(self) -> Path | None:
"""
Get the filesystem path to this module's locales directory.
Returns:
Path to locales directory, or None if not configured
"""
if not self.is_self_contained or not self.locales_path:
return None
return self.get_module_dir() / self.locales_path
def get_import_path(self, component: str) -> str | None:
"""
Get the Python import path for a module component.
@@ -221,6 +235,8 @@ class ModuleDefinition:
expected_dirs.append("schemas")
if self.templates_path:
expected_dirs.append(self.templates_path)
if self.locales_path:
expected_dirs.append(self.locales_path)
for dir_name in expected_dirs:
dir_path = module_dir / dir_name