From 71123e5fae67d18a934d23447b52ec7ab6b34b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 22 Oct 2026 14:57:09 +0200 Subject: [PATCH] test(i18n): hold every catalogue to the English placeholders Key parity let 38 pt-BR strings through that renamed or dropped a {placeholder}: a renamed one renders as a raw token, a dropped one loses what the sentence was for. Every non-English bundle now has to use exactly the placeholders of the English string, with one allowance: a singular plural form may spell out or add {count}. --- .../__tests__/i18n-locale-integrity.test.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/lib/__tests__/i18n-locale-integrity.test.ts b/src/lib/__tests__/i18n-locale-integrity.test.ts index 341a3c453..bf8e3b860 100644 --- a/src/lib/__tests__/i18n-locale-integrity.test.ts +++ b/src/lib/__tests__/i18n-locale-integrity.test.ts @@ -345,6 +345,62 @@ describe("i18n locale file integrity", () => { }, ); + /** + * Placeholder parity. `t()` replaces `{name}` tokens literally, so a + * translation that renames one ("{when}" for "{date}") renders the raw + * token, and one that drops one ("Plano limitado" for "Your {provider} + * plan does not include {list}") loses the information the sentence was + * for. Neither breaks the key-parity check above, which is how 38 of them + * reached a bundle unnoticed. + * + * A singular plural form (`…One`) may name the count in words instead of + * `{count}`, or name it where English does not. + */ + function placeholderSet(value: string, singular: boolean): string { + const names = new Set( + [...value.matchAll(/\{(\w+)\}/g)].map((match) => match[1]), + ); + if (singular) names.delete("count"); + return [...names].sort().join(","); + } + + it.each(NON_EN_LOCALES)( + "$locale uses exactly the English placeholders", + ({ locale, path }) => { + const enFlat: [string, string][] = []; + flattenValues(JSON.parse(readFileSync(EN_PATH, "utf8")), "", enFlat); + const other = new Map(); + const otherFlat: [string, string][] = []; + flattenValues(JSON.parse(readFileSync(path, "utf8")), "", otherFlat); + for (const [key, value] of otherFlat) other.set(key, value); + + // A floor, so an empty or unreadable English bundle cannot pass. + requireFloor( + "English strings carrying a placeholder", + enFlat.filter(([, value]) => /\{\w+\}/.test(value)), + 1000, + ); + const drift = enFlat + .filter(([key, enValue]) => { + const value = other.get(key); + if (value === undefined) return false; + const singular = key.endsWith("One"); + return ( + placeholderSet(value, singular) !== + placeholderSet(enValue, singular) + ); + }) + .map( + ([key, enValue]) => + ` ${key}: en {${placeholderSet(enValue, false)}} vs ${locale} {${placeholderSet(other.get(key) ?? "", false)}}`, + ); + expect( + drift, + `Translations whose placeholders differ from English:\n${drift.join("\n")}`, + ).toEqual([]); + }, + ); + it("DE locale renders Health Score pillar labels in German", () => { const de = JSON.parse(readFileSync(DE_PATH, "utf8")) as Record< string,