check:basemap-labels: the basemap's place names follow the reader's language on both engines
BUILD-LOCALIZATION-SURVIVES-THE-PROVIDER-SWAP asked for a check; the
behaviour landed in 319064a1 (library/maps/localize-labels.js) without
one. This reads the labels the map actually DRAWS, at ?lang=en and
?lang=ru on the same view, under both providers:
Mapbox the Standard basemap's 'place-labels' featureset (a plain
queryRenderedFeatures cannot see the imported basemap):
Germany→Германия, Ukraine→Украина, Egypt→Египет… must flip.
MapLibre each country symbol's evaluated text-field: under ru it must
be the feature's name:ru, under en not. English there
restores OSM's own label, so no English word is expected.
It serves this checkout on an OS-assigned port. --dir points it at
another tree for the negative control.
Negative-tested: doxa-simple-map built from 319064a1's parent (no
localize-labels) and served with --dir FAILS on both engines (Mapbox:
68 country labels, none flip; MapLibre: 0/49 in Russian). This tree
PASSES on both (5 known countries flip on Mapbox; 49/49 on MapLibre,
e.g. United Kingdom → Великобритания). A first version counted the
pre-fix Mapbox run as NOT MEASURED instead of FAIL, since it saw no
English names. "Measured" now means enough country labels were drawn.
This commit is contained in:
parent
0eae38b20a
commit
c77207b53a
2 changed files with 100 additions and 1 deletions
97
internal/check-basemap-labels-localized.mjs
Normal file
97
internal/check-basemap-labels-localized.mjs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-basemap-labels-localized — the BASEMAP's place names follow the reader's language, on BOTH
|
||||
* engines (card BUILD-LOCALIZATION-SURVIVES-THE-PROVIDER-SWAP; the code under test is
|
||||
* library/maps/localize-labels.js, 319064a1 / 4839f879).
|
||||
*
|
||||
* USAGE: node internal/check-basemap-labels-localized.mjs [--dir <tree root>] [--leaf bundle/profile]
|
||||
* Serves THIS checkout's public/ on an OS-assigned port (so it cannot measure another tree),
|
||||
* or --dir (used for the negative control: a tree whose bundle predates the fix).
|
||||
* Exit 0 = both engines localize · 1 = an engine does not · 2 = could not measure.
|
||||
*
|
||||
* WHAT IS READ — the RENDERED label, not our own guess at an expression:
|
||||
* Mapbox Standard keeps its labels in the IMPORTED basemap, invisible to a plain
|
||||
* queryRenderedFeatures. The 'place-labels' featureset of the 'basemap' import returns
|
||||
* each country label with the `name` the engine drew (setLanguage localizes it).
|
||||
* MapLibre queryRenderedFeatures returns each symbol's EVALUATED text-field, i.e. the text drawn.
|
||||
* WHAT IS ASSERTED, per engine, between ?lang=en and ?lang=ru on the same view (30°E 45°N, z3):
|
||||
* Mapbox known countries flip: Germany→Германия, Ukraine→Украина, Turkey→Турция, Egypt→Египет
|
||||
* (at least two of them seen in both runs).
|
||||
* MapLibre for the country labels (identified by their name:en property), the ru run draws the
|
||||
* feature's name:ru, and the en run does not. (English on MapLibre restores the style's
|
||||
* ORIGINAL label, OSM's Latin name — 'Deutschland', not 'Germany' — so no English word
|
||||
* is expected there; the flip to Cyrillic is the evidence.)
|
||||
*
|
||||
* NEGATIVE-TESTED 2026-09-25: the same leaf built from 319064a1's parent (before localize-labels
|
||||
* existed), served with --dir, FAILS on both engines.
|
||||
*/
|
||||
import { chromium } from 'playwright'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { createServer } from 'node:http'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const argv = process.argv.slice(2)
|
||||
const arg = (k, d) => { const i = argv.indexOf(k); return i >= 0 && argv[i + 1] ? argv[i + 1] : d }
|
||||
const DIR = resolve(arg('--dir', resolve(ROOT, 'public')))
|
||||
const LEAF = arg('--leaf', 'doxa-simple-map/doxa-simple-map')
|
||||
const PAIRS = { Germany: 'Германия', Ukraine: 'Украина', Turkey: 'Турция', Egypt: 'Египет', Kazakhstan: 'Казахстан', Poland: 'Польша' }
|
||||
|
||||
const port = await new Promise((ok) => { const s = createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => ok(p)) }) })
|
||||
const srv = spawn(process.execPath, [resolve(ROOT, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', DIR], { stdio: 'ignore' })
|
||||
await new Promise((r) => setTimeout(r, 900))
|
||||
const up = await fetch(`http://127.0.0.1:${port}/doxa-maps/${LEAF}/index.html`).then((r) => r.ok).catch(() => false)
|
||||
if (!up) { srv.kill(); console.log(`BASEMAP-LABELS: NOT MEASURED — could not serve ${DIR}`); process.exit(2) }
|
||||
|
||||
const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] })
|
||||
async function labels(provider, lang) {
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } })
|
||||
await page.goto(`http://127.0.0.1:${port}/doxa-maps/${LEAF}/index.html?lang=${lang}${provider === 'maplibre' ? '&provider=maplibre' : ''}`, { waitUntil: 'networkidle' })
|
||||
const FIND = `(() => { const f = (r) => { for (const e of r.querySelectorAll('*')) { if (e.__doxaMap) return e.__doxaMap; if (e.shadowRoot) { const m = f(e.shadowRoot); if (m) return m } } return null }; return f(document) })()`
|
||||
let ok = false
|
||||
for (let i = 0; i < 60 && !ok; i++) { await page.waitForTimeout(500); ok = await page.evaluate(`(() => { const m = ${FIND}; return !!(m && m.isStyleLoaded && m.isStyleLoaded()) })()`).catch(() => false) }
|
||||
if (!ok) { await page.close(); return null }
|
||||
await page.evaluate(`(() => { const m = ${FIND}; m.jumpTo({ center: [30, 45], zoom: 3 }) })()`)
|
||||
await page.waitForTimeout(7000)
|
||||
const out = await page.evaluate(`(() => { const m = ${FIND}
|
||||
if (${JSON.stringify(provider)} === 'mapbox') {
|
||||
let fs = []; try { fs = m.queryRenderedFeatures({ target: { featuresetId: 'place-labels', importId: 'basemap' } }) } catch (e) { return { error: e.message } }
|
||||
return { names: fs.filter((f) => f.properties && f.properties.class === 'country').map((f) => f.properties.name) }
|
||||
}
|
||||
const text = (tf) => (tf && Array.isArray(tf.sections)) ? tf.sections.map((s) => s.text).join('') : (typeof tf === 'string' ? tf : '')
|
||||
const feats = m.queryRenderedFeatures().filter((f) => f.layer && f.layer.type === 'symbol' && /country/.test(f.layer.id) && f.properties['name:en'])
|
||||
return { countries: feats.map((f) => ({ en: f.properties['name:en'], ru: f.properties['name:ru'] || '', drawn: text(f.layer.layout && f.layer.layout['text-field']) })) }
|
||||
})()`)
|
||||
await page.close()
|
||||
return out
|
||||
}
|
||||
|
||||
const results = []
|
||||
for (const provider of ['mapbox', 'maplibre']) {
|
||||
const en = await labels(provider, 'en'), ru = await labels(provider, 'ru')
|
||||
if (!en || !ru || en.error || ru.error) { results.push({ provider, measured: false, why: (en && en.error) || (ru && ru.error) || 'map never finished its style' }); continue }
|
||||
if (provider === 'mapbox') {
|
||||
const seen = Object.keys(PAIRS).filter((k) => en.names.includes(k))
|
||||
const flipped = seen.filter((k) => ru.names.includes(PAIRS[k]))
|
||||
// MEASURED = the engine drew enough country labels to judge. Not "saw two known English names":
|
||||
// without localization Mapbox Standard draws a different default form, and that absence IS the
|
||||
// defect — the first version of this line reported the pre-fix build as NOT MEASURED instead of
|
||||
// FAIL (68 country labels drawn, none English), which would let the gate pass by abstaining.
|
||||
results.push({ provider, measured: en.names.length >= 5 && ru.names.length >= 5, ok: seen.length >= 2 && flipped.length === seen.length,
|
||||
detail: `en saw ${seen.join(', ') || '(none)'}; ru drew ${flipped.map((k) => PAIRS[k]).join(', ') || '(none of them)'} (${en.names.length}/${ru.names.length} country labels)` })
|
||||
} else {
|
||||
const byEn = (arr) => new Map(arr.map((c) => [c.en, c]))
|
||||
const E = byEn(en.countries), R = byEn(ru.countries)
|
||||
const common = [...R.keys()].filter((k) => E.has(k) && R.get(k).ru)
|
||||
const localized = common.filter((k) => R.get(k).drawn === R.get(k).ru && E.get(k).drawn !== R.get(k).ru)
|
||||
results.push({ provider, measured: common.length >= 2, ok: common.length >= 2 && localized.length === common.length,
|
||||
detail: `${localized.length}/${common.length} countries drawn in Russian under ru and not under en` + (common[0] ? ` — e.g. ${common[0]}: en "${E.get(common[0]).drawn}" → ru "${R.get(common[0]).drawn}"` : '') })
|
||||
}
|
||||
}
|
||||
await browser.close(); srv.kill()
|
||||
|
||||
for (const r of results) console.log(` ${r.provider.padEnd(9)} ${!r.measured ? 'NOT MEASURED' : r.ok ? 'ok ' : 'FAIL'} ${r.detail || r.why}`)
|
||||
if (results.some((r) => r.measured && !r.ok)) { console.log('BASEMAP-LABELS: FAIL — a basemap does not follow the reader\'s language on every engine.'); process.exit(1) }
|
||||
if (results.some((r) => !r.measured)) { console.log('BASEMAP-LABELS: NOT MEASURED — an engine gave too few labels to judge.'); process.exit(2) }
|
||||
console.log(`BASEMAP-LABELS: PASS — ${LEAF}'s basemap place names follow the reader's language on Mapbox AND MapLibre.`)
|
||||
|
|
@ -125,6 +125,7 @@
|
|||
"check:sfc-parse": "node internal/check-sfc-script-parses.mjs",
|
||||
"check:url-resolver": "node internal/check-url-resolver.mjs",
|
||||
"check:studio-all": "node internal/check-studio-every-plugin.mjs",
|
||||
"check:basemap-labels": "node internal/check-basemap-labels-localized.mjs",
|
||||
"check:vision-card-numbers": "node internal/check-vision-card-numbers.mjs",
|
||||
"check:map-embed-cells": "node internal/check-map-embed-cells.mjs",
|
||||
"check:map-unmount-race": "node internal/check-map-unmount-guard.mjs",
|
||||
|
|
@ -304,7 +305,8 @@
|
|||
"check:gallery-embed:live-cors": "the Research Gallery on a foreign page under the LIVE static layer's CORS rule (Access-Control-Allow-Origin on files inside the tree's folders, NOT on the root manifest.json, measured on nuxtbase): it must still boot every tab, by reading the CORS-readable manifest copy generate-tree writes beside its bundle.",
|
||||
"check:leaves-mount": "DID THE ELEMENT ACTUALLY MOUNT, on all five gospel leaves. Exists because a ReferenceError thrown during setup dies INSIDE the custom-element boundary: the page loads, every asset returns 200, pageerror fires zero times, and the element sits there with an empty shadow root. A healthy-looking dead page. This asks the cheap unambiguous question the rich gates are too slow to ask on every leaf — is anything there — in five steps that each name their own failure: tag defined, host present, shadowRoot attached, shadow root non-empty, visible text rendered. It does NOT trust the absence of console errors; zero errors is not the same as working. --poison for the negative test.",
|
||||
"check:gospel-all": "EVERYTHING gospel-resources owns, in one command. Its ten gates were all hand-run, which is how a dead bundle shipped: a duplicate module killed all five leaves and the one gate run that day was pointed at the map. check:no-orphan-gates was right that they were reachable — reachable is not run. Deliberately NOT in gates:pre/post: three need a browser, the shared wave holds browser gates under load, and a bundle owner should not be able to slow everyone else's build by adding to a list they do not own. Static gates run first and stop the run on failure, so a scarce browser slot is never spent confirming a table is inconsistent. Exit 75 = NOT MEASURED, never folded into a pass.",
|
||||
"check:no-conflict-markers": "Nothing tracked may carry an unresolved merge. Exists because a merge from this worktree committed a BUILT bundle with 21 conflict markers - both sides concatenated, 1.13MB - and every gospel leaf rendered dead while the SRI hash matched and every request returned 200. The cause was a landing script that resolved one expected file then ran \"git add -A public\", staging every other file including the ones still holding markers. For a built file: take either side, REBUILD, commit the rebuild, then run this before pushing. --poison for the negative test."
|
||||
"check:no-conflict-markers": "Nothing tracked may carry an unresolved merge. Exists because a merge from this worktree committed a BUILT bundle with 21 conflict markers - both sides concatenated, 1.13MB - and every gospel leaf rendered dead while the SRI hash matched and every request returned 200. The cause was a landing script that resolved one expected file then ran \"git add -A public\", staging every other file including the ones still holding markers. For a built file: take either side, REBUILD, commit the rebuild, then run this before pushing. --poison for the negative test.",
|
||||
"check:basemap-labels": "the BASEMAP's place names follow the reader's language on BOTH engines (library/maps/localize-labels.js): reads the RENDERED country labels at ?lang=en and ?lang=ru — Mapbox via the Standard basemap's place-labels featureset, MapLibre via each symbol's evaluated text — and requires them to flip. Serves this checkout on an OS-assigned port; --dir <tree> for a negative control. Negative-tested: a bundle built before 319064a1 FAILS on both engines."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/android": "^7.6.9",
|
||||
|
|
|
|||
Loading…
Reference in a new issue