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:
2026-02-01 21:02:56 +01:00
parent 09d7d282c6
commit d7a0ff8818
307 changed files with 5536 additions and 3826 deletions

View File

@@ -12,8 +12,8 @@ This is a self-contained module with:
- Templates: app.modules.cms.templates (namespaced as cms/)
"""
from app.modules.base import ModuleDefinition
from models.database.admin_menu_config import FrontendType
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
from app.modules.enums import FrontendType
def _get_admin_router():
@@ -53,6 +53,57 @@ cms_module = ModuleDefinition(
"media", # Media library
],
},
# New module-driven menu definitions
menus={
FrontendType.ADMIN: [
MenuSectionDefinition(
id="contentMgmt",
label_key="cms.menu.content_management",
icon="document-text",
order=70,
items=[
MenuItemDefinition(
id="content-pages",
label_key="cms.menu.content_pages",
icon="document-text",
route="/admin/content-pages",
order=20,
),
MenuItemDefinition(
id="vendor-themes",
label_key="cms.menu.vendor_themes",
icon="color-swatch",
route="/admin/vendor-themes",
order=30,
),
],
),
],
FrontendType.VENDOR: [
MenuSectionDefinition(
id="shop",
label_key="cms.menu.shop_content",
icon="document-text",
order=40,
items=[
MenuItemDefinition(
id="content-pages",
label_key="cms.menu.content_pages",
icon="document-text",
route="/vendor/{vendor_code}/content-pages",
order=10,
),
MenuItemDefinition(
id="media",
label_key="cms.menu.media_library",
icon="photograph",
route="/vendor/{vendor_code}/media",
order=20,
),
],
),
],
},
is_core=True, # CMS is a core module - content management is fundamental
# Self-contained module configuration
is_self_contained=True,

View File

@@ -1,126 +1,203 @@
{
"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"
"platform": {
"nav": {
"pricing": "Preise",
"find_shop": "Finden Sie Ihren Shop",
"start_trial": "Kostenlos testen",
"admin_login": "Admin-Login",
"vendor_login": "Händler-Login",
"toggle_menu": "Menü umschalten",
"toggle_dark_mode": "Dunkelmodus umschalten"
},
"features": {
"title": "Funktionen-Abschnitt",
"section_title": "Abschnittstitel",
"cards": "Funktionskarten",
"add_card": "Karte hinzufügen",
"icon": "Icon-Name",
"feature_title": "Titel",
"feature_description": "Beschreibung"
"hero": {
"badge": "{trial_days}-Tage kostenlose Testversion - Keine Kreditkarte erforderlich",
"title": "Leichtes OMS für Letzshop-Verkäufer",
"subtitle": "Bestellverwaltung, Lager und Rechnungsstellung für den luxemburgischen E-Commerce. Schluss mit Tabellenkalkulationen. Führen Sie Ihr Geschäft.",
"cta_trial": "Kostenlos testen",
"cta_find_shop": "Finden Sie Ihren Letzshop"
},
"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."
"title": "Einfache, transparente Preise",
"subtitle": "Wählen Sie den Plan, der zu Ihrem Unternehmen passt. Alle Pläne beinhalten eine {trial_days}-tägige kostenlose Testversion.",
"monthly": "Monatlich",
"annual": "Jährlich",
"save_months": "Sparen Sie 2 Monate!",
"most_popular": "AM BELIEBTESTEN",
"recommended": "EMPFOHLEN",
"contact_sales": "Kontaktieren Sie uns",
"start_trial": "Kostenlos testen",
"per_month": "/Monat",
"per_year": "/Jahr",
"custom": "Individuell",
"orders_per_month": "{count} Bestellungen/Monat",
"unlimited_orders": "Unbegrenzte Bestellungen",
"products_limit": "{count} Produkte",
"unlimited_products": "Unbegrenzte Produkte",
"team_members": "{count} Teammitglieder",
"unlimited_team": "Unbegrenztes Team",
"letzshop_sync": "Letzshop-Synchronisierung",
"eu_vat_invoicing": "EU-MwSt-Rechnungen",
"analytics_dashboard": "Analyse-Dashboard",
"api_access": "API-Zugang",
"multi_channel": "Multi-Channel-Integration",
"products": "Produkte",
"team_member": "Teammitglied",
"unlimited": "Unbegrenzt",
"order_history": "Monate Bestellhistorie",
"trial_note": "Alle Pläne beinhalten eine {trial_days}-tägige kostenlose Testversion. Keine Kreditkarte erforderlich.",
"back_home": "Zurück zur Startseite"
},
"features": {
"letzshop_sync": "Letzshop-Synchronisierung",
"inventory_basic": "Grundlegende Lagerverwaltung",
"inventory_locations": "Lagerstandorte",
"inventory_purchase_orders": "Bestellungen",
"invoice_lu": "Luxemburg-MwSt-Rechnungen",
"invoice_eu_vat": "EU-MwSt-Rechnungen",
"invoice_bulk": "Massenrechnungen",
"customer_view": "Kundenliste",
"customer_export": "Kundenexport",
"analytics_dashboard": "Analyse-Dashboard",
"accounting_export": "Buchhaltungsexport",
"api_access": "API-Zugang",
"automation_rules": "Automatisierungsregeln",
"team_roles": "Teamrollen und Berechtigungen",
"white_label": "White-Label-Option",
"multi_vendor": "Multi-Händler-Unterstützung",
"custom_integrations": "Individuelle Integrationen",
"sla_guarantee": "SLA-Garantie",
"dedicated_support": "Dedizierter Kundenbetreuer"
},
"addons": {
"title": "Erweitern Sie Ihre Plattform",
"subtitle": "Fügen Sie Ihre Marke, professionelle E-Mail und erweiterte Sicherheit hinzu.",
"per_year": "/Jahr",
"per_month": "/Monat",
"custom_domain": "Eigene Domain",
"custom_domain_desc": "Nutzen Sie Ihre eigene Domain (meinedomain.com)",
"premium_ssl": "Premium SSL",
"premium_ssl_desc": "EV-Zertifikat für Vertrauenssiegel",
"email_package": "E-Mail-Paket",
"email_package_desc": "Professionelle E-Mail-Adressen"
},
"find_shop": {
"title": "Finden Sie Ihren Letzshop",
"subtitle": "Verkaufen Sie bereits auf Letzshop? Geben Sie Ihre Shop-URL ein, um zu beginnen.",
"placeholder": "Geben Sie Ihre Letzshop-URL ein (z.B. letzshop.lu/vendors/mein-shop)",
"button": "Meinen Shop finden",
"claim_shop": "Diesen Shop beanspruchen",
"already_claimed": "Bereits beansprucht",
"no_account": "Sie haben kein Letzshop-Konto?",
"signup_letzshop": "Registrieren Sie sich zuerst bei Letzshop",
"then_connect": ", dann kommen Sie zurück, um Ihren Shop zu verbinden.",
"search_placeholder": "Letzshop-URL oder Shopname eingeben...",
"search_button": "Suchen",
"examples": "Beispiele:",
"claim_button": "Diesen Shop beanspruchen und kostenlos testen",
"not_found": "Wir konnten keinen Letzshop mit dieser URL finden. Bitte überprüfen Sie und versuchen Sie es erneut.",
"or_signup": "Oder registrieren Sie sich ohne Letzshop-Verbindung",
"need_help": "Brauchen Sie Hilfe?",
"no_account_yet": "Sie haben noch kein Letzshop-Konto? Kein Problem!",
"create_letzshop": "Letzshop-Konto erstellen",
"signup_without": "Ohne Letzshop registrieren",
"looking_up": "Suche Ihren Shop...",
"found": "Gefunden:",
"claimed_badge": "Bereits beansprucht"
},
"signup": {
"step_plan": "Plan wählen",
"step_shop": "Shop beanspruchen",
"step_account": "Konto",
"step_payment": "Zahlung",
"choose_plan": "Wählen Sie Ihren Plan",
"save_percent": "Sparen Sie {percent}%",
"trial_info": "Wir erfassen Ihre Zahlungsdaten, aber Sie werden erst nach Ende der Testphase belastet.",
"connect_shop": "Verbinden Sie Ihren Letzshop",
"connect_optional": "Optional: Verknüpfen Sie Ihr Letzshop-Konto, um Bestellungen automatisch zu synchronisieren.",
"connect_continue": "Verbinden und fortfahren",
"skip_step": "Diesen Schritt überspringen",
"create_account": "Erstellen Sie Ihr Konto",
"first_name": "Vorname",
"last_name": "Nachname",
"company_name": "Firmenname",
"email": "E-Mail",
"password": "Passwort",
"password_hint": "Mindestens 8 Zeichen",
"continue": "Weiter",
"continue_payment": "Weiter zur Zahlung",
"back": "Zurück",
"add_payment": "Zahlungsmethode hinzufügen",
"no_charge_note": "Sie werden erst nach Ablauf Ihrer {trial_days}-tägigen Testphase belastet.",
"processing": "Verarbeitung...",
"start_trial": "Kostenlose Testversion starten",
"creating_account": "Erstelle Ihr Konto..."
},
"success": {
"title": "Willkommen bei Wizamart!",
"subtitle": "Ihr Konto wurde erstellt und Ihre {trial_days}-tägige kostenlose Testphase hat begonnen.",
"what_next": "Was kommt als Nächstes?",
"step_connect": "Letzshop verbinden:",
"step_connect_desc": "Fügen Sie Ihren API-Schlüssel hinzu, um Bestellungen automatisch zu synchronisieren.",
"step_invoicing": "Rechnungsstellung einrichten:",
"step_invoicing_desc": "Konfigurieren Sie Ihre Rechnungseinstellungen für die luxemburgische Compliance.",
"step_products": "Produkte importieren:",
"step_products_desc": "Synchronisieren Sie Ihren Produktkatalog von Letzshop.",
"go_to_dashboard": "Zum Dashboard",
"login_dashboard": "Zum Dashboard anmelden",
"need_help": "Brauchen Sie Hilfe beim Einstieg?",
"contact_support": "Kontaktieren Sie unser Support-Team"
},
"cta": {
"title": "Call-to-Action-Abschnitt",
"main_title": "Titel",
"subtitle": "Untertitel",
"buttons": "Schaltflächen",
"add_button": "Schaltfläche hinzufügen"
"title": "Bereit, Ihre Bestellungen zu optimieren?",
"subtitle": "Schließen Sie sich Letzshop-Händlern an, die Wizamart für ihre Bestellverwaltung vertrauen. Starten Sie heute Ihre {trial_days}-tägige kostenlose Testversion.",
"button": "Kostenlos testen"
},
"footer": {
"tagline": "Leichtes OMS für Letzshop-Verkäufer. Verwalten Sie Bestellungen, Lager und Rechnungen.",
"quick_links": "Schnelllinks",
"platform": "Plattform",
"contact": "Kontakt",
"copyright": "© {year} Wizamart. Entwickelt für den luxemburgischen E-Commerce.",
"privacy": "Datenschutzerklärung",
"terms": "Nutzungsbedingungen",
"about": "Über uns",
"faq": "FAQ",
"contact_us": "Kontaktieren Sie uns"
},
"modern": {
"badge_integration": "Offizielle Integration",
"badge_connect": "In 2 Minuten verbinden",
"hero_title_1": "Für den luxemburgischen E-Commerce entwickelt",
"hero_title_2": "Das Back-Office, das Letzshop Ihnen nicht gibt",
"hero_subtitle": "Synchronisieren Sie Bestellungen, verwalten Sie Lager, erstellen Sie Rechnungen mit korrekter MwSt und besitzen Sie Ihre Kundendaten. Alles an einem Ort.",
"cta_trial": "{trial_days}-Tage kostenlos testen",
"cta_how": "Sehen Sie, wie es funktioniert",
"hero_note": "Keine Kreditkarte erforderlich. Einrichtung in 5 Minuten. Jederzeit kündbar.",
"pain_title": "Kommt Ihnen das bekannt vor?",
"pain_subtitle": "Das sind die täglichen Frustrationen von Letzshop-Verkäufern",
"pain_manual": "Manuelle Bestelleingabe",
"pain_manual_desc": "Bestellungen von Letzshop in Tabellenkalkulationen kopieren. Jeden. Einzelnen. Tag.",
"pain_inventory": "Lagerchaos",
"pain_inventory_desc": "Der Bestand in Letzshop stimmt nicht mit der Realität überein. Überverkäufe passieren.",
"pain_vat": "Falsche MwSt-Rechnungen",
"pain_vat_desc": "EU-Kunden brauchen die korrekte MwSt. Ihr Buchhalter beschwert sich.",
"pain_customers": "Verlorene Kunden",
"pain_customers_desc": "Letzshop besitzt Ihre Kundendaten. Sie können nicht retargeten oder Loyalität aufbauen.",
"how_title": "So funktioniert es",
"how_subtitle": "Vom Chaos zur Kontrolle in 4 Schritten",
"how_step1": "Letzshop verbinden",
"how_step1_desc": "Geben Sie Ihre Letzshop-API-Zugangsdaten ein. In 2 Minuten erledigt, keine technischen Kenntnisse erforderlich.",
"how_step2": "Bestellungen kommen rein",
"how_step2_desc": "Bestellungen werden automatisch synchronisiert. Bestätigen und Tracking direkt von Wizamart hinzufügen.",
"how_step3": "Rechnungen erstellen",
"how_step3_desc": "Ein Klick, um konforme PDF-Rechnungen mit korrekter MwSt für jedes EU-Land zu erstellen.",
"how_step4": "Ihr Geschäft ausbauen",
"how_step4_desc": "Exportieren Sie Kunden für Marketing. Verfolgen Sie Lagerbestände. Konzentrieren Sie sich auf den Verkauf, nicht auf Tabellenkalkulationen.",
"features_title": "Alles, was ein Letzshop-Verkäufer braucht",
"features_subtitle": "Die operativen Tools, die Letzshop nicht bietet",
"cta_final_title": "Bereit, die Kontrolle über Ihr Letzshop-Geschäft zu übernehmen?",
"cta_final_subtitle": "Schließen Sie sich luxemburgischen Händlern an, die aufgehört haben, gegen Tabellenkalkulationen zu kämpfen, und begonnen haben, ihr Geschäft auszubauen.",
"cta_final_note": "Keine Kreditkarte erforderlich. Einrichtung in 5 Minuten. Volle Professional-Funktionen während der Testphase."
}
},
"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..."
}
}

View File

@@ -1,126 +1,203 @@
{
"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"
"platform": {
"nav": {
"pricing": "Tarifs",
"find_shop": "Trouvez votre boutique",
"start_trial": "Essai gratuit",
"admin_login": "Connexion Admin",
"vendor_login": "Connexion Vendeur",
"toggle_menu": "Basculer le menu",
"toggle_dark_mode": "Basculer le mode sombre"
},
"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"
"hero": {
"badge": "Essai gratuit de {trial_days} jours - Aucune carte de crédit requise",
"title": "OMS léger pour les vendeurs Letzshop",
"subtitle": "Gestion des commandes, stocks et facturation conçue pour le e-commerce luxembourgeois. Arrêtez de jongler avec les tableurs. Gérez votre entreprise.",
"cta_trial": "Essai gratuit",
"cta_find_shop": "Trouvez votre boutique Letzshop"
},
"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."
"title": "Tarification simple et transparente",
"subtitle": "Choisissez le plan adapté à votre entreprise. Tous les plans incluent un essai gratuit de {trial_days} jours.",
"monthly": "Mensuel",
"annual": "Annuel",
"save_months": "Économisez 2 mois !",
"most_popular": "LE PLUS POPULAIRE",
"recommended": "RECOMMANDÉ",
"contact_sales": "Contactez-nous",
"start_trial": "Essai gratuit",
"per_month": "/mois",
"per_year": "/an",
"custom": "Sur mesure",
"orders_per_month": "{count} commandes/mois",
"unlimited_orders": "Commandes illimitées",
"products_limit": "{count} produits",
"unlimited_products": "Produits illimités",
"team_members": "{count} membres d'équipe",
"unlimited_team": "Équipe illimitée",
"letzshop_sync": "Synchronisation Letzshop",
"eu_vat_invoicing": "Facturation TVA UE",
"analytics_dashboard": "Tableau de bord analytique",
"api_access": "Accès API",
"multi_channel": "Intégration multi-canal",
"products": "produits",
"team_member": "membre d'équipe",
"unlimited": "Illimité",
"order_history": "mois d'historique",
"trial_note": "Tous les plans incluent un essai gratuit de {trial_days} jours. Aucune carte de crédit requise.",
"back_home": "Retour à l'accueil"
},
"features": {
"letzshop_sync": "Synchronisation Letzshop",
"inventory_basic": "Gestion de stock de base",
"inventory_locations": "Emplacements d'entrepôt",
"inventory_purchase_orders": "Bons de commande",
"invoice_lu": "Facturation TVA Luxembourg",
"invoice_eu_vat": "Facturation TVA UE",
"invoice_bulk": "Facturation en masse",
"customer_view": "Liste des clients",
"customer_export": "Export clients",
"analytics_dashboard": "Tableau de bord analytique",
"accounting_export": "Export comptable",
"api_access": "Accès API",
"automation_rules": "Règles d'automatisation",
"team_roles": "Rôles et permissions",
"white_label": "Option marque blanche",
"multi_vendor": "Support multi-vendeurs",
"custom_integrations": "Intégrations personnalisées",
"sla_guarantee": "Garantie SLA",
"dedicated_support": "Gestionnaire de compte dédié"
},
"addons": {
"title": "Améliorez votre plateforme",
"subtitle": "Ajoutez votre marque, e-mail professionnel et sécurité renforcée.",
"per_year": "/an",
"per_month": "/mois",
"custom_domain": "Domaine personnalisé",
"custom_domain_desc": "Utilisez votre propre domaine (mondomaine.com)",
"premium_ssl": "SSL Premium",
"premium_ssl_desc": "Certificat EV pour les badges de confiance",
"email_package": "Pack Email",
"email_package_desc": "Adresses e-mail professionnelles"
},
"find_shop": {
"title": "Trouvez votre boutique Letzshop",
"subtitle": "Vous vendez déjà sur Letzshop ? Entrez l'URL de votre boutique pour commencer.",
"placeholder": "Entrez votre URL Letzshop (ex: letzshop.lu/vendors/ma-boutique)",
"button": "Trouver ma boutique",
"claim_shop": "Réclamer cette boutique",
"already_claimed": "Déjà réclamée",
"no_account": "Vous n'avez pas de compte Letzshop ?",
"signup_letzshop": "Inscrivez-vous d'abord sur Letzshop",
"then_connect": ", puis revenez connecter votre boutique.",
"search_placeholder": "Entrez l'URL Letzshop ou le nom de la boutique...",
"search_button": "Rechercher",
"examples": "Exemples :",
"claim_button": "Réclamez cette boutique et démarrez l'essai gratuit",
"not_found": "Nous n'avons pas trouvé de boutique Letzshop avec cette URL. Vérifiez et réessayez.",
"or_signup": "Ou inscrivez-vous sans connexion Letzshop",
"need_help": "Besoin d'aide ?",
"no_account_yet": "Vous n'avez pas encore de compte Letzshop ? Pas de problème !",
"create_letzshop": "Créer un compte Letzshop",
"signup_without": "S'inscrire sans Letzshop",
"looking_up": "Recherche de votre boutique...",
"found": "Trouvé :",
"claimed_badge": "Déjà réclamée"
},
"signup": {
"step_plan": "Choisir le plan",
"step_shop": "Réclamer la boutique",
"step_account": "Compte",
"step_payment": "Paiement",
"choose_plan": "Choisissez votre plan",
"save_percent": "Économisez {percent}%",
"trial_info": "Nous collecterons vos informations de paiement, mais vous ne serez pas débité avant la fin de l'essai.",
"connect_shop": "Connectez votre boutique Letzshop",
"connect_optional": "Optionnel : Liez votre compte Letzshop pour synchroniser automatiquement les commandes.",
"connect_continue": "Connecter et continuer",
"skip_step": "Passer cette étape",
"create_account": "Créez votre compte",
"first_name": "Prénom",
"last_name": "Nom",
"company_name": "Nom de l'entreprise",
"email": "E-mail",
"password": "Mot de passe",
"password_hint": "Minimum 8 caractères",
"continue": "Continuer",
"continue_payment": "Continuer vers le paiement",
"back": "Retour",
"add_payment": "Ajouter un moyen de paiement",
"no_charge_note": "Vous ne serez pas débité avant la fin de votre essai de {trial_days} jours.",
"processing": "Traitement en cours...",
"start_trial": "Démarrer l'essai gratuit",
"creating_account": "Création de votre compte..."
},
"success": {
"title": "Bienvenue sur Wizamart !",
"subtitle": "Votre compte a été créé et votre essai gratuit de {trial_days} jours a commencé.",
"what_next": "Et maintenant ?",
"step_connect": "Connecter Letzshop :",
"step_connect_desc": "Ajoutez votre clé API pour commencer à synchroniser automatiquement les commandes.",
"step_invoicing": "Configurer la facturation :",
"step_invoicing_desc": "Configurez vos paramètres de facturation pour la conformité luxembourgeoise.",
"step_products": "Importer les produits :",
"step_products_desc": "Synchronisez votre catalogue de produits depuis Letzshop.",
"go_to_dashboard": "Aller au tableau de bord",
"login_dashboard": "Connexion au tableau de bord",
"need_help": "Besoin d'aide pour démarrer ?",
"contact_support": "Contactez notre équipe support"
},
"cta": {
"title": "Section Appel à l'action",
"main_title": "Titre",
"subtitle": "Sous-titre",
"buttons": "Boutons",
"add_button": "Ajouter un bouton"
"title": "Prêt à optimiser vos commandes ?",
"subtitle": "Rejoignez les vendeurs Letzshop qui font confiance à Wizamart pour leur gestion de commandes. Commencez votre essai gratuit de {trial_days} jours aujourd'hui.",
"button": "Essai gratuit"
},
"footer": {
"tagline": "OMS léger pour les vendeurs Letzshop. Gérez commandes, stocks et facturation.",
"quick_links": "Liens rapides",
"platform": "Plateforme",
"contact": "Contact",
"copyright": "© {year} Wizamart. Conçu pour le e-commerce luxembourgeois.",
"privacy": "Politique de confidentialité",
"terms": "Conditions d'utilisation",
"about": "À propos",
"faq": "FAQ",
"contact_us": "Nous contacter"
},
"modern": {
"badge_integration": "Intégration officielle",
"badge_connect": "Connexion en 2 minutes",
"hero_title_1": "Conçu pour le e-commerce luxembourgeois",
"hero_title_2": "Le back-office que Letzshop ne vous donne pas",
"hero_subtitle": "Synchronisez les commandes, gérez les stocks, générez des factures avec la TVA correcte et possédez vos données clients. Tout en un seul endroit.",
"cta_trial": "Essai gratuit de {trial_days} jours",
"cta_how": "Voir comment ça marche",
"hero_note": "Aucune carte de crédit requise. Configuration en 5 minutes. Annulez à tout moment.",
"pain_title": "Ça vous dit quelque chose ?",
"pain_subtitle": "Ce sont les frustrations quotidiennes des vendeurs Letzshop",
"pain_manual": "Saisie manuelle des commandes",
"pain_manual_desc": "Copier-coller les commandes de Letzshop vers des tableurs. Chaque. Jour.",
"pain_inventory": "Chaos des stocks",
"pain_inventory_desc": "Le stock dans Letzshop ne correspond pas à la réalité. Les surventes arrivent.",
"pain_vat": "Mauvaises factures TVA",
"pain_vat_desc": "Les clients UE ont besoin de la TVA correcte. Votre comptable se plaint.",
"pain_customers": "Clients perdus",
"pain_customers_desc": "Letzshop possède vos données clients. Vous ne pouvez pas les recibler ou fidéliser.",
"how_title": "Comment ça marche",
"how_subtitle": "Du chaos au contrôle en 4 étapes",
"how_step1": "Connecter Letzshop",
"how_step1_desc": "Entrez vos identifiants API Letzshop. Fait en 2 minutes, aucune compétence technique requise.",
"how_step2": "Les commandes arrivent",
"how_step2_desc": "Les commandes se synchronisent automatiquement. Confirmez et ajoutez le suivi directement depuis Wizamart.",
"how_step3": "Générer des factures",
"how_step3_desc": "Un clic pour créer des factures PDF conformes avec la TVA correcte pour tout pays UE.",
"how_step4": "Développez votre entreprise",
"how_step4_desc": "Exportez les clients pour le marketing. Suivez les stocks. Concentrez-vous sur la vente, pas les tableurs.",
"features_title": "Tout ce dont un vendeur Letzshop a besoin",
"features_subtitle": "Les outils opérationnels que Letzshop ne fournit pas",
"cta_final_title": "Prêt à prendre le contrôle de votre entreprise Letzshop ?",
"cta_final_subtitle": "Rejoignez les vendeurs luxembourgeois qui ont arrêté de lutter contre les tableurs et ont commencé à développer leur entreprise.",
"cta_final_note": "Aucune carte de crédit requise. Configuration en 5 minutes. Toutes les fonctionnalités Pro pendant l'essai."
}
},
"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..."
}
}

View File

@@ -1,126 +1,203 @@
{
"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"
"platform": {
"nav": {
"pricing": "Präisser",
"find_shop": "Fannt Äre Buttek",
"start_trial": "Gratis Testen",
"admin_login": "Admin Login",
"vendor_login": "Händler Login",
"toggle_menu": "Menü wiesselen",
"toggle_dark_mode": "Däischter Modus wiesselen"
},
"features": {
"title": "Funktiounen-Sektioun",
"section_title": "Sektiounstitel",
"cards": "Funktiounskaarten",
"add_card": "Kaart derbäisetzen",
"icon": "Icon-Numm",
"feature_title": "Titel",
"feature_description": "Beschreiwung"
"hero": {
"badge": "{trial_days}-Deeg gratis Testversioun - Keng Kreditkaart néideg",
"title": "Liichtt OMS fir Letzshop Verkeefer",
"subtitle": "Bestellungsverwaltung, Lager an Rechnungsstellung fir de lëtzebuergeschen E-Commerce. Schluss mat Tabellen. Féiert Äert Geschäft.",
"cta_trial": "Gratis Testen",
"cta_find_shop": "Fannt Äre Letzshop Buttek"
},
"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."
"title": "Einfach, transparent Präisser",
"subtitle": "Wielt de Plang deen zu Ärer Firma passt. All Pläng enthalen eng {trial_days}-Deeg gratis Testversioun.",
"monthly": "Monatslech",
"annual": "Jäerlech",
"save_months": "Spuert 2 Méint!",
"most_popular": "AM BELÉIFSTEN",
"recommended": "EMPFOHLEN",
"contact_sales": "Kontaktéiert eis",
"start_trial": "Gratis Testen",
"per_month": "/Mount",
"per_year": "/Joer",
"custom": "Personnaliséiert",
"orders_per_month": "{count} Bestellungen/Mount",
"unlimited_orders": "Onbegrenzt Bestellungen",
"products_limit": "{count} Produkter",
"unlimited_products": "Onbegrenzt Produkter",
"team_members": "{count} Teammemberen",
"unlimited_team": "Onbegrenzt Team",
"letzshop_sync": "Letzshop Synchronisatioun",
"eu_vat_invoicing": "EU TVA Rechnungen",
"analytics_dashboard": "Analyse Dashboard",
"api_access": "API Zougang",
"multi_channel": "Multi-Channel Integratioun",
"products": "Produkter",
"team_member": "Teammember",
"unlimited": "Onbegrenzt",
"order_history": "Méint Bestellungshistorique",
"trial_note": "All Pläng enthalen eng {trial_days}-Deeg gratis Testversioun. Keng Kreditkaart néideg.",
"back_home": "Zréck op d'Haaptsäit"
},
"features": {
"letzshop_sync": "Letzshop Synchronisatioun",
"inventory_basic": "Basis Lagerverwaltung",
"inventory_locations": "Lagerstanduerten",
"inventory_purchase_orders": "Bestellungen",
"invoice_lu": "Lëtzebuerg TVA Rechnungen",
"invoice_eu_vat": "EU TVA Rechnungen",
"invoice_bulk": "Massrechnungen",
"customer_view": "Clientelëscht",
"customer_export": "Client Export",
"analytics_dashboard": "Analyse Dashboard",
"accounting_export": "Comptabilitéits Export",
"api_access": "API Zougang",
"automation_rules": "Automatiséierungsreegelen",
"team_roles": "Team Rollen an Autorisatiounen",
"white_label": "White-Label Optioun",
"multi_vendor": "Multi-Händler Ënnerstëtzung",
"custom_integrations": "Personnaliséiert Integratiounen",
"sla_guarantee": "SLA Garantie",
"dedicated_support": "Dedizéierte Kontobetreier"
},
"addons": {
"title": "Erweidert Är Plattform",
"subtitle": "Füügt Är Mark, professionell Email a verbessert Sécherheet derbäi.",
"per_year": "/Joer",
"per_month": "/Mount",
"custom_domain": "Eegen Domain",
"custom_domain_desc": "Benotzt Är eegen Domain (mengdomain.lu)",
"premium_ssl": "Premium SSL",
"premium_ssl_desc": "EV Zertifikat fir Vertrauensbadgen",
"email_package": "Email Package",
"email_package_desc": "Professionell Email Adressen"
},
"find_shop": {
"title": "Fannt Äre Letzshop Buttek",
"subtitle": "Verkaaft Dir schonn op Letzshop? Gitt Är Buttek URL an fir unzefänken.",
"placeholder": "Gitt Är Letzshop URL an (z.B. letzshop.lu/vendors/mäi-buttek)",
"button": "Mäi Buttek fannen",
"claim_shop": "Dëse Buttek reklaméieren",
"already_claimed": "Scho reklaméiert",
"no_account": "Kee Letzshop Kont?",
"signup_letzshop": "Registréiert Iech éischt bei Letzshop",
"then_connect": ", dann kommt zréck fir Äre Buttek ze verbannen.",
"search_placeholder": "Letzshop URL oder Butteknumm aginn...",
"search_button": "Sichen",
"examples": "Beispiller:",
"claim_button": "Dëse Buttek reklaméieren a gratis testen",
"not_found": "Mir konnten keen Letzshop Buttek mat dëser URL fannen. Iwwerpréift w.e.g. a probéiert nach eng Kéier.",
"or_signup": "Oder registréiert Iech ouni Letzshop Verbindung",
"need_help": "Braucht Dir Hëllef?",
"no_account_yet": "Dir hutt nach keen Letzshop Kont? Keen Problem!",
"create_letzshop": "Letzshop Kont erstellen",
"signup_without": "Ouni Letzshop registréieren",
"looking_up": "Sich Äre Buttek...",
"found": "Fonnt:",
"claimed_badge": "Scho reklaméiert"
},
"signup": {
"step_plan": "Plang wielen",
"step_shop": "Buttek reklaméieren",
"step_account": "Kont",
"step_payment": "Bezuelung",
"choose_plan": "Wielt Äre Plang",
"save_percent": "Spuert {percent}%",
"trial_info": "Mir sammelen Är Bezuelungsinformatiounen, awer Dir gitt eréischt nom Enn vun der Testperiod belaaschtt.",
"connect_shop": "Verbannt Äre Letzshop Buttek",
"connect_optional": "Optional: Verlinkt Äre Letzshop Kont fir Bestellungen automatesch ze synchroniséieren.",
"connect_continue": "Verbannen a weider",
"skip_step": "Dëse Schrëtt iwwersprangen",
"create_account": "Erstellt Äre Kont",
"first_name": "Virnumm",
"last_name": "Numm",
"company_name": "Firmennumm",
"email": "Email",
"password": "Passwuert",
"password_hint": "Mindestens 8 Zeechen",
"continue": "Weider",
"continue_payment": "Weider zur Bezuelung",
"back": "Zréck",
"add_payment": "Bezuelungsmethod derbäisetzen",
"no_charge_note": "Dir gitt eréischt nom Enn vun Ärer {trial_days}-Deeg Testperiod belaaschtt.",
"processing": "Veraarbechtung...",
"start_trial": "Gratis Testversioun starten",
"creating_account": "Erstellt Äre Kont..."
},
"success": {
"title": "Wëllkomm bei Wizamart!",
"subtitle": "Äre Kont gouf erstallt an Är {trial_days}-Deeg gratis Testversioun huet ugefaang.",
"what_next": "Wat kënnt duerno?",
"step_connect": "Letzshop verbannen:",
"step_connect_desc": "Füügt Äre API Schlëssel derbäi fir Bestellungen automatesch ze synchroniséieren.",
"step_invoicing": "Rechnungsstellung astellen:",
"step_invoicing_desc": "Konfiguréiert Är Rechnungsastellungen fir Lëtzebuerger Konformitéit.",
"step_products": "Produkter importéieren:",
"step_products_desc": "Synchroniséiert Äre Produktkatalog vu Letzshop.",
"go_to_dashboard": "Zum Dashboard",
"login_dashboard": "Am Dashboard umellen",
"need_help": "Braucht Dir Hëllef beim Ufänken?",
"contact_support": "Kontaktéiert eist Support Team"
},
"cta": {
"title": "Call-to-Action-Sektioun",
"main_title": "Titel",
"subtitle": "Ënnertitel",
"buttons": "Knäpp",
"add_button": "Knapp derbäisetzen"
"title": "Prett fir Är Bestellungen ze optiméieren?",
"subtitle": "Schléisst Iech Letzshop Händler un déi Wizamart fir hir Bestellungsverwaltung vertrauen. Fänkt haut Är {trial_days}-Deeg gratis Testversioun un.",
"button": "Gratis Testen"
},
"footer": {
"tagline": "Liichtt OMS fir Letzshop Verkeefer. Verwaltt Bestellungen, Lager an Rechnungen.",
"quick_links": "Séier Linken",
"platform": "Plattform",
"contact": "Kontakt",
"copyright": "© {year} Wizamart. Gemaach fir de lëtzebuergeschen E-Commerce.",
"privacy": "Dateschutzrichtlinn",
"terms": "Notzungsbedéngungen",
"about": "Iwwer eis",
"faq": "FAQ",
"contact_us": "Kontaktéiert eis"
},
"modern": {
"badge_integration": "Offiziell Integratioun",
"badge_connect": "An 2 Minutten verbannen",
"hero_title_1": "Gemaach fir de lëtzebuergeschen E-Commerce",
"hero_title_2": "De Back-Office dee Letzshop Iech net gëtt",
"hero_subtitle": "Synchroniséiert Bestellungen, verwaltt Lager, erstellt Rechnunge mat der korrekter TVA a besëtzt Är Clientsdaten. Alles un engem Plaz.",
"cta_trial": "{trial_days}-Deeg gratis testen",
"cta_how": "Kuckt wéi et funktionéiert",
"hero_note": "Keng Kreditkaart néideg. Setup an 5 Minutten. Ëmmer kënnegen.",
"pain_title": "Kënnt Iech dat bekannt vir?",
"pain_subtitle": "Dat sinn d'deeglech Frustratioune vu Letzshop Verkeefer",
"pain_manual": "Manuell Bestellungsagab",
"pain_manual_desc": "Bestellunge vu Letzshop an Tabelle kopéieren. All. Eenzelen. Dag.",
"pain_inventory": "Lager Chaos",
"pain_inventory_desc": "De Stock an Letzshop stëmmt net mat der Realitéit iwwereneen. Iwwerverkeef passéieren.",
"pain_vat": "Falsch TVA Rechnungen",
"pain_vat_desc": "EU Cliente brauchen déi korrekt TVA. Äre Comptabel beschwéiert sech.",
"pain_customers": "Verluer Clienten",
"pain_customers_desc": "Letzshop besëtzt Är Clientsdaten. Dir kënnt se net retargeten oder Loyalitéit opbauen.",
"how_title": "Wéi et funktionéiert",
"how_subtitle": "Vum Chaos zur Kontroll an 4 Schrëtt",
"how_step1": "Letzshop verbannen",
"how_step1_desc": "Gitt Är Letzshop API Zougangsdaten an. An 2 Minutte fäerdeg, keng technesch Kenntnisser néideg.",
"how_step2": "Bestellunge kommen eran",
"how_step2_desc": "Bestellunge ginn automatesch synchroniséiert. Confirméiert an Tracking direkt vu Wizamart derbäisetzen.",
"how_step3": "Rechnunge generéieren",
"how_step3_desc": "Ee Klick fir konform PDF Rechnunge mat korrekter TVA fir all EU Land ze erstellen.",
"how_step4": "Äert Geschäft ausbauen",
"how_step4_desc": "Exportéiert Clientë fir Marketing. Verfolgt Lagerstänn. Konzentréiert Iech op de Verkaf, net op Tabellen.",
"features_title": "Alles wat e Letzshop Verkeefer brauch",
"features_subtitle": "D'operativ Tools déi Letzshop net bitt",
"cta_final_title": "Prett fir d'Kontroll iwwer Äert Letzshop Geschäft ze iwwerhuelen?",
"cta_final_subtitle": "Schléisst Iech lëtzebuerger Händler un déi opgehalen hunn géint Tabellen ze kämpfen an ugefaang hunn hiert Geschäft auszbauen.",
"cta_final_note": "Keng Kreditkaart néideg. Setup an 5 Minutten. Voll Professional Fonctiounen während der Testperiod."
}
},
"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..."
}
}

View File

@@ -2,19 +2,24 @@
"""
CMS module database models.
This is the canonical location for CMS models. Module models are automatically
discovered and registered with SQLAlchemy's Base.metadata at startup.
This is the canonical location for CMS models including:
- ContentPage: CMS pages (marketing, vendor default pages)
- MediaFile: Vendor media library
- VendorTheme: Vendor storefront theme configuration
Usage:
from app.modules.cms.models import ContentPage
from app.modules.cms.models import ContentPage, MediaFile, VendorTheme
For media models:
from models.database.media import MediaFile # Core media file storage
from app.modules.catalog.models import ProductMedia # Product-media associations
For product-media associations:
from app.modules.catalog.models import ProductMedia
"""
from app.modules.cms.models.content_page import ContentPage
from app.modules.cms.models.media import MediaFile
from app.modules.cms.models.vendor_theme import VendorTheme
__all__ = [
"ContentPage",
"MediaFile",
"VendorTheme",
]

View File

@@ -0,0 +1,124 @@
# app/modules/cms/models/media.py
"""
CORE media file model for vendor media library.
This is a CORE framework model used across multiple modules.
MediaFile provides vendor-uploaded media files (images, documents, videos).
For product-media associations, use:
from app.modules.catalog.models import ProductMedia
Files are stored in vendor-specific directories:
uploads/vendors/{vendor_id}/{folder}/{filename}
"""
from sqlalchemy import (
Boolean,
Column,
ForeignKey,
Index,
Integer,
String,
Text,
)
from sqlalchemy.dialects.sqlite import JSON
from sqlalchemy.orm import relationship
from app.core.database import Base
from models.database.base import TimestampMixin
class MediaFile(Base, TimestampMixin):
"""Vendor media file record.
Stores metadata about uploaded files. Actual files are stored
in the filesystem at uploads/vendors/{vendor_id}/{folder}/
"""
__tablename__ = "media_files"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
# File identification
filename = Column(String(255), nullable=False) # Stored filename (UUID-based)
original_filename = Column(String(255)) # Original uploaded filename
file_path = Column(String(500), nullable=False) # Relative path from uploads/
# File properties
media_type = Column(String(20), nullable=False) # image, video, document
mime_type = Column(String(100))
file_size = Column(Integer) # bytes
# Image/video dimensions
width = Column(Integer)
height = Column(Integer)
# Thumbnail (for images/videos)
thumbnail_path = Column(String(500))
# Metadata
alt_text = Column(String(500))
description = Column(Text)
folder = Column(String(100), default="general") # products, general, etc.
tags = Column(JSON) # List of tags for categorization
extra_metadata = Column(JSON) # Additional metadata (EXIF, etc.)
# Status
is_optimized = Column(Boolean, default=False)
optimized_size = Column(Integer) # Size after optimization
# Usage tracking
usage_count = Column(Integer, default=0) # How many times used
# Relationships
vendor = relationship("Vendor", back_populates="media_files")
# ProductMedia relationship uses string reference to avoid circular import
product_associations = relationship(
"ProductMedia",
back_populates="media",
cascade="all, delete-orphan",
)
__table_args__ = (
Index("idx_media_vendor_id", "vendor_id"),
Index("idx_media_vendor_folder", "vendor_id", "folder"),
Index("idx_media_vendor_type", "vendor_id", "media_type"),
Index("idx_media_filename", "filename"),
)
def __repr__(self):
return (
f"<MediaFile(id={self.id}, vendor_id={self.vendor_id}, "
f"filename='{self.filename}', type='{self.media_type}')>"
)
@property
def file_url(self) -> str:
"""Get the public URL for this file."""
return f"/uploads/{self.file_path}"
@property
def thumbnail_url(self) -> str | None:
"""Get the thumbnail URL if available."""
if self.thumbnail_path:
return f"/uploads/{self.thumbnail_path}"
return None
@property
def is_image(self) -> bool:
"""Check if this is an image file."""
return self.media_type == "image"
@property
def is_video(self) -> bool:
"""Check if this is a video file."""
return self.media_type == "video"
@property
def is_document(self) -> bool:
"""Check if this is a document file."""
return self.media_type == "document"
__all__ = ["MediaFile"]

View File

@@ -0,0 +1,139 @@
# app/modules/cms/models/vendor_theme.py
"""
Vendor Theme Configuration Model
Allows each vendor to customize their shop's appearance
"""
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Integer, String, Text
from sqlalchemy.orm import relationship
from app.core.database import Base
from models.database.base import TimestampMixin
class VendorTheme(Base, TimestampMixin):
"""
Stores theme configuration for each vendor's shop.
Each vendor can have ONE active theme:
- Custom colors (primary, secondary, accent)
- Custom fonts
- Custom logo and favicon
- Custom CSS overrides
- Layout preferences
Theme presets available: default, modern, classic, minimal, vibrant
"""
__tablename__ = "vendor_themes"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(
Integer,
ForeignKey("vendors.id", ondelete="CASCADE"),
nullable=False,
unique=True, # ONE vendor = ONE theme
)
# Basic Theme Settings
theme_name = Column(
String(100), default="default"
) # default, modern, classic, minimal, vibrant
is_active = Column(Boolean, default=True)
# Color Scheme (JSON for flexibility)
colors = Column(
JSON,
default={
"primary": "#6366f1", # Indigo
"secondary": "#8b5cf6", # Purple
"accent": "#ec4899", # Pink
"background": "#ffffff", # White
"text": "#1f2937", # Gray-800
"border": "#e5e7eb", # Gray-200
},
)
# Typography
font_family_heading = Column(String(100), default="Inter, sans-serif")
font_family_body = Column(String(100), default="Inter, sans-serif")
# Branding Assets
logo_url = Column(String(500), nullable=True) # Path to vendor logo
logo_dark_url = Column(String(500), nullable=True) # Dark mode logo
favicon_url = Column(String(500), nullable=True) # Favicon
banner_url = Column(String(500), nullable=True) # Homepage banner
# Layout Preferences
layout_style = Column(String(50), default="grid") # grid, list, masonry
header_style = Column(String(50), default="fixed") # fixed, static, transparent
product_card_style = Column(
String(50), default="modern"
) # modern, classic, minimal
# Custom CSS (for advanced customization)
custom_css = Column(Text, nullable=True)
# Social Media Links
social_links = Column(JSON, default={}) # {facebook: "url", instagram: "url", etc.}
# SEO & Meta
meta_title_template = Column(
String(200), nullable=True
) # e.g., "{product_name} - {shop_name}"
meta_description = Column(Text, nullable=True)
# Relationships - FIXED: back_populates must match the relationship name in Vendor model
vendor = relationship("Vendor", back_populates="vendor_theme")
def __repr__(self):
return (
f"<VendorTheme(vendor_id={self.vendor_id}, theme_name='{self.theme_name}')>"
)
@property
def primary_color(self):
"""Get primary color from JSON"""
return self.colors.get("primary", "#6366f1")
@property
def css_variables(self):
"""Generate CSS custom properties from theme config"""
return {
"--color-primary": self.colors.get("primary", "#6366f1"),
"--color-secondary": self.colors.get("secondary", "#8b5cf6"),
"--color-accent": self.colors.get("accent", "#ec4899"),
"--color-background": self.colors.get("background", "#ffffff"),
"--color-text": self.colors.get("text", "#1f2937"),
"--color-border": self.colors.get("border", "#e5e7eb"),
"--font-heading": self.font_family_heading,
"--font-body": self.font_family_body,
}
def to_dict(self):
"""Convert theme to dictionary for template rendering"""
return {
"theme_name": self.theme_name,
"colors": self.colors,
"fonts": {
"heading": self.font_family_heading,
"body": self.font_family_body,
},
"branding": {
"logo": self.logo_url,
"logo_dark": self.logo_dark_url,
"favicon": self.favicon_url,
"banner": self.banner_url,
},
"layout": {
"style": self.layout_style,
"header": self.header_style,
"product_card": self.product_card_style,
},
"social_links": self.social_links,
"custom_css": self.custom_css,
"css_variables": self.css_variables,
}
__all__ = ["VendorTheme"]

View File

@@ -23,7 +23,7 @@ from app.modules.cms.schemas import (
SectionUpdateResponse,
)
from app.modules.cms.services import content_page_service
from models.database.user import User
from app.modules.tenancy.models import User
admin_content_pages_router = APIRouter(prefix="/content-pages")
logger = logging.getLogger(__name__)

View File

@@ -15,7 +15,7 @@ from fastapi import APIRouter, Depends, File, Form, UploadFile
from app.api.deps import get_current_admin_api
from app.modules.core.services.image_service import image_service
from models.schema.auth import UserContext
from models.schema.image import (
from app.modules.cms.schemas.image import (
ImageDeleteResponse,
ImageStorageStats,
ImageUploadResponse,

View File

@@ -14,7 +14,7 @@ from app.api.deps import get_current_admin_api
from app.core.database import get_db
from app.modules.cms.services.media_service import media_service
from models.schema.auth import UserContext
from models.schema.media import (
from app.modules.cms.schemas.media import (
MediaDetailResponse,
MediaItemResponse,
MediaListResponse,

View File

@@ -20,7 +20,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, get_db
from app.modules.cms.services.vendor_theme_service import vendor_theme_service
from models.schema.auth import UserContext
from models.schema.vendor_theme import (
from app.modules.cms.schemas.vendor_theme import (
ThemeDeleteResponse,
ThemePresetListResponse,
ThemePresetResponse,

View File

@@ -26,7 +26,7 @@ from app.modules.cms.schemas import (
)
from app.modules.cms.services import content_page_service
from app.modules.tenancy.services.vendor_service import VendorService # noqa: MOD-004 - shared platform service
from models.database.user import User
from app.modules.tenancy.models import User
vendor_service = VendorService()

View File

@@ -16,7 +16,7 @@ from app.core.database import get_db
from app.modules.cms.exceptions import MediaOptimizationException
from app.modules.cms.services.media_service import media_service
from models.schema.auth import UserContext
from models.schema.media import (
from app.modules.cms.schemas.media import (
MediaDetailResponse,
MediaItemResponse,
MediaListResponse,

View File

@@ -11,8 +11,8 @@ from sqlalchemy.orm import Session
from app.api.deps import get_db, require_menu_access
from app.templates_config import templates
from models.database.admin_menu_config import FrontendType
from models.database.user import User
from app.modules.enums import FrontendType
from app.modules.tenancy.models import User
router = APIRouter()

View File

@@ -15,8 +15,8 @@ from app.api.deps import get_current_vendor_from_cookie_or_header, get_db
from app.modules.cms.services import content_page_service
from app.modules.core.services.platform_settings_service import platform_settings_service # noqa: MOD-004 - shared platform service
from app.templates_config import templates
from models.database.user import User
from models.database.vendor import Vendor
from app.modules.tenancy.models import User
from app.modules.tenancy.models import Vendor
logger = logging.getLogger(__name__)

View File

@@ -35,6 +35,44 @@ from app.modules.cms.schemas.homepage_sections import (
HomepageSectionsResponse,
)
# Media schemas
from app.modules.cms.schemas.media import (
FailedFileInfo,
MediaDetailResponse,
MediaItemResponse,
MediaListResponse,
MediaMetadataUpdate,
MediaUploadResponse,
MediaUsageResponse,
MessageResponse,
MultipleUploadResponse,
OptimizationResultResponse,
ProductUsageInfo,
UploadedFileInfo,
)
# Image schemas
from app.modules.cms.schemas.image import (
ImageDeleteResponse,
ImageStorageStats,
ImageUploadResponse,
ImageUrls,
)
# Theme schemas
from app.modules.cms.schemas.vendor_theme import (
ThemeDeleteResponse,
ThemePresetListResponse,
ThemePresetPreview,
ThemePresetResponse,
VendorThemeBranding,
VendorThemeColors,
VendorThemeFonts,
VendorThemeLayout,
VendorThemeResponse,
VendorThemeUpdate,
)
__all__ = [
# Content Page - Admin
"ContentPageCreate",
@@ -60,4 +98,33 @@ __all__ = [
"HomepageSections",
"SectionUpdateRequest",
"HomepageSectionsResponse",
# Media
"FailedFileInfo",
"MediaDetailResponse",
"MediaItemResponse",
"MediaListResponse",
"MediaMetadataUpdate",
"MediaUploadResponse",
"MediaUsageResponse",
"MessageResponse",
"MultipleUploadResponse",
"OptimizationResultResponse",
"ProductUsageInfo",
"UploadedFileInfo",
# Image
"ImageDeleteResponse",
"ImageStorageStats",
"ImageUploadResponse",
"ImageUrls",
# Theme
"ThemeDeleteResponse",
"ThemePresetListResponse",
"ThemePresetPreview",
"ThemePresetResponse",
"VendorThemeBranding",
"VendorThemeColors",
"VendorThemeFonts",
"VendorThemeLayout",
"VendorThemeResponse",
"VendorThemeUpdate",
]

View File

@@ -0,0 +1,46 @@
# app/modules/cms/schemas/image.py
"""
Pydantic schemas for image operations.
"""
from pydantic import BaseModel
class ImageUrls(BaseModel):
"""URLs for image variants."""
original: str
medium: str | None = None # 800px variant
thumb: str | None = None # 200px variant
# Allow arbitrary keys for flexibility
class Config:
extra = "allow"
class ImageUploadResponse(BaseModel):
"""Response from image upload."""
success: bool
image: dict | None = None
error: str | None = None
class ImageDeleteResponse(BaseModel):
"""Response from image deletion."""
success: bool
message: str
class ImageStorageStats(BaseModel):
"""Image storage statistics."""
total_files: int
total_size_bytes: int
total_size_mb: float
total_size_gb: float
directory_count: int
max_files_per_dir: int
avg_files_per_dir: float
products_estimated: int

View File

@@ -0,0 +1,198 @@
# app/modules/cms/schemas/media.py
"""
Media/file management Pydantic schemas for API validation and responses.
This module provides schemas for:
- Media library listing
- File upload responses
- Media metadata operations
- Media usage tracking
"""
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field
# ============================================================================
# SHARED RESPONSE SCHEMAS
# ============================================================================
class MessageResponse(BaseModel):
"""Generic message response for simple operations."""
message: str
# ============================================================================
# MEDIA ITEM SCHEMAS
# ============================================================================
class MediaItemResponse(BaseModel):
"""Single media item response."""
id: int
filename: str
original_filename: str | None = None
file_url: str
url: str | None = None # Alias for file_url for JS compatibility
thumbnail_url: str | None = None
media_type: str # image, video, document
mime_type: str | None = None
file_size: int | None = None # bytes
width: int | None = None # for images/videos
height: int | None = None # for images/videos
alt_text: str | None = None
description: str | None = None
folder: str | None = None
extra_metadata: dict[str, Any] | None = None
created_at: datetime
updated_at: datetime | None = None
model_config = {"from_attributes": True}
def model_post_init(self, __context: Any) -> None:
"""Set url from file_url if not provided."""
if self.url is None:
object.__setattr__(self, "url", self.file_url)
class MediaListResponse(BaseModel):
"""Paginated list of media items."""
media: list[MediaItemResponse] = []
total: int = 0
skip: int = 0
limit: int = 100
message: str | None = None
# ============================================================================
# UPLOAD RESPONSE SCHEMAS
# ============================================================================
class MediaUploadResponse(BaseModel):
"""Response for single file upload."""
success: bool = True
message: str | None = None
media: MediaItemResponse | None = None
# Legacy fields for backwards compatibility
id: int | None = None
file_url: str | None = None
thumbnail_url: str | None = None
filename: str | None = None
file_size: int | None = None
media_type: str | None = None
class UploadedFileInfo(BaseModel):
"""Information about a successfully uploaded file."""
id: int
filename: str
file_url: str
thumbnail_url: str | None = None
class FailedFileInfo(BaseModel):
"""Information about a failed file upload."""
filename: str
error: str
class MultipleUploadResponse(BaseModel):
"""Response for multiple file upload."""
uploaded_files: list[UploadedFileInfo] = []
failed_files: list[FailedFileInfo] = []
total_uploaded: int = 0
total_failed: int = 0
message: str | None = None
# ============================================================================
# MEDIA DETAIL SCHEMAS
# ============================================================================
class MediaDetailResponse(BaseModel):
"""Detailed media item response with usage info."""
id: int | None = None
filename: str | None = None
original_filename: str | None = None
file_url: str | None = None
thumbnail_url: str | None = None
media_type: str | None = None
mime_type: str | None = None
file_size: int | None = None
width: int | None = None
height: int | None = None
alt_text: str | None = None
description: str | None = None
folder: str | None = None
extra_metadata: dict[str, Any] | None = None
created_at: datetime | None = None
updated_at: datetime | None = None
message: str | None = None
model_config = {"from_attributes": True}
# ============================================================================
# MEDIA UPDATE SCHEMAS
# ============================================================================
class MediaMetadataUpdate(BaseModel):
"""Request model for updating media metadata."""
filename: str | None = Field(None, max_length=255)
alt_text: str | None = Field(None, max_length=500)
description: str | None = None
folder: str | None = Field(None, max_length=100)
metadata: dict[str, Any] | None = None # Named 'metadata' in API, stored as 'extra_metadata'
# ============================================================================
# MEDIA USAGE SCHEMAS
# ============================================================================
class ProductUsageInfo(BaseModel):
"""Information about product using this media."""
product_id: int
product_name: str
usage_type: str # main_image, gallery, variant, etc.
class MediaUsageResponse(BaseModel):
"""Response showing where media is being used."""
media_id: int | None = None
products: list[ProductUsageInfo] = []
other_usage: list[dict[str, Any]] = []
total_usage_count: int = 0
message: str | None = None
# ============================================================================
# MEDIA OPTIMIZATION SCHEMAS
# ============================================================================
class OptimizationResultResponse(BaseModel):
"""Response for media optimization operation."""
media_id: int | None = None
original_size: int | None = None
optimized_size: int | None = None
savings_percent: float | None = None
optimized_url: str | None = None
message: str | None = None

View File

@@ -0,0 +1,108 @@
# app/modules/cms/schemas/vendor_theme.py
"""
Pydantic schemas for vendor theme operations.
"""
from pydantic import BaseModel, Field
class VendorThemeColors(BaseModel):
"""Color scheme for vendor theme."""
primary: str | None = Field(None, description="Primary brand color")
secondary: str | None = Field(None, description="Secondary color")
accent: str | None = Field(None, description="Accent/CTA color")
background: str | None = Field(None, description="Background color")
text: str | None = Field(None, description="Text color")
border: str | None = Field(None, description="Border color")
class VendorThemeFonts(BaseModel):
"""Typography settings for vendor theme."""
heading: str | None = Field(None, description="Font for headings")
body: str | None = Field(None, description="Font for body text")
class VendorThemeBranding(BaseModel):
"""Branding assets for vendor theme."""
logo: str | None = Field(None, description="Logo URL")
logo_dark: str | None = Field(None, description="Dark mode logo URL")
favicon: str | None = Field(None, description="Favicon URL")
banner: str | None = Field(None, description="Banner image URL")
class VendorThemeLayout(BaseModel):
"""Layout settings for vendor theme."""
style: str | None = Field(
None, description="Product layout style (grid, list, masonry)"
)
header: str | None = Field(
None, description="Header style (fixed, static, transparent)"
)
product_card: str | None = Field(
None, description="Product card style (modern, classic, minimal)"
)
class VendorThemeUpdate(BaseModel):
"""Schema for updating vendor theme (partial updates allowed)."""
theme_name: str | None = Field(None, description="Theme preset name")
colors: dict[str, str] | None = Field(None, description="Color scheme")
fonts: dict[str, str] | None = Field(None, description="Font settings")
branding: dict[str, str | None] | None = Field(None, description="Branding assets")
layout: dict[str, str] | None = Field(None, description="Layout settings")
custom_css: str | None = Field(None, description="Custom CSS rules")
social_links: dict[str, str] | None = Field(None, description="Social media links")
class VendorThemeResponse(BaseModel):
"""Schema for vendor theme response."""
theme_name: str = Field(..., description="Theme name")
colors: dict[str, str] = Field(..., description="Color scheme")
fonts: dict[str, str] = Field(..., description="Font settings")
branding: dict[str, str | None] = Field(..., description="Branding assets")
layout: dict[str, str] = Field(..., description="Layout settings")
social_links: dict[str, str] | None = Field(
default_factory=dict, description="Social links"
)
custom_css: str | None = Field(None, description="Custom CSS")
css_variables: dict[str, str] | None = Field(
None, description="CSS custom properties"
)
class ThemePresetPreview(BaseModel):
"""Preview information for a theme preset."""
name: str = Field(..., description="Preset name")
description: str = Field(..., description="Preset description")
primary_color: str = Field(..., description="Primary color")
secondary_color: str = Field(..., description="Secondary color")
accent_color: str = Field(..., description="Accent color")
heading_font: str = Field(..., description="Heading font")
body_font: str = Field(..., description="Body font")
layout_style: str = Field(..., description="Layout style")
class ThemePresetResponse(BaseModel):
"""Response after applying a preset."""
message: str = Field(..., description="Success message")
theme: VendorThemeResponse = Field(..., description="Applied theme")
class ThemePresetListResponse(BaseModel):
"""List of available theme presets."""
presets: list[ThemePresetPreview] = Field(..., description="Available presets")
class ThemeDeleteResponse(BaseModel):
"""Response after deleting a theme."""
message: str = Field(..., description="Success message")

View File

@@ -27,7 +27,7 @@ from app.modules.cms.exceptions import (
UnsupportedMediaTypeException,
MediaFileTooLargeException,
)
from models.database.media import MediaFile
from app.modules.cms.models import MediaFile
from app.modules.catalog.models import ProductMedia
logger = logging.getLogger(__name__)

View File

@@ -24,14 +24,13 @@ from app.exceptions import (
ValidationException,
ExternalServiceException,
)
from models.database import (
Vendor,
from app.modules.tenancy.models import Vendor
from app.modules.messaging.models import (
VendorEmailSettings,
EmailProvider,
PREMIUM_EMAIL_PROVIDERS,
VendorSubscription,
TierCode,
)
from app.modules.billing.models import VendorSubscription, TierCode
logger = logging.getLogger(__name__)

View File

@@ -26,9 +26,9 @@ from app.modules.cms.exceptions import (
ThemeValidationException,
VendorThemeNotFoundException,
)
from models.database.vendor import Vendor
from models.database.vendor_theme import VendorTheme
from models.schema.vendor_theme import ThemePresetPreview, VendorThemeUpdate
from app.modules.tenancy.models import Vendor
from app.modules.cms.models import VendorTheme
from app.modules.cms.schemas.vendor_theme import ThemePresetPreview, VendorThemeUpdate
logger = logging.getLogger(__name__)

View File

@@ -28,6 +28,9 @@ function contentPagesManager() {
// Initialize
async init() {
// Load i18n translations
await I18n.loadModule('cms');
contentPagesLog.info('=== CONTENT PAGES MANAGER INITIALIZING ===');
// Prevent multiple initializations
@@ -235,7 +238,7 @@ function contentPagesManager() {
} catch (err) {
contentPagesLog.error('Error deleting page:', err);
Utils.showToast(`Failed to delete page: ${err.message}`, 'error');
Utils.showToast(I18n.t('cms.messages.failed_to_delete_page', { error: err.message }), 'error');
}
},

View File

@@ -20,24 +20,24 @@
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
{{ _("platform.hero.badge", trial_days=trial_days) }}
{{ _("cms.platform.hero.badge", trial_days=trial_days) }}
</div>
{# Headline #}
<h1 class="text-4xl md:text-5xl lg:text-6xl font-extrabold text-gray-900 dark:text-white leading-tight mb-6">
{{ _("platform.hero.title") }}
{{ _("cms.platform.hero.title") }}
</h1>
{# Subheadline #}
<p class="text-xl text-gray-600 dark:text-gray-400 max-w-3xl mx-auto mb-10">
{{ _("platform.hero.subtitle") }}
{{ _("cms.platform.hero.subtitle") }}
</p>
{# CTA Buttons #}
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="/signup"
class="inline-flex items-center justify-center px-8 py-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl shadow-lg shadow-indigo-500/30 transition-all hover:scale-105">
{{ _("platform.hero.cta_trial") }}
{{ _("cms.platform.hero.cta_trial") }}
<svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
</svg>
@@ -47,7 +47,7 @@
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
{{ _("platform.hero.cta_find_shop") }}
{{ _("cms.platform.hero.cta_find_shop") }}
</a>
</div>
</div>
@@ -68,19 +68,19 @@
{# Section Header #}
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
{{ _("platform.pricing.title") }}
{{ _("cms.platform.pricing.title") }}
</h2>
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
{{ _("platform.pricing.subtitle", trial_days=trial_days) }}
{{ _("cms.platform.pricing.subtitle", trial_days=trial_days) }}
</p>
{# Billing Toggle #}
<div class="flex justify-center mt-8">
{{ toggle_switch(
model='annual',
left_label=_("platform.pricing.monthly"),
right_label=_("platform.pricing.annual"),
right_badge=_("platform.pricing.save_months")
left_label=_("cms.platform.pricing.monthly"),
right_label=_("cms.platform.pricing.annual"),
right_badge=_("cms.platform.pricing.save_months")
) }}
</div>
</div>
@@ -95,7 +95,7 @@
{% if tier.is_popular %}
<div class="absolute -top-3 left-1/2 -translate-x-1/2">
<span class="bg-indigo-600 text-white text-xs font-bold px-3 py-1 rounded-full">
{{ _("platform.pricing.most_popular") }}
{{ _("cms.platform.pricing.most_popular") }}
</span>
</div>
{% endif %}
@@ -108,19 +108,19 @@
<template x-if="!annual">
<div>
<span class="text-4xl font-extrabold text-gray-900 dark:text-white">{{ tier.price_monthly|int }}€</span>
<span class="text-gray-500 dark:text-gray-400">{{ _("platform.pricing.per_month") }}</span>
<span class="text-gray-500 dark:text-gray-400">{{ _("cms.platform.pricing.per_month") }}</span>
</div>
</template>
<template x-if="annual">
<div>
{% if tier.price_annual %}
<span class="text-4xl font-extrabold text-gray-900 dark:text-white">{{ (tier.price_annual / 12)|round(0)|int }}€</span>
<span class="text-gray-500 dark:text-gray-400">{{ _("platform.pricing.per_month") }}</span>
<span class="text-gray-500 dark:text-gray-400">{{ _("cms.platform.pricing.per_month") }}</span>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ tier.price_annual|int }}€ {{ _("platform.pricing.per_year") }}
{{ tier.price_annual|int }}€ {{ _("cms.platform.pricing.per_year") }}
</div>
{% else %}
<span class="text-2xl font-bold text-gray-900 dark:text-white">{{ _("platform.pricing.custom") }}</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white">{{ _("cms.platform.pricing.custom") }}</span>
{% endif %}
</div>
</template>
@@ -133,28 +133,28 @@
<svg class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
{% if tier.orders_per_month %}{{ _("platform.pricing.orders_per_month", count=tier.orders_per_month) }}{% else %}{{ _("platform.pricing.unlimited_orders") }}{% endif %}
{% if tier.orders_per_month %}{{ _("cms.platform.pricing.orders_per_month", count=tier.orders_per_month) }}{% else %}{{ _("cms.platform.pricing.unlimited_orders") }}{% endif %}
</li>
{# Products #}
<li class="flex items-center text-gray-700 dark:text-gray-300">
<svg class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
{% if tier.products_limit %}{{ _("platform.pricing.products_limit", count=tier.products_limit) }}{% else %}{{ _("platform.pricing.unlimited_products") }}{% endif %}
{% if tier.products_limit %}{{ _("cms.platform.pricing.products_limit", count=tier.products_limit) }}{% else %}{{ _("cms.platform.pricing.unlimited_products") }}{% endif %}
</li>
{# Team Members #}
<li class="flex items-center text-gray-700 dark:text-gray-300">
<svg class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
{% if tier.team_members %}{{ _("platform.pricing.team_members", count=tier.team_members) }}{% else %}{{ _("platform.pricing.unlimited_team") }}{% endif %}
{% if tier.team_members %}{{ _("cms.platform.pricing.team_members", count=tier.team_members) }}{% else %}{{ _("cms.platform.pricing.unlimited_team") }}{% endif %}
</li>
{# Letzshop Sync - always included #}
<li class="flex items-center text-gray-700 dark:text-gray-300">
<svg class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
{{ _("platform.pricing.letzshop_sync") }}
{{ _("cms.platform.pricing.letzshop_sync") }}
</li>
{# EU VAT Invoicing #}
<li class="flex items-center {% if 'invoice_eu_vat' in tier.features %}text-gray-700 dark:text-gray-300{% else %}text-gray-400 dark:text-gray-600{% endif %}">
@@ -167,7 +167,7 @@
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
{% endif %}
{{ _("platform.pricing.eu_vat_invoicing") }}
{{ _("cms.platform.pricing.eu_vat_invoicing") }}
</li>
{# Analytics Dashboard #}
<li class="flex items-center {% if 'analytics_dashboard' in tier.features %}text-gray-700 dark:text-gray-300{% else %}text-gray-400 dark:text-gray-600{% endif %}">
@@ -180,7 +180,7 @@
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
{% endif %}
{{ _("platform.pricing.analytics_dashboard") }}
{{ _("cms.platform.pricing.analytics_dashboard") }}
</li>
{# API Access #}
<li class="flex items-center {% if 'api_access' in tier.features %}text-gray-700 dark:text-gray-300{% else %}text-gray-400 dark:text-gray-600{% endif %}">
@@ -193,7 +193,7 @@
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
{% endif %}
{{ _("platform.pricing.api_access") }}
{{ _("cms.platform.pricing.api_access") }}
</li>
{# Multi-channel Integration - Enterprise only #}
<li class="flex items-center {% if tier.is_enterprise %}text-gray-700 dark:text-gray-300{% else %}text-gray-400 dark:text-gray-600{% endif %}">
@@ -206,7 +206,7 @@
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
{% endif %}
{{ _("platform.pricing.multi_channel") }}
{{ _("cms.platform.pricing.multi_channel") }}
</li>
</ul>
@@ -214,14 +214,14 @@
{% if tier.is_enterprise %}
<a href="mailto:sales@wizamart.com?subject=Enterprise%20Plan%20Inquiry"
class="block w-full py-3 px-4 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white font-semibold rounded-xl text-center hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
{{ _("platform.pricing.contact_sales") }}
{{ _("cms.platform.pricing.contact_sales") }}
</a>
{% else %}
<a href="/signup?tier={{ tier.code }}"
:href="'/signup?tier={{ tier.code }}&annual=' + annual"
class="block w-full py-3 px-4 font-semibold rounded-xl text-center transition-colors
{% if tier.is_popular %}bg-indigo-600 hover:bg-indigo-700 text-white{% else %}bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-200 dark:hover:bg-indigo-900/50{% endif %}">
{{ _("platform.pricing.start_trial") }}
{{ _("cms.platform.pricing.start_trial") }}
</a>
{% endif %}
</div>
@@ -238,10 +238,10 @@
{# Section Header #}
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
{{ _("platform.addons.title") }}
{{ _("cms.platform.addons.title") }}
</h2>
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
{{ _("platform.addons.subtitle") }}
{{ _("cms.platform.addons.subtitle") }}
</p>
</div>
@@ -300,10 +300,10 @@
{# Section Header #}
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
{{ _("platform.find_shop.title") }}
{{ _("cms.platform.find_shop.title") }}
</h2>
<p class="text-lg text-gray-600 dark:text-gray-400">
{{ _("platform.find_shop.subtitle") }}
{{ _("cms.platform.find_shop.subtitle") }}
</p>
</div>
@@ -313,7 +313,7 @@
<input
type="text"
x-model="shopUrl"
placeholder="{{ _('platform.find_shop.placeholder') }}"
placeholder="{{ _('cms.platform.find_shop.placeholder') }}"
class="flex-1 px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
<button
@@ -326,7 +326,7 @@
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg>
</template>
{{ _("platform.find_shop.button") }}
{{ _("cms.platform.find_shop.button") }}
</button>
</div>
@@ -342,12 +342,12 @@
<template x-if="!vendorResult.vendor.is_claimed">
<a :href="'/signup?letzshop=' + vendorResult.vendor.slug"
class="px-6 py-2 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition-colors">
{{ _("platform.find_shop.claim_shop") }}
{{ _("cms.platform.find_shop.claim_shop") }}
</a>
</template>
<template x-if="vendorResult.vendor.is_claimed">
<span class="px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-lg">
{{ _("platform.find_shop.already_claimed") }}
{{ _("cms.platform.find_shop.already_claimed") }}
</span>
</template>
</div>
@@ -362,7 +362,7 @@
{# Help Text #}
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400 text-center">
{{ _("platform.find_shop.no_account") }} <a href="https://letzshop.lu" target="_blank" class="text-indigo-600 dark:text-indigo-400 hover:underline">{{ _("platform.find_shop.signup_letzshop") }}</a>{{ _("platform.find_shop.then_connect") }}
{{ _("cms.platform.find_shop.no_account") }} <a href="https://letzshop.lu" target="_blank" class="text-indigo-600 dark:text-indigo-400 hover:underline">{{ _("cms.platform.find_shop.signup_letzshop") }}</a>{{ _("cms.platform.find_shop.then_connect") }}
</p>
</div>
</div>
@@ -374,14 +374,14 @@
<section class="py-16 lg:py-24 bg-gradient-to-r from-indigo-600 to-purple-600">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-6">
{{ _("platform.cta.title") }}
{{ _("cms.platform.cta.title") }}
</h2>
<p class="text-xl text-indigo-100 mb-10">
{{ _("platform.cta.subtitle", trial_days=trial_days) }}
{{ _("cms.platform.cta.subtitle", trial_days=trial_days) }}
</p>
<a href="/signup"
class="inline-flex items-center px-10 py-4 bg-white text-indigo-600 font-bold rounded-xl shadow-lg hover:shadow-xl transition-all hover:scale-105">
{{ _("platform.cta.button") }}
{{ _("cms.platform.cta.button") }}
<svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
</svg>