17 Commits

Author SHA1 Message Date
9f4fd9278a Harden canonical and robots URLs to production host 2026-05-24 17:44:52 +02:00
b56238dfc4 Add address country i18n DB columns for localized checkout 2026-05-23 19:56:52 +02:00
a6a9ed2973 Add checkout i18n DB columns for basket/checkout stability 2026-05-23 16:32:09 +02:00
90a71adb4f Restore localized Wordspinner bulk/results routes 2026-05-23 14:09:19 +02:00
936d4be491 Fix contact form import to plugin path for staging runtime 2026-05-23 02:00:29 +02:00
d5d864a244 Force staging CSS cache bust for mobile header fixes 2026-05-23 01:05:04 +02:00
3f6be60a74 Fix mobile header/menu layering and cookie modal stacking 2026-05-21 00:29:48 +02:00
291b119475 Fix cookie popup text resolution per active language 2026-05-17 15:23:11 +02:00
f21ce95d72 Revert "Seed translated cookie setting fields for staging/prod parity"
This reverts commit 6cca48eaf9.
2026-05-16 02:27:37 +02:00
6cca48eaf9 Seed translated cookie setting fields for staging/prod parity 2026-05-16 02:25:30 +02:00
215eb8352d Make cookie modal locale output deterministic on staging/prod 2026-05-16 02:09:01 +02:00
fdcdff52cd Revert "Make cookie modal fully locale-aware via settings-aware resolver"
This reverts commit 3e084c1850.
2026-05-16 01:56:10 +02:00
3e084c1850 Make cookie modal fully locale-aware via settings-aware resolver 2026-05-16 01:38:17 +02:00
0ff32da99a Fix multilingual cookie consent rendering and locale support 2026-05-16 00:10:48 +02:00
3959d041c4 Use compiled layout_overrides.css in layout template 2026-05-15 00:01:04 +02:00
f3b43b1208 Always load cookie assets and mount cookie banner modal 2026-05-14 04:42:21 +02:00
76be2e7f41 Show cookie modal when consent cookie is missing 2026-05-14 04:22:08 +02:00
27 changed files with 792 additions and 41 deletions

Binary file not shown.

View File

@@ -0,0 +1,40 @@
msgid ""
msgstr ""
"Project-Id-Version: mandelstudio\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-15 00:00+0200\n"
"PO-Revision-Date: 2026-05-15 00:00+0200\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Privacy & Cookies"
msgstr "Datenschutz & Cookies"
msgid "We use cookies to make sure our website works as well as possible. If you continue using this website, we assume you agree."
msgstr "Wir verwenden Cookies, um sicherzustellen, dass unsere Website so gut wie möglich funktioniert. Wenn Sie diese Website weiter nutzen, gehen wir davon aus, dass Sie einverstanden sind."
msgid "Accept"
msgstr "Akzeptieren"
msgid "Settings"
msgstr "Einstellungen"
msgid "You can update your cookie preferences at any time."
msgstr "Sie können Ihre Cookie-Einstellungen jederzeit ändern."
msgid "Back"
msgstr "Zurück"
msgid "Cookie settings"
msgstr "Cookie-Einstellungen"
msgid "Choose which cookie categories you allow. Functional cookies are always enabled because they are required for the website to work."
msgstr "Wählen Sie aus, welche Cookie-Kategorien Sie erlauben. Funktionale Cookies sind immer aktiviert, da sie für den Betrieb der Website erforderlich sind."
msgid "Save preferences"
msgstr "Einstellungen speichern"

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,40 @@
msgid ""
msgstr ""
"Project-Id-Version: mandelstudio\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-15 00:00+0200\n"
"PO-Revision-Date: 2026-05-15 00:00+0200\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Privacy & Cookies"
msgstr "Privacidad y cookies"
msgid "We use cookies to make sure our website works as well as possible. If you continue using this website, we assume you agree."
msgstr "Usamos cookies para asegurar que nuestro sitio web funcione lo mejor posible. Si continúa usando este sitio web, asumimos que está de acuerdo."
msgid "Accept"
msgstr "Aceptar"
msgid "Settings"
msgstr "Configuración"
msgid "You can update your cookie preferences at any time."
msgstr "Puede actualizar sus preferencias de cookies en cualquier momento."
msgid "Back"
msgstr "Volver"
msgid "Cookie settings"
msgstr "Configuración de cookies"
msgid "Choose which cookie categories you allow. Functional cookies are always enabled because they are required for the website to work."
msgstr "Elija qué categorías de cookies permite. Las cookies funcionales están siempre habilitadas porque son necesarias para que el sitio web funcione."
msgid "Save preferences"
msgstr "Guardar preferencias"

Binary file not shown.

View File

@@ -0,0 +1,40 @@
msgid ""
msgstr ""
"Project-Id-Version: mandelstudio\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-15 00:00+0200\n"
"PO-Revision-Date: 2026-05-15 00:00+0200\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Privacy & Cookies"
msgstr "Confidentialité et cookies"
msgid "We use cookies to make sure our website works as well as possible. If you continue using this website, we assume you agree."
msgstr "Nous utilisons des cookies pour garantir le bon fonctionnement de notre site. Si vous continuez à utiliser ce site, nous supposons que vous êtes d'accord."
msgid "Accept"
msgstr "Accepter"
msgid "Settings"
msgstr "Paramètres"
msgid "You can update your cookie preferences at any time."
msgstr "Vous pouvez modifier vos préférences de cookies à tout moment."
msgid "Back"
msgstr "Retour"
msgid "Cookie settings"
msgstr "Paramètres des cookies"
msgid "Choose which cookie categories you allow. Functional cookies are always enabled because they are required for the website to work."
msgstr "Choisissez les catégories de cookies que vous autorisez. Les cookies fonctionnels sont toujours activés car ils sont nécessaires au fonctionnement du site."
msgid "Save preferences"
msgstr "Enregistrer les préférences"

