Architecture review fixes: partials carry the locale, mountable locales/, shared core words
CI / full-gate (push) Successful in 2m36s

This commit is contained in:
2026-08-03 23:12:18 +02:00
parent 245d1ad5b5
commit 6440c543e5
48 changed files with 337 additions and 118 deletions
+24
View File
@@ -12,6 +12,7 @@ export type Catalog = Record<string, Message>;
export const DEFAULT_LOCALE = "en-US";
const CATEGORIES: ReadonlySet<string> = new Set(["few", "many", "one", "other", "two", "zero"]);
const PLACEHOLDER = /\{\{(\w+)\}\}/g;
export function isPluralMessage(value: Message): value is PluralMessage {
return typeof value !== "string";
@@ -51,6 +52,7 @@ export function checkCatalog({ baseline, baselineLocale, catalog, locale }: Pari
problems.push(`"${key}" must be a ${isPluralMessage(expected) ? "plural message" : "string"}, like ${baselineLocale}`);
continue;
}
for (const problem of placeholderProblems(key, expected, actual, baselineLocale)) problems.push(problem);
if (!isPluralMessage(actual)) continue;
const forms = new Set(Object.keys(actual));
const missing = categories.filter((category) => !forms.has(category));
@@ -67,6 +69,28 @@ export function checkCatalog({ baseline, baselineLocale, catalog, locale }: Pari
return problems;
}
// A translation must interpolate exactly what the baseline does: a dropped {{name}} renders
// "Signed in as ", a misspelled one renders the placeholder itself — the half-translated class this
// check exists to stop, and neither is visible from the key set alone.
function placeholderProblems(key: string, expected: Message, actual: Message, baselineLocale: string): string[] {
const wanted = placeholders(expected);
const got = placeholders(actual);
const missing = [...wanted].filter((name) => !got.has(name));
const unknown = [...got].filter((name) => !wanted.has(name));
return [
...(missing.length ? [`"${key}" never uses ${missing.map((n) => `{{${n}}}`).join(", ")}, which ${baselineLocale} does`] : []),
...(unknown.length ? [`"${key}" uses ${unknown.map((n) => `{{${n}}}`).join(", ")}, which ${baselineLocale} does not supply`] : []),
];
}
function placeholders(message: Message): Set<string> {
const names = new Set<string>();
for (const text of typeof message === "string" ? [message] : Object.values(message)) {
for (const match of (text ?? "").matchAll(PLACEHOLDER)) names.add(match[1] as string);
}
return names;
}
// The plural categories a locale actually selects, sorted; unknown tags fall back to English's.
export function pluralCategories(locale: string): Intl.LDMLPluralRule[] {
try {