Use American English spelling throughout
This commit is contained in:
@@ -59,7 +59,7 @@ export function baseUrlOf(basePath) {
|
||||
*
|
||||
* The comparison is deliberately not `startsWith(base)`: that would let
|
||||
* `/mailbox` in under a `/mail` mount and serve it the app shell, which is
|
||||
* both wrong and a small open door for a neighbouring site on the same host.
|
||||
* both wrong and a small open door for a neighboring site on the same host.
|
||||
*/
|
||||
export function stripBasePath(basePath, pathname) {
|
||||
const base = normalizeBasePath(basePath);
|
||||
|
||||
+22
-22
@@ -2,15 +2,15 @@
|
||||
"""
|
||||
Generate the palette CSS blocks in web/src/styles/app.css.
|
||||
|
||||
Every colour here comes from the palette's own project (all MIT); the values
|
||||
Every color here comes from the palette's own project (all MIT); the values
|
||||
are recorded in .palette-sources/palettes-upstream.md. What this script adds is
|
||||
the *derivation*: ihasmail needs thirty-odd tokens and these projects publish
|
||||
between twelve and twenty, so the tiers in between are computed rather than
|
||||
guessed, and every text colour is then checked against the surface it sits on.
|
||||
guessed, and every text color is then checked against the surface it sits on.
|
||||
|
||||
The check is the reason this is a script and not a hand-written block. ihasmail
|
||||
claims WCAG AA, and several of these palettes do not meet it as published --
|
||||
Dracula's comment grey on its own background is about 3.0:1, well under the 4.5
|
||||
Dracula's comment gray on its own background is about 3.0:1, well under the 4.5
|
||||
that normal text needs. Lifting those tiers by eye is how a claim quietly stops
|
||||
being true; here it is arithmetic, and the script fails loudly if a token it
|
||||
emitted would not pass.
|
||||
@@ -30,7 +30,7 @@ BEGIN = "/* === generated palettes: begin === */"
|
||||
END = "/* === generated palettes: end === */"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- colour maths
|
||||
# ---------------------------------------------------------------- color maths
|
||||
|
||||
def parse(hex_: str) -> tuple[float, float, float]:
|
||||
h = hex_.lstrip("#")
|
||||
@@ -66,18 +66,18 @@ def rgba(hex_: str, alpha: float) -> str:
|
||||
return f"rgba({r}, {g}, {b}, {alpha})"
|
||||
|
||||
|
||||
def toward_contrast(colour: str, bg: str, target: float, dark_ui: bool) -> str:
|
||||
"""Nudge `colour` away from `bg` until it clears `target`.
|
||||
def toward_contrast(color: str, bg: str, target: float, dark_ui: bool) -> str:
|
||||
"""Nudge `color` away from `bg` until it clears `target`.
|
||||
|
||||
Towards white on a dark background and towards black on a light one, so a
|
||||
lifted tier keeps its hue instead of washing out to grey.
|
||||
Toward white on a dark background and toward black on a light one, so a
|
||||
lifted tier keeps its hue instead of washing out to gray.
|
||||
"""
|
||||
if contrast(colour, bg) >= target:
|
||||
return colour
|
||||
if contrast(color, bg) >= target:
|
||||
return color
|
||||
anchor = "#ffffff" if dark_ui else "#000000"
|
||||
best = colour
|
||||
best = color
|
||||
for i in range(1, 101):
|
||||
candidate = mix(colour, anchor, i / 100)
|
||||
candidate = mix(color, anchor, i / 100)
|
||||
best = candidate
|
||||
if contrast(candidate, bg) >= target:
|
||||
return candidate
|
||||
@@ -90,7 +90,7 @@ def toward_contrast(colour: str, bg: str, target: float, dark_ui: bool) -> str:
|
||||
|
||||
# ihasmail's own palette has a hand-written dark block further up the file --
|
||||
# it is the identity this project is painted in, and regenerating it would
|
||||
# quietly move colours nobody asked to move. Only its light half is derived
|
||||
# quietly move colors nobody asked to move. Only its light half is derived
|
||||
# here, which is why it appears in LIGHT_ONLY.
|
||||
LIGHT_ONLY = {"ihasmail"}
|
||||
|
||||
@@ -261,17 +261,17 @@ def build(pid: str, mode: str, src: dict[str, str]) -> tuple[dict[str, str], lis
|
||||
bg, fg = src["bg"], src["fg"]
|
||||
notes: list[str] = []
|
||||
|
||||
def lift(name: str, colour: str, target: float) -> str:
|
||||
out = toward_contrast(colour, bg, target, dark)
|
||||
if out != colour:
|
||||
notes.append(f"{name} {colour} -> {out} ({contrast(colour, bg):.2f} -> {contrast(out, bg):.2f})")
|
||||
def lift(name: str, color: str, target: float) -> str:
|
||||
out = toward_contrast(color, bg, target, dark)
|
||||
if out != color:
|
||||
notes.append(f"{name} {color} -> {out} ({contrast(color, bg):.2f} -> {contrast(out, bg):.2f})")
|
||||
return out
|
||||
|
||||
# Body text is lifted like every other text tone rather than exempted.
|
||||
# Most of these palettes publish a body colour around 4.5:1 -- their own
|
||||
# Most of these palettes publish a body color around 4.5:1 -- their own
|
||||
# target -- and ihasmail asks 7:1 of the text a reader looks at all day.
|
||||
# Rejecting a palette over that would have cost five of the six added in
|
||||
# 2026-09; nudging the published colour along its own hue costs nothing a
|
||||
# 2026-09; nudging the published color along its own hue costs nothing a
|
||||
# reader can name, and the shift is recorded in the header of the
|
||||
# generated block like every other one.
|
||||
fg = lift("fg", fg, TEXT_ON_BG["fg"])
|
||||
@@ -365,11 +365,11 @@ def main() -> int:
|
||||
"/*",
|
||||
" * Written by scripts/build-palettes.py -- edit the sources there, not here.",
|
||||
" *",
|
||||
" * Every colour is from the palette's own project (all MIT); the published",
|
||||
" * Every color is from the palette's own project (all MIT); the published",
|
||||
" * values are recorded in .palette-sources/palettes-upstream.md. The tiers",
|
||||
" * between them are derived, and every text colour is checked against the",
|
||||
" * between them are derived, and every text color is checked against the",
|
||||
" * surface it sits on: 4.5:1 for prose, 3:1 for borders and marks. Several",
|
||||
" * of these palettes do not meet that as published -- Dracula's comment grey",
|
||||
" * of these palettes do not meet that as published -- Dracula's comment gray",
|
||||
" * is about 3.0:1 on its own background -- so those tiers are lifted, which",
|
||||
" * is why this is arithmetic rather than a hand-written block.",
|
||||
" */",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* Check a catalogue against the strings the code actually asks for.
|
||||
* Check a catalog against the strings the code actually asks for.
|
||||
*
|
||||
* Two failures, and only one of them is visible without this.
|
||||
*
|
||||
@@ -8,9 +8,9 @@
|
||||
* as an untranslated word on screen, which somebody will eventually notice.
|
||||
*
|
||||
* A *stale* key -- one whose English no longer exists, usually because it was
|
||||
* mistyped when the catalogue was written -- is silent. The translation sits
|
||||
* mistyped when the catalog was written -- is silent. The translation sits
|
||||
* in the file looking correct, is never looked up, and the app renders English
|
||||
* for ever. Nothing warns, because a catalogue is only ever read by key.
|
||||
* for ever. Nothing warns, because a catalog is only ever read by key.
|
||||
*/
|
||||
/*
|
||||
* The parser, not the compiler.
|
||||
@@ -32,13 +32,13 @@ import { readFileSync, globSync } from "node:fs";
|
||||
/*
|
||||
* Two sets, because there are two questions and they need different nets.
|
||||
*
|
||||
* `wanted` is what a catalogue *owes*: the strings that actually reach t(),
|
||||
* `wanted` is what a catalog *owes*: the strings that actually reach t(),
|
||||
* tc() or plural(). Coverage is measured against it, so it has to stay strict
|
||||
* -- widening it would count every CSS class and JMAP method name as an
|
||||
* untranslated string.
|
||||
*
|
||||
* `seen` is every string literal in the source, and answers only "is this
|
||||
* catalogue key still written down anywhere". Stale detection needs the wide
|
||||
* catalog key still written down anywhere". Stale detection needs the wide
|
||||
* net: a key reaches t() as a variable often enough that a strict set reports
|
||||
* mostly false alarms.
|
||||
*/
|
||||
@@ -70,7 +70,7 @@ for (const file of globSync("web/src/**/*.{ts,tsx}").filter((f) => !f.includes("
|
||||
if (ts.isJsxText(n)) { const text = n.text.trim(); if (text) seen.add(text); }
|
||||
/*
|
||||
* A `label:` in a constant is still a string somebody has to translate --
|
||||
* it reaches t() one render later -- so it stays part of what a catalogue
|
||||
* it reaches t() one render later -- so it stays part of what a catalog
|
||||
* owes, and out of coverage it would flatter the number.
|
||||
*/
|
||||
if (ts.isPropertyAssignment(n) && n.name.getText(src) === "label" && ts.isStringLiteral(n.initializer)) wanted.add(n.initializer.text);
|
||||
@@ -81,10 +81,10 @@ for (const file of globSync("web/src/**/*.{ts,tsx}").filter((f) => !f.includes("
|
||||
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
|
||||
const fn = n.expression.text, a0 = n.arguments[0];
|
||||
if ((fn === "t" || fn === "translate" || fn === "tNode") && a0 && ts.isStringLiteral(a0)) wanted.add(a0.text);
|
||||
// tc(context, source) keys the catalogue on both, joined by the same
|
||||
// tc(context, source) keys the catalog on both, joined by the same
|
||||
// control character tc() uses. Without this the contextual entries all
|
||||
// looked stale, which is the checker's own false alarm rather than a
|
||||
// catalogue problem.
|
||||
// catalog problem.
|
||||
if (fn === "tc" && a0 && ts.isStringLiteral(a0) && n.arguments[1] && ts.isStringLiteral(n.arguments[1])) {
|
||||
// Only the contextual key is required. The plain one is tc()'s
|
||||
// fallback, not a second obligation -- asking for both would report
|
||||
@@ -104,8 +104,8 @@ for (const file of globSync("web/src/**/*.{ts,tsx}").filter((f) => !f.includes("
|
||||
}
|
||||
|
||||
/*
|
||||
* A catalogue and a picker entry are two halves of one thing, and either half
|
||||
* alone is dead weight. A catalogue with no entry in UI_LANGUAGES never
|
||||
* A catalog and a picker entry are two halves of one thing, and either half
|
||||
* alone is dead weight. A catalog with no entry in UI_LANGUAGES never
|
||||
* reaches a reader -- it builds, it passes every test, and the language simply
|
||||
* is not offered. That happened to Dutch: the entry was added by a text
|
||||
* replacement anchored on a line that did not exist on that branch, so it was
|
||||
@@ -113,17 +113,17 @@ for (const file of globSync("web/src/**/*.{ts,tsx}").filter((f) => !f.includes("
|
||||
*/
|
||||
const languagesSrc = readFileSync("web/src/lib/languages.ts", "utf8");
|
||||
const registered = new Set([...languagesSrc.matchAll(/tag:\s*"([\w-]+)"/g)].map((m) => m[1]));
|
||||
const catalogues = new Set(globSync("web/src/locales/*.ts").map((f) => f.split("/").pop().replace(".ts", "")));
|
||||
const catalogs = new Set(globSync("web/src/locales/*.ts").map((f) => f.split("/").pop().replace(".ts", "")));
|
||||
|
||||
let failed = false;
|
||||
for (const tag of catalogues) {
|
||||
for (const tag of catalogs) {
|
||||
if (!registered.has(tag)) {
|
||||
failed = true;
|
||||
console.log(`!! ${tag}.ts exists but is not in UI_LANGUAGES — the language is never offered\n`);
|
||||
}
|
||||
}
|
||||
for (const tag of registered) {
|
||||
if (tag !== "en" && !catalogues.has(tag)) {
|
||||
if (tag !== "en" && !catalogs.has(tag)) {
|
||||
failed = true;
|
||||
console.log(`!! UI_LANGUAGES offers ${tag} but there is no ${tag}.ts — it would fall back to English\n`);
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
* A string reaches a reader translated if either is true:
|
||||
*
|
||||
* 1. it is wrapped where it is written -- t(), tc(), tNode(), plural()
|
||||
* 2. it is a catalogue key, translated somewhere else
|
||||
* 2. it is a catalog key, translated somewhere else
|
||||
*
|
||||
* The second case is a real convention here, not a loophole: constant tables
|
||||
* hold English and the render site calls `t(s.label)`. What this refuses is a
|
||||
* string that is neither -- one no catalogue has a key for, which therefore
|
||||
* string that is neither -- one no catalog has a key for, which therefore
|
||||
* cannot be translated at all, however many languages ship.
|
||||
*/
|
||||
/*
|
||||
@@ -56,7 +56,7 @@ const EQUALITY = new Set([
|
||||
|
||||
/*
|
||||
* Product names, example addresses and URL scaffolding. These reach t() and
|
||||
* are deliberately absent from every catalogue -- translating "ihasmail" or
|
||||
* are deliberately absent from every catalog -- translating "ihasmail" or
|
||||
* "[email protected]" would be a bug, not a feature -- so they would otherwise
|
||||
* be reported for ever.
|
||||
*/
|
||||
@@ -81,7 +81,7 @@ const found = [];
|
||||
for (const file of globSync("web/src/**/*.{ts,tsx}").filter((f) => !f.includes("__tests__") && !f.includes("/locales/"))) {
|
||||
const src = ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
||||
/*
|
||||
* `strict` withdraws the catalogue-key exemption. It exists for English held
|
||||
* `strict` withdraws the catalog-key exemption. It exists for English held
|
||||
* in a constant and translated where it renders; a literal written straight
|
||||
* into a JSX attribute has no later render site to be translated at -- no
|
||||
* component here passes its props through t() -- so being a key only means
|
||||
@@ -119,7 +119,7 @@ for (const file of globSync("web/src/**/*.{ts,tsx}").filter((f) => !f.includes("
|
||||
/*
|
||||
* English assembled around values: `aria-label={`Remove ${email}`}`.
|
||||
*
|
||||
* The literal cannot be a catalogue key as written, so whether it is a key is
|
||||
* The literal cannot be a catalog key as written, so whether it is a key is
|
||||
* not asked. Neither is looksLikeUi, which reads the opening of a sentence:
|
||||
* `${name} — shared by ${owner}` opens with a value and its words come after.
|
||||
* Any run of letters between the values counts. The only template literals
|
||||
@@ -151,7 +151,7 @@ for (const file of globSync("web/src/**/*.{ts,tsx}").filter((f) => !f.includes("
|
||||
&& n.expression.expression.getText(src) === "toast" && TOASTS.has(n.expression.name.text)) {
|
||||
const a0 = n.arguments[0];
|
||||
if (a0 && ts.isStringLiteral(a0) && !wrapped.has(a0)) report(a0, a0.text);
|
||||
/* A template literal cannot be a catalogue key at all, so it is always a find. */
|
||||
/* A template literal cannot be a catalog key at all, so it is always a find. */
|
||||
if (a0 && ts.isTemplateExpression(a0)) report(a0, a0.head.text + "{}");
|
||||
}
|
||||
ts.forEachChild(n, visit);
|
||||
@@ -160,11 +160,11 @@ for (const file of globSync("web/src/**/*.{ts,tsx}").filter((f) => !f.includes("
|
||||
}
|
||||
|
||||
if (!found.length) {
|
||||
console.log("i18n literals: none -- every user-visible string is wrapped or has a catalogue key");
|
||||
console.log("i18n literals: none -- every user-visible string is wrapped or has a catalog key");
|
||||
process.exit(0);
|
||||
}
|
||||
console.log(`${found.length} user-visible string(s) the extractor cannot see and no catalogue can translate:\n`);
|
||||
console.log(`${found.length} user-visible string(s) the extractor cannot see and no catalog can translate:\n`);
|
||||
for (const f of found) console.log(` ${f.file}:${f.line}\n ${JSON.stringify(f.text)}`);
|
||||
console.log("\nWrap them in t() / plural(), or -- for a label held in a constant and");
|
||||
console.log("translated where it renders -- make sure the English is a catalogue key.");
|
||||
console.log("translated where it renders -- make sure the English is a catalog key.");
|
||||
process.exit(process.argv.includes("--check") ? 1 : 0);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* Every source string a catalogue needs, straight out of the calls.
|
||||
* Every source string a catalog needs, straight out of the calls.
|
||||
*
|
||||
* The English text is the key, so the catalogue's keys are not a list somebody
|
||||
* The English text is the key, so the catalog's keys are not a list somebody
|
||||
* maintains -- they are whatever t(), tNode() and plural() are actually asked
|
||||
* for. Reading them from the code means a catalogue can never drift out of
|
||||
* for. Reading them from the code means a catalog can never drift out of
|
||||
* step with the app in the one direction that matters: a key that no longer
|
||||
* exists is dead weight, but a call with no key is an untranslated string
|
||||
* nobody noticed.
|
||||
|
||||
Reference in New Issue
Block a user