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:
@@ -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
|
||||
|
||||
@@ -63,6 +63,8 @@ cms_module = ModuleDefinition(
|
||||
exceptions_path="app.modules.cms.exceptions",
|
||||
# Templates remain in core for now (admin/content-pages*.html)
|
||||
templates_path=None,
|
||||
# Module-specific translations (accessible via cms.* keys)
|
||||
locales_path="locales",
|
||||
)
|
||||
|
||||
|
||||
|
||||
126
app/modules/cms/locales/de.json
Normal file
126
app/modules/cms/locales/de.json
Normal file
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"title": "Content-Verwaltung",
|
||||
"description": "Verwalten Sie Inhaltsseiten, Medienbibliothek und Händler-Themes",
|
||||
"pages": {
|
||||
"title": "Inhaltsseiten",
|
||||
"subtitle": "Verwalten Sie Plattform- und Händler-Inhaltsseiten",
|
||||
"create": "Seite erstellen",
|
||||
"edit": "Seite bearbeiten",
|
||||
"delete": "Seite löschen",
|
||||
"list": "Alle Seiten",
|
||||
"empty": "Keine Seiten gefunden",
|
||||
"empty_search": "Keine Seiten entsprechen Ihrer Suche",
|
||||
"create_first": "Erste Seite erstellen"
|
||||
},
|
||||
"page": {
|
||||
"title": "Seitentitel",
|
||||
"slug": "Slug",
|
||||
"slug_help": "URL-sichere Kennung (Kleinbuchstaben, Zahlen, Bindestriche)",
|
||||
"content": "Inhalt",
|
||||
"content_format": "Inhaltsformat",
|
||||
"format_html": "HTML",
|
||||
"format_markdown": "Markdown",
|
||||
"platform": "Plattform",
|
||||
"vendor_override": "Händler-Überschreibung",
|
||||
"vendor_override_none": "Keine (Plattform-Standard)",
|
||||
"vendor_override_help_default": "Dies ist eine plattformweite Standardseite",
|
||||
"vendor_override_help_vendor": "Diese Seite überschreibt den Standard nur für den ausgewählten Händler"
|
||||
},
|
||||
"tiers": {
|
||||
"platform": "Plattform-Marketing",
|
||||
"vendor_default": "Händler-Standard",
|
||||
"vendor_override": "Händler-Überschreibung"
|
||||
},
|
||||
"seo": {
|
||||
"title": "SEO & Metadaten",
|
||||
"meta_description": "Meta-Beschreibung",
|
||||
"meta_description_help": "Zeichen (150-160 empfohlen)",
|
||||
"meta_keywords": "Meta-Schlüsselwörter",
|
||||
"meta_keywords_placeholder": "schlüsselwort1, schlüsselwort2, schlüsselwort3"
|
||||
},
|
||||
"navigation": {
|
||||
"title": "Navigation & Anzeige",
|
||||
"display_order": "Anzeigereihenfolge",
|
||||
"display_order_help": "Niedriger = zuerst",
|
||||
"show_in_header": "Im Header anzeigen",
|
||||
"show_in_footer": "Im Footer anzeigen",
|
||||
"show_in_legal": "Im Rechtsbereich anzeigen",
|
||||
"show_in_legal_help": "Untere Leiste neben dem Copyright"
|
||||
},
|
||||
"publishing": {
|
||||
"published": "Veröffentlicht",
|
||||
"draft": "Entwurf",
|
||||
"publish_help": "Diese Seite öffentlich sichtbar machen"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Startseiten-Abschnitte",
|
||||
"subtitle": "Mehrsprachiger Inhalt",
|
||||
"loading": "Abschnitte werden geladen...",
|
||||
"hero": {
|
||||
"title": "Hero-Abschnitt",
|
||||
"badge_text": "Badge-Text",
|
||||
"main_title": "Titel",
|
||||
"subtitle": "Untertitel",
|
||||
"buttons": "Schaltflächen",
|
||||
"add_button": "Schaltfläche hinzufügen"
|
||||
},
|
||||
"features": {
|
||||
"title": "Funktionen-Abschnitt",
|
||||
"section_title": "Abschnittstitel",
|
||||
"cards": "Funktionskarten",
|
||||
"add_card": "Karte hinzufügen",
|
||||
"icon": "Icon-Name",
|
||||
"feature_title": "Titel",
|
||||
"feature_description": "Beschreibung"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Preise-Abschnitt",
|
||||
"section_title": "Abschnittstitel",
|
||||
"use_tiers": "Abonnement-Stufen aus der Datenbank verwenden",
|
||||
"use_tiers_help": "Wenn aktiviert, werden Preiskarten dynamisch aus Ihrer Abonnement-Stufenkonfiguration abgerufen."
|
||||
},
|
||||
"cta": {
|
||||
"title": "Call-to-Action-Abschnitt",
|
||||
"main_title": "Titel",
|
||||
"subtitle": "Untertitel",
|
||||
"buttons": "Schaltflächen",
|
||||
"add_button": "Schaltfläche hinzufügen"
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"title": "Medienbibliothek",
|
||||
"upload": "Hochladen",
|
||||
"upload_file": "Datei hochladen",
|
||||
"delete": "Löschen",
|
||||
"empty": "Keine Mediendateien",
|
||||
"upload_first": "Laden Sie Ihre erste Datei hoch"
|
||||
},
|
||||
"themes": {
|
||||
"title": "Händler-Themes",
|
||||
"subtitle": "Verwalten Sie Händler-Theme-Anpassungen"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Speichern",
|
||||
"saving": "Speichern...",
|
||||
"update": "Seite aktualisieren",
|
||||
"create": "Seite erstellen",
|
||||
"cancel": "Abbrechen",
|
||||
"back_to_list": "Zurück zur Liste",
|
||||
"preview": "Vorschau",
|
||||
"revert_to_default": "Auf Standard zurücksetzen"
|
||||
},
|
||||
"messages": {
|
||||
"created": "Seite erfolgreich erstellt",
|
||||
"updated": "Seite erfolgreich aktualisiert",
|
||||
"deleted": "Seite erfolgreich gelöscht",
|
||||
"reverted": "Auf Standardseite zurückgesetzt",
|
||||
"error_loading": "Fehler beim Laden der Seite",
|
||||
"error_saving": "Fehler beim Speichern der Seite",
|
||||
"confirm_delete": "Sind Sie sicher, dass Sie diese Seite löschen möchten?"
|
||||
},
|
||||
"filters": {
|
||||
"all_pages": "Alle Seiten",
|
||||
"all_platforms": "Alle Plattformen",
|
||||
"search_placeholder": "Seiten suchen..."
|
||||
}
|
||||
}
|
||||
126
app/modules/cms/locales/en.json
Normal file
126
app/modules/cms/locales/en.json
Normal file
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"title": "Content Management",
|
||||
"description": "Manage content pages, media library, and vendor themes",
|
||||
"pages": {
|
||||
"title": "Content Pages",
|
||||
"subtitle": "Manage platform and vendor content pages",
|
||||
"create": "Create Page",
|
||||
"edit": "Edit Page",
|
||||
"delete": "Delete Page",
|
||||
"list": "All Pages",
|
||||
"empty": "No pages found",
|
||||
"empty_search": "No pages match your search",
|
||||
"create_first": "Create First Page"
|
||||
},
|
||||
"page": {
|
||||
"title": "Page Title",
|
||||
"slug": "Slug",
|
||||
"slug_help": "URL-safe identifier (lowercase, numbers, hyphens only)",
|
||||
"content": "Content",
|
||||
"content_format": "Content Format",
|
||||
"format_html": "HTML",
|
||||
"format_markdown": "Markdown",
|
||||
"platform": "Platform",
|
||||
"vendor_override": "Vendor Override",
|
||||
"vendor_override_none": "None (Platform Default)",
|
||||
"vendor_override_help_default": "This is a platform-wide default page",
|
||||
"vendor_override_help_vendor": "This page overrides the default for selected vendor only"
|
||||
},
|
||||
"tiers": {
|
||||
"platform": "Platform Marketing",
|
||||
"vendor_default": "Vendor Default",
|
||||
"vendor_override": "Vendor Override"
|
||||
},
|
||||
"seo": {
|
||||
"title": "SEO & Metadata",
|
||||
"meta_description": "Meta Description",
|
||||
"meta_description_help": "characters (150-160 recommended)",
|
||||
"meta_keywords": "Meta Keywords",
|
||||
"meta_keywords_placeholder": "keyword1, keyword2, keyword3"
|
||||
},
|
||||
"navigation": {
|
||||
"title": "Navigation & Display",
|
||||
"display_order": "Display Order",
|
||||
"display_order_help": "Lower = first",
|
||||
"show_in_header": "Show in Header",
|
||||
"show_in_footer": "Show in Footer",
|
||||
"show_in_legal": "Show in Legal",
|
||||
"show_in_legal_help": "Bottom bar next to copyright"
|
||||
},
|
||||
"publishing": {
|
||||
"published": "Published",
|
||||
"draft": "Draft",
|
||||
"publish_help": "Make this page visible to the public"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Homepage Sections",
|
||||
"subtitle": "Multi-language content",
|
||||
"loading": "Loading sections...",
|
||||
"hero": {
|
||||
"title": "Hero Section",
|
||||
"badge_text": "Badge Text",
|
||||
"main_title": "Title",
|
||||
"subtitle": "Subtitle",
|
||||
"buttons": "Buttons",
|
||||
"add_button": "Add Button"
|
||||
},
|
||||
"features": {
|
||||
"title": "Features Section",
|
||||
"section_title": "Section Title",
|
||||
"cards": "Feature Cards",
|
||||
"add_card": "Add Feature Card",
|
||||
"icon": "Icon name",
|
||||
"feature_title": "Title",
|
||||
"feature_description": "Description"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Pricing Section",
|
||||
"section_title": "Section Title",
|
||||
"use_tiers": "Use subscription tiers from database",
|
||||
"use_tiers_help": "When enabled, pricing cards are dynamically pulled from your subscription tier configuration."
|
||||
},
|
||||
"cta": {
|
||||
"title": "Call to Action Section",
|
||||
"main_title": "Title",
|
||||
"subtitle": "Subtitle",
|
||||
"buttons": "Buttons",
|
||||
"add_button": "Add Button"
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"title": "Media Library",
|
||||
"upload": "Upload",
|
||||
"upload_file": "Upload File",
|
||||
"delete": "Delete",
|
||||
"empty": "No media files",
|
||||
"upload_first": "Upload your first file"
|
||||
},
|
||||
"themes": {
|
||||
"title": "Vendor Themes",
|
||||
"subtitle": "Manage vendor theme customizations"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"update": "Update Page",
|
||||
"create": "Create Page",
|
||||
"cancel": "Cancel",
|
||||
"back_to_list": "Back to List",
|
||||
"preview": "Preview",
|
||||
"revert_to_default": "Revert to Default"
|
||||
},
|
||||
"messages": {
|
||||
"created": "Page created successfully",
|
||||
"updated": "Page updated successfully",
|
||||
"deleted": "Page deleted successfully",
|
||||
"reverted": "Reverted to default page",
|
||||
"error_loading": "Error loading page",
|
||||
"error_saving": "Error saving page",
|
||||
"confirm_delete": "Are you sure you want to delete this page?"
|
||||
},
|
||||
"filters": {
|
||||
"all_pages": "All Pages",
|
||||
"all_platforms": "All Platforms",
|
||||
"search_placeholder": "Search pages..."
|
||||
}
|
||||
}
|
||||
126
app/modules/cms/locales/fr.json
Normal file
126
app/modules/cms/locales/fr.json
Normal file
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"title": "Gestion de contenu",
|
||||
"description": "Gestion des pages de contenu, de la bibliothèque de médias et des thèmes",
|
||||
"pages": {
|
||||
"title": "Pages de contenu",
|
||||
"subtitle": "Gérez les pages de contenu de la plateforme et des vendeurs",
|
||||
"create": "Créer une page",
|
||||
"edit": "Modifier la page",
|
||||
"delete": "Supprimer la page",
|
||||
"list": "Toutes les pages",
|
||||
"empty": "Aucune page trouvée",
|
||||
"empty_search": "Aucune page ne correspond à votre recherche",
|
||||
"create_first": "Créer la première page"
|
||||
},
|
||||
"page": {
|
||||
"title": "Titre de la page",
|
||||
"slug": "Slug",
|
||||
"slug_help": "Identifiant URL (minuscules, chiffres, tirets uniquement)",
|
||||
"content": "Contenu",
|
||||
"content_format": "Format du contenu",
|
||||
"format_html": "HTML",
|
||||
"format_markdown": "Markdown",
|
||||
"platform": "Plateforme",
|
||||
"vendor_override": "Remplacement vendeur",
|
||||
"vendor_override_none": "Aucun (page par défaut)",
|
||||
"vendor_override_help_default": "Ceci est une page par défaut pour toute la plateforme",
|
||||
"vendor_override_help_vendor": "Cette page remplace la page par défaut pour le vendeur sélectionné"
|
||||
},
|
||||
"tiers": {
|
||||
"platform": "Marketing plateforme",
|
||||
"vendor_default": "Défaut vendeur",
|
||||
"vendor_override": "Remplacement vendeur"
|
||||
},
|
||||
"seo": {
|
||||
"title": "SEO & Métadonnées",
|
||||
"meta_description": "Meta Description",
|
||||
"meta_description_help": "caractères (150-160 recommandés)",
|
||||
"meta_keywords": "Mots-clés",
|
||||
"meta_keywords_placeholder": "mot-clé1, mot-clé2, mot-clé3"
|
||||
},
|
||||
"navigation": {
|
||||
"title": "Navigation & Affichage",
|
||||
"display_order": "Ordre d'affichage",
|
||||
"display_order_help": "Plus bas = premier",
|
||||
"show_in_header": "Afficher dans l'en-tête",
|
||||
"show_in_footer": "Afficher dans le pied de page",
|
||||
"show_in_legal": "Afficher dans les mentions légales",
|
||||
"show_in_legal_help": "Barre en bas à côté du copyright"
|
||||
},
|
||||
"publishing": {
|
||||
"published": "Publié",
|
||||
"draft": "Brouillon",
|
||||
"publish_help": "Rendre cette page visible au public"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Sections de la page d'accueil",
|
||||
"subtitle": "Contenu multilingue",
|
||||
"loading": "Chargement des sections...",
|
||||
"hero": {
|
||||
"title": "Section Hero",
|
||||
"badge_text": "Texte du badge",
|
||||
"main_title": "Titre",
|
||||
"subtitle": "Sous-titre",
|
||||
"buttons": "Boutons",
|
||||
"add_button": "Ajouter un bouton"
|
||||
},
|
||||
"features": {
|
||||
"title": "Section Fonctionnalités",
|
||||
"section_title": "Titre de la section",
|
||||
"cards": "Cartes de fonctionnalités",
|
||||
"add_card": "Ajouter une carte",
|
||||
"icon": "Nom de l'icône",
|
||||
"feature_title": "Titre",
|
||||
"feature_description": "Description"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Section Tarifs",
|
||||
"section_title": "Titre de la section",
|
||||
"use_tiers": "Utiliser les niveaux d'abonnement de la base de données",
|
||||
"use_tiers_help": "Si activé, les cartes de tarifs sont extraites dynamiquement de la configuration des niveaux d'abonnement."
|
||||
},
|
||||
"cta": {
|
||||
"title": "Section Appel à l'action",
|
||||
"main_title": "Titre",
|
||||
"subtitle": "Sous-titre",
|
||||
"buttons": "Boutons",
|
||||
"add_button": "Ajouter un bouton"
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"title": "Bibliothèque de médias",
|
||||
"upload": "Télécharger",
|
||||
"upload_file": "Télécharger un fichier",
|
||||
"delete": "Supprimer",
|
||||
"empty": "Aucun fichier média",
|
||||
"upload_first": "Téléchargez votre premier fichier"
|
||||
},
|
||||
"themes": {
|
||||
"title": "Thèmes vendeurs",
|
||||
"subtitle": "Gérez les personnalisations de thèmes des vendeurs"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Enregistrer",
|
||||
"saving": "Enregistrement...",
|
||||
"update": "Mettre à jour la page",
|
||||
"create": "Créer la page",
|
||||
"cancel": "Annuler",
|
||||
"back_to_list": "Retour à la liste",
|
||||
"preview": "Aperçu",
|
||||
"revert_to_default": "Revenir à la valeur par défaut"
|
||||
},
|
||||
"messages": {
|
||||
"created": "Page créée avec succès",
|
||||
"updated": "Page mise à jour avec succès",
|
||||
"deleted": "Page supprimée avec succès",
|
||||
"reverted": "Retour à la page par défaut",
|
||||
"error_loading": "Erreur lors du chargement de la page",
|
||||
"error_saving": "Erreur lors de l'enregistrement de la page",
|
||||
"confirm_delete": "Êtes-vous sûr de vouloir supprimer cette page ?"
|
||||
},
|
||||
"filters": {
|
||||
"all_pages": "Toutes les pages",
|
||||
"all_platforms": "Toutes les plateformes",
|
||||
"search_placeholder": "Rechercher des pages..."
|
||||
}
|
||||
}
|
||||
126
app/modules/cms/locales/lb.json
Normal file
126
app/modules/cms/locales/lb.json
Normal file
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"title": "Inhalts-Verwaltung",
|
||||
"description": "Verwaltet Inhaltsäiten, Mediebibliothéik an Händler-Themen",
|
||||
"pages": {
|
||||
"title": "Inhaltsäiten",
|
||||
"subtitle": "Verwaltet Plattform- an Händler-Inhaltsäiten",
|
||||
"create": "Säit erstellen",
|
||||
"edit": "Säit änneren",
|
||||
"delete": "Säit läschen",
|
||||
"list": "All Säiten",
|
||||
"empty": "Keng Säite fonnt",
|
||||
"empty_search": "Keng Säite passen op Är Sich",
|
||||
"create_first": "Éischt Säit erstellen"
|
||||
},
|
||||
"page": {
|
||||
"title": "Säitentitel",
|
||||
"slug": "Slug",
|
||||
"slug_help": "URL-sécher Kennung (Klengbuschtawen, Zuelen, Bindestricher)",
|
||||
"content": "Inhalt",
|
||||
"content_format": "Inhaltsformat",
|
||||
"format_html": "HTML",
|
||||
"format_markdown": "Markdown",
|
||||
"platform": "Plattform",
|
||||
"vendor_override": "Händler-Iwwerschreiwung",
|
||||
"vendor_override_none": "Keng (Plattform-Standard)",
|
||||
"vendor_override_help_default": "Dëst ass eng plattformwäit Standardsäit",
|
||||
"vendor_override_help_vendor": "Dës Säit iwwerschreift de Standard nëmme fir de gewielte Händler"
|
||||
},
|
||||
"tiers": {
|
||||
"platform": "Plattform-Marketing",
|
||||
"vendor_default": "Händler-Standard",
|
||||
"vendor_override": "Händler-Iwwerschreiwung"
|
||||
},
|
||||
"seo": {
|
||||
"title": "SEO & Metadaten",
|
||||
"meta_description": "Meta-Beschreiwung",
|
||||
"meta_description_help": "Zeechen (150-160 recommandéiert)",
|
||||
"meta_keywords": "Meta-Schlësselwierder",
|
||||
"meta_keywords_placeholder": "schlësselwuert1, schlësselwuert2, schlësselwuert3"
|
||||
},
|
||||
"navigation": {
|
||||
"title": "Navigatioun & Affichage",
|
||||
"display_order": "Uweisungsreiefolleg",
|
||||
"display_order_help": "Méi niddreg = éischt",
|
||||
"show_in_header": "Am Header weisen",
|
||||
"show_in_footer": "Am Footer weisen",
|
||||
"show_in_legal": "Am Rechtsberäich weisen",
|
||||
"show_in_legal_help": "Ënnescht Leist nieft dem Copyright"
|
||||
},
|
||||
"publishing": {
|
||||
"published": "Verëffentlecht",
|
||||
"draft": "Entworf",
|
||||
"publish_help": "Dës Säit ëffentlech siichtbar maachen"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Haaptsäit-Sektiounen",
|
||||
"subtitle": "Méisproochegen Inhalt",
|
||||
"loading": "Sektiounen ginn gelueden...",
|
||||
"hero": {
|
||||
"title": "Hero-Sektioun",
|
||||
"badge_text": "Badge-Text",
|
||||
"main_title": "Titel",
|
||||
"subtitle": "Ënnertitel",
|
||||
"buttons": "Knäpp",
|
||||
"add_button": "Knapp derbäisetzen"
|
||||
},
|
||||
"features": {
|
||||
"title": "Funktiounen-Sektioun",
|
||||
"section_title": "Sektiounstitel",
|
||||
"cards": "Funktiounskaarten",
|
||||
"add_card": "Kaart derbäisetzen",
|
||||
"icon": "Icon-Numm",
|
||||
"feature_title": "Titel",
|
||||
"feature_description": "Beschreiwung"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Präisser-Sektioun",
|
||||
"section_title": "Sektiounstitel",
|
||||
"use_tiers": "Abonnement-Stufen aus der Datebank benotzen",
|
||||
"use_tiers_help": "Wann aktivéiert, ginn d'Präiskaarten dynamesch aus Ärer Abonnement-Stufekonfiguratioun ofgeruff."
|
||||
},
|
||||
"cta": {
|
||||
"title": "Call-to-Action-Sektioun",
|
||||
"main_title": "Titel",
|
||||
"subtitle": "Ënnertitel",
|
||||
"buttons": "Knäpp",
|
||||
"add_button": "Knapp derbäisetzen"
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"title": "Mediebibliothéik",
|
||||
"upload": "Eroplueden",
|
||||
"upload_file": "Fichier eroplueden",
|
||||
"delete": "Läschen",
|
||||
"empty": "Keng Mediefichieren",
|
||||
"upload_first": "Luet Äre éischte Fichier erop"
|
||||
},
|
||||
"themes": {
|
||||
"title": "Händler-Themen",
|
||||
"subtitle": "Verwaltet Händler-Theme-Personnalisatiounen"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Späicheren",
|
||||
"saving": "Späicheren...",
|
||||
"update": "Säit aktualiséieren",
|
||||
"create": "Säit erstellen",
|
||||
"cancel": "Ofbriechen",
|
||||
"back_to_list": "Zréck op d'Lëscht",
|
||||
"preview": "Virschau",
|
||||
"revert_to_default": "Op Standard zrécksetzen"
|
||||
},
|
||||
"messages": {
|
||||
"created": "Säit erfollegräich erstallt",
|
||||
"updated": "Säit erfollegräich aktualiséiert",
|
||||
"deleted": "Säit erfollegräich geläscht",
|
||||
"reverted": "Op Standardsäit zréckgesat",
|
||||
"error_loading": "Feeler beim Lueden vun der Säit",
|
||||
"error_saving": "Feeler beim Späichere vun der Säit",
|
||||
"confirm_delete": "Sidd Dir sécher, datt Dir dës Säit läsche wëllt?"
|
||||
},
|
||||
"filters": {
|
||||
"all_pages": "All Säiten",
|
||||
"all_platforms": "All Plattformen",
|
||||
"search_placeholder": "Säite sichen..."
|
||||
}
|
||||
}
|
||||
@@ -61,57 +61,145 @@ LANGUAGE_FLAGS = {
|
||||
# Translation Storage
|
||||
# ============================================================================
|
||||
|
||||
# Path to locale files
|
||||
# Path to core locale files
|
||||
LOCALES_PATH = Path(__file__).parent.parent.parent / "static" / "locales"
|
||||
|
||||
# Path to modules directory (for module-specific locales)
|
||||
MODULES_PATH = Path(__file__).parent.parent / "modules"
|
||||
|
||||
# In-memory cache for loaded translations
|
||||
_translations: dict[str, dict] = {}
|
||||
|
||||
# Cache for module locale directories
|
||||
_module_locale_dirs: list[tuple[str, Path]] | None = None
|
||||
|
||||
|
||||
def get_locales_path() -> Path:
|
||||
"""Get the path to locale files."""
|
||||
"""Get the path to core locale files."""
|
||||
return LOCALES_PATH
|
||||
|
||||
|
||||
def get_module_locale_dirs() -> list[tuple[str, Path]]:
|
||||
"""
|
||||
Discover module locale directories.
|
||||
|
||||
Returns:
|
||||
List of (module_code, locales_path) tuples for modules with locales
|
||||
"""
|
||||
global _module_locale_dirs
|
||||
|
||||
if _module_locale_dirs is not None:
|
||||
return _module_locale_dirs
|
||||
|
||||
_module_locale_dirs = []
|
||||
|
||||
if not MODULES_PATH.exists():
|
||||
return _module_locale_dirs
|
||||
|
||||
for module_dir in sorted(MODULES_PATH.iterdir()):
|
||||
if module_dir.is_dir():
|
||||
locales_dir = module_dir / "locales"
|
||||
if locales_dir.exists() and locales_dir.is_dir():
|
||||
module_code = module_dir.name
|
||||
_module_locale_dirs.append((module_code, locales_dir))
|
||||
logger.debug(f"[i18n] Found module locales: {module_code} at {locales_dir}")
|
||||
|
||||
return _module_locale_dirs
|
||||
|
||||
|
||||
def _load_json_file(file_path: Path) -> dict:
|
||||
"""Load a JSON file and return its contents."""
|
||||
try:
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Invalid JSON in translation file {file_path}: {e}")
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading translation file {file_path}: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def _deep_merge(base: dict, override: dict) -> dict:
|
||||
"""
|
||||
Deep merge two dictionaries. Override values take precedence.
|
||||
|
||||
Args:
|
||||
base: Base dictionary
|
||||
override: Dictionary with values to merge/override
|
||||
|
||||
Returns:
|
||||
Merged dictionary
|
||||
"""
|
||||
result = base.copy()
|
||||
|
||||
for key, value in override.items():
|
||||
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
||||
result[key] = _deep_merge(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@lru_cache(maxsize=10)
|
||||
def load_translations(language: str) -> dict:
|
||||
"""
|
||||
Load translations for a specific language from JSON file.
|
||||
Load translations for a specific language from JSON files.
|
||||
|
||||
Loads core translations first, then merges module-specific translations.
|
||||
Module translations are namespaced under their module code.
|
||||
|
||||
Args:
|
||||
language: Language code (en, fr, de, lb)
|
||||
|
||||
Returns:
|
||||
Dictionary of translations, empty dict if file not found
|
||||
|
||||
Example:
|
||||
Core: {"common": {"save": "Save"}}
|
||||
CMS module: {"pages": {"title": "Page Title"}}
|
||||
Result: {"common": {"save": "Save"}, "cms": {"pages": {"title": "Page Title"}}}
|
||||
"""
|
||||
if language not in SUPPORTED_LANGUAGES:
|
||||
logger.warning(f"Unsupported language requested: {language}")
|
||||
language = DEFAULT_LANGUAGE
|
||||
|
||||
locale_file = LOCALES_PATH / f"{language}.json"
|
||||
|
||||
if not locale_file.exists():
|
||||
logger.warning(f"Translation file not found: {locale_file}")
|
||||
# Fall back to default language
|
||||
# Load core translations
|
||||
core_file = LOCALES_PATH / f"{language}.json"
|
||||
if core_file.exists():
|
||||
translations = _load_json_file(core_file)
|
||||
else:
|
||||
logger.warning(f"Core translation file not found: {core_file}")
|
||||
if language != DEFAULT_LANGUAGE:
|
||||
return load_translations(DEFAULT_LANGUAGE)
|
||||
return {}
|
||||
translations = {}
|
||||
|
||||
try:
|
||||
with open(locale_file, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Invalid JSON in translation file {locale_file}: {e}")
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading translation file {locale_file}: {e}")
|
||||
return {}
|
||||
# Load and merge module translations
|
||||
for module_code, locales_dir in get_module_locale_dirs():
|
||||
module_file = locales_dir / f"{language}.json"
|
||||
if module_file.exists():
|
||||
module_translations = _load_json_file(module_file)
|
||||
if module_translations:
|
||||
# Namespace module translations under module code
|
||||
if module_code in translations:
|
||||
# Merge with existing (e.g., if core has some cms.* keys)
|
||||
translations[module_code] = _deep_merge(
|
||||
translations[module_code], module_translations
|
||||
)
|
||||
else:
|
||||
translations[module_code] = module_translations
|
||||
logger.debug(f"[i18n] Loaded {language} translations for module: {module_code}")
|
||||
|
||||
return translations
|
||||
|
||||
|
||||
def clear_translation_cache():
|
||||
"""Clear the translation cache (useful for development/testing)."""
|
||||
global _module_locale_dirs
|
||||
load_translations.cache_clear()
|
||||
_translations.clear()
|
||||
_module_locale_dirs = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user