Binary file not shown.

View File

@@ -0,0 +1,40 @@
msgid ""
msgstr ""
"Project-Id-Version: mandelstudio\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-15 00:00+0200\n"
"PO-Revision-Date: 2026-05-15 00:00+0200\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Privacy & Cookies"
msgstr "Privacy e cookie"
msgid "We use cookies to make sure our website works as well as possible. If you continue using this website, we assume you agree."
msgstr "Utilizziamo i cookie per assicurarci che il nostro sito web funzioni nel miglior modo possibile. Se continui a utilizzare questo sito web, presumiamo che tu sia d'accordo."
msgid "Accept"
msgstr "Accetta"
msgid "Settings"
msgstr "Impostazioni"
msgid "You can update your cookie preferences at any time."
msgstr "Puoi aggiornare le tue preferenze sui cookie in qualsiasi momento."
msgid "Back"
msgstr "Indietro"
msgid "Cookie settings"
msgstr "Impostazioni cookie"
msgid "Choose which cookie categories you allow. Functional cookies are always enabled because they are required for the website to work."
msgstr "Scegli quali categorie di cookie consentire. I cookie funzionali sono sempre abilitati perché necessari al funzionamento del sito web."
msgid "Save preferences"
msgstr "Salva preferenze"

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,40 @@
msgid ""
msgstr ""
"Project-Id-Version: mandelstudio\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-15 00:00+0200\n"
"PO-Revision-Date: 2026-05-15 00:00+0200\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Privacy & Cookies"
msgstr "Privacidade e cookies"
msgid "We use cookies to make sure our website works as well as possible. If you continue using this website, we assume you agree."
msgstr "Usamos cookies para garantir que nosso site funcione da melhor forma possível. Se você continuar usando este site, presumimos que concorda."
msgid "Accept"
msgstr "Aceitar"
msgid "Settings"
msgstr "Configurações"
msgid "You can update your cookie preferences at any time."
msgstr "Você pode atualizar suas preferências de cookies a qualquer momento."
msgid "Back"
msgstr "Voltar"
msgid "Cookie settings"
msgstr "Configurações de cookies"
msgid "Choose which cookie categories you allow. Functional cookies are always enabled because they are required for the website to work."
msgstr "Escolha quais categorias de cookies você permite. Os cookies funcionais estão sempre ativados porque são necessários para o funcionamento do site."
msgid "Save preferences"
msgstr "Salvar preferências"

Binary file not shown.

View File

@@ -0,0 +1,40 @@
msgid ""
msgstr ""
"Project-Id-Version: mandelstudio\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-15 00:00+0200\n"
"PO-Revision-Date: 2026-05-15 00:00+0200\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Privacy & Cookies"
msgstr "Конфиденциальность и файлы cookie"
msgid "We use cookies to make sure our website works as well as possible. If you continue using this website, we assume you agree."
msgstr "Мы используем файлы cookie, чтобы наш сайт работал как можно лучше. Если вы продолжаете пользоваться этим сайтом, мы считаем, что вы согласны."
msgid "Accept"
msgstr "Принять"
msgid "Settings"
msgstr "Настройки"
msgid "You can update your cookie preferences at any time."
msgstr "Вы можете изменить свои настройки cookie в любое время."
msgid "Back"
msgstr "Назад"
msgid "Cookie settings"
msgstr "Настройки cookie"
msgid "Choose which cookie categories you allow. Functional cookies are always enabled because they are required for the website to work."
msgstr "Выберите, какие категории cookie вы разрешаете. Функциональные cookie всегда включены, так как они необходимы для работы сайта."
msgid "Save preferences"
msgstr "Сохранить настройки"

View File

@@ -0,0 +1,63 @@
# Generated by Django 5.2.11 on 2026-05-15 00:00
from django.db import migrations
import wagtail.fields
class Migration(migrations.Migration):
dependencies = [
("cookie_jar", "0007_cookiesettings_cookie_message_de_and_more"),
]
operations = [
migrations.AddField(
model_name="cookiesettings",
name="popup_cookie_message_de",
field=wagtail.fields.RichTextField(
blank=True, null=True, verbose_name="Popup cookie statement"
),
),
migrations.AddField(
model_name="cookiesettings",
name="popup_cookie_message_en",
field=wagtail.fields.RichTextField(
blank=True, null=True, verbose_name="Popup cookie statement"
),
),
migrations.AddField(
model_name="cookiesettings",
name="popup_cookie_message_es",
field=wagtail.fields.RichTextField(
blank=True, null=True, verbose_name="Popup cookie statement"
),
),
migrations.AddField(
model_name="cookiesettings",
name="popup_cookie_message_fr",
field=wagtail.fields.RichTextField(
blank=True, null=True, verbose_name="Popup cookie statement"
),
),
migrations.AddField(
model_name="cookiesettings",
name="popup_cookie_message_it",
field=wagtail.fields.RichTextField(
blank=True, null=True, verbose_name="Popup cookie statement"
),
),
migrations.AddField(
model_name="cookiesettings",
name="popup_cookie_message_pt",
field=wagtail.fields.RichTextField(
blank=True, null=True, verbose_name="Popup cookie statement"
),
),
migrations.AddField(
model_name="cookiesettings",
name="popup_cookie_message_ru",
field=wagtail.fields.RichTextField(
blank=True, null=True, verbose_name="Popup cookie statement"
),
),
]

