docs(i18n): document CMS template translations and multi-language content pages
Some checks failed
CI / dependency-scanning (push) Successful in 34s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
CI / ruff (push) Successful in 11s
CI / pytest (push) Failing after 48m7s
CI / validate (push) Successful in 28s

Add sections covering CMS locale file structure, translated template
inventory, TranslatableText pattern for sections, and the new
title_translations/content_translations model API with migration cms_002.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 18:00:00 +01:00
parent b8aa484653
commit 784bcb9d23
3 changed files with 441 additions and 2 deletions

View File

@@ -0,0 +1,72 @@
# app/modules/hosting/routes/api/admin_services.py
"""
Admin API routes for client service management.
"""
import logging
from fastapi import APIRouter, Depends, Path
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api
from app.core.database import get_db
from app.modules.hosting.schemas.client_service import (
ClientServiceCreate,
ClientServiceResponse,
ClientServiceUpdate,
)
from app.modules.hosting.services.client_service_service import client_service_service
from app.modules.tenancy.schemas.auth import UserContext
router = APIRouter(prefix="/sites/{site_id}/services")
logger = logging.getLogger(__name__)
@router.get("", response_model=list[ClientServiceResponse])
def list_services(
site_id: int = Path(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""List services for a hosted site."""
return client_service_service.get_for_site(db, site_id)
@router.post("", response_model=ClientServiceResponse)
def create_service(
data: ClientServiceCreate,
site_id: int = Path(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Create a new service for a hosted site."""
service = client_service_service.create(db, site_id, data.model_dump(exclude_none=True))
db.commit()
return service
@router.put("/{service_id}", response_model=ClientServiceResponse)
def update_service(
data: ClientServiceUpdate,
site_id: int = Path(...),
service_id: int = Path(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Update a client service."""
service = client_service_service.update(db, service_id, data.model_dump(exclude_none=True))
db.commit()
return service
@router.delete("/{service_id}")
def delete_service(
site_id: int = Path(...),
service_id: int = Path(...),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Delete a client service."""
client_service_service.delete(db, service_id)
db.commit()
return {"message": "Service deleted"} # noqa: API001

View File

@@ -295,19 +295,169 @@ curl http://localhost:8000/api/v1/language/current
curl http://localhost:8000/api/v1/language/list curl http://localhost:8000/api/v1/language/list
``` ```
## CMS Template i18n
All platform-facing templates use the `_()` translation function with keys from module-scoped locale files.
### Locale File Structure
CMS locale files are at `app/modules/cms/locales/{en,fr,de,lb}.json` with 340 keys organized as:
```
platform.nav.* — Navigation labels
platform.hero.* — Default homepage hero section
platform.pricing.* — Default homepage pricing section
platform.features.* — Default homepage feature labels
platform.signup.* — Signup flow
platform.success.* — Post-signup success page
platform.cta.* — Call-to-action section
platform.content_page.* — Content page UI chrome
platform.footer.* — Footer labels
platform.modern.* — Modern homepage template (~90 keys)
platform.minimal.* — Minimal homepage template (17 keys)
platform.find_shop.* — Find-your-shop page
platform.addons.* — Add-on pricing labels
messages.* — Flash/toast messages
confirmations.* — Confirmation dialogs
permissions.* — Permission labels
features.* — Feature gate labels
menu.* — Menu section labels
```
### Template Usage
```jinja2
{# Simple key lookup #}
{{ _("cms.platform.modern.hero_title_1") }}
{# With interpolation #}
{{ _("cms.platform.modern.cta_trial", trial_days=14) }}
```
### Translated Templates (Complete)
| Template | Keys Used | Status |
|----------|-----------|--------|
| `homepage-default.html` + section partials | `platform.hero.*`, `platform.pricing.*`, etc. | Fully translated |
| `homepage-modern.html` | `platform.modern.*` (~90 keys) | Fully translated |
| `homepage-minimal.html` | `platform.minimal.*` (17 keys) | Fully translated |
| `signup.html` | `platform.signup.*` | Fully translated |
| `signup-success.html` | `platform.success.*` | Fully translated |
| `content-page.html` | `platform.content_page.*` | Fully translated |
| `find-shop.html` | `platform.find_shop.*` | Fully translated |
| `base.html` | `platform.nav.*`, `platform.footer.*` | Fully translated |
### CMS Section Translations (TranslatableText Pattern)
Homepage sections stored in the `ContentPage.sections` JSON column use the `TranslatableText` pattern:
```json
{
"hero": {
"title": {"translations": {"fr": "Bienvenue", "en": "Welcome", "de": "Willkommen", "lb": "Wëllkomm"}}
}
}
```
Resolution in templates via `_t()` macro:
```jinja2
{% macro _t(field, fallback='') %}
{%- if field and field.translations -%}
{{ field.translations.get(lang) or field.translations.get(default_lang) or fallback }}
{%- endif -%}
{% endmacro %}
```
## Multi-Language Content Pages
### Migration: `cms_002_add_title_content_translations`
Added two nullable JSON columns to `content_pages`:
```sql
ALTER TABLE content_pages ADD COLUMN title_translations JSON;
ALTER TABLE content_pages ADD COLUMN content_translations JSON;
```
### Data Format
```json
{
"en": "About Us",
"fr": "À propos",
"de": "Über uns",
"lb": "Iwwer eis"
}
```
### Model API
```python
# Get translated title with fallback chain:
# title_translations[lang] → title_translations[default_lang] → self.title
page.get_translated_title(lang="de", default_lang="fr")
# Same for content:
page.get_translated_content(lang="de", default_lang="fr")
```
### Template Usage
In `content-page.html`:
```jinja2
{% set _lang = current_language|default('fr') %}
{% set _page_title = page.get_translated_title(_lang) %}
{% set _page_content = page.get_translated_content(_lang) %}
<h1>{{ _page_title }}</h1>
{{ _page_content | safe }}
```
In `base.html` (nav/footer):
```jinja2
{{ page.get_translated_title(current_language|default('fr')) }}
```
### Seed Script
The seed script (`scripts/seed/create_default_content_pages.py`) uses the `tt()` helper:
```python
def tt(en, fr, de, lb=None):
"""Build a language-keyed dict for title_translations."""
d = {"en": en, "fr": fr, "de": de}
if lb: d["lb"] = lb
return d
```
All platform pages, legal pages, and store defaults include `title_translations` with en/fr/de/lb values.
## Future Enhancements ## Future Enhancements
1. **Admin Language Support**: Currently admin is English-only. The system is designed to easily add admin language support later. 1. **Admin Language Support**: Currently admin is English-only. The system is designed to easily add admin language support later.
2. **Translation Management UI**: Add a UI for stores to manage their own translations (product descriptions, category names, etc.). 2. **Translation Management UI**: Add a language-tabbed title/content editor to the CMS admin for editing `title_translations` and `content_translations`.
3. **RTL Language Support**: The `is_rtl_language()` function is ready for future RTL language support (Arabic, Hebrew, etc.). 3. **RTL Language Support**: The `is_rtl_language()` function is ready for future RTL language support (Arabic, Hebrew, etc.).
4. **Auto-Translation**: Integration with translation APIs for automatic content translation. 4. **Auto-Translation**: Integration with translation APIs for automatic content translation.
5. **Content Translation**: Translate `content_translations` for platform marketing pages (currently only titles are translated; content remains English-only in the seed data).
## Rollback ## Rollback
To rollback this migration: To rollback the content page translations:
```bash
alembic downgrade cms_001
```
This will remove `title_translations` and `content_translations` from `content_pages`.
To rollback the base language settings:
```bash ```bash
alembic downgrade -1 alembic downgrade -1

View File

@@ -538,10 +538,107 @@ def _loyalty_homepage_sections() -> dict:
} }
def _hostwizard_homepage_sections() -> dict:
"""hostwizard.lu — web hosting & website building landing page."""
return {
"hero": {
"enabled": True,
"title": t(
"Votre site web professionnel au Luxembourg",
"Your Professional Website in Luxembourg",
"Ihre professionelle Website in Luxemburg",
"Är professionell Websäit zu Lëtzebuerg",
),
"subtitle": t(
"Sites web, domaines, e-mail et hébergement — tout en un pour les entreprises luxembourgeoises.",
"Websites, domains, email, and hosting — all-in-one for Luxembourg businesses.",
"Websites, Domains, E-Mail und Hosting — alles in einem für luxemburgische Unternehmen.",
"Websäiten, Domänen, E-Mail an Hosting — alles an engem fir lëtzebuerger Betriber.",
),
"cta_text": t("Demander un devis", "Get a Quote", "Angebot anfordern", "Offert ufroen"),
"cta_url": "/contact",
},
"features": {
"enabled": True,
"title": t("Nos services", "Our Services", "Unsere Dienstleistungen", "Eis Servicer"),
"items": [
{
"title": t("Création de sites web", "Website Creation", "Website-Erstellung", "Websäit Erstellen"),
"description": t(
"Sites web professionnels avec CMS intégré pour gérer votre contenu facilement.",
"Professional websites with integrated CMS to manage your content easily.",
"Professionelle Websites mit integriertem CMS zur einfachen Verwaltung Ihrer Inhalte.",
"Professionell Websäiten mat integréiertem CMS fir Ären Inhalt einfach ze geréieren.",
),
"icon": "globe",
},
{
"title": t("Noms de domaine", "Domain Names", "Domainnamen", "Domänennimm"),
"description": t(
"Enregistrement et gestion de domaines .lu et internationaux.",
"Registration and management of .lu and international domains.",
"Registrierung und Verwaltung von .lu und internationalen Domains.",
"Registréierung a Gestioun vun .lu an internationalen Domänen.",
),
"icon": "at-symbol",
},
{
"title": t("E-mail professionnel", "Professional Email", "Professionelle E-Mail", "Professionell E-Mail"),
"description": t(
"Boîtes mail personnalisées avec votre nom de domaine.",
"Custom mailboxes with your domain name.",
"Individuelle Postfächer mit Ihrem Domainnamen.",
"Personaliséiert Postfächer mat Ärem Domännumm.",
),
"icon": "mail",
},
{
"title": t("Hébergement & SSL", "Hosting & SSL", "Hosting & SSL", "Hosting & SSL"),
"description": t(
"Hébergement sécurisé avec certificat SSL inclus.",
"Secure hosting with included SSL certificate.",
"Sicheres Hosting mit inkludiertem SSL-Zertifikat.",
"Séchert Hosting mat abegraff SSL-Zertifikat.",
),
"icon": "shield-check",
},
],
},
"pricing": {
"enabled": True,
"title": t("Tarifs", "Pricing", "Preise", "Präisser"),
"subtitle": t(
"Des formules adaptées à chaque entreprise.",
"Plans tailored to every business.",
"Pläne für jedes Unternehmen.",
"Pläng fir all Betrib.",
),
},
"cta": {
"enabled": True,
"title": t(
"Prêt à mettre votre entreprise en ligne ?",
"Ready to bring your business online?",
"Bereit, Ihr Unternehmen online zu bringen?",
"Prett fir Äre Betrib online ze bréngen?",
),
"subtitle": t(
"Contactez-nous pour un site web gratuit de démonstration.",
"Contact us for a free demo website.",
"Kontaktieren Sie uns für eine kostenlose Demo-Website.",
"Kontaktéiert eis fir eng gratis Demo-Websäit.",
),
"cta_text": t("Nous contacter", "Contact Us", "Kontaktieren", "Kontaktéiert eis"),
"cta_url": "/contact",
},
}
HOMEPAGE_SECTIONS = { HOMEPAGE_SECTIONS = {
"main": _wizard_homepage_sections, "main": _wizard_homepage_sections,
"oms": _oms_homepage_sections, "oms": _oms_homepage_sections,
"loyalty": _loyalty_homepage_sections, "loyalty": _loyalty_homepage_sections,
"hosting": _hostwizard_homepage_sections,
} }
@@ -775,6 +872,126 @@ def _get_platform_pages(platform_code: str) -> list[dict]:
}, },
] ]
if platform_code == "hosting":
return [
{
"slug": "about",
"title": "About HostWizard",
"title_translations": tt("About HostWizard", "À propos de HostWizard", "Über HostWizard", "Iwwer HostWizard"),
"content": """<div class="prose-content">
<h2>About HostWizard</h2>
<p>HostWizard (hostwizard.lu) provides professional web hosting, domain registration, and website creation for Luxembourg businesses.</p>
<h3>Our Services</h3>
<ul>
<li><strong>Website Creation:</strong> Professional websites with an integrated CMS for easy content management</li>
<li><strong>Domain Registration:</strong> .lu and international domain registration and management</li>
<li><strong>Professional Email:</strong> Custom mailboxes with your domain name</li>
<li><strong>Secure Hosting:</strong> Fast, reliable hosting with SSL certificates included</li>
<li><strong>Maintenance:</strong> Ongoing website maintenance and support</li>
</ul>
<h3>Built for Luxembourg</h3>
<p>Multilingual support (FR/DE/EN/LB) and tailored for the Luxembourg business landscape.</p>
</div>""",
"meta_description": "HostWizard — professional web hosting, domains, and website creation for Luxembourg businesses.",
"show_in_footer": True,
"show_in_header": True,
"display_order": 1,
},
{
"slug": "services",
"title": "Our Services",
"title_translations": tt("Our Services", "Nos services", "Unsere Dienstleistungen", "Eis Servicer"),
"content": """<div class="prose-content">
<h2>HostWizard Services</h2>
<h3>Website Creation</h3>
<p>We build professional websites for your business with our integrated CMS. You can edit your content anytime, or let us handle it for you.</p>
<h3>Domain Names</h3>
<p>Register and manage .lu domains and international domain names. We handle DNS configuration and renewals.</p>
<h3>Professional Email</h3>
<p>Get professional email addresses with your domain name (e.g., info@yourbusiness.lu). Multiple mailboxes available.</p>
<h3>Hosting & SSL</h3>
<p>Fast, secure hosting with free SSL certificates. Your website is always online and protected.</p>
<h3>Website Maintenance</h3>
<p>Ongoing updates, security patches, and content changes. We keep your website running smoothly.</p>
</div>""",
"meta_description": "HostWizard services — website creation, domains, email, hosting, and maintenance for Luxembourg businesses.",
"show_in_footer": True,
"show_in_header": True,
"display_order": 2,
},
{
"slug": "pricing",
"title": "Pricing",
"title_translations": tt("Pricing", "Tarifs", "Preise", "Präisser"),
"content": """<div class="prose-content">
<h2>HostWizard Pricing</h2>
<p>Transparent pricing for all our services. No hidden fees.</p>
<h3>Website Packages</h3>
<p>Contact us for a personalized quote based on your needs. We start with a free POC (proof of concept) website so you can see the result before committing.</p>
<h3>Domain Registration</h3>
<p>.lu domains starting from €29/year. International domains available.</p>
<h3>Email Hosting</h3>
<p>Professional email from €5/mailbox/month.</p>
<h3>Website Maintenance</h3>
<p>Monthly maintenance plans starting from €49/month.</p>
<p><strong>Contact us for a custom quote:</strong> info@hostwizard.lu</p>
</div>""",
"meta_description": "HostWizard pricing — transparent pricing for websites, domains, email, and hosting.",
"show_in_footer": True,
"show_in_header": True,
"display_order": 3,
},
{
"slug": "contact",
"title": "Contact HostWizard",
"title_translations": tt("Contact HostWizard", "Contacter HostWizard", "HostWizard kontaktieren", "HostWizard kontaktéieren"),
"content": """<div class="prose-content">
<h2>Contact HostWizard</h2>
<p>Ready to bring your business online? Get in touch with our team.</p>
<h3>General Inquiries</h3>
<ul>
<li><strong>Email:</strong> info@hostwizard.lu</li>
</ul>
<h3>Sales</h3>
<p>Interested in a website for your business?</p>
<ul>
<li><strong>Email:</strong> sales@hostwizard.lu</li>
</ul>
<h3>Support</h3>
<p>Already a customer? Our support team is here to help.</p>
<ul>
<li><strong>Email:</strong> support@hostwizard.lu</li>
</ul>
</div>""",
"meta_description": "Contact HostWizard for web hosting, domains, and website creation in Luxembourg.",
"show_in_footer": True,
"show_in_header": True,
"display_order": 4,
},
{
"slug": "faq",
"title": "FAQ",
"title_translations": tt("FAQ", "FAQ", "FAQ", "FAQ"),
"content": """<div class="prose-content">
<h2>Frequently Asked Questions</h2>
<h4>What is HostWizard?</h4>
<p>HostWizard provides web hosting, domain registration, email hosting, and website creation services for Luxembourg businesses.</p>
<h4>How does the POC website work?</h4>
<p>We create a free proof-of-concept website for your business. If you like it, we can make it your live website on your own domain.</p>
<h4>What domains can I register?</h4>
<p>We support .lu domains and most international domain extensions (.com, .eu, .net, etc.).</p>
<h4>Do you offer multilingual websites?</h4>
<p>Yes! Our CMS supports French, German, English, and Luxembourgish out of the box.</p>
<h4>What is included in website maintenance?</h4>
<p>Security updates, content changes, performance monitoring, and technical support.</p>
</div>""",
"meta_description": "Frequently asked questions about HostWizard web hosting and website creation services.",
"show_in_footer": True,
"show_in_header": False,
"display_order": 5,
},
]
return [] return []