From 94cfb7bcbbc9c2cbe6ea24c919453577f777cbff Mon Sep 17 00:00:00 2001 From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com> Date: Tue, 4 Aug 2026 03:04:14 +0200 Subject: [PATCH] test: tighten i18n allowlist, add substring and brand-leak guards Remove 7 now-stale IDENTICAL_VALUE_ALLOWLIST entries (audit.title, auth.roleOperationsManager, auth.roleRentalEmployee, demo.scenarios.startScenario, demo.scenarios.roles.operations_manager/ rental_employee, navigation.items.audit) now that they are genuinely translated -- their old comments describing them as "deliberately untranslated" were no longer true. Add two new checks: one closing the embedded-English/Dutch-substring blind spot the whole-string identity test structurally cannot catch (a mid-sentence phrase surviving inside otherwise-translated prose), one asserting no locale file contains "MobilityOps" or the word "PoC". --- frontend/e2e/i18n-coverage.spec.ts | 80 +++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/frontend/e2e/i18n-coverage.spec.ts b/frontend/e2e/i18n-coverage.spec.ts index c83e01c..bd7c3f9 100644 --- a/frontend/e2e/i18n-coverage.spec.ts +++ b/frontend/e2e/i18n-coverage.spec.ts @@ -97,6 +97,24 @@ test("no locale file defines an 'appName' key or the literal brand string", () = } }); +test("no locale file contains the internal project name 'MobilityOps' or the word 'PoC'", () => { + for (const language of LANGUAGES) { + for (const namespace of namespaces) { + const raw = JSON.stringify(loadNamespace(language, namespace)); + expect( + raw.includes("MobilityOps"), + `${language}/${namespace}.json contains "MobilityOps" -- the visible product name is ` + + `always "Fleet Ops" (via {{productName}}); "MobilityOps" is a technical/repo-only identifier`, + ).toBe(false); + expect( + /\bPoC\b/.test(raw), + `${language}/${namespace}.json contains "PoC" -- Fleet Ops is never described as a PoC ` + + `in user-facing copy`, + ).toBe(false); + } + } +}); + // --- Translation-quality: prove values were actually translated, not copy-pasted --- // Sleutelpariteit alone doesn't prove translation happened (a locale file could contain // the literal English string under the right key and still pass). For every "real prose" @@ -110,19 +128,13 @@ test("no locale file defines an 'appName' key or the literal brand string", () = // a precise allowlist by key path, not a broad word-level allowlist, so it can't quietly // hide an unrelated real mistranslation under the same key in a different namespace. const IDENTICAL_VALUE_ALLOWLIST = new Set([ - "audit.title", // "Audit trail" kept as an established cross-language compliance term "audit.diff.was", // "{{field}}: was {{value}}" -- "was" is spelled identically in Dutch - "auth.roleOperationsManager", // deliberately-untranslated role title (see demo.json roles) - "auth.roleRentalEmployee", - "demo.scenarios.roles.operations_manager", // same convention, scenario-overview role labels - "demo.scenarios.roles.rental_employee", "common.language.nl-BE", // language-picker options show each language's own endonym "common.language.fr-BE", "common.footer.productLine", // "{{productName}} Demo" -- brief-specified exact footer text "common.orgName", // "Northstar Mobility" -- fictional org proper noun, same in all 3 "dashboard.attention.openRecord", // "Open {{title}}" -- "open" is also the Dutch imperative "demo.scenarios.durationValue", // "± {{minutes}} min" -- unit abbreviation, same in all 3 - "demo.scenarios.startScenario", // "start"/"scenario" are naturalised loanwords in Dutch "demo.about.limitationsTitle", // "Limitations" -- identical spelling in French "demo.integrationSummary.titles.mcp_hub", // "ITWorx MCP Hub" -- proper noun "fleet.list.columns.attention", // "Attention" -- identical spelling in French @@ -131,7 +143,6 @@ const IDENTICAL_VALUE_ALLOWLIST = new Set([ "knowledge.questionLabel", // "Question" -- identical spelling in French "knowledge.retrievalFlow.question", "knowledge.questionLabelExchange", - "navigation.items.audit", // "Audit trail" kept as an established cross-language term "returns.result.inspection", // "Inspection" -- identical spelling in French ]); @@ -185,6 +196,61 @@ test("nl-BE and fr-BE translations are not suspiciously identical to en-GB or ea } }); +// --- Embedded English/Dutch fragments inside otherwise-translated prose --- +// The whole-string identity check above only catches a value that is IDENTICAL to +// en-GB end-to-end. It cannot catch a real bug class found during the Fleet Ops final +// localization pass: a sentence gets 95% translated but a role/status noun phrase is +// left embedded mid-sentence, e.g. nl-BE "... moet door de Operations Manager worden +// goedgekeurd." This scan flags known English fragments appearing literally inside any +// nl-BE or fr-BE string value, and known Dutch fragments leaking into fr-BE (copy-paste +// mistakes). Deliberately limited to unambiguous multi-word phrases (not single common +// words like "Open" or "Field", which collide with genuine Dutch/French vocabulary). +const FORBIDDEN_ENGLISH_FRAGMENTS = [ + "Operations Manager", + "Operations Managers", + "Rental Employee", + "Rental Employees", + "Audit trail", + "Start scenario", +]; +const FORBIDDEN_DUTCH_FRAGMENTS_IN_FR = [ + "Operationsmanager", + "Verhuurmedewerker", + "Auditgeschiedenis", + "Scenario starten", +]; + +function collectStringLeaves(value: unknown, prefix = ""): Array<{ path: string; value: string }> { + if (typeof value === "string") return [{ path: prefix, value }]; + if (value === null || typeof value !== "object") return []; + return Object.entries(value as Record).flatMap(([key, nested]) => + collectStringLeaves(nested, prefix ? `${prefix}.${key}` : key), + ); +} + +test("no known English role/status fragments leak into nl-BE or fr-BE prose", () => { + const findings: string[] = []; + for (const namespace of namespaces) { + const nl = loadNamespace("nl-BE", namespace); + const fr = loadNamespace("fr-BE", namespace); + for (const { path: keyPath, value } of collectStringLeaves(nl)) { + for (const fragment of FORBIDDEN_ENGLISH_FRAGMENTS) { + if (value.includes(fragment)) { + findings.push(`nl-BE/${namespace}.json:${keyPath} contains English fragment "${fragment}": "${value}"`); + } + } + } + for (const { path: keyPath, value } of collectStringLeaves(fr)) { + for (const fragment of [...FORBIDDEN_ENGLISH_FRAGMENTS, ...FORBIDDEN_DUTCH_FRAGMENTS_IN_FR]) { + if (value.includes(fragment)) { + findings.push(`fr-BE/${namespace}.json:${keyPath} contains foreign-language fragment "${fragment}": "${value}"`); + } + } + } + } + expect(findings, findings.join("\n")).toEqual([]); +}); + // --- Hardcoded JSX text (section 11D) --- // A targeted, deliberately narrow static scan: JSX text nodes (`>literal text<`, not a // `{...}` expression) containing two or more real words are almost always user-facing