View File

@@ -0,0 +1,56 @@
from django.db import migrations
LANGUAGES = ("en", "de", "fr", "es", "it", "pt", "ru")
def _add_column_if_missing(schema_editor, table_name, column_name, column_sql):
connection = schema_editor.connection
with connection.cursor() as cursor:
existing = {
row.name
for row in connection.introspection.get_table_description(cursor, table_name)
}
if column_name in existing:
return
quoted_table = schema_editor.quote_name(table_name)
quoted_column = schema_editor.quote_name(column_name)
cursor.execute(
f"ALTER TABLE {quoted_table} ADD COLUMN {quoted_column} {column_sql}"
)
def add_checkout_i18n_columns(apps, schema_editor):
for lang in LANGUAGES:
_add_column_if_missing(
schema_editor,
"checkout_fixedsurcharge",
f"name_{lang}",
"varchar(32) NULL",
)
_add_column_if_missing(
schema_editor,
"checkout_percentagesurcharge",
f"name_{lang}",
"varchar(32) NULL",
)
_add_column_if_missing(
schema_editor,
"checkout_paymentmethod",
f"label_{lang}",
"varchar(32) NULL",
)
class Migration(migrations.Migration):
dependencies = [
("mandelstudio", "0004_contact_messages"),
("checkout", "0017_remove_unused_price_fields"),
]
operations = [
migrations.RunPython(
add_checkout_i18n_columns,
migrations.RunPython.noop,
)
]

View File

@@ -0,0 +1,43 @@
from django.db import migrations
LANGUAGES = ("en", "de", "fr", "es", "it", "pt", "ru")
def _add_column_if_missing(schema_editor, table_name, column_name, column_sql):
connection = schema_editor.connection
with connection.cursor() as cursor:
existing = {
row.name
for row in connection.introspection.get_table_description(cursor, table_name)
}
if column_name in existing:
return
quoted_table = schema_editor.quote_name(table_name)
quoted_column = schema_editor.quote_name(column_name)
cursor.execute(
f"ALTER TABLE {quoted_table} ADD COLUMN {quoted_column} {column_sql}"
)
def add_address_i18n_columns(apps, schema_editor):
for lang in LANGUAGES:
_add_column_if_missing(
schema_editor,
"address_country",
f"printable_name_{lang}",
"varchar(128) NULL",
)
class Migration(migrations.Migration):
dependencies = [
("mandelstudio", "0005_checkout_i18n_columns"),
]
operations = [
migrations.RunPython(
add_address_i18n_columns,
migrations.RunPython.noop,
)
]

View File

@@ -174,6 +174,16 @@ ACTIVE_VERTICAL = "agency"
WAGTAIL_I18N_ENABLED = True WAGTAIL_I18N_ENABLED = True
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES WAGTAIL_CONTENT_LANGUAGES = LANGUAGES
# Ocyan i18n field mapping (language-aware columns per active language).
_translated_fields = dict(globals().get("OCYAN_I18N_TRANSLATED_FIELDS", {}))
_translated_fields.update(
{
"cookie_jar.cookiesettings.cookie_message": True,
"cookie_jar.cookiesettings.popup_cookie_message": True,
}
)
OCYAN_I18N_TRANSLATED_FIELDS = _translated_fields
CONTENT_GUARD_STRICT = True CONTENT_GUARD_STRICT = True
CONTENT_GUARD_BLOCK_MEDIUM = False CONTENT_GUARD_BLOCK_MEDIUM = False
CONTENT_GUARD_LOCALES = [code for code, _label in LANGUAGES] CONTENT_GUARD_LOCALES = [code for code, _label in LANGUAGES]

View File

@@ -18,6 +18,7 @@ ALLOWED_HOSTS = [
"www.mandelblog.com", "www.mandelblog.com",
"mandelblog.com", "mandelblog.com",
] ]
CANONICAL_BASE_URL = "https://www.mandelblog.com"
if "salt_target" in globals(): if "salt_target" in globals():
ALLOWED_HOSTS.append("mandelstudio.%s" % salt_target) # pylint: disable=E0602 ALLOWED_HOSTS.append("mandelstudio.%s" % salt_target) # pylint: disable=E0602
# pylint: disable=E0602 # pylint: disable=E0602

View File

