From 820ab1aaa4538c194557a80d4bdacae63c63b900 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Wed, 4 Mar 2026 23:38:52 +0100 Subject: [PATCH] feat(i18n): add multilingual platform descriptions and HostWizard demo data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../cms/platform/sections/_pricing.html | 22 +-- ...4_add_platform_description_translations.py | 31 ++++ app/modules/tenancy/models/platform.py | 18 +++ .../tenancy/routes/api/admin_platforms.py | 3 + .../tenancy/static/admin/js/platform-edit.js | 40 +++++- .../tenancy/admin/platform-edit.html | 32 ++++- scripts/seed/init_production.py | 79 +++++++++-- scripts/seed/seed_demo.py | 134 +++++++++++++++++- 8 files changed, 327 insertions(+), 32 deletions(-) create mode 100644 app/modules/tenancy/migrations/versions/tenancy_004_add_platform_description_translations.py diff --git a/app/modules/cms/templates/cms/platform/sections/_pricing.html b/app/modules/cms/templates/cms/platform/sections/_pricing.html index 5537e324..87ac9f46 100644 --- a/app/modules/cms/templates/cms/platform/sections/_pricing.html +++ b/app/modules/cms/templates/cms/platform/sections/_pricing.html @@ -11,7 +11,7 @@ #} {# Helper macro: resolve a TranslatableText field with fallback #} -{% macro _t(field, fallback='') %} +{% macro _t(field, lang, default_lang, fallback='') %} {%- if field and field.translations -%} {{ field.translations.get(lang) or field.translations.get(default_lang) or fallback }} {%- else -%} @@ -25,12 +25,12 @@
{# Section header #}
- {% set title = _t(pricing.title, 'Simple, Transparent Pricing') %} + {% set title = _t(pricing.title, lang, default_lang, 'Simple, Transparent Pricing') %}

{{ title }}

- {% set subtitle = _t(pricing.subtitle) %} + {% set subtitle = _t(pricing.subtitle, lang, default_lang) %} {% if subtitle|trim %}

{{ subtitle }} @@ -40,13 +40,13 @@ {# Pricing toggle (monthly/annual) #} {% if pricing.use_subscription_tiers and tiers %} - {% set monthly_label = _t(pricing.monthly_label, 'Monthly') %} - {% set annual_label = _t(pricing.annual_label, 'Annual') %} - {% set save_text = _t(pricing.save_text, 'Save 2 months!') %} - {% set popular_badge = _t(pricing.popular_badge, 'Most Popular') %} - {% set cta_text = _t(pricing.cta_text, 'Start Free Trial') %} - {% set per_month = _t(pricing.per_month_label, '/month') %} - {% set per_year = _t(pricing.per_year_label, '/year') %} + {% set monthly_label = _t(pricing.monthly_label, lang, default_lang, 'Monthly') %} + {% set annual_label = _t(pricing.annual_label, lang, default_lang, 'Annual') %} + {% set save_text = _t(pricing.save_text, lang, default_lang, 'Save 2 months!') %} + {% set popular_badge = _t(pricing.popular_badge, lang, default_lang, 'Most Popular') %} + {% set cta_text = _t(pricing.cta_text, lang, default_lang, 'Start Free Trial') %} + {% set per_month = _t(pricing.per_month_label, lang, default_lang, '/month') %} + {% set per_year = _t(pricing.per_year_label, lang, default_lang, '/year') %}

{# Billing toggle #} @@ -128,7 +128,7 @@
{% else %} {# 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') %}
{{ coming_soon }}
diff --git a/app/modules/tenancy/migrations/versions/tenancy_004_add_platform_description_translations.py b/app/modules/tenancy/migrations/versions/tenancy_004_add_platform_description_translations.py new file mode 100644 index 00000000..21e45444 --- /dev/null +++ b/app/modules/tenancy/migrations/versions/tenancy_004_add_platform_description_translations.py @@ -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") diff --git a/app/modules/tenancy/models/platform.py b/app/modules/tenancy/models/platform.py index 64522eeb..1f0dbfde 100644 --- a/app/modules/tenancy/models/platform.py +++ b/app/modules/tenancy/models/platform.py @@ -71,6 +71,13 @@ class Platform(Base, TimestampMixin): 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 # ======================================================================== @@ -226,6 +233,17 @@ class Platform(Base, TimestampMixin): # 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 def base_url(self) -> str: """Get the base URL for this platform (for link generation).""" diff --git a/app/modules/tenancy/routes/api/admin_platforms.py b/app/modules/tenancy/routes/api/admin_platforms.py index 36930463..cec9a1ab 100644 --- a/app/modules/tenancy/routes/api/admin_platforms.py +++ b/app/modules/tenancy/routes/api/admin_platforms.py @@ -41,6 +41,7 @@ class PlatformResponse(BaseModel): code: str name: str description: str | None = None + description_translations: dict[str, str] | None = None domain: str | None = None path_prefix: str | None = None logo: str | None = None @@ -76,6 +77,7 @@ class PlatformUpdateRequest(BaseModel): name: str | None = None description: str | None = None + description_translations: dict[str, str] | None = None domain: str | None = None path_prefix: str | None = None logo: str | None = None @@ -115,6 +117,7 @@ def _build_platform_response(db: Session, platform) -> PlatformResponse: code=platform.code, name=platform.name, description=platform.description, + description_translations=platform.description_translations, domain=platform.domain, path_prefix=platform.path_prefix, logo=platform.logo, diff --git a/app/modules/tenancy/static/admin/js/platform-edit.js b/app/modules/tenancy/static/admin/js/platform-edit.js index 8bb0943a..b15936ea 100644 --- a/app/modules/tenancy/static/admin/js/platform-edit.js +++ b/app/modules/tenancy/static/admin/js/platform-edit.js @@ -22,10 +22,20 @@ function platformEdit() { success: null, platformCode: null, + // Language editing + currentLang: 'fr', + languageNames: { + fr: 'Fran\u00e7ais', + de: 'Deutsch', + en: 'English', + lb: 'L\u00ebtzebuergesch', + }, + // Form data formData: { name: '', description: '', + description_translations: {}, domain: '', path_prefix: '', logo: '', @@ -46,7 +56,7 @@ function platformEdit() { { code: 'fr', name: 'French' }, { code: 'de', name: 'German' }, { code: 'en', name: 'English' }, - { code: 'lu', name: 'Luxembourgish' }, + { code: 'lb', name: 'Luxembourgish' }, ], // Lifecycle @@ -92,23 +102,34 @@ function platformEdit() { const response = await apiClient.get(`/admin/platforms/${this.platformCode}`); 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 this.formData = { name: response.name || '', description: response.description || '', + description_translations: descTranslations, domain: response.domain || '', path_prefix: response.path_prefix || '', logo: response.logo || '', logo_dark: response.logo_dark || '', favicon: response.favicon || '', default_language: response.default_language || 'fr', - supported_languages: response.supported_languages || ['fr', 'de', 'en'], + supported_languages: langs, is_active: response.is_active ?? true, is_public: response.is_public ?? true, theme_config: response.theme_config || {}, settings: response.settings || {}, }; + // Set current language tab to default language + this.currentLang = response.default_language || 'fr'; + platformEditLog.info(`Loaded platform: ${this.platformCode}`); } catch (err) { platformEditLog.error('Error loading platform:', err); @@ -126,9 +147,14 @@ function platformEdit() { try { // 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 = { name: this.formData.name, - description: this.formData.description || null, + description: baseDesc || null, + description_translations: this.formData.description_translations, domain: this.formData.domain || null, path_prefix: this.formData.path_prefix || null, logo: this.formData.logo || null, @@ -199,9 +225,17 @@ function platformEdit() { // Don't allow removing the last language if (this.formData.supported_languages.length > 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 { this.formData.supported_languages.push(code); + // Initialize empty translation for new language + if (!this.formData.description_translations[code]) { + this.formData.description_translations[code] = ''; + } } }, diff --git a/app/modules/tenancy/templates/tenancy/admin/platform-edit.html b/app/modules/tenancy/templates/tenancy/admin/platform-edit.html index 60d95532..72692742 100644 --- a/app/modules/tenancy/templates/tenancy/admin/platform-edit.html +++ b/app/modules/tenancy/templates/tenancy/admin/platform-edit.html @@ -130,19 +130,41 @@ - -