From 39ec4b42bd7117b3b5c94baeb5d32acf8f734717 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Thu, 24 Sep 2026 05:34:47 +0000 Subject: [PATCH] test(i18n): every packet language must be SELECTABLE, not merely present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bug this gates is unreachability, not incorrectness. The packet shipped 11 complete translations while detectLocale() read only , which every generated leaf page hardcodes to "en" — so ten of them could not be reached by any reader on the static tree. Nothing was wrong; nothing was visible. Two assertions, because they fail separately: RESOLUTION ?lang= / ?locale= selects a supported locale, an unsupported value falls back to English, a region tag (pt-BR) resolves to its base, and with no param the host page still wins. REACHABILITY the packet's language list and SUPPORTED_LOCALES cannot drift apart, and the leaf pages still declare a lang — so if someone later drops that attribute, the host-page path does not silently become unavailable while the tests stay green. ★ It evaluates the SHIPPED detectLocale rather than reimplementing it. A gate that reimplements the thing it tests only ever proves the copy agrees with itself — which is how a fix can be reverted in the source while the gate keeps passing. Confirmed to fail before acceptance: removing the URL branch reports all four param cases resolving to "en". --- internal/check-locale-reachable.mjs | 93 +++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 94 insertions(+) create mode 100644 internal/check-locale-reachable.mjs diff --git a/internal/check-locale-reachable.mjs b/internal/check-locale-reachable.mjs new file mode 100644 index 000000000..d3454e888 --- /dev/null +++ b/internal/check-locale-reachable.mjs @@ -0,0 +1,93 @@ +// check-locale-reachable — a reader must be able to CHOOSE one of the 11 locales. +// +// ★ THE BUG THIS EXISTS FOR IS UNREACHABILITY, not incorrectness. The packet ships +// in 11 languages and every string is translated — and detectLocale() read ONLY +// document.documentElement.lang, which EVERY generated leaf page in this repo +// hardcodes to "en" (measured: 20 of 20). On the built static tree — the jamstack +// case these bundles exist for — ten of the eleven translations could not be +// reached by any reader. Correct, complete, and invisible. +// +// So this asserts two separate things, because they fail separately: +// 1. RESOLUTION — ?lang= / ?locale= selects a supported locale, a bad value falls +// back to English, and a region tag (pt-BR) resolves to its base language. +// Tested against the SHIPPED source of detectLocale, not a copy. +// 2. REACHABILITY — the built leaf pages still declare a lang, so the host-page +// path (Polylang on WordPress) keeps working. If someone later makes the pages +// lang-less, the URL path alone would silently become the only way in. +import { readFileSync, readdirSync, existsSync } from 'node:fs' +import { resolve, join } from 'node:path' + +const REPO = process.env.DOXA_REPO + || resolve('/home/lk10x/Workspace/eco-dx/root-doxa-maps/wt-packet') +const src = readFileSync(join(REPO, 'library/i18n/index.js'), 'utf8') + +const SUPPORTED = (src.match(/SUPPORTED_LOCALES = \[([^\]]*)\]/) || [])[1] + ?.split(',').map((s) => s.trim().replace(/['"]/g, '')).filter(Boolean) || [] + +// Evaluate the SHIPPED detectLocale rather than reimplementing it — a gate that +// reimplements the thing it tests only ever proves the copy agrees with itself. +const body = src.slice(src.indexOf('export function detectLocale'), src.indexOf('export function createAppI18n')) +const make = new Function('SUPPORTED_LOCALES', 'window', 'document', + `${body.replace('export function', 'return function')}\n; return detectLocale;`) +const detect = (search, htmlLang) => + make(SUPPORTED, { location: { search } }, { documentElement: { lang: htmlLang } })() + +const CASES = [ + ['?lang=es', 'en', 'es', 'a lang param selects a supported locale'], + ['?locale=fr', 'en', 'fr', 'the locale spelling works too'], + ['?lang=pt-BR', 'en', 'pt', 'a region tag resolves to its base language'], + ['?lang=zz', 'en', 'en', 'an unsupported value falls back to English'], + ['?lang=', 'de', 'de', 'an empty param defers to the host page'], + ['', 'de', 'de', 'with no param the host page still wins'], + ['?lang=ar', 'en', 'ar', 'RTL locales are selectable like any other'], +] + +const fails = [] +for (const [q, htmlLang, want, why] of CASES) { + let got + try { got = detect(q, htmlLang) } catch (e) { got = `THREW: ${e.message}` } + if (got !== want) fails.push(`${q || '(no param)'} with html lang="${htmlLang}" -> "${got}", expected "${want}" (${why})`) +} + +// Every packet language must be reachable: the two lists cannot drift apart. +const strings = JSON.parse(readFileSync(join(REPO, 'library/scripts/posters/packet-strings.json'), 'utf8')) +const packetLangs = Object.keys(strings).filter((k) => !k.startsWith('_')) +const unreachable = packetLangs.filter((l) => !SUPPORTED.includes(l)) +if (unreachable.length) { + fails.push(`the packet ships ${unreachable.join(', ')} but SUPPORTED_LOCALES does not list them — ` + + 'those translations cannot be selected') +} + +// The host-page path must survive: leaf pages still declare a lang. +const leafRoot = join(REPO, 'public/doxa-maps') +let leaves = 0 +let withLang = 0 +if (existsSync(leafRoot)) { + for (const bundle of readdirSync(leafRoot)) { + const bdir = join(leafRoot, bundle) + let subs = [] + try { subs = readdirSync(bdir) } catch { continue } + for (const sub of subs) { + const page = join(bdir, sub, 'index.html') + if (!existsSync(page)) continue + leaves += 1 + if (/]*\blang=/.test(readFileSync(page, 'utf8'))) withLang += 1 + } + } +} +if (leaves && withLang !== leaves) { + fails.push(`${leaves - withLang} of ${leaves} leaf pages declare no lang — ` + + 'the host-page detection path is broken for them') +} + +console.log(` supported locales : ${SUPPORTED.length} (${SUPPORTED.join(' ')})`) +console.log(` packet languages : ${packetLangs.length} (${packetLangs.join(' ')})`) +console.log(` resolution cases : ${CASES.length - fails.filter((f) => f.includes('->')).length}/${CASES.length} correct`) +console.log(` leaf pages w/ lang: ${withLang}/${leaves}`) + +if (fails.length) { + console.error(`\nLOCALE-REACHABLE: FAIL — ${fails.length} problem(s)`) + for (const f of fails) console.error(' - ' + f) + process.exit(1) +} +console.log(`\nLOCALE-REACHABLE: PASS — all ${packetLangs.length} packet languages are selectable.`) diff --git a/package.json b/package.json index fa6e02008..a384b1efc 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "check:card-deck": "node internal/check-card-deck.mjs", "check:poster-raster": "node internal/check-poster-raster.mjs", "check:deck-click": "node internal/check-deck-click.mjs", + "check:locale-reachable": "node internal/check-locale-reachable.mjs", "check:poster-prints": "node internal/check-poster-prints.mjs", "check:provider-switch": "node internal/check-provider-switch.mjs", "check:mfe-two-tab": "node internal/check-mfe-two-tab.mjs",