@@ -1,5 +1,6 @@
from django.contrib.sitemaps.views import index as sitemap_index_view from django.contrib.sitemaps.views import index as sitemap_index_view
from django.contrib.sitemaps.views import sitemap as sitemap_section_view from django.contrib.sitemaps.views import sitemap as sitemap_section_view
from django.conf import settings
from django.http import HttpResponse from django.http import HttpResponse
from wagtail.models import Locale, Page from wagtail.models import Locale, Page
@@ -15,6 +16,13 @@ from ocyan.plugin.wagtail_oscar_integration.sitemap import (
) )
def _absolute_sitemap_url(request, path: str) -> str:
canonical_base = getattr(settings, "CANONICAL_BASE_URL", "").rstrip("/")
if canonical_base:
return f"{canonical_base}{path}"
return request.build_absolute_uri(path)
class WagtailSitemap(BaseWagtailSitemap): class WagtailSitemap(BaseWagtailSitemap):
def items(self): def items(self):
page_ids = [] page_ids = []
@@ -74,7 +82,7 @@ def sitemap_section(request, section=None):
def robots_txt(request): def robots_txt(request):
sitemap_url = request.build_absolute_uri("/sitemap.xml") sitemap_url = _absolute_sitemap_url(request, "/sitemap.xml")
content = "\n".join( content = "\n".join(
[ [
"User-agent: *", "User-agent: *",

File diff suppressed because one or more lines are too long

View File

@@ -80,7 +80,7 @@ header {
#cookie_popup_body.cookie-consent-overlay { #cookie_popup_body.cookie-consent-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 1080; z-index: 9999;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -99,6 +99,8 @@ header {
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.25); box-shadow: 0 24px 64px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
position: relative;
z-index: 10000;
} }
.cookie-consent-panel { .cookie-consent-panel {
@@ -138,12 +140,14 @@ header {
#cookie_popup_acceptButton_settings, #cookie_popup_acceptButton_settings,
#cookie_model_saveButton { #cookie_model_saveButton {
flex: 1 1 0; flex: 1 1 0;
height: 46px; min-height: 48px;
height: auto;
padding: 12px 16px;
border-radius: 10px; border-radius: 10px;
border: 1px solid transparent; border: 1px solid transparent;
font-size: 17px; font-size: 17px;
font-weight: 600; font-weight: 600;
line-height: 1; line-height: 1.2;
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease; transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
} }
@@ -204,25 +208,207 @@ header {
} }
} }
body.cookie-consent-open {
overflow: hidden;
.header-right,
.theme-switcher-toggle,
.wagtail-userbar,
[class*="help"],
[class*="assistant"],
[class*="chat-widget"],
[class*="cart"],
[class*="basket"],
[class*="floating"] {
opacity: 0 !important;
pointer-events: none !important;
}
}
@media (max-width: 991.98px) {
header .header-inner > .container {
display: flex;
flex-wrap: nowrap;
align-items: center;
column-gap: 0.375rem;
padding-top: 0.625rem;
padding-bottom: 0.625rem;
}
header .header-inner > .container > .navbar-brand {
order: 1;
margin: 0;
flex: 0 1 auto;
}
header .header-inner > .container > .navbar-brand img {
width: auto;
height: auto;
max-height: 74px;
max-width: 190px;
}
header .header-inner > .container > .header-right {
order: 2;
margin-left: auto;
flex: 0 0 auto;
gap: 0.375rem;
}
header .header-right .language-dropdown,
header .header-right .basket-dropdown,
header .header-right > a.user-button.menu-circle,
header .header-right .menu-circle {
width: 36px;
height: 36px;
min-width: 36px;
min-height: 36px;
flex-basis: 36px;
}
header .header-inner > .container > .navbar-toggler {
order: 3;
margin-left: 0.125rem;
flex: 0 0 auto;
}
header .header-inner > .container > .navbar-collapse {
order: 4;
display: none;
}
header .header-inner > .container > .navbar-collapse.show,
header .header-inner > .container > .navbar-collapse.collapsing {
display: block;
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100dvh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
background: #fff;
z-index: 9000;
}
header .header-inner .navbar-collapse .brand-wrapper {
display: block;
}
header .header-inner > .container > .navbar-collapse.show .brand-wrapper,
header .header-inner > .container > .navbar-collapse.collapsing .brand-wrapper {
position: absolute;
top: 16px;
left: 50%;
transform: translateX(-50%);
width: auto;
margin: 0;
z-index: 2;
text-align: center;
}
header .header-inner > .container > .navbar-collapse.show .brand-wrapper .navbar-brand,
header .header-inner > .container > .navbar-collapse.collapsing .brand-wrapper .navbar-brand {
margin: 0;
display: inline-flex;
}
header .header-inner > .container > .navbar-collapse.show .brand-wrapper img,
header .header-inner > .container > .navbar-collapse.collapsing .brand-wrapper img {
max-height: 92px;
width: auto;
}
header .header-inner > .container > .navbar-collapse.show .navbar-nav,
header .header-inner > .container > .navbar-collapse.collapsing .navbar-nav {
padding-top: 124px;
}
header .header-inner > .container > .navbar-collapse.show .brand-wrapper::after,
header .header-inner > .container > .navbar-collapse.collapsing .brand-wrapper::after {
content: "";
position: absolute;
left: -80px;
right: -80px;
bottom: -14px;
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
}
header .header-right,
header .header-right .basket-dropdown,
header .header-right .menu-circle {
position: relative;
z-index: 100;
}
}
@media (max-width: 991.98px) {
body.mobile-menu-open {
header .header-inner > .container > .navbar-brand {
opacity: 0;
pointer-events: none;
}
.header-right,
.theme-switcher-toggle,
.wagtail-userbar,
[class*="help"],
[class*="assistant"],
[class*="chat-widget"],
[class*="cart"],
[class*="basket"],
[class*="floating"] {
opacity: 0 !important;
pointer-events: none !important;
}
}
}
@media (max-width: 640px) { @media (max-width: 640px) {
#cookie_popup_body.cookie-consent-overlay { #cookie_popup_body.cookie-consent-overlay {
padding: 12px; padding: 12px 14px 14px;
padding-top: calc(12px + env(safe-area-inset-top));
padding-bottom: calc(14px + env(safe-area-inset-bottom));
.cookie-consent-modal { .cookie-consent-modal {
padding: 18px; max-width: 420px;
max-height: 82vh;
overflow-y: auto;
padding: 18px 16px;
border-radius: 14px; border-radius: 14px;
} }
.cookie-banner-title { .cookie-banner-title {
margin-bottom: 12px;
font-size: 21px; font-size: 21px;
} }
#cookie_popup_content p { #cookie_popup_content p {
font-size: 15px; font-size: 16px;
line-height: 1.45;
} }
.cookie-consent-actions { .cookie-consent-actions {
flex-direction: column; flex-direction: column;
gap: 10px;
margin-top: 16px;
}
#cookie_popup_acceptButton,
#cookie_popup_settingsToggle,
#cookie_popup_acceptButton_settings,
#cookie_model_saveButton {
width: 100%;
font-size: 16px;
}
.cookie-consent-hint {
margin-top: 12px;
font-size: 13px;
} }
} }
} }

