From bce5c1f725043965c2499515f18e93e9578bd204 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Mon, 4 May 2026 00:14:07 +0200 Subject: [PATCH] fix(cms): nl/pt/ru i18n + gate external_url in public response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups to PR #372 (external-URL toggle for imprint / privacy CMS pages): 1. **i18n.** PR #372 added 6 new `cms.*` keys to the en + de locales but the project ships 5 locales total. Adds the missing nl / pt / ru translations so the admin CMS page renders in the active language for those users instead of falling back to English literals next to the German/Dutch/Portuguese/Russian surrounding strings. 2. **API shape.** `publicCMS.js` returned `external_url` unconditionally — even when `use_external_url` is false the URL value was still emitted in the public response. The frontend correctly gated on both flags so it worked, but the API surface was leaking a value the admin had explicitly disabled. The value still lives in the DB (so the toggle can be flipped back on without losing it), but the public endpoint now returns `null` whenever the toggle is off. Note: kept the existing `logo_url` shape unchanged. Its semantics are different — null means "fall back to global branding" and consumers rely on always having the field, so emitting it unconditionally is intentional there. No frontend change needed: both `GalleryLayout` and `LegalPage` already gate on `use_external_url && external_url`, so the short-circuit handles `external_url: null` correctly. --- backend/src/routes/publicCMS.js | 7 +++++-- frontend/src/i18n/locales/nl.json | 8 +++++++- frontend/src/i18n/locales/pt.json | 8 +++++++- frontend/src/i18n/locales/ru.json | 8 +++++++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/backend/src/routes/publicCMS.js b/backend/src/routes/publicCMS.js index e354bb01..cafdadf7 100644 --- a/backend/src/routes/publicCMS.js +++ b/backend/src/routes/publicCMS.js @@ -27,9 +27,12 @@ router.get('/pages/:slug', async (req, res) => { logo_url: page.logo_url || null, // Per-page external-URL override. When use_external_url is true and // external_url is set, consumers should redirect / link out instead - // of rendering the internal title/content. + // of rendering the internal title/content. external_url is gated by + // use_external_url so the public response never exposes a URL the + // admin has saved-but-disabled (the value stays in the DB so the + // toggle can be flipped back on, but it shouldn't leak via the API). use_external_url: !!page.use_external_url, - external_url: page.external_url || null, + external_url: page.use_external_url && page.external_url ? page.external_url : null, updated_at: page.updated_at }); } catch (error) { diff --git a/frontend/src/i18n/locales/nl.json b/frontend/src/i18n/locales/nl.json index e959c0b6..fbbaa4f6 100644 --- a/frontend/src/i18n/locales/nl.json +++ b/frontend/src/i18n/locales/nl.json @@ -1753,7 +1753,13 @@ "lastUpdated": "Laatst bijgewerkt:", "impressum": "Colofon", "datenschutz": "Privacybeleid", - "pageUpdated": "Pagina succesvol bijgewerkt" + "pageUpdated": "Pagina succesvol bijgewerkt", + "useExternalUrl": "Externe URL gebruiken", + "useExternalUrlHelp": "Bezoekers worden doorgestuurd naar een externe pagina in plaats van de interne inhoud te zien. De interne titel en inhoud blijven als fallback opgeslagen.", + "externalUrl": "Externe URL", + "externalUrlPlaceholder": "https://example.com/impressum", + "externalUrlInvalid": "Moet een geldige https://-URL zijn", + "externalUrlActive": "Externe URL is actief — interne inhoud blijft bewaard maar wordt niet aan bezoekers getoond." }, "eventTypes": { "title": "Evenementtypes", diff --git a/frontend/src/i18n/locales/pt.json b/frontend/src/i18n/locales/pt.json index 248b7c90..1e5c8b73 100644 --- a/frontend/src/i18n/locales/pt.json +++ b/frontend/src/i18n/locales/pt.json @@ -1753,7 +1753,13 @@ "lastUpdated": "Última atualização:", "impressum": "Aviso Legal", "datenschutz": "Política de Privacidade", - "pageUpdated": "Página atualizada com sucesso" + "pageUpdated": "Página atualizada com sucesso", + "useExternalUrl": "Usar URL externa", + "useExternalUrlHelp": "Redirecione os visitantes para uma página externa em vez de mostrar o conteúdo interno. O título e o conteúdo internos permanecem salvos como fallback.", + "externalUrl": "URL externa", + "externalUrlPlaceholder": "https://example.com/impressum", + "externalUrlInvalid": "Deve ser uma URL https:// válida", + "externalUrlActive": "URL externa está ativa — o conteúdo interno é preservado, mas não é exibido aos visitantes." }, "eventTypes": { "title": "Tipos de Evento", diff --git a/frontend/src/i18n/locales/ru.json b/frontend/src/i18n/locales/ru.json index 5089e87e..e895c762 100644 --- a/frontend/src/i18n/locales/ru.json +++ b/frontend/src/i18n/locales/ru.json @@ -1753,7 +1753,13 @@ "lastUpdated": "Последнее обновление:", "impressum": "Правовая информация", "datenschutz": "Политика конфиденциальности", - "pageUpdated": "Страница успешно обновлена" + "pageUpdated": "Страница успешно обновлена", + "useExternalUrl": "Использовать внешний URL", + "useExternalUrlHelp": "Перенаправлять посетителей на внешнюю страницу вместо показа внутреннего содержимого. Внутренний заголовок и содержимое остаются сохранёнными как резервная копия.", + "externalUrl": "Внешний URL", + "externalUrlPlaceholder": "https://example.com/impressum", + "externalUrlInvalid": "Должен быть действительный URL вида https://", + "externalUrlActive": "Внешний URL активен — внутреннее содержимое сохранено, но не отображается посетителям." }, "eventTypes": { "title": "Типы событий",