Some checks failed
- Phase 5: Translate homepage-modern.html (~90 new locale keys, all hardcoded strings replaced with _() calls for dashboard mock, features, pricing tiers, testimonial sections) - Phase 6: Translate homepage-minimal.html (17 new locale keys for fallback content, features, and CTA sections) - Phase 7: Add multi-language page.title/content support with title_translations and content_translations JSON columns, Alembic migration cms_002, translated title/content resolution in templates, and seed script updates with tt() helper - Phase 8: Complete lb.json audit — fill 6 missing keys (messages, confirmations), also backfill same keys in fr.json and de.json All 4 locale files now have 340 keys with full parity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
209 lines
6.8 KiB
Python
209 lines
6.8 KiB
Python
"""
|
|
Homepage Section Schemas with Dynamic Multi-Language Support.
|
|
|
|
Language codes are NOT hardcoded - they come from platform.supported_languages.
|
|
The TranslatableText class stores translations as a dict where keys are language codes.
|
|
|
|
Example JSON structure:
|
|
{
|
|
"hero": {
|
|
"enabled": true,
|
|
"title": {"translations": {"fr": "Bienvenue", "en": "Welcome"}},
|
|
"subtitle": {"translations": {...}},
|
|
"buttons": [...]
|
|
},
|
|
"features": {...},
|
|
"pricing": {...},
|
|
"cta": {...}
|
|
}
|
|
"""
|
|
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class TranslatableText(BaseModel):
|
|
"""
|
|
Text field with translations stored as language-keyed dict.
|
|
|
|
Languages come from platform.supported_languages (not hardcoded).
|
|
Use .get(lang, default_lang) to retrieve translation with fallback.
|
|
"""
|
|
|
|
translations: dict[str, str] = Field(
|
|
default_factory=dict, description="Language code -> translated text mapping"
|
|
)
|
|
|
|
def get(self, lang: str, default_lang: str = "fr") -> str:
|
|
"""Get translation with fallback to default language."""
|
|
return self.translations.get(lang) or self.translations.get(default_lang) or ""
|
|
|
|
def set(self, lang: str, text: str) -> None:
|
|
"""Set translation for a language."""
|
|
self.translations[lang] = text
|
|
|
|
def has_translation(self, lang: str) -> bool:
|
|
"""Check if translation exists for language."""
|
|
return bool(self.translations.get(lang))
|
|
|
|
|
|
class HeroButton(BaseModel):
|
|
"""Button in hero or CTA section."""
|
|
|
|
text: TranslatableText = Field(default_factory=TranslatableText)
|
|
url: str = ""
|
|
style: str = Field(default="primary", description="primary, secondary, outline")
|
|
|
|
|
|
class HeroSection(BaseModel):
|
|
"""Hero section configuration."""
|
|
|
|
enabled: bool = True
|
|
badge_text: TranslatableText | None = None
|
|
title: TranslatableText = Field(default_factory=TranslatableText)
|
|
subtitle: TranslatableText = Field(default_factory=TranslatableText)
|
|
background_type: str = Field(
|
|
default="gradient", description="gradient, image, solid"
|
|
)
|
|
background_image: str | None = None
|
|
buttons: list[HeroButton] = Field(default_factory=list)
|
|
|
|
|
|
class FeatureCard(BaseModel):
|
|
"""Single feature in features section."""
|
|
|
|
icon: str = ""
|
|
title: TranslatableText = Field(default_factory=TranslatableText)
|
|
description: TranslatableText = Field(default_factory=TranslatableText)
|
|
|
|
|
|
class ProductCard(BaseModel):
|
|
"""Single product/offering card in products section."""
|
|
|
|
icon: str = ""
|
|
title: TranslatableText = Field(default_factory=TranslatableText)
|
|
description: TranslatableText = Field(default_factory=TranslatableText)
|
|
url: str = ""
|
|
badge: TranslatableText | None = None
|
|
link_text: TranslatableText | None = None
|
|
|
|
|
|
class ProductsSection(BaseModel):
|
|
"""Product/offering showcase section (e.g. wizard.lu multi-product landing)."""
|
|
|
|
enabled: bool = True
|
|
title: TranslatableText = Field(default_factory=TranslatableText)
|
|
subtitle: TranslatableText | None = None
|
|
products: list[ProductCard] = Field(default_factory=list)
|
|
|
|
|
|
class FeaturesSection(BaseModel):
|
|
"""Features section configuration."""
|
|
|
|
enabled: bool = True
|
|
title: TranslatableText = Field(default_factory=TranslatableText)
|
|
subtitle: TranslatableText | None = None
|
|
features: list[FeatureCard] = Field(default_factory=list)
|
|
layout: str = Field(default="grid", description="grid, list, cards")
|
|
|
|
|
|
class PricingSection(BaseModel):
|
|
"""Pricing section configuration."""
|
|
|
|
enabled: bool = True
|
|
title: TranslatableText = Field(default_factory=TranslatableText)
|
|
subtitle: TranslatableText | None = None
|
|
use_subscription_tiers: bool = Field(
|
|
default=True, description="Pull pricing from subscription_tiers table dynamically"
|
|
)
|
|
# UI label overrides (CMS-driven, with hardcoded English fallback in template)
|
|
monthly_label: TranslatableText | None = None
|
|
annual_label: TranslatableText | None = None
|
|
save_text: TranslatableText | None = None
|
|
popular_badge: TranslatableText | None = None
|
|
cta_text: TranslatableText | None = None
|
|
per_month_label: TranslatableText | None = None
|
|
per_year_label: TranslatableText | None = None
|
|
coming_soon_text: TranslatableText | None = None
|
|
|
|
|
|
class CTASection(BaseModel):
|
|
"""Call-to-action section configuration."""
|
|
|
|
enabled: bool = True
|
|
title: TranslatableText = Field(default_factory=TranslatableText)
|
|
subtitle: TranslatableText | None = None
|
|
buttons: list[HeroButton] = Field(default_factory=list)
|
|
background_type: str = Field(
|
|
default="gradient", description="gradient, image, solid"
|
|
)
|
|
|
|
|
|
class HomepageSections(BaseModel):
|
|
"""Complete homepage sections structure."""
|
|
|
|
hero: HeroSection | None = None
|
|
products: ProductsSection | None = None
|
|
features: FeaturesSection | None = None
|
|
pricing: PricingSection | None = None
|
|
cta: CTASection | None = None
|
|
|
|
@classmethod
|
|
def get_empty_structure(cls, languages: list[str]) -> "HomepageSections":
|
|
"""
|
|
Create empty section structure with language placeholders.
|
|
|
|
Args:
|
|
languages: List of language codes from platform.supported_languages
|
|
|
|
Returns:
|
|
HomepageSections with empty translations for all languages
|
|
"""
|
|
|
|
def make_translatable(langs: list[str]) -> TranslatableText:
|
|
return TranslatableText(translations=dict.fromkeys(langs, ""))
|
|
|
|
return cls(
|
|
hero=HeroSection(
|
|
title=make_translatable(languages),
|
|
subtitle=make_translatable(languages),
|
|
buttons=[],
|
|
),
|
|
products=ProductsSection(
|
|
title=make_translatable(languages),
|
|
products=[],
|
|
),
|
|
features=FeaturesSection(
|
|
title=make_translatable(languages),
|
|
features=[],
|
|
),
|
|
pricing=PricingSection(
|
|
title=make_translatable(languages),
|
|
use_subscription_tiers=True,
|
|
),
|
|
cta=CTASection(
|
|
title=make_translatable(languages),
|
|
buttons=[],
|
|
),
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# API Request/Response Schemas
|
|
# =============================================================================
|
|
|
|
|
|
class SectionUpdateRequest(BaseModel):
|
|
"""Request to update a single section."""
|
|
|
|
section_name: str = Field(..., description="hero, products, features, pricing, or cta")
|
|
section_data: dict = Field(..., description="Section configuration")
|
|
|
|
|
|
class HomepageSectionsResponse(BaseModel):
|
|
"""Response containing all homepage sections with platform language info."""
|
|
|
|
sections: HomepageSections | None = None
|
|
supported_languages: list[str] = Field(default_factory=lambda: ["fr", "de", "en"])
|
|
default_language: str = "fr"
|