View File

@@ -1,44 +1,53 @@
{% load i18n %} {% load i18n %}
{% load wagtailcore_tags ocyanjson %} {% load wagtailcore_tags ocyanjson %}
{% load mandelstudio_i18n %}
{% with settings.cookie_jar.CookieSettings as cookie_settings %} {% with settings.cookie_jar.CookieSettings as cookie_settings %}
{% with request.LANGUAGE_CODE|default:'nl' as language_code %}
{% with language_code|slice:':2' as lang %}
{% if cookie_jar.needs_approval or cookie_jar.site_settings.strict_cookies %} {% if cookie_jar.needs_approval or cookie_jar.site_settings.strict_cookies %}
{% if cookie_jar.needs_display or cookie_jar.site_settings.strict_cookies and not cookie_jar.cookie %} {% if cookie_jar.needs_display or cookie_jar.cookie is None %}
<div id="cookie_popup_body" class="cookie-consent-overlay" role="region" aria-label="{% trans 'Cookie settings' %}"> <div id="cookie_popup_body" class="cookie-consent-overlay" role="region" aria-label="{% if lang == 'ru' %}Настройки cookie{% elif lang == 'de' %}Cookie-Einstellungen{% elif lang == 'fr' %}Paramètres des cookies{% elif lang == 'es' %}Configuración de cookies{% elif lang == 'it' %}Impostazioni cookie{% elif lang == 'pt' %}Definições de cookies{% elif lang == 'en' %}Cookie settings{% else %}Cookie instellingen{% endif %}">
<div class="cookie-consent-modal" role="dialog" aria-modal="true" aria-labelledby="cookie-consent-title"> <div class="cookie-consent-modal" role="dialog" aria-modal="true" aria-labelledby="cookie-consent-title">
<div class="cookie-consent-panel is-active" id="cookie-consent-main-panel"> <div class="cookie-consent-panel is-active" id="cookie-consent-main-panel">
<div class="cookie-banner-title" id="cookie-consent-title"> <div class="cookie-banner-title" id="cookie-consent-title">
<i class="fa fa-shield-halved" aria-hidden="true"></i> <i class="fa fa-shield-halved" aria-hidden="true"></i>
<span>{% trans 'Privacy & Cookies' %}</span> <span>{% if lang == 'ru' %}Конфиденциальность и файлы cookie{% elif lang == 'de' %}Datenschutz & Cookies{% elif lang == 'fr' %}Confidentialité & Cookies{% elif lang == 'es' %}Privacidad y Cookies{% elif lang == 'it' %}Privacy e Cookie{% elif lang == 'pt' %}Privacidade & Cookies{% else %}Privacy & Cookies{% endif %}</span>
</div> </div>
<div id="cookie_popup_content"> <div id="cookie_popup_content">
{% localized_setting_text cookie_settings "cookie_message" as cookie_message_text %}
{% if cookie_message_text %}
{{ cookie_message_text|richtext }}
{% else %}
<p> <p>
{% blocktrans %} {% blocktrans %}
We use cookies to make sure our website works as well as possible. If you continue using this website, we assume you agree. We use cookies to make sure our website works as well as possible. If you continue using this website, we assume you agree.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
{% endif %}
</div> </div>
<div id="cookie_buttons" class="cookie-consent-actions"> <div id="cookie_buttons" class="cookie-consent-actions">
<button type="button" id="cookie_popup_acceptButton" data-cookie-key="{{ cookie_jar.cookie_key }}">{% trans 'Accept' %}</button> <button type="button" id="cookie_popup_acceptButton" data-cookie-key="{{ cookie_jar.cookie_key }}">{% if lang == 'ru' %}Принять{% elif lang == 'de' %}Akzeptieren{% elif lang == 'fr' %}Accepter{% elif lang == 'es' %}Aceptar{% elif lang == 'it' %}Accetta{% elif lang == 'pt' %}Aceitar{% elif lang == 'en' %}Accept{% else %}Accepteer{% endif %}</button>
<button type="button" id="cookie_popup_settingsToggle">{% trans 'Settings' %}</button> <button type="button" id="cookie_popup_settingsToggle">{% if lang == 'ru' %}Настройки{% elif lang == 'de' %}Einstellungen{% elif lang == 'fr' %}Paramètres{% elif lang == 'es' %}Configuración{% elif lang == 'it' %}Impostazioni{% elif lang == 'pt' %}Definições{% elif lang == 'en' %}Settings{% else %}Instellingen{% endif %}</button>
</div> </div>
<div class="cookie-consent-hint"> <div class="cookie-consent-hint">
{% trans 'You can update your cookie preferences at any time.' %} {% if lang == 'ru' %}Вы можете изменить свои настройки cookie в любое время.{% elif lang == 'de' %}Sie können Ihre Cookie-Einstellungen jederzeit aktualisieren.{% elif lang == 'fr' %}Vous pouvez mettre à jour vos préférences de cookies à tout moment.{% elif lang == 'es' %}Puede actualizar sus preferencias de cookies en cualquier momento.{% elif lang == 'it' %}Puoi aggiornare le tue preferenze sui cookie in qualsiasi momento.{% elif lang == 'pt' %}Pode atualizar as suas preferências de cookies a qualquer momento.{% elif lang == 'en' %}You can update your cookie preferences at any time.{% else %}U kunt uw cookievoorkeuren op elk moment aanpassen.{% endif %}
</div> </div>
</div> </div>
<div class="cookie-consent-panel" id="cookie-consent-settings-panel"> <div class="cookie-consent-panel" id="cookie-consent-settings-panel">
<button type="button" class="cookie-consent-back" id="cookie_popup_backButton"> <button type="button" class="cookie-consent-back" id="cookie_popup_backButton">
<i class="fa fa-arrow-left" aria-hidden="true"></i> <i class="fa fa-arrow-left" aria-hidden="true"></i>
<span>{% trans 'Back' %}</span> <span>{% if lang == 'ru' %}Назад{% elif lang == 'de' %}Zurück{% elif lang == 'fr' %}Retour{% elif lang == 'es' %}Volver{% elif lang == 'it' %}Indietro{% elif lang == 'pt' %}Voltar{% elif lang == 'en' %}Back{% else %}Terug{% endif %}</span>
</button> </button>
<div class="cookie-banner-title"> <div class="cookie-banner-title">
<i class="fa fa-sliders" aria-hidden="true"></i> <i class="fa fa-sliders" aria-hidden="true"></i>
<span>{% trans 'Cookie settings' %}</span> <span>{% if lang == 'ru' %}Настройки cookie{% elif lang == 'de' %}Cookie-Einstellungen{% elif lang == 'fr' %}Paramètres des cookies{% elif lang == 'es' %}Configuración de cookies{% elif lang == 'it' %}Impostazioni cookie{% elif lang == 'pt' %}Definições de cookies{% elif lang == 'en' %}Cookie settings{% else %}Cookie instellingen{% endif %}</span>
</div> </div>
<div id="cookie_popup_content_modal"> <div id="cookie_popup_content_modal">
{% if cookie_settings.popup_cookie_message %} {% localized_setting_text cookie_settings "popup_cookie_message" as popup_cookie_message_text %}
{{ cookie_settings.popup_cookie_message|richtext }} {% if popup_cookie_message_text %}
{{ popup_cookie_message_text|richtext }}
{% else %} {% else %}
<p> <p>
{% blocktrans %} {% blocktrans %}
@@ -49,14 +58,16 @@
</div> </div>
{% include "cookie_jar/partials/cookie_checkboxes.html" %} {% include "cookie_jar/partials/cookie_checkboxes.html" %}
<div class="cookie-consent-actions cookie-consent-actions-settings"> <div class="cookie-consent-actions cookie-consent-actions-settings">
<button type="button" id="cookie_popup_acceptButton_settings">{% trans 'Accept' %}</button> <button type="button" id="cookie_popup_acceptButton_settings">{% if lang == 'ru' %}Принять{% elif lang == 'de' %}Akzeptieren{% elif lang == 'fr' %}Accepter{% elif lang == 'es' %}Aceptar{% elif lang == 'it' %}Accetta{% elif lang == 'pt' %}Aceitar{% elif lang == 'en' %}Accept{% else %}Accepteer{% endif %}</button>
<button type="button" id="cookie_model_saveButton" data-cookie-key="{{ cookie_jar.cookie_key }}">{% trans 'Save preferences' %}</button> <button type="button" id="cookie_model_saveButton" data-cookie-key="{{ cookie_jar.cookie_key }}">{% if lang == 'ru' %}Сохранить настройки{% elif lang == 'de' %}Einstellungen speichern{% elif lang == 'fr' %}Enregistrer les préférences{% elif lang == 'es' %}Guardar preferencias{% elif lang == 'it' %}Salva preferenze{% elif lang == 'pt' %}Guardar preferências{% elif lang == 'en' %}Save preferences{% else %}Voorkeuren opslaan{% endif %}</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endwith %}
{% endwith %}
{% endwith %} {% endwith %}
<script> <script>
@@ -65,6 +76,31 @@
return document.getElementById(id); return document.getElementById(id);
} }
function setConsentOpenState(isOpen) {
if (!document.body) return;
document.body.classList.toggle("cookie-consent-open", !!isOpen);
}
function setMobileMenuOpenState(isOpen) {
if (!document.body) return;
document.body.classList.toggle("mobile-menu-open", !!isOpen);
}
function syncMobileMenuState(navbar) {
if (!navbar) return;
var isOpen = navbar.classList.contains("show") || navbar.classList.contains("collapsing");
setMobileMenuOpenState(isOpen);
}
function closeOpenMenus() {
document.querySelectorAll(".dropdown-menu.show").forEach(function (menu) {
menu.classList.remove("show");
});
document.querySelectorAll("[aria-expanded='true']").forEach(function (trigger) {
trigger.setAttribute("aria-expanded", "false");
});
}
function showSettings(event) { function showSettings(event) {
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
@@ -91,10 +127,31 @@
} }
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
var popupBody = byId("cookie_popup_body");
var settingsBtn = byId("cookie_popup_settingsToggle"); var settingsBtn = byId("cookie_popup_settingsToggle");
var backBtn = byId("cookie_popup_backButton"); var backBtn = byId("cookie_popup_backButton");
var acceptSettingsBtn = byId("cookie_popup_acceptButton_settings"); var acceptSettingsBtn = byId("cookie_popup_acceptButton_settings");
var acceptBtn = byId("cookie_popup_acceptButton"); var acceptBtn = byId("cookie_popup_acceptButton");
var navbar = byId("navbarSupportedContent");
if (popupBody) {
setConsentOpenState(true);
closeOpenMenus();
}
if (navbar) {
syncMobileMenuState(navbar);
["show.bs.collapse", "shown.bs.collapse", "hide.bs.collapse", "hidden.bs.collapse"].forEach(function (eventName) {
navbar.addEventListener(eventName, function () {
syncMobileMenuState(navbar);
});
});
var observer = new MutationObserver(function () {
syncMobileMenuState(navbar);
});
observer.observe(navbar, { attributes: true, attributeFilter: ["class"] });
}
if (settingsBtn) { if (settingsBtn) {
settingsBtn.addEventListener("click", showSettings); settingsBtn.addEventListener("click", showSettings);
@@ -108,6 +165,17 @@
acceptBtn.click(); acceptBtn.click();
}); });
} }
if (acceptBtn) {
acceptBtn.addEventListener("click", function () {
setConsentOpenState(false);
});
}
window.addEventListener("beforeunload", function () {
setConsentOpenState(false);
setMobileMenuOpenState(false);
});
}); });
})(); })();
</script> </script>

