Merge pull request #498 from MBombeck/release/v1.28.41

release/v1.28.41 — complete the translations, guard the gap
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-07-16 02:49:07 +02:00
commit 9569d1c528
11 files changed
+195 -15

No files matched your search

+11
View File
@@ -2,6 +2,17 @@
## [Unreleased]
## [1.28.41] — 2026-07-16 — Complete the translations, and guard the gap
One reminder-adjacent label — the recovery-driver "functional impact" track on
the illness insights card — was showing its internal name instead of a
translation in every language; it is now translated in all six. The class of
bug behind it (a label resolved through a computed key the static check can't
see, so a missing translation ships silently — the same seam as the well-being
labels fixed a release ago) is now closed structurally: a new test walks each
such key space against every language bundle, so a missing translation fails
the build instead of reaching a user.
## [1.28.40] — 2026-07-16 — Insight assessments lead with meaning, not a number
The per-metric insight assessments read like a data readout — they opened on
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
version: 1.28.40
version: 1.28.41
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
+2 -2
View File
@@ -7751,12 +7751,12 @@
"RECOVERY_SCORE": "Recovery",
"OXYGEN_SATURATION": "Sauerstoffsättigung",
"BODY_TEMPERATURE": "Körpertemperatur",
"SKIN_TEMPERATURE": "Hauttemperatur",
"RESPIRATORY_RATE": "Atemfrequenz",
"BLOOD_PRESSURE_SYS": "Systolischer Blutdruck",
"BLOOD_PRESSURE_DIA": "Diastolischer Blutdruck",
"PULSE": "Pulse",
"WEIGHT": "Weight"
"WEIGHT": "Weight",
"FUNCTIONAL_IMPACT": "Funktionelle Beeinträchtigung"
},
"listLoadError": "Episoden konnten nicht geladen werden."
},
+2 -2
View File
@@ -7751,12 +7751,12 @@
"RECOVERY_SCORE": "Recovery",
"OXYGEN_SATURATION": "Oxygen saturation",
"BODY_TEMPERATURE": "Body temperature",
"SKIN_TEMPERATURE": "Skin temperature",
"RESPIRATORY_RATE": "Respiratory rate",
"BLOOD_PRESSURE_SYS": "Systolic blood pressure",
"BLOOD_PRESSURE_DIA": "Diastolic blood pressure",
"PULSE": "Pulse",
"WEIGHT": "Weight"
"WEIGHT": "Weight",
"FUNCTIONAL_IMPACT": "Functional impact"
},
"listLoadError": "Could not load your episodes."
},
+2 -2
View File
@@ -7751,12 +7751,12 @@
"RECOVERY_SCORE": "Recovery",
"OXYGEN_SATURATION": "Saturación de oxígeno",
"BODY_TEMPERATURE": "Temperatura corporal",
"SKIN_TEMPERATURE": "Temperatura de la piel",
"RESPIRATORY_RATE": "Frecuencia respiratoria",
"BLOOD_PRESSURE_SYS": "Presión arterial sistólica",
"BLOOD_PRESSURE_DIA": "Presión arterial diastólica",
"PULSE": "Pulse",
"WEIGHT": "Weight"
"WEIGHT": "Weight",
"FUNCTIONAL_IMPACT": "Impacto funcional"
},
"listLoadError": "No se pudieron cargar tus episodios."
},
+2 -2
View File
@@ -7751,12 +7751,12 @@
"RECOVERY_SCORE": "Recovery",
"OXYGEN_SATURATION": "Saturation en oxygène",
"BODY_TEMPERATURE": "Température corporelle",
"SKIN_TEMPERATURE": "Température cutanée",
"RESPIRATORY_RATE": "Fréquence respiratoire",
"BLOOD_PRESSURE_SYS": "Pression artérielle systolique",
"BLOOD_PRESSURE_DIA": "Pression artérielle diastolique",
"PULSE": "Pulse",
"WEIGHT": "Weight"
"WEIGHT": "Weight",
"FUNCTIONAL_IMPACT": "Retentissement fonctionnel"
},
"listLoadError": "Impossible de charger vos épisodes."
},
+2 -2
View File
@@ -7751,12 +7751,12 @@
"RECOVERY_SCORE": "Recovery",
"OXYGEN_SATURATION": "Saturazione di ossigeno",
"BODY_TEMPERATURE": "Temperatura corporea",
"SKIN_TEMPERATURE": "Temperatura cutanea",
"RESPIRATORY_RATE": "Frequenza respiratoria",
"BLOOD_PRESSURE_SYS": "Pressione arteriosa sistolica",
"BLOOD_PRESSURE_DIA": "Pressione arteriosa diastolica",
"PULSE": "Pulse",
"WEIGHT": "Weight"
"WEIGHT": "Weight",
"FUNCTIONAL_IMPACT": "Impatto funzionale"
},
"listLoadError": "Impossibile caricare gli episodi."
},
+2 -2
View File
@@ -7751,12 +7751,12 @@
"RECOVERY_SCORE": "Recovery",
"OXYGEN_SATURATION": "Saturacja tlenem",
"BODY_TEMPERATURE": "Temperatura ciała",
"SKIN_TEMPERATURE": "Temperatura skóry",
"RESPIRATORY_RATE": "Częstość oddechów",
"BLOOD_PRESSURE_SYS": "Ciśnienie skurczowe",
"BLOOD_PRESSURE_DIA": "Ciśnienie rozkurczowe",
"PULSE": "Pulse",
"WEIGHT": "Weight"
"WEIGHT": "Weight",
"FUNCTIONAL_IMPACT": "Wpływ na funkcjonowanie"
},
"listLoadError": "Nie udało się załadować epizodów."
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "healthlog",
"version": "1.28.40",
"version": "1.28.41",
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
"license": "PolyForm-Noncommercial-1.0.0",
"homepage": "https://healthlog.dev",
+1 -1
View File
@@ -36,7 +36,7 @@ try {
// v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild.
const CACHE_VERSION =
(typeof self !== "undefined" && self.__APP_VERSION__) ||
/* @sw-version-fallback */ "v1.28.40";
/* @sw-version-fallback */ "v1.28.41";
const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
// v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`
@@ -0,0 +1,169 @@
import { describe, expect, it } from "vitest";
import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { resolveKey } from "@/lib/i18n/resolve-key";
import {
ILLNESS_SCAN_TYPES,
FUNCTIONAL_IMPACT_RETURN_KEY,
} from "@/lib/illness/correlation";
import {
illnessTypeEnum,
illnessLifecycleEnum,
} from "@/lib/validations/illness";
import {
allergyCategoryEnum,
allergyTypeEnum,
allergySeverityEnum,
allergyStatusEnum,
} from "@/lib/validations/allergy";
import { familyRelationshipEnum } from "@/lib/validations/family-history";
import { INSTRUMENTS } from "@/lib/mental-health/instruments";
/**
* Enum-driven exhaustiveness guard for DYNAMICALLY-built i18n keys.
*
* The three existing i18n guards each see one face of the problem and miss the
* fourth:
* - `i18n-call-site-coverage.test.ts` sees only literal `t("ns.key")` calls,
* so a runtime-assembled `t(\`ns.${x}\`)` key is invisible to it.
* - `i18n-reverse-coverage.test.ts` sees bundle orphans, but treats any
* `ns.${x}` template as covering its whole subtree — so a MEMBER missing
* from the subtree is invisible.
* - `i18n-locale-integrity.test.ts` sees cross-locale parity, so it only
* fires when a key is present in SOME locale but not all.
*
* None of them assert the class that actually ships broken: "for enum E, every
* member's dynamically-built key resolves, non-empty, in every locale." Because
* parity is perfect, a key missing from ONE locale is missing from ALL SIX at
* once — exactly the seam that let `measurementReminders.types.{WHO5_SCORE,
* SCI_SCORE,WAIST_CIRCUMFERENCE}` (v1.28.38) and `illness.vital.FUNCTIONAL_IMPACT`
* (v1.28.41) ship absent from every bundle.
*
* This test derives the EXPECTED key set from the SOURCE enum/const and asserts
* every `prefix + member` resolves. Adding an enum member without a bundle key
* fails CI by construction — no hand-maintained floor to drift.
*
* ── Covered key spaces (source is a clean, finite, import-safe enum/const) ──
* - illness.vital.* ILLNESS_SCAN_TYPES ∪ FUNCTIONAL_IMPACT_RETURN_KEY
* - illness.type.* illnessTypeEnum
* - illness.lifecycle.* illnessLifecycleEnum
* - records.allergies.category.* allergyCategoryEnum
* - records.allergies.type.* allergyTypeEnum
* - records.allergies.severity.* allergySeverityEnum ∪ "NONE" sentinel
* - records.allergies.status.* allergyStatusEnum
* - records.family.relationship.* familyRelationshipEnum
* - mentalHealth.instrument.* } INSTRUMENTS i18nKey slugs
* - mentalHealth.instrumentSub.* } (PHQ-9 / GAD-7 / WHO-5 / SCI —
* - mentalHealth.instrumentDescription.* } the WHO-5/SCI class that bit before)
*
* ── Deliberately EXCLUDED (would produce false positives) ──
* - `insights.workouts.sport.*` — the Strava render path stores a raw
* `sport_type` and the render site legitimately falls back to that raw
* string, so a "missing" key is a graceful degradation, not a broken token
* (audit LOW #2). Asserting it would fail on a path that is by-design lax.
* - `settings.sections.*.description`, `settings.ai.providerChain.types.admin-codex`,
* `insights.<metric>.emptyState.cta` — structurally unreachable behind a
* tsc-exhaustive map / a `!= null` guard / a server-only filter (audit LOW).
* Registering them would demand bundle keys for code paths users never hit.
* - `measurementReminders.types.*` — already exhaustively covered by
* `measurement-reminders/__tests__/type-labels-i18n.test.ts`; its source
* const lives in a client component, not a lib-level export.
* - Prisma cycle enums, medication/document/lab/nutrient catalogs — clean
* enums the audit flags as candidates; left for a follow-up extension. Add
* them here once their exact prefix→member mapping is confirmed 1:1 (the
* cycle cervix* prefixes and catalog slug maps need per-space verification
* to stay false-positive-free).
*/
const MESSAGES_DIR = join(__dirname, "../../messages");
const LOCALES = readdirSync(MESSAGES_DIR)
.filter((f) => f.endsWith(".json"))
.map((f) => ({
locale: f.replace(/\.json$/, ""),
messages: JSON.parse(readFileSync(join(MESSAGES_DIR, f), "utf8")) as Record<
string,
unknown
>,
}));
/** PHQ-9 / GAD-7 / WHO-5 / SCI lowercase i18n slugs, straight from the source. */
const INSTRUMENT_SLUGS = Object.values(INSTRUMENTS).map((i) => i.i18nKey);
interface KeySpace {
/** The dotted prefix the key is built from (`t(\`prefix.${member}\`)`). */
prefix: string;
/** The finite member set, derived from the SOURCE enum/const. */
members: readonly string[];
}
const REGISTRY: readonly KeySpace[] = [
// illness.* — the currently-broken seed (FUNCTIONAL_IMPACT wins recovery-gap ties).
{
prefix: "illness.vital",
members: [...ILLNESS_SCAN_TYPES.map(String), FUNCTIONAL_IMPACT_RETURN_KEY],
},
{ prefix: "illness.type", members: illnessTypeEnum.options },
{ prefix: "illness.lifecycle", members: illnessLifecycleEnum.options },
// records.allergies.* — Zod enums; severity carries a "NONE" (not-assessed) sentinel.
{
prefix: "records.allergies.category",
members: allergyCategoryEnum.options,
},
{ prefix: "records.allergies.type", members: allergyTypeEnum.options },
{
prefix: "records.allergies.severity",
members: [...allergySeverityEnum.options, "NONE"],
},
{ prefix: "records.allergies.status", members: allergyStatusEnum.options },
// records.family.relationship.*
{
prefix: "records.family.relationship",
members: familyRelationshipEnum.options,
},
// mentalHealth.* — the WHO-5/SCI class whose absence originally motivated this guard.
{ prefix: "mentalHealth.instrument", members: INSTRUMENT_SLUGS },
{ prefix: "mentalHealth.instrumentSub", members: INSTRUMENT_SLUGS },
{ prefix: "mentalHealth.instrumentDescription", members: INSTRUMENT_SLUGS },
];
describe("dynamic-key exhaustiveness (enum-derived)", () => {
it("discovers all six shipped locales", () => {
expect(LOCALES.map((l) => l.locale).sort()).toEqual([
"de",
"en",
"es",
"fr",
"it",
"pl",
]);
});
it("every registered key space has a non-empty member set", () => {
for (const space of REGISTRY) {
expect(
space.members.length,
`${space.prefix} resolved to an empty member set — source import broke`,
).toBeGreaterThan(0);
}
});
for (const { locale, messages } of LOCALES) {
for (const space of REGISTRY) {
for (const member of space.members) {
const key = `${space.prefix}.${member}`;
it(`resolves ${key} in ${locale}`, () => {
const value = resolveKey(messages, key);
expect(value, `${key} missing in ${locale}.json`).toBeTypeOf(
"string",
);
expect((value ?? "").trim().length).toBeGreaterThan(0);
});
}
}
}
});