Files
HealthLog/src/components/settings/__tests__/module-description-length.test.ts
T
Marc-André Bombeck beb070c0a0 fix(settings): cut five module rows back to one sentence
Environment, external assistant access, Documents, mental wellbeing and
nutrient intake ran 120-165 characters in German where their thirteen
neighbours sat at 30-75. Every one of the five carried the same second
sentence, "off by default", which restates what the switch beside the row
already shows.

The mental-wellbeing row also named two screeners where the module ships
four, and its second sentence held the one statement in the app about what
happens to the answers. That statement moves to the mental-wellbeing page
itself, where somebody is about to answer them.

A guard holds every module row to one sentence in all six locales, and
under the 120-character guideline in English and German.
2026-08-08 15:17:05 +02:00

78 lines
2.9 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { MODULE_KEYS } from "@/lib/modules/registry";
import { locales } from "@/lib/i18n/config";
/**
* A module row in the "What you track" hub gets ONE sentence, same as a card
* header's description slot (design standards §3).
*
* Five of the eighteen rows had grown a second sentence — always the same
* one, "Off by default", and in one case a third clause after it. The
* neighbours sat at 30-75 characters and those five at 120-165, so the hub
* read as two different lists stacked on each other. The switch beside the
* row already says whether the module is on, so the sentence was restating
* what the control shows.
*
* The single-sentence half of the rule is checked in every locale: it is a
* structural claim, not a length one, and a translation that re-adds the
* clause is the same defect. The character cap is checked in EN and DE, the
* two the maintainer reads, matching `card-description-length.test.ts`.
*/
const ROOT = process.cwd();
const MAX_CHARS = 120;
function bundle(locale: string): Record<string, { description?: string }> {
const raw = readFileSync(join(ROOT, "messages", `${locale}.json`), "utf8");
return JSON.parse(raw).modules;
}
describe("module hub descriptions", () => {
it("names a description for every module in every locale", () => {
// Positive control: without it, a renamed `modules` namespace would make
// the two checks below iterate an empty set and report green.
for (const locale of locales) {
const modules = bundle(locale);
const missing = MODULE_KEYS.filter(
(key) => !modules[key]?.description?.trim(),
);
expect(missing, `${locale} is missing descriptions`).toEqual([]);
}
});
it("holds each one to a single sentence, in every locale", () => {
const offenders: string[] = [];
for (const locale of locales) {
const modules = bundle(locale);
for (const key of MODULE_KEYS) {
const text = modules[key].description!;
// Drop the closing terminator, then any sentence break left inside
// is a second sentence. A semicolon splice counts — it is how the
// longest of the five carried its third clause.
const body = text.replace(/[.!?…]\s*$/, "");
if (/[.!?;]\s/.test(body)) {
offenders.push(`${locale}/${key}: ${text}`);
}
}
}
expect(offenders).toEqual([]);
});
it("holds each one under the character guideline in EN and DE", () => {
const offenders: string[] = [];
for (const locale of ["en", "de"] as const) {
const modules = bundle(locale);
for (const key of MODULE_KEYS) {
const text = modules[key].description!;
if (text.length > MAX_CHARS) {
offenders.push(`${locale}/${key}: ${text.length} chars`);
}
}
}
expect(offenders).toEqual([]);
});
});