View File

@@ -15,7 +15,7 @@
{{ block.super }} {{ block.super }}
{# Ensure Carbasa webshop styling is present so responsive header/footer render correctly. #} {# Ensure Carbasa webshop styling is present so responsive header/footer render correctly. #}
<link rel="stylesheet" type="text/x-scss" href="{% static 'carbasa/webshop_base.scss' %}"> <link rel="stylesheet" type="text/x-scss" href="{% static 'carbasa/webshop_base.scss' %}">
<link rel="stylesheet" type="text/x-scss" href="{% static 'mandelstudio/scss/layout_overrides.scss' %}"> <link rel="stylesheet" type="text/css" href="{% static 'mandelstudio/css/layout_overrides.css' %}">
{% endblock %} {% endblock %}
{% block extrahead %} {% block extrahead %}
@@ -26,9 +26,7 @@
<link rel="preconnect" href="https://www.google-analytics.com/"> <link rel="preconnect" href="https://www.google-analytics.com/">
{% endif %} {% endif %}
{{ block.super }} {{ block.super }}
{% if cookie_jar.needs_approval or cookie_jar.site_settings.strict_cookies %}
<link rel="stylesheet" type="text/css" href="{% static 'cookie_jar/css/cookie_jar.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'cookie_jar/css/cookie_jar.css' %}">
{% endif %}
{% for header_snippet in cookie_jar.activated_snippet_header_templates %} {% for header_snippet in cookie_jar.activated_snippet_header_templates %}
{% include header_snippet %} {% include header_snippet %}
{% endfor %} {% endfor %}
@@ -62,9 +60,7 @@
{% block extrascripts %} {% block extrascripts %}
{% include "oscar/partials/extrascripts.html" %} {% include "oscar/partials/extrascripts.html" %}
{{ block.super }} {{ block.super }}
{% if cookie_jar.needs_approval or cookie_jar.site_settings.strict_cookies %}
<script src="{% static 'cookie_jar/js/cookie_jar.js' %}"></script> <script src="{% static 'cookie_jar/js/cookie_jar.js' %}"></script>
{% endif %}
{% endblock %} {% endblock %}
{% block onbodyload %} {% block onbodyload %}
@@ -94,9 +90,6 @@ oxyan.initImageZoom()
{% for footer_snippet in cookie_jar.activated_snippet_footer_templates %} {% for footer_snippet in cookie_jar.activated_snippet_footer_templates %}
{% include footer_snippet %} {% include footer_snippet %}
{% endfor %} {% endfor %}
{% if cookie_jar.needs_approval or cookie_jar.site_settings.strict_cookies %}
<script src="{% static 'cookie_jar/js/cookie_jar.js' %}"></script>
{% endif %}
{% include "cookie_jar/cookie_banner.html" %} {% include "cookie_jar/cookie_banner.html" %}
{% if cookie_jar.needs_approval or cookie_jar.site_settings.strict_cookies %} {% if cookie_jar.needs_approval or cookie_jar.site_settings.strict_cookies %}
{% include "cookie_jar/partials/preferences_saved_toast.html" %} {% include "cookie_jar/partials/preferences_saved_toast.html" %}

View File

@@ -2,12 +2,41 @@ from __future__ import annotations
from django import template from django import template
from django.conf import settings from django.conf import settings
from urllib.parse import urlsplit, urlunsplit
from wagtail.models import Page from wagtail.models import Page
register = template.Library() register = template.Library()
def _canonical_base_url() -> str:
return getattr(settings, "CANONICAL_BASE_URL", "").rstrip("/")
def _with_canonical_host(url_or_path: str | None) -> str:
value = url_or_path or ""
base = _canonical_base_url()
if not base:
return value
if not value:
return f"{base}/"
if value.startswith("/"):
return f"{base}{value}"
parsed = urlsplit(value)
if parsed.scheme and parsed.netloc:
canonical = urlsplit(base)
return urlunsplit(
(
canonical.scheme,
canonical.netloc,
parsed.path,
parsed.query,
parsed.fragment,
)
)
return f"{base}/{value.lstrip('/')}"
def _normalize_language_code(language_code: str | None) -> str: def _normalize_language_code(language_code: str | None) -> str:
return (language_code or settings.LANGUAGE_CODE).split("-")[0] return (language_code or settings.LANGUAGE_CODE).split("-")[0]
@@ -41,10 +70,10 @@ def _translated_pages(page):
def _build_absolute_url(request, path: str | None, page=None) -> str: def _build_absolute_url(request, path: str | None, page=None) -> str:
if path and request is not None: if path and request is not None:
return request.build_absolute_uri(path) return _with_canonical_host(request.build_absolute_uri(path))
if page is not None: if page is not None:
return getattr(page, "full_url", "") or path or "" return _with_canonical_host(getattr(page, "full_url", "") or path or "")
return path or "" return _with_canonical_host(path or "")
@register.simple_tag @register.simple_tag
@@ -89,7 +118,7 @@ def page_canonical_url(context):
if page is not None and getattr(page, "url", None): if page is not None and getattr(page, "url", None):
return _build_absolute_url(request, page.url, page) return _build_absolute_url(request, page.url, page)
if request is not None: if request is not None:
return request.build_absolute_uri() return _with_canonical_host(request.build_absolute_uri())
return "" return ""

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from django import template from django import template
from django.utils.translation import get_language
from mandelstudio.i18n_utils import normalize_set_language_next from mandelstudio.i18n_utils import normalize_set_language_next
@@ -29,3 +30,22 @@ def skip_to_content_text(context) -> str:
def language_neutral_path(value: str | None) -> str: def language_neutral_path(value: str | None) -> str:
"""Normalize a path for set_language by removing any leading language prefix.""" """Normalize a path for set_language by removing any leading language prefix."""
return normalize_set_language_next(value) return normalize_set_language_next(value)
@register.simple_tag
def localized_setting_text(settings_obj, field_name: str, fallback_text: str = "") -> str:
"""
Resolve a translated settings field by active language and fallback to base field.
"""
if not settings_obj or not field_name:
return fallback_text
language = (get_language() or "").lower().split("-")[0]
if language and language != "nl":
translated_name = f"{field_name}_{language}"
translated_value = getattr(settings_obj, translated_name, None)
if translated_value:
return translated_value
base_value = getattr(settings_obj, field_name, None)
return base_value or fallback_text

View File

@@ -1,5 +1,5 @@
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from django.urls import path from django.urls import include, path
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from ocyan.core.fender import config from ocyan.core.fender import config
@@ -7,8 +7,11 @@ from ocyan.main.urls import urlpatterns as ocyan_urlpatterns
from ocyan.plugin.contact_form.entrypoint import SHOP_BASE_URL from ocyan.plugin.contact_form.entrypoint import SHOP_BASE_URL
from ocyan.plugin.wagtail_oscar_integration.constants import CACHE_DURATION from ocyan.plugin.wagtail_oscar_integration.constants import CACHE_DURATION
from ocyan.plugin.wordspinner.views.ai_search import ai_search_view from ocyan.plugin.wordspinner.views.ai_search import ai_search_view
from ocyan.plugin.wordspinner.views.bulk import BulkAIOperationsView
from ocyan.plugin.wordspinner.views.bulk import ProcessNextBulkAIJobView
from ocyan.plugin.wordspinner.views.results import GeneratedResultsListView
from contact_form.views import post_contact_form from ocyan.plugin.contact_form.views import post_contact_form
from .i18n_views import set_language_normalized from .i18n_views import set_language_normalized
from .sitemaps import robots_txt, sitemap_index, sitemap_section from .sitemaps import robots_txt, sitemap_index, sitemap_section
@@ -36,6 +39,13 @@ contact_form_urlpatterns = [
), ),
] ]
wordspinner_urlpatterns = [
path(
"wordspinner/",
include(("ocyan.plugin.wordspinner.urls", "wordspinner"), namespace="wordspinner"),
),
]
# Ensure public AI search routes are resolved before Wagtail catch-all URLs. # Ensure public AI search routes are resolved before Wagtail catch-all URLs.
ai_search_urlpatterns = [ ai_search_urlpatterns = [
path("ai-search/", ai_search_view, name="wordspinner_ai_search_public"), path("ai-search/", ai_search_view, name="wordspinner_ai_search_public"),
@@ -56,13 +66,38 @@ ai_search_urlpatterns = [
), ),
] ]
wordspinner_i18n_aliases = [
path(
"<str:lang_code>/wordspinner/ai/bulk/",
BulkAIOperationsView.as_view(),
name="wordspinner_bulk_ai_operations_i18n",
),
path(
"<str:lang_code>/wordspinner/ai/bulk/process-next/",
ProcessNextBulkAIJobView.as_view(),
name="wordspinner_bulk_ai_process_next_i18n",
),
path(
"<str:lang_code>/wordspinner/results/",
GeneratedResultsListView.as_view(),
name="wordspinner_generated_results_i18n",
),
]
if config.i18n_enabled: if config.i18n_enabled:
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(
*wordspinner_urlpatterns,
*contact_form_urlpatterns, *contact_form_urlpatterns,
*ai_search_urlpatterns, *ai_search_urlpatterns,
*wordspinner_i18n_aliases,
prefix_default_language=False, prefix_default_language=False,
) )
else: else:
urlpatterns += contact_form_urlpatterns + ai_search_urlpatterns urlpatterns += (
wordspinner_urlpatterns
+ contact_form_urlpatterns
+ ai_search_urlpatterns
+ wordspinner_i18n_aliases
)
urlpatterns += ocyan_urlpatterns urlpatterns += ocyan_urlpatterns