mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
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.
67 lines
2.4 KiB
TypeScript
67 lines
2.4 KiB
TypeScript
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);
|
||
});
|
||
});
|