doxa-maps/internal/check-locale-reachable.mjs
doxa-maps-88655c70 09014f8a03
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Rename the output folder to dx-maps
Driver decision: no embeddable URL carries "doxa" in its folder. The tree's name is one line in
vite.doxa-maps-build-config.json ("name": "dx-maps"); everything else follows it.

- __DOXA_TREE_NAME__ (the vite define rega landed, from MFE_NAME = internal/_tree-name.mjs) bakes the name into the
  bundles for the runtime code that must recognise the tree in a URL. StackLayout.vue's
  resolveBundleSrc anchored on a literal /doxa-maps/ and would have broken every composite's nested
  bundles on a renamed tree; it now anchors on the define. The Research Gallery's dev fallback uses
  it (one reader: rega's define, not a second one).
- The native shells follow via internal/sync-app-shells.mjs: capacitor webDir public/dx-maps, Tauri
  frontendDist ../../public/dx-maps (its window url is tree-relative since the one-folder landing).
- The WordPress contract test reads the name from config instead of carrying three literals.
- check:locale-reachable extracted detectLocale alone and died with "localeFromUrl is not defined"
  after 09d03c60 factored a helper out; it now follows the calls it makes (already red on main).
- generate-tree removes its temp asset pool even when it crashes (a failed build leaked ~50 MB into
  /tmp each time; measured on a full disk).
- Capability exemplars regenerated for the new paths.

Proof: rm -rf public && bun run build -> public/ = README.md + dx-maps/; the 25 built bundles carry
zero 'doxa-maps/' paths (scanned); check:sri, check:drop, check:parse, check:public-hygiene PASS.
Tag names (doxa-* -> dx-*) are the next, separate landing.
2026-09-25 19:26:14 +00:00

146 lines
7.3 KiB
JavaScript

// 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, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const REPO = process.env.DOXA_REPO
// FOURTH instance of this pattern in one flight (check-zone-census, check-share-policy,
// check-poster-prints, now this). DOXA_REPO still wins; only the default becomes portable,
// per the idiom check-no-absolute-paths.mjs prints in its own failure text. A gate that
// hardcodes its author's worktree turns build:unlocked red for every other builder.
|| resolve(dirname(fileURLToPath(import.meta.url)), '..')
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.
// ★ TAKE THE FUNCTION, NOT THE GAP BETWEEN TWO LANDMARKS.
// This used to slice from `export function detectLocale` to `export function createAppI18n` and eval
// whatever lay between. That is a promise about the ORDER OF A FILE the gate does not own: the day
// someone exported a helper between those two lines, the slice carried a bare `export` into
// new Function() and this gate died with "Unexpected token 'export'" — twice, once for a helper I
// added myself. It was then "fixed" by moving the helper, which only re-armed the trap for the next
// person. Extract detectLocale by matching its own braces, and strip any export keyword that
// survives, so the gate reads the function it names and nothing else.
function extractFunction(source, name) {
const start = source.search(new RegExp('(export\\s+)?function\\s+' + name + '\\s*\\('))
if (start < 0) return ''
const open = source.indexOf('{', start)
if (open < 0) return ''
let depth = 0
for (let i = open; i < source.length; i++) {
const ch = source[i]
if (ch === '{') depth++
else if (ch === '}') {
depth--
if (depth === 0) return source.slice(start, i + 1)
}
}
return ''
}
const body = extractFunction(src, 'detectLocale').replace(/\bexport\s+/g, '')
if (!body) {
console.error('LOCALE-REACHABLE: FAIL — could not find detectLocale() in library/i18n/index.js.')
console.error(' The gate evaluates the SHIPPED function rather than reimplementing it, so if the')
console.error(' function is renamed this must be updated — silence here would be a gate that')
console.error(' stopped testing anything.')
process.exit(1)
}
// …and every top-level helper of the same file that detectLocale CALLS, transitively. 09d03c60 moved
// the URL half into localeFromUrl(), and a gate that extracted detectLocale alone died with
// "localeFromUrl is not defined" on every case — reporting a working feature as broken. Following the
// calls keeps the gate on the shipped code whatever it is factored into.
const helpers = []
const seen = new Set(['detectLocale'])
const queue = [body]
while (queue.length) {
const code = queue.shift()
for (const [, name] of code.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
if (seen.has(name)) continue
seen.add(name)
const fn = extractFunction(src, name)
if (fn) { const clean = fn.replace(/\bexport\s+/g, ''); helpers.push(clean); queue.push(clean) }
}
}
const make = new Function('SUPPORTED_LOCALES', 'window', 'document',
`${helpers.join('\n')}\n${body}\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/${TREE_NAME}`)
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 (/<html[^>]*\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.`)