diff --git a/app/modules/hosting/routes/api/admin_services.py b/app/modules/hosting/routes/api/admin_services.py
new file mode 100644
index 00000000..c920eddc
--- /dev/null
+++ b/app/modules/hosting/routes/api/admin_services.py
@@ -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
diff --git a/docs/development/migration/language-i18n-implementation.md b/docs/development/migration/language-i18n-implementation.md
index 48e3c1b0..aeeb203e 100644
--- a/docs/development/migration/language-i18n-implementation.md
+++ b/docs/development/migration/language-i18n-implementation.md
@@ -295,19 +295,169 @@ curl http://localhost:8000/api/v1/language/current
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) %}
+
+
{{ _page_title }}
+{{ _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
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.).
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
-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
alembic downgrade -1
diff --git a/scripts/seed/create_default_content_pages.py b/scripts/seed/create_default_content_pages.py
index 3125395f..1089dcc1 100755
--- a/scripts/seed/create_default_content_pages.py
+++ b/scripts/seed/create_default_content_pages.py
@@ -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 = {
"main": _wizard_homepage_sections,
"oms": _oms_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": """
+
About HostWizard
+
HostWizard (hostwizard.lu) provides professional web hosting, domain registration, and website creation for Luxembourg businesses.
+
Our Services
+
+ - Website Creation: Professional websites with an integrated CMS for easy content management
+ - Domain Registration: .lu and international domain registration and management
+ - Professional Email: Custom mailboxes with your domain name
+ - Secure Hosting: Fast, reliable hosting with SSL certificates included
+ - Maintenance: Ongoing website maintenance and support
+
+
Built for Luxembourg
+
Multilingual support (FR/DE/EN/LB) and tailored for the Luxembourg business landscape.
+
""",
+ "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": """
+
HostWizard Services
+
Website Creation
+
We build professional websites for your business with our integrated CMS. You can edit your content anytime, or let us handle it for you.
+
Domain Names
+
Register and manage .lu domains and international domain names. We handle DNS configuration and renewals.
+
Professional Email
+
Get professional email addresses with your domain name (e.g., info@yourbusiness.lu). Multiple mailboxes available.
+
Hosting & SSL
+
Fast, secure hosting with free SSL certificates. Your website is always online and protected.
+
Website Maintenance
+
Ongoing updates, security patches, and content changes. We keep your website running smoothly.
+
""",
+ "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": """
+
HostWizard Pricing
+
Transparent pricing for all our services. No hidden fees.
+
Website Packages
+
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.
+
Domain Registration
+
.lu domains starting from €29/year. International domains available.
+
Email Hosting
+
Professional email from €5/mailbox/month.
+
Website Maintenance
+
Monthly maintenance plans starting from €49/month.
+
Contact us for a custom quote: info@hostwizard.lu
+
""",
+ "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": """
+
Contact HostWizard
+
Ready to bring your business online? Get in touch with our team.
+
General Inquiries
+
+ - Email: info@hostwizard.lu
+
+
Sales
+
Interested in a website for your business?
+
+ - Email: sales@hostwizard.lu
+
+
Support
+
Already a customer? Our support team is here to help.
+
+ - Email: support@hostwizard.lu
+
+
""",
+ "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": """
+
Frequently Asked Questions
+
What is HostWizard?
+
HostWizard provides web hosting, domain registration, email hosting, and website creation services for Luxembourg businesses.
+
How does the POC website work?
+
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.
+
What domains can I register?
+
We support .lu domains and most international domain extensions (.com, .eu, .net, etc.).
+
Do you offer multilingual websites?
+
Yes! Our CMS supports French, German, English, and Luxembourgish out of the box.
+
What is included in website maintenance?
+
Security updates, content changes, performance monitoring, and technical support.
+
""",
+ "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 []