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

+ +

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

+ +

Sales

+

Interested in a website for your business?

+ +

Support

+

Already a customer? Our support team is here to help.

+ +
""", + "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 []