diff --git a/frontend/src/components/legal/LegalLayout.tsx b/frontend/src/components/legal/LegalLayout.tsx
index 1fda88ba..5b0d30bd 100644
--- a/frontend/src/components/legal/LegalLayout.tsx
+++ b/frontend/src/components/legal/LegalLayout.tsx
@@ -9,9 +9,10 @@
import { useEffect, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
-import { LANGUAGES, setLanguage } from "../../i18n";
+import { setLanguage, type LangCode } from "../../i18n";
import { formatDate } from "../../lib/format";
import { send } from "../../lib/http";
+import LanguageSwitcher from "../LanguageSwitcher";
// The day the text last changed materially — bump it with every edit of either body.
const LAST_UPDATED = "2026-09-17";
@@ -77,20 +78,8 @@ export default function LegalLayout({ title, children }: { title: string; childr
Siftlode
-
- {LANGUAGES.map((lang) => (
-
- ))}
-
+ {/* The landing page's own picker, so the public pages switch language one way. */}
+
{title}
diff --git a/frontend/src/i18n/locales/en/legal.json b/frontend/src/i18n/locales/en/legal.json
index 40e80736..ab344824 100644
--- a/frontend/src/i18n/locales/en/legal.json
+++ b/frontend/src/i18n/locales/en/legal.json
@@ -2,6 +2,5 @@
"lastUpdated": "Last updated: {{date}}",
"home": "Home",
"contact": "Contact",
- "operatorFallback": "your instance's administrator",
- "languageLabel": "Language"
+ "operatorFallback": "your instance's administrator"
}
diff --git a/frontend/src/i18n/locales/hu/legal.json b/frontend/src/i18n/locales/hu/legal.json
index 8efe0910..ee2ac430 100644
--- a/frontend/src/i18n/locales/hu/legal.json
+++ b/frontend/src/i18n/locales/hu/legal.json
@@ -2,6 +2,5 @@
"lastUpdated": "Utoljára frissítve: {{date}}",
"home": "Kezdőlap",
"contact": "Kapcsolat",
- "operatorFallback": "a példány adminisztrátora",
- "languageLabel": "Nyelv"
+ "operatorFallback": "a példány adminisztrátora"
}
diff --git a/frontend/src/lib/format.test.ts b/frontend/src/lib/format.test.ts
index ab28ce42..cb723338 100644
--- a/frontend/src/lib/format.test.ts
+++ b/frontend/src/lib/format.test.ts
@@ -1,6 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
RELATIVE_UNITS,
+ formatDate,
+ formatDateTime,
formatNumber,
formatRuntime,
formatTotalHours,
@@ -191,3 +193,33 @@ describe("formatRuntime", () => {
}
});
});
+
+// Dates had the same bug as numbers (a bare `toLocaleString()`), and the same trap for a test: the
+// runtime's own locale and time zone are the dev box's, so both languages — and a zone west of UTC —
+// are pinned explicitly.
+describe("formatDateTime / formatDate", () => {
+ const savedTz = process.env.TZ;
+ afterEach(() => {
+ i18nStub.language = "en";
+ if (savedTz === undefined) delete process.env.TZ;
+ else process.env.TZ = savedTz;
+ });
+
+ it("formats a timestamp in the UI language, not in the runtime's", () => {
+ const iso = "2026-09-17T08:30:14Z";
+ const asEnglish = formatDateTime(iso);
+ i18nStub.language = "hu";
+ const asHungarian = formatDateTime(iso);
+ expect(asEnglish).toBe(new Date(iso).toLocaleString("en"));
+ expect(asHungarian).toBe(new Date(iso).toLocaleString("hu"));
+ expect(asEnglish).not.toBe(asHungarian);
+ expect(formatDate(iso, "en")).not.toBe(formatDate(iso, "hu"));
+ });
+
+ it("shows a date-only value as that calendar day in every time zone", () => {
+ process.env.TZ = "America/New_York";
+ expect(formatDate("2026-09-17", "en")).toBe("Sep 17, 2026");
+ process.env.TZ = "Pacific/Kiritimati";
+ expect(formatDate("2026-09-17", "en")).toBe("Sep 17, 2026");
+ });
+});
diff --git a/frontend/src/lib/format.ts b/frontend/src/lib/format.ts
index 0dde20e3..ede19328 100644
--- a/frontend/src/lib/format.ts
+++ b/frontend/src/lib/format.ts
@@ -176,8 +176,16 @@ export function formatDateTime(iso: string | null | undefined): string {
}
// Absolute day-precision date (e.g. "12 Jan 2017"), shown alongside a relative time. Empty for null.
+// A DATE-only value ("2026-09-17": a release, the legal pages' last update) is a calendar day, not an
+// instant: `new Date` reads it as UTC midnight, which is still the day before west of UTC, so it is
+// shown in UTC. A timestamp is shown in the viewer's own zone, as it should be.
export function formatDate(iso: string | null | undefined, lang: string): string {
- return iso
- ? new Date(iso).toLocaleDateString(lang, { year: "numeric", month: "short", day: "numeric" })
- : "";
+ if (!iso) return "";
+ const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(iso);
+ return new Date(iso).toLocaleDateString(lang, {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ ...(dateOnly ? { timeZone: "UTC" } : {}),
+ });
}