test(i18n): every packet language must be SELECTABLE, not merely present
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
The bug this gates is unreachability, not incorrectness. The packet shipped 11
complete translations while detectLocale() read only <html lang>, 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".
This commit is contained in:
parent
e2a47b1366
commit
39ec4b42bd
2 changed files with 94 additions and 0 deletions
93
internal/check-locale-reachable.mjs
Normal file
93
internal/check-locale-reachable.mjs
Normal file
|
|
@ -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 (/<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.`)
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue