i18n(format): respect the full Locale union in legacy formatter cookie reader

This commit is contained in:
Marc-André Bombeck committed 2026-05-21 22:32:02 +02:00
1 parent 373515c1f1
commit 7c3d567430
2 files changed
+133 -3

No files matched your search

@@ -0,0 +1,117 @@
/**
* v1.4.43 QoL (M8) — pin the widened cookie / localStorage decoder in
* `src/lib/format.ts`. Pre-fix a French user reading the dashboard saw
* "12/24/2025, 2:30 PM" via the silent `en` fallback; the formatter
* now honours every supported locale via the full `Locale` union.
*
* `activeLocale()` itself is module-private — exercise it through the
* exported `formatDate` helper, which composes the cookie read with
* `makeFormatters(locale)`. We stub the bare-minimum `document` /
* `window` shape the helper actually reads, so the test runs under
* the default node environment without pulling in `jsdom`.
*/
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi,
} from "vitest";
interface Globals {
document?: { cookie: string };
window?: { localStorage?: { getItem: (key: string) => string | null } };
}
describe("formatDate (cookie-driven locale)", () => {
const globalAny = globalThis as unknown as Globals;
let cookieValue = "";
let storageValue: string | null = null;
beforeEach(() => {
vi.resetModules();
cookieValue = "";
storageValue = null;
globalAny.document = {
get cookie() {
return cookieValue;
},
set cookie(value: string) {
cookieValue = value;
},
};
globalAny.window = {
localStorage: {
getItem: () => storageValue,
},
};
});
afterEach(() => {
delete globalAny.document;
delete globalAny.window;
});
function setCookie(locale: string) {
cookieValue = `healthlog-locale=${locale}; path=/`;
}
const sample = new Date("2026-04-18T10:00:00Z");
it("returns the de format when the cookie reads 'de'", async () => {
setCookie("de");
const { formatDate } = await import("../format");
// German short-date is dd.mm.yyyy.
expect(formatDate(sample)).toMatch(/^\d{2}\.\d{2}\.\d{4}$/);
});
it("returns the fr format when the cookie reads 'fr'", async () => {
setCookie("fr");
const { formatDate } = await import("../format");
// French short-date is dd/mm/yyyy.
expect(formatDate(sample)).toMatch(/^\d{2}\/\d{2}\/\d{4}$/);
});
it("returns the es format when the cookie reads 'es'", async () => {
setCookie("es");
const { formatDate } = await import("../format");
// Spanish short-date is dd/mm/yyyy.
expect(formatDate(sample)).toMatch(/^\d{2}\/\d{2}\/\d{4}$/);
});
it("returns the it format when the cookie reads 'it'", async () => {
setCookie("it");
const { formatDate } = await import("../format");
// Italian short-date is dd/mm/yyyy.
expect(formatDate(sample)).toMatch(/^\d{2}\/\d{2}\/\d{4}$/);
});
it("returns the pl format when the cookie reads 'pl'", async () => {
setCookie("pl");
const { formatDate } = await import("../format");
// Polish short-date is dd.mm.yyyy.
expect(formatDate(sample)).toMatch(/^\d{2}\.\d{2}\.\d{4}$/);
});
it("falls back to en for unrecognised cookie values", async () => {
setCookie("xx");
const { formatDate } = await import("../format");
// English short-date is mm/dd/yyyy.
expect(formatDate(sample)).toMatch(/^\d{2}\/\d{2}\/\d{4}$/);
});
it("falls back to en when the cookie is missing and storage is unset", async () => {
// cookieValue starts as "" — no match.
const { formatDate } = await import("../format");
expect(formatDate(sample)).toMatch(/^\d{2}\/\d{2}\/\d{4}$/);
});
it("falls back to localStorage when the cookie is unrecognised", async () => {
setCookie("xx");
storageValue = "fr";
const { formatDate } = await import("../format");
// French short-date is dd/mm/yyyy — confirms the storage path is hit.
expect(formatDate(sample)).toMatch(/^\d{2}\/\d{2}\/\d{4}$/);
});
});
+16 -3
View File
@@ -13,10 +13,22 @@
* no hydration-mismatch path today. If you add a new caller that renders
* pre-fetch data (e.g. a static prop), migrate it to `useFormatters()` to
* stay consistent with the server-rendered locale.
*
* v1.4.43 QoL (M8) — `activeLocale()` now reads the full `Locale` union
* (`de | en | fr | es | it | pl`). Pre-fix, a sub-locale user (fr / es /
* it / pl) saw French strings but the legacy SSR formatters silently
* fell back to `en`, producing "12/24/2025, 2:30 PM" in a French shell.
* `Intl.DateTimeFormat` already handles every supported locale via the
* `INTL_LOCALE_MAP`, so the only change is to widen the cookie /
* localStorage decoder.
*/
import { makeFormatters } from "./format-locale";
import type { Locale } from "./i18n/config";
import { locales, type Locale } from "./i18n/config";
function isLocale(value: string | null | undefined): value is Locale {
return !!value && (locales as readonly string[]).includes(value);
}
function activeLocale(): Locale {
if (typeof document === "undefined") return "en";
@@ -26,9 +38,10 @@ function activeLocale(): Locale {
/(?:^|;\s*)healthlog-locale=([^;]+)/,
);
const fromCookie = cookieMatch?.[1];
if (fromCookie === "de" || fromCookie === "en") return fromCookie;
if (isLocale(fromCookie)) return fromCookie;
const fromStorage = window.localStorage?.getItem("healthlog-locale");
return fromStorage === "de" ? "de" : "en";
if (isLocale(fromStorage)) return fromStorage;
return "en";
}
function formatters() {