From b3fb9afa2ce52f7e03b0f3b98f473ffe731b94fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 8 Oct 2026 19:02:00 +0200 Subject: [PATCH] Lead the privacy and about pages with the reader's language The privacy policy is German first in every body, but its section headings, title and table of contents led in English and its date read 2026-08-11 on both lines. Headings now lead in German with the English title beside them, the contents list follows, and each date line writes the date as its language does. The about page's headings, date and sign-in link follow the reader's locale. No legal wording changed. --- messages/de.json | 7 +++ messages/en.json | 7 +++ messages/es.json | 7 +++ messages/fr.json | 7 +++ messages/it.json | 7 +++ messages/ko.json | 7 +++ messages/pl.json | 7 +++ src/app/about/page.tsx | 30 ++++++++--- src/app/privacy/__tests__/page.test.tsx | 67 +++++++++++++++++-------- src/app/privacy/page.tsx | 61 +++++++++++++++------- 10 files changed, 159 insertions(+), 48 deletions(-) diff --git a/messages/de.json b/messages/de.json index c0f43f7b2..712c80243 100644 --- a/messages/de.json +++ b/messages/de.json @@ -12497,5 +12497,12 @@ "title": "Lebensereignis löschen?", "body": "Titel und Notiz sind danach nicht mehr da." } + }, + "aboutPage": { + "eyebrow": "Info", + "title": "Über HealthLog", + "lastUpdated": "Zuletzt aktualisiert: {date}", + "project": "Projekt", + "credits": "Danksagungen" } } diff --git a/messages/en.json b/messages/en.json index 2b4d08d79..e94ccfa96 100644 --- a/messages/en.json +++ b/messages/en.json @@ -12497,5 +12497,12 @@ "title": "Delete this life event?", "body": "Its title and note will be gone." } + }, + "aboutPage": { + "eyebrow": "About", + "title": "About HealthLog", + "lastUpdated": "Last updated: {date}", + "project": "Project", + "credits": "Credits" } } diff --git a/messages/es.json b/messages/es.json index 2c0901a0b..4816ec62e 100644 --- a/messages/es.json +++ b/messages/es.json @@ -12497,5 +12497,12 @@ "title": "¿Eliminar este acontecimiento vital?", "body": "Su título y su nota desaparecerán." } + }, + "aboutPage": { + "eyebrow": "Información", + "title": "Acerca de HealthLog", + "lastUpdated": "Última actualización: {date}", + "project": "Proyecto", + "credits": "Créditos" } } diff --git a/messages/fr.json b/messages/fr.json index 6627cc1c4..47293067c 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -12497,5 +12497,12 @@ "title": "Supprimer cet événement de vie ?", "body": "Son titre et sa note disparaîtront." } + }, + "aboutPage": { + "eyebrow": "À propos", + "title": "À propos de HealthLog", + "lastUpdated": "Dernière mise à jour : {date}", + "project": "Projet", + "credits": "Crédits" } } diff --git a/messages/it.json b/messages/it.json index 5dc38aa73..1861817da 100644 --- a/messages/it.json +++ b/messages/it.json @@ -12497,5 +12497,12 @@ "title": "Eliminare questo evento di vita?", "body": "Titolo e nota non ci saranno più." } + }, + "aboutPage": { + "eyebrow": "Informazioni", + "title": "Informazioni su HealthLog", + "lastUpdated": "Ultimo aggiornamento: {date}", + "project": "Progetto", + "credits": "Riconoscimenti" } } diff --git a/messages/ko.json b/messages/ko.json index 9ddb7604e..95744e729 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -12497,5 +12497,12 @@ "title": "이 삶의 사건을 삭제할까요?", "body": "제목과 메모가 사라져요." } + }, + "aboutPage": { + "eyebrow": "정보", + "title": "HealthLog 정보", + "lastUpdated": "마지막 업데이트: {date}", + "project": "프로젝트", + "credits": "크레딧" } } diff --git a/messages/pl.json b/messages/pl.json index de6cd01aa..4f1b2aa9a 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -12497,5 +12497,12 @@ "title": "Usunąć to wydarzenie życiowe?", "body": "Tytuł i notatka znikną." } + }, + "aboutPage": { + "eyebrow": "Informacje", + "title": "O HealthLog", + "lastUpdated": "Ostatnia aktualizacja: {date}", + "project": "Projekt", + "credits": "Podziękowania" } } diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 9d039b5e6..cbfe211f5 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,6 +1,10 @@ import type { Metadata } from "next"; import Link from "next/link"; +import { resolveIntlLocale } from "@/lib/format-locale"; +import { resolveServerLocale } from "@/lib/i18n/server-locale"; +import { getServerTranslator } from "@/lib/i18n/server-translator"; + /** * v1.4.27 B3 — Public about / credits page. * @@ -67,7 +71,17 @@ function Section({ ); } -export default function AboutPage() { +export default async function AboutPage() { + // The headings, the date and the sign-in link follow the reader's locale; + // the body is the attribution text the licences ask for and stays as + // written. + const locale = await resolveServerLocale(); + const { t } = getServerTranslator(locale); + const lastUpdated = new Intl.DateTimeFormat(resolveIntlLocale(locale), { + dateStyle: "medium", + timeZone: "UTC", + }).format(new Date(`${LAST_UPDATED}T12:00:00Z`)); + return (
@@ -82,7 +96,7 @@ export default function AboutPage() { href="/auth/login" className="text-muted-foreground hover:text-foreground inline-flex min-h-11 items-center text-sm" > - Sign in + {t("auth.login")}
@@ -99,17 +113,19 @@ export default function AboutPage() { >

- About + {t("aboutPage.eyebrow")}

- About HealthLog + {t("aboutPage.title")}

- Last updated: {LAST_UPDATED} +

-
+

HealthLog is an open-source, self-hostable personal-health-tracking application. The source code lives at{" "} @@ -127,7 +143,7 @@ export default function AboutPage() {

-
+

HealthLog stands on a number of open-source libraries and public data sources. The list below covers the third-party assets that ship diff --git a/src/app/privacy/__tests__/page.test.tsx b/src/app/privacy/__tests__/page.test.tsx index 8bef1084e..18805bd10 100644 --- a/src/app/privacy/__tests__/page.test.tsx +++ b/src/app/privacy/__tests__/page.test.tsx @@ -20,9 +20,10 @@ import PrivacyPage from "../page"; * enumerated verbatim by identifier. Drift from the iOS source * (`HealthKitWireConverter.swift` + `HealthKitService.swift`) * would be a compliance hole. - * 4. The numbered section headings are all present (English form, so - * the scannable spine matches the in-app `Settings → Privacy` - * link target and the table of contents above each section). + * 4. The numbered section headings are all present, German first with + * the English title beside it, so the scannable spine still matches + * the in-app `Settings → Privacy` link target and the table of + * contents. * 5. v1.4.40 SB-3 additions — consent receipt endpoint, TLS 1.3, * operator email + deletion-route disclosure, retention windows. */ @@ -46,31 +47,53 @@ describe("", () => { it("advertises the policy version + last-updated date", () => { const html = render(); expect(html).toContain(`Policy version ${POLICY_VERSION}`); - expect(html).toContain(`Last updated: ${LAST_UPDATED}`); - expect(html).toContain(`Stand: ${LAST_UPDATED}`); + // Each language writes the date its own way, on its own line. + expect(html).toContain("Stand: 11. August 2026"); + expect(html).toContain("Last updated: 11 August 2026"); + expect(html).toContain(`dateTime="${LAST_UPDATED}"`); expect(html).toContain('data-slot="privacy-last-updated"'); }); it("renders the numbered section headings", () => { const html = render(); - // 11 numbered sections — H2 headings carry the English form as the - // canonical scan spine, with the German title appearing inline in - // each section body. The TOC at the top mirrors both. - const expectedHeadings = [ - "1. Overview", - "2. Data we collect", - "3. Why we collect each category", - "4. Third-party sub-processors", - "5. Data storage and retention", - "6. Your rights (GDPR Art. 15-22, DSGVO)", - "7. Medical-device boundary (EU MDR 2017/745, MDCG 2021-24)", - "8. Apple App Store privacy categories", - "9. Children", - "10. Changes to this policy", - "11. Contact", + // 11 numbered sections. The H2 leads in German, like the body under it, + // and carries the English title after it so the English table of + // contents and the in-app link target still scan the same spine. + const expectedHeadings: Array<[string, string]> = [ + ["1. Überblick", "Overview"], + ["2. Erhobene Daten", "Data we collect"], + ["3. Zwecke der Verarbeitung", "Why we collect each category"], + [ + "4. Auftragsverarbeiter und Drittanbieter", + "Third-party sub-processors", + ], + [ + "5. Speicherung, Speicherdauer und Verschlüsselung", + "Data storage and retention", + ], + [ + "6. Ihre Rechte (DSGVO Art. 15–22)", + "Your rights (GDPR Art. 15-22, DSGVO)", + ], + [ + "7. Medizinprodukte-Grenze (EU-MDR 2017/745, MDCG 2021-24)", + "Medical-device boundary (EU MDR 2017/745, MDCG 2021-24)", + ], + [ + "8. Apple-App-Store-Datenschutzkategorien", + "Apple App Store privacy categories", + ], + ["9. Kinder", "Children"], + ["10. Änderungen dieser Erklärung", "Changes to this policy"], + ["11. Kontakt", "Contact"], ]; - for (const heading of expectedHeadings) { - expect(html, `missing H2 ${JSON.stringify(heading)}`).toContain(heading); + for (const [de, en] of expectedHeadings) { + expect(html, `missing H2 ${JSON.stringify(de)}`).toContain( + `${de.replaceAll("&", "&")}`, + ); + expect(html, `missing English title ${JSON.stringify(en)}`).toContain( + ` / ${en}`, + ); } }); diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx index 4262f88c2..51f901682 100644 --- a/src/app/privacy/page.tsx +++ b/src/app/privacy/page.tsx @@ -38,6 +38,16 @@ export const dynamic = "force-dynamic"; const POLICY_VERSION = "1.37.12"; const LAST_UPDATED = "2026-08-11"; +const LAST_UPDATED_DATE = new Date(`${LAST_UPDATED}T12:00:00Z`); +// The date as each language writes it, on the line that language reads. +const LAST_UPDATED_DE = new Intl.DateTimeFormat("de-DE", { + dateStyle: "long", + timeZone: "UTC", +}).format(LAST_UPDATED_DATE); +const LAST_UPDATED_EN = new Intl.DateTimeFormat("en-GB", { + dateStyle: "long", + timeZone: "UTC", +}).format(LAST_UPDATED_DATE); export const metadata: Metadata = { title: "Datenschutzerklärung / Privacy Policy — HealthLog", @@ -67,8 +77,14 @@ function Section({ }: SectionProps) { return (

+ {/* German first, like the body under it; the English title rides + along so the scan spine still matches the English table of + contents and the in-app link target. */}

- {`${numberLabel}. ${titleEn}`} + {`${numberLabel}. ${titleDe}`} + + {` / ${titleEn}`} +

- Deutsch — {titleDe} + Deutsch

{bodyDe}
@@ -166,7 +182,7 @@ function SubProcessor({ rel="noopener noreferrer" className="text-primary underline underline-offset-2" > - Privacy policy / Datenschutzerklärung + Datenschutzerklärung / Privacy policy

@@ -212,13 +228,20 @@ export default function PrivacyPage() { Policy version {POLICY_VERSION}

- Datenschutzerklärung / Privacy Policy + Datenschutzerklärung{" "} + + / Privacy Policy +

- Last updated: {LAST_UPDATED} · Stand: {LAST_UPDATED} +

Diese Erklärung ist deutsch-englisch geführt. Jeder Abschnitt @@ -253,7 +276,7 @@ export default function PrivacyPage() { className="hover:text-foreground hover:underline" href="#intro" > - 1. Overview / Überblick + 1. Überblick / Overview

  • @@ -261,7 +284,7 @@ export default function PrivacyPage() { className="hover:text-foreground hover:underline" href="#data-we-collect" > - 2. Data we collect / Erhobene Daten + 2. Erhobene Daten / Data we collect
  • @@ -269,7 +292,7 @@ export default function PrivacyPage() { className="hover:text-foreground hover:underline" href="#purpose" > - 3. Why we collect each category / Zwecke der Verarbeitung + 3. Zwecke der Verarbeitung / Why we collect each category
  • @@ -277,7 +300,7 @@ export default function PrivacyPage() { className="hover:text-foreground hover:underline" href="#sub-processors" > - 4. Third-party sub-processors / Auftragsverarbeiter + 4. Auftragsverarbeiter / Third-party sub-processors
  • @@ -285,8 +308,8 @@ export default function PrivacyPage() { className="hover:text-foreground hover:underline" href="#storage" > - 5. Data storage and retention / Speicherung & - Speicherdauer + 5. Speicherung & Speicherdauer / Data storage and + retention
  • @@ -294,7 +317,7 @@ export default function PrivacyPage() { className="hover:text-foreground hover:underline" href="#rights" > - 6. Your rights (GDPR Art. 15-22, DSGVO) / Ihre Rechte + 6. Ihre Rechte / Your rights (GDPR Art. 15-22, DSGVO)
  • @@ -302,8 +325,8 @@ export default function PrivacyPage() { className="hover:text-foreground hover:underline" href="#medical-boundary" > - 7. Medical-device boundary (EU MDR 2017/745, MDCG 2021-24) / - Medizinprodukte-Grenze + 7. Medizinprodukte-Grenze / Medical-device boundary (EU MDR + 2017/745, MDCG 2021-24)
  • @@ -311,8 +334,8 @@ export default function PrivacyPage() { className="hover:text-foreground hover:underline" href="#apple-categories" > - 8. Apple App Store privacy categories / Apple-App-Store- - Datenschutzkategorien + 8. Apple-App-Store- Datenschutzkategorien / Apple App Store + privacy categories
  • @@ -320,7 +343,7 @@ export default function PrivacyPage() { className="hover:text-foreground hover:underline" href="#children" > - 9. Children / Kinder + 9. Kinder / Children
  • @@ -328,7 +351,7 @@ export default function PrivacyPage() { className="hover:text-foreground hover:underline" href="#changes" > - 10. Changes to this policy / Änderungen + 10. Änderungen / Changes to this policy
  • @@ -336,7 +359,7 @@ export default function PrivacyPage() { className="hover:text-foreground hover:underline" href="#contact" > - 11. Contact / Kontakt + 11. Kontakt / Contact