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