mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
refactor(i18n): shorten the five descriptions the gate could not see
The gate the diet shipped with matched `<SettingsCardHeader` in three component folders. That is not where every card description lives, so five real ones were outside its reach — including the full backup's, which the sweep's own shortlist had named and which stayed at 351 German characters across three sentences while the guard reported zero violations. Two ways in were missing. A card header can sit in a folder the walk never entered (`labs`), and a description can be FORWARDED to the header by a shell — `ExportCardShell`, `ImportCardShell` — where the prop is on the shell's tag, not the header's. The gate now matches both, over seven roots, and the "finds what it is meant to check" floor moved with it. `SubPageShell` is deliberately not included, with the reason written at the list: its description renders as `text-foreground text-sm`, the body tier, where the standards allow prose. Gating it would enforce a rule §3 does not make for that tier. The five, all six locales: - full backup — the twelve-item enumeration was the description. It is now the card's opening body paragraph, where a list of what is in the file belongs; the description is one sentence. - local OCR — the how-it-works sentence stays in the slot; "less accurate, review every value" is safety copy and reads foreground in the body. - CSV import, dose-history import — second sentence to the body. - the ECG page — the "does not interpret the waveform" claim gets its own paragraph rather than riding the page explainer. Proven by reverting the full backup's text: the widened gate reports it as both two sentences and over length, at the call site, in both languages. The old gate could not see the key at all.
This commit is contained in:
1 parent
ff61aec51f
commit
23eae2823e
12 files changed
+172
-73
No files matched your search
@@ -76,6 +76,11 @@ export default function InsightsEcgPage() {
|
||||
title={t("insights.ecg.sectionTitle")}
|
||||
description={t("insights.subPage.ecgDescription")}
|
||||
>
|
||||
{/* What the app does and does not claim about a trace. Its own
|
||||
paragraph, so the page explainer above stays one sentence. */}
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("insights.subPage.ecgDisclaimer")}
|
||||
</p>
|
||||
<EcgSection enabled={isAuthenticated} hideHeading />
|
||||
</SubPageShell>
|
||||
);
|
||||
|
||||
@@ -144,6 +144,9 @@ export function LabsSettings() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* "Less accurate — review every value" is safety copy, so it reads
|
||||
foreground in the body rather than muted beside the switch. */}
|
||||
<p className="text-sm">{t("labs.localOcr.detail")}</p>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Biomarker catalog — define / edit / delete. */}
|
||||
|
||||
@@ -7,10 +7,10 @@ import { join } from "node:path";
|
||||
* (design standards §3).
|
||||
*
|
||||
* Scope is deliberately the strings that actually reach a card header's
|
||||
* description slot — `<SettingsCardHeader description={t("…")}>` — because
|
||||
* that is the slot the rule is about. A hint in a card body is prose and is
|
||||
* allowed to be prose; `scripts/measure-description-lengths.mjs` sweeps those
|
||||
* for review without gating them.
|
||||
* description slot, whether the prop sits on the header or on a shell that
|
||||
* forwards it. That is the slot the rule is about. A hint in a card BODY is
|
||||
* prose and is allowed to be prose; `scripts/measure-description-lengths.mjs`
|
||||
* sweeps those for review without gating them.
|
||||
*
|
||||
* Both EN and DE are checked. DE runs 15-25% longer than EN, and a rule that
|
||||
* only holds in English produces a slot that wraps to two lines in half the
|
||||
@@ -46,19 +46,57 @@ function walk(dir: string, out: string[] = []): string[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Every `description={t("…")}` inside a `<SettingsCardHeader …>` element. */
|
||||
/**
|
||||
* Elements whose `description` prop reaches `SettingsCardHeader`'s slot.
|
||||
*
|
||||
* The first cut of this guard matched `<SettingsCardHeader` only, and only
|
||||
* under three component folders. That left five real card descriptions
|
||||
* invisible to it — one of them the 351-character, three-sentence full-backup
|
||||
* text that the audit's own shortlist had named. Two ways in were missing:
|
||||
* a card header rendered from a folder the walk never entered, and a
|
||||
* description FORWARDED to the header by a shell (`ExportCardShell`,
|
||||
* `ImportCardShell`), where the prop is on the shell's tag rather than the
|
||||
* header's.
|
||||
*
|
||||
* `SubPageShell` is deliberately NOT here. Its description renders as
|
||||
* `<p className="text-foreground text-sm">` — the body tier, where the
|
||||
* standards allow prose. Gating it would enforce a rule §3 does not make for
|
||||
* that tier.
|
||||
*/
|
||||
const DESCRIPTION_HOSTS = [
|
||||
"SettingsCardHeader",
|
||||
"ExportCardShell",
|
||||
"ImportCardShell",
|
||||
];
|
||||
|
||||
/** Component roots that can host a card header, directly or via a shell. */
|
||||
const ROOTS = [
|
||||
join("src", "components", "settings"),
|
||||
join("src", "components", "admin"),
|
||||
join("src", "components", "measurement-reminders"),
|
||||
join("src", "components", "labs"),
|
||||
join("src", "components", "insights"),
|
||||
join("src", "components", "medications"),
|
||||
join("src", "components", "records"),
|
||||
];
|
||||
|
||||
/** Every `description={t("…")}` on an element that owns the header's slot. */
|
||||
function describedKeys(): Map<string, string[]> {
|
||||
const keys = new Map<string, string[]>();
|
||||
for (const dir of ["settings", "admin", "measurement-reminders"]) {
|
||||
for (const file of walk(join(ROOT, "src", "components", dir))) {
|
||||
for (const root of ROOTS) {
|
||||
for (const file of walk(join(ROOT, root))) {
|
||||
const source = readFileSync(file, "utf8");
|
||||
const header = /<SettingsCardHeader\b/g;
|
||||
const host = new RegExp(`<(?:${DESCRIPTION_HOSTS.join("|")})\\b`, "g");
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = header.exec(source))) {
|
||||
while ((match = host.exec(source))) {
|
||||
// The element ends at the first `/>` or `>` that closes the opening
|
||||
// tag; a `description={t("x")}` after that belongs to something else.
|
||||
const end = source.indexOf("/>", match.index);
|
||||
const slice = source.slice(match.index, end === -1 ? undefined : end);
|
||||
const selfClose = source.indexOf("/>", match.index);
|
||||
const childrenOpen = source.indexOf("\n >", match.index);
|
||||
const end = [selfClose, childrenOpen]
|
||||
.filter((i) => i !== -1)
|
||||
.sort((a, b) => a - b)[0];
|
||||
const slice = source.slice(match.index, end ?? undefined);
|
||||
const desc = /description=\{t\(\s*"([^"]+)"/.exec(slice);
|
||||
if (!desc) continue;
|
||||
const list = keys.get(desc[1]) ?? [];
|
||||
@@ -96,7 +134,7 @@ describe("card header descriptions — one sentence, ≤120 characters", () => {
|
||||
|
||||
it("finds the description slots it is meant to check", () => {
|
||||
// A matcher that quietly matches nothing passes every assertion below it.
|
||||
expect(keys.size).toBeGreaterThan(25);
|
||||
expect(keys.size).toBeGreaterThan(35);
|
||||
});
|
||||
|
||||
for (const locale of ["en", "de"] as const) {
|
||||
|
||||
@@ -484,7 +484,16 @@ function FullBackupCard() {
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{/* v1.28 backup-completeness — the description above lists what's
|
||||
{/* What is in the file, and the shape it has. This used to be the
|
||||
card's description — twelve enumerated record types in a muted
|
||||
`text-xs` meta slot, which is neither the tier nor the length that
|
||||
slot is for. */}
|
||||
<p className="text-sm">
|
||||
{t("settings.sections.export.cards.fullBackup.contents")}{" "}
|
||||
{t("settings.sections.export.cards.fullBackup.detail")}
|
||||
</p>
|
||||
|
||||
{/* v1.28 backup-completeness — the paragraph above lists what's
|
||||
included; this line honestly discloses what isn't (document
|
||||
binaries, workout GPS/sample series) rather than implying
|
||||
"everything" is in the file. Always visible, not gated on the
|
||||
|
||||
@@ -113,6 +113,10 @@ export function CsvImportCard() {
|
||||
title={t("settings.sections.export.import.csv.title")}
|
||||
description={t("settings.sections.export.import.csv.description")}
|
||||
>
|
||||
<p className="text-sm">
|
||||
{t("settings.sections.export.import.csv.detail")}
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={textareaId} className="text-xs">
|
||||
{t("settings.sections.export.import.csv.pasteLabel")}
|
||||
|
||||
@@ -216,6 +216,10 @@ export function DoseHistoryImportCard() {
|
||||
title={t("settings.sections.export.import.doseHistory.title")}
|
||||
description={t("settings.sections.export.import.doseHistory.description")}
|
||||
>
|
||||
<p className="text-sm">
|
||||
{t("settings.sections.export.import.doseHistory.detail")}
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={textareaId} className="text-xs">
|
||||
{t("settings.sections.export.import.doseHistory.pasteLabel")}
|
||||
|
||||
Reference in new issue
Block a user