13 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
10 changed files with 470 additions and 29 deletions

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

@@ -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,19 +1,23 @@
{% 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.cookie is None %} {% 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">
{% if cookie_settings.cookie_message %} {% localized_setting_text cookie_settings "cookie_message" as cookie_message_text %}
{{ cookie_settings.cookie_message|richtext }} {% if cookie_message_text %}
{{ cookie_message_text|richtext }}
{% else %} {% else %}
<p> <p>
{% blocktrans %} {% blocktrans %}
@@ -23,26 +27,27 @@
{% endif %} {% 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 %}
@@ -53,8 +58,8 @@
</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>
@@ -62,6 +67,8 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% endwith %}
{% endwith %}
<script> <script>
(function () { (function () {
@@ -69,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();
@@ -95,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);
@@ -112,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

@@ -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