feat(i18n): add multilingual platform descriptions and HostWizard demo data
Some checks failed
Some checks failed
- Add description_translations JSON column to Platform model + migration - Add language tabs to platform admin edit form for multilingual descriptions - Update API schemas to include description_translations in request/response - Translate pricing section UI labels via _t() macro (monthly/annual/CTA/etc.) - Add Luxembourgish (lb) support to all platforms (OMS, Main, Loyalty, Hosting) - Seed description_translations, contact emails, and social links for all platforms - Add LuxWeb Agency demo merchant with hosting stores, team, and content pages - Fix language code typo: lu → lb in platform-edit.js availableLanguages - Fix store content pages to use correct primary platform instead of hardcoded OMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
#}
|
#}
|
||||||
|
|
||||||
{# Helper macro: resolve a TranslatableText field with fallback #}
|
{# Helper macro: resolve a TranslatableText field with fallback #}
|
||||||
{% macro _t(field, fallback='') %}
|
{% macro _t(field, lang, default_lang, fallback='') %}
|
||||||
{%- if field and field.translations -%}
|
{%- if field and field.translations -%}
|
||||||
{{ field.translations.get(lang) or field.translations.get(default_lang) or fallback }}
|
{{ field.translations.get(lang) or field.translations.get(default_lang) or fallback }}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
@@ -25,12 +25,12 @@
|
|||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
{# Section header #}
|
{# Section header #}
|
||||||
<div class="text-center mb-12">
|
<div class="text-center mb-12">
|
||||||
{% set title = _t(pricing.title, 'Simple, Transparent Pricing') %}
|
{% set title = _t(pricing.title, lang, default_lang, 'Simple, Transparent Pricing') %}
|
||||||
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{% set subtitle = _t(pricing.subtitle) %}
|
{% set subtitle = _t(pricing.subtitle, lang, default_lang) %}
|
||||||
{% if subtitle|trim %}
|
{% if subtitle|trim %}
|
||||||
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||||
{{ subtitle }}
|
{{ subtitle }}
|
||||||
@@ -40,13 +40,13 @@
|
|||||||
|
|
||||||
{# Pricing toggle (monthly/annual) #}
|
{# Pricing toggle (monthly/annual) #}
|
||||||
{% if pricing.use_subscription_tiers and tiers %}
|
{% if pricing.use_subscription_tiers and tiers %}
|
||||||
{% set monthly_label = _t(pricing.monthly_label, 'Monthly') %}
|
{% set monthly_label = _t(pricing.monthly_label, lang, default_lang, 'Monthly') %}
|
||||||
{% set annual_label = _t(pricing.annual_label, 'Annual') %}
|
{% set annual_label = _t(pricing.annual_label, lang, default_lang, 'Annual') %}
|
||||||
{% set save_text = _t(pricing.save_text, 'Save 2 months!') %}
|
{% set save_text = _t(pricing.save_text, lang, default_lang, 'Save 2 months!') %}
|
||||||
{% set popular_badge = _t(pricing.popular_badge, 'Most Popular') %}
|
{% set popular_badge = _t(pricing.popular_badge, lang, default_lang, 'Most Popular') %}
|
||||||
{% set cta_text = _t(pricing.cta_text, 'Start Free Trial') %}
|
{% set cta_text = _t(pricing.cta_text, lang, default_lang, 'Start Free Trial') %}
|
||||||
{% set per_month = _t(pricing.per_month_label, '/month') %}
|
{% set per_month = _t(pricing.per_month_label, lang, default_lang, '/month') %}
|
||||||
{% set per_year = _t(pricing.per_year_label, '/year') %}
|
{% set per_year = _t(pricing.per_year_label, lang, default_lang, '/year') %}
|
||||||
|
|
||||||
<div x-data="{ annual: false }" class="space-y-8">
|
<div x-data="{ annual: false }" class="space-y-8">
|
||||||
{# Billing toggle #}
|
{# Billing toggle #}
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{# Placeholder when no tiers available #}
|
{# Placeholder when no tiers available #}
|
||||||
{% set coming_soon = _t(pricing.coming_soon_text, 'Pricing plans coming soon') %}
|
{% set coming_soon = _t(pricing.coming_soon_text, lang, default_lang, 'Pricing plans coming soon') %}
|
||||||
<div class="text-center text-gray-500 dark:text-gray-400 py-8">
|
<div class="text-center text-gray-500 dark:text-gray-400 py-8">
|
||||||
{{ coming_soon }}
|
{{ coming_soon }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""add description_translations to platforms
|
||||||
|
|
||||||
|
Revision ID: tenancy_004
|
||||||
|
Revises: billing_002
|
||||||
|
Create Date: 2026-03-04
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "tenancy_004"
|
||||||
|
down_revision = "billing_002"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"platforms",
|
||||||
|
sa.Column(
|
||||||
|
"description_translations",
|
||||||
|
sa.JSON(),
|
||||||
|
nullable=True,
|
||||||
|
comment="Language-keyed description dict for multi-language support",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("platforms", "description_translations")
|
||||||
@@ -71,6 +71,13 @@ class Platform(Base, TimestampMixin):
|
|||||||
comment="Platform description for admin/marketing purposes",
|
comment="Platform description for admin/marketing purposes",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
description_translations = Column(
|
||||||
|
JSON,
|
||||||
|
nullable=True,
|
||||||
|
default=None,
|
||||||
|
comment="Language-keyed description dict for multi-language support",
|
||||||
|
)
|
||||||
|
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
# Domain Routing
|
# Domain Routing
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
@@ -226,6 +233,17 @@ class Platform(Base, TimestampMixin):
|
|||||||
# Properties
|
# Properties
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
|
|
||||||
|
def get_translated_description(self, lang: str, default_lang: str = "fr") -> str:
|
||||||
|
"""Return description in the requested language with fallback."""
|
||||||
|
if self.description_translations:
|
||||||
|
return (
|
||||||
|
self.description_translations.get(lang)
|
||||||
|
or self.description_translations.get(default_lang)
|
||||||
|
or self.description
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
return self.description or ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_url(self) -> str:
|
def base_url(self) -> str:
|
||||||
"""Get the base URL for this platform (for link generation)."""
|
"""Get the base URL for this platform (for link generation)."""
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class PlatformResponse(BaseModel):
|
|||||||
code: str
|
code: str
|
||||||
name: str
|
name: str
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
|
description_translations: dict[str, str] | None = None
|
||||||
domain: str | None = None
|
domain: str | None = None
|
||||||
path_prefix: str | None = None
|
path_prefix: str | None = None
|
||||||
logo: str | None = None
|
logo: str | None = None
|
||||||
@@ -76,6 +77,7 @@ class PlatformUpdateRequest(BaseModel):
|
|||||||
|
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
|
description_translations: dict[str, str] | None = None
|
||||||
domain: str | None = None
|
domain: str | None = None
|
||||||
path_prefix: str | None = None
|
path_prefix: str | None = None
|
||||||
logo: str | None = None
|
logo: str | None = None
|
||||||
@@ -115,6 +117,7 @@ def _build_platform_response(db: Session, platform) -> PlatformResponse:
|
|||||||
code=platform.code,
|
code=platform.code,
|
||||||
name=platform.name,
|
name=platform.name,
|
||||||
description=platform.description,
|
description=platform.description,
|
||||||
|
description_translations=platform.description_translations,
|
||||||
domain=platform.domain,
|
domain=platform.domain,
|
||||||
path_prefix=platform.path_prefix,
|
path_prefix=platform.path_prefix,
|
||||||
logo=platform.logo,
|
logo=platform.logo,
|
||||||
|
|||||||
@@ -22,10 +22,20 @@ function platformEdit() {
|
|||||||
success: null,
|
success: null,
|
||||||
platformCode: null,
|
platformCode: null,
|
||||||
|
|
||||||
|
// Language editing
|
||||||
|
currentLang: 'fr',
|
||||||
|
languageNames: {
|
||||||
|
fr: 'Fran\u00e7ais',
|
||||||
|
de: 'Deutsch',
|
||||||
|
en: 'English',
|
||||||
|
lb: 'L\u00ebtzebuergesch',
|
||||||
|
},
|
||||||
|
|
||||||
// Form data
|
// Form data
|
||||||
formData: {
|
formData: {
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
description_translations: {},
|
||||||
domain: '',
|
domain: '',
|
||||||
path_prefix: '',
|
path_prefix: '',
|
||||||
logo: '',
|
logo: '',
|
||||||
@@ -46,7 +56,7 @@ function platformEdit() {
|
|||||||
{ code: 'fr', name: 'French' },
|
{ code: 'fr', name: 'French' },
|
||||||
{ code: 'de', name: 'German' },
|
{ code: 'de', name: 'German' },
|
||||||
{ code: 'en', name: 'English' },
|
{ code: 'en', name: 'English' },
|
||||||
{ code: 'lu', name: 'Luxembourgish' },
|
{ code: 'lb', name: 'Luxembourgish' },
|
||||||
],
|
],
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
@@ -92,23 +102,34 @@ function platformEdit() {
|
|||||||
const response = await apiClient.get(`/admin/platforms/${this.platformCode}`);
|
const response = await apiClient.get(`/admin/platforms/${this.platformCode}`);
|
||||||
this.platform = response;
|
this.platform = response;
|
||||||
|
|
||||||
|
// Build description_translations with empty strings for all supported languages
|
||||||
|
const langs = response.supported_languages || ['fr', 'de', 'en'];
|
||||||
|
const descTranslations = {};
|
||||||
|
for (const lang of langs) {
|
||||||
|
descTranslations[lang] = (response.description_translations && response.description_translations[lang]) || '';
|
||||||
|
}
|
||||||
|
|
||||||
// Populate form data
|
// Populate form data
|
||||||
this.formData = {
|
this.formData = {
|
||||||
name: response.name || '',
|
name: response.name || '',
|
||||||
description: response.description || '',
|
description: response.description || '',
|
||||||
|
description_translations: descTranslations,
|
||||||
domain: response.domain || '',
|
domain: response.domain || '',
|
||||||
path_prefix: response.path_prefix || '',
|
path_prefix: response.path_prefix || '',
|
||||||
logo: response.logo || '',
|
logo: response.logo || '',
|
||||||
logo_dark: response.logo_dark || '',
|
logo_dark: response.logo_dark || '',
|
||||||
favicon: response.favicon || '',
|
favicon: response.favicon || '',
|
||||||
default_language: response.default_language || 'fr',
|
default_language: response.default_language || 'fr',
|
||||||
supported_languages: response.supported_languages || ['fr', 'de', 'en'],
|
supported_languages: langs,
|
||||||
is_active: response.is_active ?? true,
|
is_active: response.is_active ?? true,
|
||||||
is_public: response.is_public ?? true,
|
is_public: response.is_public ?? true,
|
||||||
theme_config: response.theme_config || {},
|
theme_config: response.theme_config || {},
|
||||||
settings: response.settings || {},
|
settings: response.settings || {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set current language tab to default language
|
||||||
|
this.currentLang = response.default_language || 'fr';
|
||||||
|
|
||||||
platformEditLog.info(`Loaded platform: ${this.platformCode}`);
|
platformEditLog.info(`Loaded platform: ${this.platformCode}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
platformEditLog.error('Error loading platform:', err);
|
platformEditLog.error('Error loading platform:', err);
|
||||||
@@ -126,9 +147,14 @@ function platformEdit() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Build update payload (only changed fields)
|
// Build update payload (only changed fields)
|
||||||
|
// Sync base description from default language translation
|
||||||
|
const defaultLang = this.formData.default_language || 'fr';
|
||||||
|
const baseDesc = this.formData.description_translations[defaultLang] || this.formData.description;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name: this.formData.name,
|
name: this.formData.name,
|
||||||
description: this.formData.description || null,
|
description: baseDesc || null,
|
||||||
|
description_translations: this.formData.description_translations,
|
||||||
domain: this.formData.domain || null,
|
domain: this.formData.domain || null,
|
||||||
path_prefix: this.formData.path_prefix || null,
|
path_prefix: this.formData.path_prefix || null,
|
||||||
logo: this.formData.logo || null,
|
logo: this.formData.logo || null,
|
||||||
@@ -199,9 +225,17 @@ function platformEdit() {
|
|||||||
// Don't allow removing the last language
|
// Don't allow removing the last language
|
||||||
if (this.formData.supported_languages.length > 1) {
|
if (this.formData.supported_languages.length > 1) {
|
||||||
this.formData.supported_languages.splice(index, 1);
|
this.formData.supported_languages.splice(index, 1);
|
||||||
|
// Switch tab if the removed language was active
|
||||||
|
if (this.currentLang === code) {
|
||||||
|
this.currentLang = this.formData.supported_languages[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.formData.supported_languages.push(code);
|
this.formData.supported_languages.push(code);
|
||||||
|
// Initialize empty translation for new language
|
||||||
|
if (!this.formData.description_translations[code]) {
|
||||||
|
this.formData.description_translations[code] = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -130,19 +130,41 @@
|
|||||||
<span x-show="errors.name" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.name"></span>
|
<span x-show="errors.name" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.name"></span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description (multilingual) -->
|
||||||
<label class="block mb-4 text-sm">
|
<div class="block mb-4 text-sm">
|
||||||
<span class="text-gray-700 dark:text-gray-400">
|
<span class="text-gray-700 dark:text-gray-400">
|
||||||
Description
|
Description
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!-- Language Tabs -->
|
||||||
|
<div class="mt-2 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<nav class="flex -mb-px space-x-2">
|
||||||
|
<template x-for="lang in formData.supported_languages" :key="lang">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="currentLang = lang"
|
||||||
|
:class="currentLang === lang
|
||||||
|
? 'border-purple-500 text-purple-600 dark:text-purple-400'
|
||||||
|
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
|
||||||
|
class="py-2 px-3 border-b-2 font-medium text-xs transition-colors"
|
||||||
|
>
|
||||||
|
<span x-text="languageNames[lang] || lang.toUpperCase()"></span>
|
||||||
|
<span x-show="lang === formData.default_language" class="ml-1 text-xs text-gray-400">(default)</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Per-language textarea -->
|
||||||
<textarea
|
<textarea
|
||||||
x-model="formData.description"
|
x-model="formData.description_translations[currentLang]"
|
||||||
rows="3"
|
rows="3"
|
||||||
maxlength="500"
|
maxlength="500"
|
||||||
:disabled="saving"
|
:disabled="saving"
|
||||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
|
:placeholder="'Description in ' + (languageNames[currentLang] || currentLang)"
|
||||||
|
class="block w-full mt-2 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
|
||||||
></textarea>
|
></textarea>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<!-- Default Language -->
|
<!-- Default Language -->
|
||||||
<label class="block mb-4 text-sm">
|
<label class="block mb-4 text-sm">
|
||||||
|
|||||||
@@ -214,45 +214,103 @@ def create_default_platforms(db: Session) -> list[Platform]:
|
|||||||
"code": "oms",
|
"code": "oms",
|
||||||
"name": "OMS",
|
"name": "OMS",
|
||||||
"description": "Order Management System for multi-store e-commerce",
|
"description": "Order Management System for multi-store e-commerce",
|
||||||
|
"description_translations": {
|
||||||
|
"fr": "Système de gestion des commandes pour le commerce multi-boutiques",
|
||||||
|
"de": "Bestellverwaltungssystem für Multi-Store-E-Commerce",
|
||||||
|
"en": "Order Management System for multi-store e-commerce",
|
||||||
|
"lb": "Bestellverwaltungssystem fir Multi-Store-E-Commerce",
|
||||||
|
},
|
||||||
"domain": "omsflow.lu",
|
"domain": "omsflow.lu",
|
||||||
"path_prefix": "oms",
|
"path_prefix": "oms",
|
||||||
"default_language": "fr",
|
"default_language": "fr",
|
||||||
"supported_languages": ["fr", "de", "en"],
|
"supported_languages": ["fr", "de", "en", "lb"],
|
||||||
"settings": {},
|
"settings": {
|
||||||
|
"contact_email": "info@omsflow.lu",
|
||||||
|
"support_email": "support@omsflow.lu",
|
||||||
|
},
|
||||||
"theme_config": {},
|
"theme_config": {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "main",
|
"code": "main",
|
||||||
"name": "Wizard",
|
"name": "Wizard",
|
||||||
"description": "Main marketing site showcasing all Orion platforms",
|
"description": "Main marketing site showcasing all Orion platforms",
|
||||||
|
"description_translations": {
|
||||||
|
"fr": "Site marketing principal présentant toutes les plateformes Orion",
|
||||||
|
"de": "Zentrale Marketingseite für alle Orion-Plattformen",
|
||||||
|
"en": "Main marketing site showcasing all Orion platforms",
|
||||||
|
"lb": "Haaptmarketingsäit fir all Orion-Plattformen",
|
||||||
|
},
|
||||||
"domain": "wizard.lu",
|
"domain": "wizard.lu",
|
||||||
"path_prefix": None,
|
"path_prefix": None,
|
||||||
"default_language": "fr",
|
"default_language": "fr",
|
||||||
"supported_languages": ["fr", "de", "en"],
|
"supported_languages": ["fr", "de", "en", "lb"],
|
||||||
"settings": {"is_marketing_site": True},
|
"settings": {
|
||||||
"theme_config": {"primary_color": "#2563EB", "secondary_color": "#3B82F6"},
|
"is_marketing_site": True,
|
||||||
|
"contact_email": "info@wizard.lu",
|
||||||
|
"support_email": "support@wizard.lu",
|
||||||
|
},
|
||||||
|
"theme_config": {
|
||||||
|
"primary_color": "#2563EB",
|
||||||
|
"secondary_color": "#3B82F6",
|
||||||
|
"social": {
|
||||||
|
"facebook": "https://facebook.com/wizard.lu",
|
||||||
|
"linkedin": "https://linkedin.com/company/wizard-lu",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "loyalty",
|
"code": "loyalty",
|
||||||
"name": "Loyalty",
|
"name": "Loyalty",
|
||||||
"description": "Customer loyalty program platform for Luxembourg businesses",
|
"description": "Customer loyalty program platform for Luxembourg businesses",
|
||||||
|
"description_translations": {
|
||||||
|
"fr": "Plateforme de programme de fidélité pour les entreprises luxembourgeoises",
|
||||||
|
"de": "Kundenbindungsprogramm-Plattform für luxemburgische Unternehmen",
|
||||||
|
"en": "Customer loyalty program platform for Luxembourg businesses",
|
||||||
|
"lb": "Clientsfidélitéitsprogramm-Plattform fir lëtzebuerger Betriber",
|
||||||
|
},
|
||||||
"domain": "rewardflow.lu",
|
"domain": "rewardflow.lu",
|
||||||
"path_prefix": "loyalty",
|
"path_prefix": "loyalty",
|
||||||
"default_language": "fr",
|
"default_language": "fr",
|
||||||
"supported_languages": ["fr", "de", "en"],
|
"supported_languages": ["fr", "de", "en", "lb"],
|
||||||
"settings": {"features": ["points", "rewards", "tiers", "analytics"]},
|
"settings": {
|
||||||
"theme_config": {"primary_color": "#8B5CF6", "secondary_color": "#A78BFA"},
|
"features": ["points", "rewards", "tiers", "analytics"],
|
||||||
|
"contact_email": "info@rewardflow.lu",
|
||||||
|
"support_email": "support@rewardflow.lu",
|
||||||
|
},
|
||||||
|
"theme_config": {
|
||||||
|
"primary_color": "#8B5CF6",
|
||||||
|
"secondary_color": "#A78BFA",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "hosting",
|
"code": "hosting",
|
||||||
"name": "HostWizard",
|
"name": "HostWizard",
|
||||||
"description": "Web hosting, domains, email, and website building for Luxembourg businesses",
|
"description": "Web hosting, domains, email, and website building for Luxembourg businesses",
|
||||||
|
"description_translations": {
|
||||||
|
"fr": "Hébergement web, domaines, e-mail et création de sites pour les entreprises luxembourgeoises",
|
||||||
|
"de": "Webhosting, Domains, E-Mail und Website-Erstellung für luxemburgische Unternehmen",
|
||||||
|
"en": "Web hosting, domains, email, and website building for Luxembourg businesses",
|
||||||
|
"lb": "Webhosting, Domainer, E-Mail a Websäit-Erstellung fir lëtzebuerger Betriber",
|
||||||
|
},
|
||||||
"domain": "hostwizard.lu",
|
"domain": "hostwizard.lu",
|
||||||
"path_prefix": "hosting",
|
"path_prefix": "hosting",
|
||||||
"default_language": "fr",
|
"default_language": "fr",
|
||||||
"supported_languages": ["fr", "de", "en", "lb"],
|
"supported_languages": ["fr", "de", "en", "lb"],
|
||||||
"settings": {"features": ["hosting", "domains", "email", "ssl", "poc_sites"]},
|
"settings": {
|
||||||
"theme_config": {"primary_color": "#0D9488", "secondary_color": "#14B8A6"},
|
"features": ["hosting", "domains", "email", "ssl", "poc_sites"],
|
||||||
|
"contact_email": "info@hostwizard.lu",
|
||||||
|
"support_email": "support@hostwizard.lu",
|
||||||
|
"sales_email": "sales@hostwizard.lu",
|
||||||
|
},
|
||||||
|
"theme_config": {
|
||||||
|
"primary_color": "#0D9488",
|
||||||
|
"secondary_color": "#14B8A6",
|
||||||
|
"social": {
|
||||||
|
"facebook": "https://facebook.com/hostwizard.lu",
|
||||||
|
"linkedin": "https://linkedin.com/company/hostwizard",
|
||||||
|
"instagram": "https://instagram.com/hostwizard.lu",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -271,6 +329,7 @@ def create_default_platforms(db: Session) -> list[Platform]:
|
|||||||
code=pdef["code"],
|
code=pdef["code"],
|
||||||
name=pdef["name"],
|
name=pdef["name"],
|
||||||
description=pdef["description"],
|
description=pdef["description"],
|
||||||
|
description_translations=pdef.get("description_translations"),
|
||||||
domain=pdef["domain"],
|
domain=pdef["domain"],
|
||||||
path_prefix=pdef["path_prefix"],
|
path_prefix=pdef["path_prefix"],
|
||||||
default_language=pdef["default_language"],
|
default_language=pdef["default_language"],
|
||||||
|
|||||||
@@ -140,6 +140,19 @@ DEMO_COMPANIES = [
|
|||||||
"business_address": "789 Library Lane, Esch-sur-Alzette, L-9012, Luxembourg",
|
"business_address": "789 Library Lane, Esch-sur-Alzette, L-9012, Luxembourg",
|
||||||
"tax_number": "LU34567890",
|
"tax_number": "LU34567890",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "LuxWeb Agency S.à r.l.",
|
||||||
|
"description": "Web design and hosting agency serving Luxembourg businesses",
|
||||||
|
"owner_email": "marc.owner@luxweb.lu",
|
||||||
|
"owner_password": "password123", # noqa: SEC001
|
||||||
|
"owner_first_name": "Marc",
|
||||||
|
"owner_last_name": "Weber",
|
||||||
|
"contact_email": "info@luxweb.lu",
|
||||||
|
"contact_phone": "+352 456 789 012",
|
||||||
|
"website": "https://www.luxweb.lu",
|
||||||
|
"business_address": "12 Rue du Web, Differdange, L-4501, Luxembourg",
|
||||||
|
"tax_number": "LU45678901",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Demo store configurations (linked to merchants by index)
|
# Demo store configurations (linked to merchants by index)
|
||||||
@@ -212,6 +225,26 @@ DEMO_STORES = [
|
|||||||
"theme_preset": "modern",
|
"theme_preset": "modern",
|
||||||
"custom_domain": None,
|
"custom_domain": None,
|
||||||
},
|
},
|
||||||
|
# LuxWeb Agency stores (hosting platform)
|
||||||
|
{
|
||||||
|
"merchant_index": 3, # LuxWeb Agency
|
||||||
|
"store_code": "LUXWEBSITES",
|
||||||
|
"name": "LuxWeb Sites",
|
||||||
|
"subdomain": "luxweb",
|
||||||
|
"description": "Professional websites for Luxembourg businesses",
|
||||||
|
"theme_preset": "modern",
|
||||||
|
"custom_domain": "luxweb.lu",
|
||||||
|
"custom_domain_platform": "hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"merchant_index": 3, # LuxWeb Agency
|
||||||
|
"store_code": "LUXWEBHOSTING",
|
||||||
|
"name": "LuxWeb Hosting",
|
||||||
|
"subdomain": "luxwebhosting",
|
||||||
|
"description": "Web hosting, domains, and email services",
|
||||||
|
"theme_preset": "modern",
|
||||||
|
"custom_domain": None,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Demo subscriptions (linked to merchants by index)
|
# Demo subscriptions (linked to merchants by index)
|
||||||
@@ -223,6 +256,8 @@ DEMO_SUBSCRIPTIONS = [
|
|||||||
{"merchant_index": 1, "platform_code": "loyalty", "tier_code": "essential", "trial_days": 14},
|
{"merchant_index": 1, "platform_code": "loyalty", "tier_code": "essential", "trial_days": 14},
|
||||||
# BookWorld: OMS (business, active)
|
# BookWorld: OMS (business, active)
|
||||||
{"merchant_index": 2, "platform_code": "oms", "tier_code": "business", "trial_days": 0},
|
{"merchant_index": 2, "platform_code": "oms", "tier_code": "business", "trial_days": 0},
|
||||||
|
# LuxWeb Agency: Hosting (professional, active)
|
||||||
|
{"merchant_index": 3, "platform_code": "hosting", "tier_code": "professional", "trial_days": 0},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Demo team members (linked to merchants by index, assigned to stores by store_code)
|
# Demo team members (linked to merchants by index, assigned to stores by store_code)
|
||||||
@@ -276,6 +311,25 @@ DEMO_TEAM_MEMBERS = [
|
|||||||
"role": "manager",
|
"role": "manager",
|
||||||
"store_codes": ["BOOKSTORE", "BOOKDIGITAL"],
|
"store_codes": ["BOOKSTORE", "BOOKDIGITAL"],
|
||||||
},
|
},
|
||||||
|
# LuxWeb Agency team
|
||||||
|
{
|
||||||
|
"merchant_index": 3,
|
||||||
|
"email": "sophie.dev@luxweb.lu",
|
||||||
|
"password": "password123", # noqa: SEC001
|
||||||
|
"first_name": "Sophie",
|
||||||
|
"last_name": "Developer",
|
||||||
|
"role": "manager",
|
||||||
|
"store_codes": ["LUXWEBSITES", "LUXWEBHOSTING"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"merchant_index": 3,
|
||||||
|
"email": "tom.support@luxweb.lu",
|
||||||
|
"password": "password123", # noqa: SEC001
|
||||||
|
"first_name": "Tom",
|
||||||
|
"last_name": "Support",
|
||||||
|
"role": "support",
|
||||||
|
"store_codes": ["LUXWEBHOSTING"],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Theme presets
|
# Theme presets
|
||||||
@@ -397,6 +451,69 @@ STORE_CONTENT_PAGES = {
|
|||||||
"show_in_footer": True,
|
"show_in_footer": True,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"LUXWEBSITES": [
|
||||||
|
{
|
||||||
|
"slug": "about",
|
||||||
|
"title": "About LuxWeb Sites",
|
||||||
|
"content": """
|
||||||
|
<div class="prose-content">
|
||||||
|
<h2>Welcome to LuxWeb Sites</h2>
|
||||||
|
<p>Professional web design and development for Luxembourg businesses.
|
||||||
|
We create modern, responsive websites that help your business grow online.</p>
|
||||||
|
|
||||||
|
<h3>Our Services</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Website Design:</strong> Custom designs tailored to your brand</li>
|
||||||
|
<li><strong>E-commerce:</strong> Online shops with secure payment integration</li>
|
||||||
|
<li><strong>Multilingual Sites:</strong> FR, DE, EN, and LB support built-in</li>
|
||||||
|
<li><strong>SEO Optimization:</strong> Get found on Google Luxembourg</li>
|
||||||
|
<li><strong>Maintenance:</strong> Ongoing updates and technical support</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Why LuxWeb?</h3>
|
||||||
|
<p>Based in Luxembourg, we understand the local market. All our sites are GDPR compliant
|
||||||
|
and optimized for the multilingual Luxembourg audience.</p>
|
||||||
|
|
||||||
|
<h3>Contact Us</h3>
|
||||||
|
<p>12 Rue du Web, Differdange<br>
|
||||||
|
Email: info@luxweb.lu | Phone: +352 456 789 012</p>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
"meta_description": "LuxWeb Sites - Professional web design for Luxembourg businesses",
|
||||||
|
"show_in_header": True,
|
||||||
|
"show_in_footer": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "contact",
|
||||||
|
"title": "Contact LuxWeb",
|
||||||
|
"content": """
|
||||||
|
<div class="prose-content">
|
||||||
|
<h2>Get in Touch</h2>
|
||||||
|
|
||||||
|
<h3>Request a Quote</h3>
|
||||||
|
<p>Tell us about your project and we'll get back to you within 24 hours.</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Email:</strong> info@luxweb.lu</li>
|
||||||
|
<li><strong>Phone:</strong> +352 456 789 012</li>
|
||||||
|
<li><strong>WhatsApp:</strong> +352 456 789 012</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Technical Support</h3>
|
||||||
|
<p>Existing clients can reach our support team:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Email:</strong> support@luxweb.lu</li>
|
||||||
|
<li><strong>Hours:</strong> Monday-Friday, 8am-6pm CET</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Office</h3>
|
||||||
|
<p>12 Rue du Web<br>Differdange, L-4501<br>Luxembourg</p>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
"meta_description": "Contact LuxWeb for web design, hosting, and domain services in Luxembourg",
|
||||||
|
"show_in_header": True,
|
||||||
|
"show_in_footer": True,
|
||||||
|
},
|
||||||
|
],
|
||||||
"BOOKSTORE": [
|
"BOOKSTORE": [
|
||||||
{
|
{
|
||||||
"slug": "about",
|
"slug": "about",
|
||||||
@@ -1157,13 +1274,22 @@ def create_demo_store_content_pages(db: Session, stores: list[Store]) -> int:
|
|||||||
"""
|
"""
|
||||||
created_count = 0
|
created_count = 0
|
||||||
|
|
||||||
# Get the OMS platform ID (stores are registered on OMS)
|
# Build store→primary platform lookup
|
||||||
from app.modules.tenancy.models import Platform
|
from app.modules.tenancy.models import Platform
|
||||||
|
|
||||||
|
store_primary_platform: dict[int, int] = {}
|
||||||
|
sp_rows = db.execute(
|
||||||
|
select(StorePlatform.store_id, StorePlatform.platform_id)
|
||||||
|
.where(StorePlatform.is_primary == True) # noqa: E712
|
||||||
|
).all()
|
||||||
|
for store_id, platform_id in sp_rows:
|
||||||
|
store_primary_platform[store_id] = platform_id
|
||||||
|
|
||||||
|
# Fallback: OMS platform ID
|
||||||
oms_platform = db.execute(
|
oms_platform = db.execute(
|
||||||
select(Platform).where(Platform.code == "oms")
|
select(Platform).where(Platform.code == "oms")
|
||||||
).scalar_one_or_none()
|
).scalar_one_or_none()
|
||||||
default_platform_id = oms_platform.id if oms_platform else 1
|
fallback_platform_id = oms_platform.id if oms_platform else 1
|
||||||
|
|
||||||
for store in stores:
|
for store in stores:
|
||||||
store_pages = STORE_CONTENT_PAGES.get(store.store_code, [])
|
store_pages = STORE_CONTENT_PAGES.get(store.store_code, [])
|
||||||
@@ -1171,6 +1297,8 @@ def create_demo_store_content_pages(db: Session, stores: list[Store]) -> int:
|
|||||||
if not store_pages:
|
if not store_pages:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
platform_id = store_primary_platform.get(store.id, fallback_platform_id)
|
||||||
|
|
||||||
for page_data in store_pages:
|
for page_data in store_pages:
|
||||||
# Check if this store page already exists
|
# Check if this store page already exists
|
||||||
existing = db.execute(
|
existing = db.execute(
|
||||||
@@ -1185,7 +1313,7 @@ def create_demo_store_content_pages(db: Session, stores: list[Store]) -> int:
|
|||||||
|
|
||||||
# Create store content page override
|
# Create store content page override
|
||||||
page = ContentPage(
|
page = ContentPage(
|
||||||
platform_id=default_platform_id,
|
platform_id=platform_id,
|
||||||
store_id=store.id,
|
store_id=store.id,
|
||||||
slug=page_data["slug"],
|
slug=page_data["slug"],
|
||||||
title=page_data["title"],
|
title=page_data["title"],
|
||||||
|
|||||||
Reference in New Issue
Block a user