Merge the final display polish into release/v1.42.0

This commit is contained in:
Marc-André Bombeck committed 2026-10-09 17:25:35 +02:00
commit dcfec1f35d
144 files changed
+4866 -2817

No files matched your search

@@ -81,6 +81,8 @@ const DENSE_TYPES: ReadonlySet<string> = new Set([
* list, keyed `<file>::<type expression>`, with why each stays bounded.
*/
const ALLOWED: Record<string, string> = {
"lib/analytics/summaries-slice.ts::(shorthand)":
"one hourly-mean type (pulse) over one local day, the latest: the same rows the day view reads for that day, bounded by a day, not by the sampling history",
"lib/insights/comprehensive-generate.ts::(shorthand)":
'Only reached for SLEEP_DURATION (guarded by `type === "SLEEP_DURATION"`); a few stage rows per night.',
"lib/insights/derived/vascular-age.ts::VASCULAR_AGE_TYPE":
+18 -13
View File
@@ -3,14 +3,18 @@ import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
/**
* No middle-dot separator in visible strings.
* No middle-dot or dash separator in visible strings.
*
* A value like "Added {date} · {size}" or "Weight · body fat · muscle" reads
* as a list of fragments rather than as the app's plain voice. Facts are
* joined with a comma, a colon or a real phrase instead. This guard walks
* every bundle under `messages/` and fails on any string that contains a
* space, U+00B7 and a space. A middle dot inside a unit ("mL/(kg·min)") has
* no surrounding spaces and is not matched.
* as a list of fragments rather than as the app's plain voice, and so does
* "{what} — tomorrow" or "Could not save — try again": the dash stands in
* for a comma, a colon or a full stop the sentence should have said. Facts
* are joined with a comma, a colon or a real phrase instead. This guard
* walks every bundle under `messages/` and fails on any string that contains
* a space, then U+00B7 (middle dot), U+2014 (em dash) or U+2013 (en dash),
* then a space. A middle dot inside a unit ("mL/(kg·min)") and a range
* ("70–100 mmHg", "{start}–{end}") have no surrounding spaces and are not
* matched: an unspaced en dash is how a range is written, not a pause.
*
* A key that genuinely needs the pattern goes into `ALLOWED` with the reason
* written next to it. An allowed key that no longer contains the pattern
@@ -18,7 +22,10 @@ import { join } from "node:path";
*/
const MESSAGES_DIR = join(__dirname, "../../messages");
const SEPARATOR = " · ";
const SEPARATORS = [" · ", " — ", " – "] as const;
const hasSeparator = (value: string) =>
SEPARATORS.some((separator) => value.includes(separator));
/** Key path -> why the separator is right there. Empty by intent. */
const ALLOWED: Record<string, string> = {};
@@ -41,7 +48,7 @@ const bundles = readdirSync(MESSAGES_DIR)
.filter((file) => file.endsWith(".json"))
.sort();
describe("message bundles carry no ' · ' separator", () => {
describe("message bundles carry no ' · ', ' — ' or ' – ' separator", () => {
it("finds the bundles", () => {
expect(bundles.length).toBeGreaterThanOrEqual(7);
});
@@ -57,18 +64,16 @@ describe("message bundles carry no ' · ' separator", () => {
expect(values.size).toBeGreaterThan(1000);
});
it(`${file} has no dot separator outside the allowlist`, () => {
it(`${file} has no dot or dash separator outside the allowlist`, () => {
const offenders = [...values]
.filter(
([key, value]) => value.includes(SEPARATOR) && !(key in ALLOWED),
)
.filter(([key, value]) => hasSeparator(value) && !(key in ALLOWED))
.map(([key, value]) => `${key} = ${JSON.stringify(value)}`);
expect(offenders).toEqual([]);
});
it(`${file} has no stale allowlist entry`, () => {
const stale = Object.keys(ALLOWED).filter(
(key) => !(values.get(key) ?? "").includes(SEPARATOR),
(key) => !hasSeparator(values.get(key) ?? ""),
);
expect(stale).toEqual([]);
});
@@ -0,0 +1,66 @@
import { describe, it, expect } from "vitest";
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join, relative } from "node:path";
/**
* The code-side twin of `i18n-no-dot-separator.test.ts`: a string the code
* composes itself (an aria-label `${action} — ${name}`, a page title, a
* Coach prefill) carries no spaced em or en dash either. The bundles say
* "Move up: Ramipril" and "No connection: changes can't be saved", so a
* label put together in a component says it the same way.
*
* The matcher looks at string and template literals on one line, outside
* comments: a quote or backtick, then text, a space, U+2014 or U+2013, a
* space. A range written in code ("10–12", `${from}–${to}`) has no spaces
* around its dash and is not matched. A template literal broken across lines
* escapes the one-line matcher, which is this guard's known limit.
*/
const ROOT = join(__dirname, "..");
const DIRS = ["components", "app"].map((d) => join(ROOT, d));
const LITERAL = /(["'`])[^"'`]* [—–] [^"'`]*\1/;
function walk(dir: string, out: string[] = []): string[] {
for (const name of readdirSync(dir)) {
const path = join(dir, name);
if (statSync(path).isDirectory()) {
if (name === "__tests__") continue;
walk(path, out);
} else if (/\.(ts|tsx)$/.test(name) && !/\.test\./.test(name)) {
out.push(path);
}
}
return out;
}
const isComment = (line: string) => /^\s*(\/\/|\*|\/\*|\{\/\*)/.test(line);
describe("composed UI strings carry no spaced dash", () => {
const files = DIRS.flatMap((d) => walk(d));
it("walks the component and app trees", () => {
expect(files.length).toBeGreaterThan(500);
});
it("has no ' — ' or ' – ' inside a string or template literal", () => {
const offenders: string[] = [];
for (const file of files) {
readFileSync(file, "utf8")
.split("\n")
.forEach((line, i) => {
if (isComment(line)) return;
const code = line.replace(/\s\/\/\s.*$/, "");
if (LITERAL.test(code)) {
offenders.push(`${relative(ROOT, file)}:${i + 1}: ${line.trim()}`);
}
});
}
expect(offenders).toEqual([]);
});
it("catches the pattern it is written for", () => {
expect(LITERAL.test("aria-label={`${a} — ${b}`}")).toBe(true);
expect(LITERAL.test('title: "About – HealthLog"')).toBe(true);
expect(LITERAL.test("`${from}–${to}`")).toBe(false);
});
});