fix(ux): honest deletes, stable hydration and tidy numbers after release review

Server and first client render now agree everywhere query state used to
leak into the chrome, checkboxes no longer nest inside buttons, delete
confirmations admit the undo they offer, raw step counts drop their
decimals, the truncated unit label wraps instead, streaks know their
singular, the medication header speaks the user's date format, a failed
coach turn keeps the user's message and says what went wrong, settings
status notes follow a language switch, the locale only flips once its
bundle actually loaded, and the last raw api fetches in hooks move onto
the shared client.
This commit is contained in:
Marc-André Bombeck committed 2026-06-11 03:30:18 +02:00
1 parent 25f41f88fe
commit a8149e0bdf
35 files changed
+347 -191

No files matched your search

+20 -7
View File
@@ -77,6 +77,19 @@ function getSavedLocale(): Locale | null {
return null;
}
// Persist a locale choice everywhere the next render / request reads
// it: localStorage (client preference), cookie (SSR handoff — layout +
// metadata render in the user's language), <html lang>. Called ONLY
// alongside the `setActive` flip so a failed bundle load never leaves
// the persisted locale pointing at strings the UI isn't showing.
function persistLocale(newLocale: Locale) {
localStorage.setItem("healthlog-locale", newLocale);
// 1-year expiry, Lax samesite, not HttpOnly so the client continues
// to own it.
document.cookie = `healthlog-locale=${newLocale}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`;
document.documentElement.lang = newLocale;
}
export function I18nProvider({
children,
initialLocale,
@@ -142,17 +155,12 @@ export function I18nProvider({
const setLocale = useCallback((newLocale: Locale) => {
if (!(locales as readonly string[]).includes(newLocale)) return;
localStorage.setItem("healthlog-locale", newLocale);
// Also mirror to cookie so SSR (layout, metadata) renders in the
// user's language. 1-year expiry, Lax samesite, not HttpOnly so the
// client continues to own it.
document.cookie = `healthlog-locale=${newLocale}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`;
document.documentElement.lang = newLocale;
const cached = getCachedMessages(newLocale);
if (cached) {
requestedLocaleRef.current = null;
setPendingLocale(null);
persistLocale(newLocale);
setActive({ locale: newLocale, messages: cached });
return;
}
@@ -164,11 +172,16 @@ export function I18nProvider({
if (requestedLocaleRef.current !== newLocale) return;
requestedLocaleRef.current = null;
setPendingLocale(null);
// Persist atomically with the flip — never before the bundle is
// in hand, so cookie / <html lang> / localStorage can't drift
// onto a locale the UI failed to load.
persistLocale(newLocale);
setActive({ locale: newLocale, messages: loaded });
})
.catch(() => {
// Bundle fetch failed (offline mid-session, …) — stay on the
// current locale rather than rendering the new one in EN.
// current locale rather than rendering the new one in EN, and
// leave cookie / <html lang> / localStorage untouched.
if (requestedLocaleRef.current !== newLocale) return;
requestedLocaleRef.current = null;
setPendingLocale(null);
+26
View File
@@ -90,6 +90,32 @@ export function getDisplayTransform(
return perType[preference] ?? perType.metric;
}
/**
* v1.16.4 — count metrics are integers by definition; any fractional
* part on a stored sample is source noise (HK chunk splitting, device
* estimation), so the raw-value surfaces render them with 0 decimals
* ("9.689 steps", never "9.688,595 steps"). Every other type keeps the
* formatter's default precision (≤3 digits, trailing zeros dropped).
*/
const INTEGER_DISPLAY_TYPES: ReadonlySet<string> = new Set([
"ACTIVITY_STEPS",
"FLIGHTS_CLIMBED",
"FALL_COUNT",
// kcal + daylight minutes — whole units are the meaningful display
// grain for these day-cumulative counts.
"ACTIVE_ENERGY_BURNED",
"TIME_IN_DAYLIGHT",
]);
/**
* Display fraction digits for a RAW (untransformed) value of `type`.
* `0` for integer count metrics, `undefined` (= formatter default)
* otherwise — pass the result straight into `Formatters.number`.
*/
export function rawDisplayFractionDigits(type: string): number | undefined {
return INTEGER_DISPLAY_TYPES.has(type) ? 0 : undefined;
}
/**
* Apply a transform's factor to a raw canonical value. Pure helper —
* keeps the multiply in one place so the chart, list cell, and tooltip