mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Output of `node scripts/strip-planning-tags.mjs --write`: 3,157 comment lines in 1,308 files lose the work-item identifier at their head. Release versions and issue references stay; nothing outside comment text changes, which the script checks per file against a comment-free printout. healthlog/no-planning-tags is enabled at error level with this commit, so the tree it lands on is clean.
156 lines
6.2 KiB
TypeScript
156 lines
6.2 KiB
TypeScript
/**
|
|
* Legacy locale-aware date/time helpers.
|
|
*
|
|
* These originally hard-coded `de-DE`. They now defer to `makeFormatters`
|
|
* using a best-effort read of `healthlog-locale` from cookie → localStorage,
|
|
* falling back to `en`. New code should prefer `useFormatters()` from
|
|
* `@/lib/i18n/context` (client) or `makeFormatters(locale)` from
|
|
* `@/lib/format-locale` (server).
|
|
*
|
|
* SSR caveat: on the server `activeLocale()` always returns `en` because
|
|
* `document` is undefined. All current call sites render their formatted
|
|
* strings only after a `useQuery` fetch — i.e. post-hydration — so there is
|
|
* 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 — `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 { readStoredDateFormat } from "./date-format";
|
|
import { readStoredTimeFormat } from "./time-format";
|
|
import { readStoredTimezone } from "./timezone-mirror";
|
|
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";
|
|
// Prefer the cookie (what SSR used) over localStorage to stay in sync
|
|
// across hydration. Falls back to localStorage for backwards compat.
|
|
const cookieMatch = document.cookie.match(
|
|
/(?:^|;\s*)healthlog-locale=([^;]+)/,
|
|
);
|
|
const fromCookie = cookieMatch?.[1];
|
|
if (isLocale(fromCookie)) return fromCookie;
|
|
const fromStorage = window.localStorage?.getItem("healthlog-locale");
|
|
if (isLocale(fromStorage)) return fromStorage;
|
|
return "en";
|
|
}
|
|
|
|
function formatters() {
|
|
// Honour the mirrored hour-cycle preference, the mirrored profile timezone
|
|
// (issue #490) AND the mirrored date order (issue #922) so these legacy
|
|
// helpers render the same clock, zone and field order as `useFormatters()`
|
|
// call sites. The date mirror was the one of the three that was never read
|
|
// here, which is why the measurements list ignored the profile setting the
|
|
// entry form's `<DateField>` obeyed. SSR reads AUTO + "" (→ Berlin) — same
|
|
// post-hydration caveat as `activeLocale()` above.
|
|
return makeFormatters(
|
|
activeLocale(),
|
|
readStoredTimezone(),
|
|
readStoredTimeFormat(),
|
|
readStoredDateFormat(),
|
|
);
|
|
}
|
|
|
|
/** Locale-aware "19.02.2026, 14:30" or "02/19/2026, 2:30 PM". */
|
|
export function formatDateTime(date: Date | string): string {
|
|
return formatters().dateTime(date);
|
|
}
|
|
|
|
/** Locale-aware "19.02.2026" or "02/19/2026". */
|
|
export function formatDate(date: Date | string): string {
|
|
return formatters().date(date);
|
|
}
|
|
|
|
/** Locale-aware short date without year unless `includeYear`. */
|
|
export function formatDateShort(
|
|
date: Date | string,
|
|
includeYear = false,
|
|
): string {
|
|
const f = formatters();
|
|
return includeYear ? f.date(date) : f.dateShort(date);
|
|
}
|
|
|
|
/** Locale-aware "14:30". */
|
|
export function formatTime(date: Date | string): string {
|
|
return formatters().time(date);
|
|
}
|
|
|
|
/**
|
|
* v1.4.43 — render a timestamp as a relative phrase when it
|
|
* happened within the last 24 hours, otherwise fall back to the
|
|
* absolute locale-aware date+time. Closes the visual inconsistency
|
|
* the measurement-list surfaced: the same screen could show "vor 3
|
|
* min" (briefing) alongside "21.05.2026, 14:32" (list rows) for
|
|
* adjacent events.
|
|
*
|
|
* Buckets:
|
|
* - < 1 m → "just now"
|
|
* - < 1 h → "vor N min" / "N min ago"
|
|
* - < 24 h → "vor N Std." / "N h ago"
|
|
* - ≥ 24 h → absolute `formatDateTime`
|
|
*
|
|
* The relative branch reuses the `insights.relative*` i18n keys (same
|
|
* keys `formatRelativeTime()` reads from `@/lib/i18n/relative-time`).
|
|
* Caller supplies a `t()` translator so this helper stays usable
|
|
* server-side; clients should pass `useTranslations().t`. The buckets
|
|
* mirror the briefing's existing copy so users never see two
|
|
* different relative phrasings in the same view.
|
|
*/
|
|
export function formatDateOrRelative(
|
|
iso: Date | string,
|
|
t: (key: string, params?: Record<string, string | number>) => string,
|
|
/**
|
|
* Optional "now" for deterministic tests. Defaults to `Date.now()`.
|
|
*/
|
|
nowMs: number = Date.now(),
|
|
): string {
|
|
const target = iso instanceof Date ? iso.getTime() : new Date(iso).getTime();
|
|
if (Number.isNaN(target)) return "";
|
|
const diffMs = nowMs - target;
|
|
// Future timestamps fall back to absolute — relative copy doesn't
|
|
// model them and "in 3 min" would mislead in a health-log context.
|
|
if (diffMs < 0) return formatDateTime(iso);
|
|
if (diffMs < 60_000) return t("insights.relativeJustNow");
|
|
// v1.4.49.2 — One/Other pluralisation. Mirrors the
|
|
// `src/lib/i18n/relative-time.ts:24-48` pattern (which got this fix in
|
|
// v1.4.43 H6 for the "vor 1 Minuten" bug). This twin helper was missed
|
|
// in the v1.4.43 sweep, so the bare `t("insights.relativeMinutesAgo",
|
|
// { count })` and `t("insights.relativeHoursAgo", { count })` calls
|
|
// returned the key itself — the translation bundle only carries the
|
|
// pluralised `*One` / `*Other` variants. Reported by the maintainer as raw
|
|
// `insights.relativeHoursAgo` leaking onto medication cards, recent-
|
|
// achievements, admin sections, and every other consumer of
|
|
// `formatDateOrRelative`.
|
|
const minutes = Math.floor(diffMs / 60_000);
|
|
if (minutes < 60) {
|
|
return t(
|
|
minutes === 1
|
|
? "insights.relativeMinutesAgoOne"
|
|
: "insights.relativeMinutesAgoOther",
|
|
{ count: minutes },
|
|
);
|
|
}
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) {
|
|
return t(
|
|
hours === 1
|
|
? "insights.relativeHoursAgoOne"
|
|
: "insights.relativeHoursAgoOther",
|
|
{ count: hours },
|
|
);
|
|
}
|
|
return formatDateTime(iso);
|
|
}
|