Files
HealthLog/src/__tests__/ui-code-no-dash-separator.test.ts
T
Marc-André Bombeck 86a3856ae9 fix(i18n): drop spaced dashes as separators from every visible string
A dash between two parts of a sentence ("{what} — tomorrow", "Could not
record {name} — try again", "End {name} — …") stands in for a comma, a
colon or a full stop the sentence should say. Every such string in the
seven bundles now says it in plain words. A range keeps its unspaced en
dash ("70–100 mmHg", "{start}–{end}"); that is how a range is written,
not a pause, and the spaced range forms became unspaced ones.

Labels the components put together themselves follow the same rule:
aria-labels read "Move up: Ramipril", a medication and its dose read
"Ramipril 5 mg", page titles use "|", Coach prefills are two sentences,
and the API's user-facing error messages lost their dashes too.

The bundle guard now refuses " — " and " – " beside " · ", with an empty
allowlist, and a sibling guard walks src/components and src/app for a
string or template literal that joins its parts with a spaced dash.
2026-10-09 16:05:32 +02:00

67 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});