diff --git a/internal/check-leaf-english-leaks.mjs b/internal/check-leaf-english-leaks.mjs index 019ad1836..ecab93236 100644 --- a/internal/check-leaf-english-leaks.mjs +++ b/internal/check-leaf-english-leaks.mjs @@ -43,9 +43,22 @@ const HERE = dirname(fileURLToPath(import.meta.url)) const REPO = resolve(HERE, '..') const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173' const LOCALES = (process.env.LOCALES || 'ar,zh,ru,hi').split(',').map((s) => s.trim()).filter(Boolean) +// ★ --self-test PROVES THE EXCLUSIONS DO NOT BLIND THE GATE. +// The numeric-sibling rule is GENERAL: "an element whose own text sits beside a sibling rendering +// a number is enumerating data". A general exclusion can go green by excluding too much, which is +// the same failure as never checking — and this one shipped before it was ever confirmed by a run. +// So the self-test INJECTS a real chrome leak in the exact position the rule is most likely to +// swallow: an English catalogue value in a label that HAS a numeric sibling, plus one with no +// numeric sibling as a control. The first MUST still be caught; if it is not, the rule is too +// broad and the gate is lying. +const SELF_TEST = process.argv.includes('--self-test') // /: -const TARGETS = (process.argv.slice(2).length ? process.argv.slice(2) : [ +// ⚠ FILTER THE FLAGS OUT. Every non-flag argv is a target, so `--self-test` was consumed as a +// leaf path and all four locales reported HTTP 404 — the NOT MEASURED guard caught it instead of +// reporting a clean run over nothing, which is exactly what that guard is for. +const ARGS = process.argv.slice(2).filter((a) => !a.startsWith('--')) +const TARGETS = (ARGS.length ? ARGS : [ 'dashboard-plus-grid/dashboard-plus-grid:dashboard-plus-grid', 'research-map-plus-grid/research-map-plus-grid:research-map-plus-grid', ]).map((s) => { @@ -77,6 +90,11 @@ console.log('LEAF-ENGLISH-LEAKS') console.log(` tree identity: OK (served manifest matches this checkout)`) const en = catalogue('en') +// A value that IS translated in every locale, so it is a genuine leak when it renders in English. +// Chosen from the catalogue rather than invented, so the self-test exercises the same comparison +// the real run does. +const SELF_TEST_KEY = 'buttons.prayerList' +const SELF_TEST_VALUE = en[SELF_TEST_KEY] || 'Prayer List' const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader'] }) const rows = [] const fails = [] @@ -92,6 +110,23 @@ for (const t of TARGETS) { { waitUntil: 'load', timeout: 60000 }) if (!res || !res.ok()) { rows.push({ t: t.path, lc, skipped: `HTTP ${res ? res.status() : '?'}` }); await page.close(); continue } await page.waitForTimeout(16000) + if (SELF_TEST) { + await page.evaluate(([tag, enVal]) => { + const host = document.querySelector(tag) + const sr = host && host.shadowRoot + if (!sr) return + const mk = (withNumber) => { + const row = document.createElement('div') + const lbl = document.createElement('span') + lbl.textContent = enVal + row.appendChild(lbl) + if (withNumber) { const n = document.createElement('b'); n.textContent = '1,234'; row.appendChild(n) } + sr.appendChild(row) + } + mk(true) // the hard case: a leak sitting beside a count + mk(false) // control: a leak with no numeric sibling + }, [t.tag, SELF_TEST_VALUE]) + } labels = await page.evaluate((tag) => { const host = document.querySelector(tag) const sr = host && host.shadowRoot @@ -192,6 +227,29 @@ for (const r of rows) { console.log(` ${r.t} @ ${r.lc} labels ${r.labels} · comparable ${r.comparable ?? 0} · ` + `${r.leaks ? `LEAKS ${r.leaks}` : 'no leaks'} · ${r.errs ?? 0} error(s)`) } +// ── SELF-TEST VERDICT ─────────────────────────────────────────────────────── +// Inverted on purpose: here a CAUGHT leak is the pass. Two were injected per leaf x locale — one +// beside a number (the case the numeric-sibling rule could swallow) and one without (the control). +// Both must be caught. Catching only the control would mean the rule is too broad and every real +// leak that happens to sit beside a count is invisible. +if (SELF_TEST) { + const caught = fails.filter((f) => f.includes(SELF_TEST_KEY)).length + const expected = rows.filter((r) => !r.skipped).length + console.log(`\nSELF-TEST: injected 2 leaks per combination (one beside a number, one not).`) + console.log(` combinations measured : ${expected}`) + console.log(` ${SELF_TEST_KEY} caught: ${caught}`) + if (!expected) { console.error('SELF-TEST: NOT MEASURED — no combination rendered.'); process.exit(2) } + if (caught < expected) { + console.error(`\nSELF-TEST: FAIL — the gate missed the injected leak in ` + + `${expected - caught} of ${expected} combination(s). An exclusion is too broad: a real leak`) + console.error(' beside a count would be invisible. Narrow the rule rather than shipping it.') + process.exit(1) + } + console.log(`\nSELF-TEST: PASS — every injected leak was caught, including the one beside a ` + + 'number, so the data/facet/enumerated exclusions narrow the population without blinding it.') + process.exit(0) +} + if (fails.length) { console.error(`\nLEAF-ENGLISH-LEAKS: FAIL — ${fails.length} problem(s)`) for (const f of fails.slice(0, 20)) console.error(` - ${f}`) diff --git a/library/i18n/locales/ar/studio.json b/library/i18n/locales/ar/studio.json index c3b519408..faa061824 100644 --- a/library/i18n/locales/ar/studio.json +++ b/library/i18n/locales/ar/studio.json @@ -39,5 +39,11 @@ "tokenSourceNone": "لا يوجد رمز Mapbox: لا يمكن تحميل خرائط Mapbox (خرائط MapLibre لا تحتاج رمزًا)", "tokenSecret": "هذا رمز سري (sk.…). الصق رمزًا عامًا (pk.…): هذه الصفحة عامة.", "tokenNotPk": "يبدأ رمز Mapbox العام بـ pk.", - "tokenUrlFailed": "تعذّر الحصول على رمز من هذا الرابط: {why}" + "tokenUrlFailed": "تعذّر الحصول على رمز من هذا الرابط: {why}", + "settings": "الإعدادات", + "mapEngine": "محرّك الخرائط", + "engineDefault": "الافتراضي لكل خريطة", + "engineMapboxNote": "يحتاج إلى رمز", + "engineMaplibreNote": "لا يحتاج إلى رمز", + "tokenNotNeeded": "تم اختيار MapLibre: خرائطه لا تحتاج إلى رمز Mapbox" } diff --git a/library/i18n/locales/de/studio.json b/library/i18n/locales/de/studio.json index 5210ca9d5..8e9f1c2f1 100644 --- a/library/i18n/locales/de/studio.json +++ b/library/i18n/locales/de/studio.json @@ -39,5 +39,11 @@ "tokenSourceNone": "Kein Mapbox-Token: Mapbox-Karten können nicht laden (MapLibre-Karten brauchen keinen)", "tokenSecret": "Das ist ein geheimer Token (sk.…). Fügen Sie einen öffentlichen ein (pk.…): Diese Seite ist öffentlich.", "tokenNotPk": "Ein öffentlicher Mapbox-Token beginnt mit pk.", - "tokenUrlFailed": "Von dieser URL kam kein Token: {why}" + "tokenUrlFailed": "Von dieser URL kam kein Token: {why}", + "settings": "Einstellungen", + "mapEngine": "Karten-Engine", + "engineDefault": "Je Karte eigenes", + "engineMapboxNote": "braucht einen Token", + "engineMaplibreNote": "kein Token nötig", + "tokenNotNeeded": "MapLibre ist gewählt: Seine Karten brauchen keinen Mapbox-Token" } diff --git a/library/i18n/locales/en/studio.json b/library/i18n/locales/en/studio.json index cc8ff20ef..d7b826e92 100644 --- a/library/i18n/locales/en/studio.json +++ b/library/i18n/locales/en/studio.json @@ -39,5 +39,11 @@ "tokenSourceNone": "No Mapbox token: Mapbox maps cannot load (MapLibre maps need none)", "tokenSecret": "That is a secret token (sk.…). Paste a public one (pk.…): this page is public.", "tokenNotPk": "A Mapbox public token starts with pk.", - "tokenUrlFailed": "Could not get a token from that URL: {why}" + "tokenUrlFailed": "Could not get a token from that URL: {why}", + "settings": "Settings", + "mapEngine": "Map engine", + "engineDefault": "Each map’s own", + "engineMapboxNote": "needs a token", + "engineMaplibreNote": "no token needed", + "tokenNotNeeded": "MapLibre is selected: its maps need no Mapbox token" } diff --git a/library/i18n/locales/es/studio.json b/library/i18n/locales/es/studio.json index bc46827d9..6972a60f2 100644 --- a/library/i18n/locales/es/studio.json +++ b/library/i18n/locales/es/studio.json @@ -39,5 +39,11 @@ "tokenSourceNone": "Sin token de Mapbox: los mapas de Mapbox no cargan (los de MapLibre no lo necesitan)", "tokenSecret": "Ese es un token secreto (sk.…). Pega uno público (pk.…): esta página es pública.", "tokenNotPk": "Un token público de Mapbox empieza por pk.", - "tokenUrlFailed": "No se pudo obtener un token de esa URL: {why}" + "tokenUrlFailed": "No se pudo obtener un token de esa URL: {why}", + "settings": "Ajustes", + "mapEngine": "Motor de mapas", + "engineDefault": "El de cada mapa", + "engineMapboxNote": "necesita un token", + "engineMaplibreNote": "no necesita token", + "tokenNotNeeded": "MapLibre está seleccionado: sus mapas no necesitan token de Mapbox" } diff --git a/library/i18n/locales/fr/studio.json b/library/i18n/locales/fr/studio.json index 087d3e54e..a80829f4b 100644 --- a/library/i18n/locales/fr/studio.json +++ b/library/i18n/locales/fr/studio.json @@ -39,5 +39,11 @@ "tokenSourceNone": "Aucun jeton Mapbox : les cartes Mapbox ne peuvent pas charger (MapLibre n’en a pas besoin)", "tokenSecret": "C’est un jeton secret (sk.…). Collez un jeton public (pk.…) : cette page est publique.", "tokenNotPk": "Un jeton public Mapbox commence par pk.", - "tokenUrlFailed": "Impossible d’obtenir un jeton depuis cette URL : {why}" + "tokenUrlFailed": "Impossible d’obtenir un jeton depuis cette URL : {why}", + "settings": "Paramètres", + "mapEngine": "Moteur de carte", + "engineDefault": "Celui de chaque carte", + "engineMapboxNote": "nécessite un jeton", + "engineMaplibreNote": "aucun jeton requis", + "tokenNotNeeded": "MapLibre est sélectionné : ses cartes n’ont pas besoin de jeton Mapbox" } diff --git a/library/i18n/locales/hi/studio.json b/library/i18n/locales/hi/studio.json index 050f635d3..47d7f54ff 100644 --- a/library/i18n/locales/hi/studio.json +++ b/library/i18n/locales/hi/studio.json @@ -39,5 +39,11 @@ "tokenSourceNone": "कोई Mapbox टोकन नहीं: Mapbox मानचित्र लोड नहीं हो सकते (MapLibre को इसकी ज़रूरत नहीं)", "tokenSecret": "यह एक गुप्त टोकन है (sk.…)। सार्वजनिक टोकन (pk.…) चिपकाएँ: यह पेज सार्वजनिक है।", "tokenNotPk": "Mapbox सार्वजनिक टोकन pk. से शुरू होता है", - "tokenUrlFailed": "उस URL से टोकन नहीं मिल सका: {why}" + "tokenUrlFailed": "उस URL से टोकन नहीं मिल सका: {why}", + "settings": "सेटिंग्स", + "mapEngine": "मानचित्र इंजन", + "engineDefault": "हर मानचित्र का अपना", + "engineMapboxNote": "टोकन चाहिए", + "engineMaplibreNote": "टोकन की ज़रूरत नहीं", + "tokenNotNeeded": "MapLibre चुना गया है: इसके मानचित्रों को Mapbox टोकन नहीं चाहिए" } diff --git a/library/i18n/locales/it/studio.json b/library/i18n/locales/it/studio.json index ac41b6df7..350287008 100644 --- a/library/i18n/locales/it/studio.json +++ b/library/i18n/locales/it/studio.json @@ -39,5 +39,11 @@ "tokenSourceNone": "Nessun token Mapbox: le mappe Mapbox non si caricano (MapLibre non ne ha bisogno)", "tokenSecret": "Questo è un token segreto (sk.…). Incolla un token pubblico (pk.…): questa pagina è pubblica.", "tokenNotPk": "Un token pubblico Mapbox inizia con pk.", - "tokenUrlFailed": "Impossibile ottenere un token da quell’URL: {why}" + "tokenUrlFailed": "Impossibile ottenere un token da quell’URL: {why}", + "settings": "Impostazioni", + "mapEngine": "Motore mappe", + "engineDefault": "Quello di ogni mappa", + "engineMapboxNote": "richiede un token", + "engineMaplibreNote": "nessun token necessario", + "tokenNotNeeded": "MapLibre è selezionato: le sue mappe non richiedono un token Mapbox" } diff --git a/library/i18n/locales/pt/studio.json b/library/i18n/locales/pt/studio.json index 9a824c894..aeca3abfb 100644 --- a/library/i18n/locales/pt/studio.json +++ b/library/i18n/locales/pt/studio.json @@ -39,5 +39,11 @@ "tokenSourceNone": "Sem token do Mapbox: os mapas Mapbox não carregam (os MapLibre não precisam)", "tokenSecret": "Esse é um token secreto (sk.…). Cole um público (pk.…): esta página é pública.", "tokenNotPk": "Um token público do Mapbox começa por pk.", - "tokenUrlFailed": "Não foi possível obter um token desse URL: {why}" + "tokenUrlFailed": "Não foi possível obter um token desse URL: {why}", + "settings": "Definições", + "mapEngine": "Motor de mapas", + "engineDefault": "O de cada mapa", + "engineMapboxNote": "precisa de um token", + "engineMaplibreNote": "não precisa de token", + "tokenNotNeeded": "MapLibre está selecionado: os seus mapas não precisam de token do Mapbox" } diff --git a/library/i18n/locales/ro/studio.json b/library/i18n/locales/ro/studio.json index 6665219ea..6f4a26b87 100644 --- a/library/i18n/locales/ro/studio.json +++ b/library/i18n/locales/ro/studio.json @@ -39,5 +39,11 @@ "tokenSourceNone": "Niciun token Mapbox: hărțile Mapbox nu se pot încărca (MapLibre nu are nevoie)", "tokenSecret": "Acesta este un token secret (sk.…). Lipește unul public (pk.…): pagina este publică.", "tokenNotPk": "Un token public Mapbox începe cu pk.", - "tokenUrlFailed": "Nu s-a putut obține un token de la acea adresă: {why}" + "tokenUrlFailed": "Nu s-a putut obține un token de la acea adresă: {why}", + "settings": "Setări", + "mapEngine": "Motor hărți", + "engineDefault": "Al fiecărei hărți", + "engineMapboxNote": "necesită un token", + "engineMaplibreNote": "nu necesită token", + "tokenNotNeeded": "MapLibre este selectat: hărțile sale nu necesită token Mapbox" } diff --git a/library/i18n/locales/ru/studio.json b/library/i18n/locales/ru/studio.json index 877fb6f0f..7447b0c47 100644 --- a/library/i18n/locales/ru/studio.json +++ b/library/i18n/locales/ru/studio.json @@ -39,5 +39,11 @@ "tokenSourceNone": "Нет токена Mapbox: карты Mapbox не загрузятся (MapLibre он не нужен)", "tokenSecret": "Это секретный токен (sk.…). Вставьте публичный (pk.…): эта страница общедоступна.", "tokenNotPk": "Публичный токен Mapbox начинается с pk.", - "tokenUrlFailed": "Не удалось получить токен по этому адресу: {why}" + "tokenUrlFailed": "Не удалось получить токен по этому адресу: {why}", + "settings": "Настройки", + "mapEngine": "Движок карт", + "engineDefault": "Свой у каждой карты", + "engineMapboxNote": "нужен токен", + "engineMaplibreNote": "токен не нужен", + "tokenNotNeeded": "Выбран MapLibre: его картам не нужен токен Mapbox" } diff --git a/library/i18n/locales/zh/studio.json b/library/i18n/locales/zh/studio.json index df56e8091..a9aa023d6 100644 --- a/library/i18n/locales/zh/studio.json +++ b/library/i18n/locales/zh/studio.json @@ -39,5 +39,11 @@ "tokenSourceNone": "没有 Mapbox 令牌:Mapbox 地图无法加载(MapLibre 地图不需要)", "tokenSecret": "这是秘密令牌 (sk.…)。请粘贴公开令牌 (pk.…):此页面是公开的。", "tokenNotPk": "Mapbox 公开令牌以 pk. 开头", - "tokenUrlFailed": "无法从该网址获取令牌:{why}" + "tokenUrlFailed": "无法从该网址获取令牌:{why}", + "settings": "设置", + "mapEngine": "地图引擎", + "engineDefault": "各地图自身的", + "engineMapboxNote": "需要令牌", + "engineMaplibreNote": "无需令牌", + "tokenNotNeeded": "已选择 MapLibre:其地图无需 Mapbox 令牌" } diff --git a/package.json b/package.json index fcf3eaca5..3f9d0361a 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "check:kpi-unit": "node internal/check-kpi-unit-once.mjs", "check:label-joins": "node internal/check-label-joins.mjs", "check:leaf-english-leaks": "node internal/check-leaf-english-leaks.mjs", + "check:leaf-english-leaks:self-test": "node internal/check-leaf-english-leaks.mjs --self-test", "check:leaves-mount": "node internal/check-gospel-leaves-mount.mjs", "check:leaves-mount:poison": "node internal/check-gospel-leaves-mount.mjs --poison", "check:legend-export-reachable": "node internal/check-legend-export-reachable.mjs", diff --git a/plugin-designs/plugin-studio/plugin-studio.vue b/plugin-designs/plugin-studio/plugin-studio.vue index baee84bf5..84961b822 100644 --- a/plugin-designs/plugin-studio/plugin-studio.vue +++ b/plugin-designs/plugin-studio/plugin-studio.vue @@ -83,34 +83,9 @@ app-wrapper-material/languages.js (persisted, offline, no dep) — same switcher as the SPA. --> - + - +