@@ -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. */}
- 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
@@ -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)
@@ -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