152 lines
10 KiB
JavaScript
152 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-legend-mirrors.mjs — the legends must MIRROR, and their labels must not become values.
|
|
*
|
|
* rega's Arabic survey on main 52eeb0db: the research-dashboard legends sat on the wrong edge with
|
|
* ENGLISH text — 70 of 85 letters Latin — in Big Picture and in the Research Gallery. Two faults
|
|
* that look like one, and they fail in opposite directions if you fix them carelessly.
|
|
*
|
|
* ── 1. A PHYSICAL INSET DOES NOT MIRROR ──────────────────────────────────────────────────────
|
|
* `right:8px` pins a box to the physical right edge in every locale, so in Arabic it stays where an
|
|
* English reader expects it while everything around it flips. The logical property is the fix, and
|
|
* `inset-inline-start:auto` / `inset-inline-end:auto` is part of the pair rather than decoration:
|
|
* it cancels a physical inset inherited from a control container, which otherwise stretches the box
|
|
* across the map once the other edge moves.
|
|
* Two surfaces are asserted because they are the two the survey named, and one of them — the
|
|
* research-map geocoder column — exists ONLY to share an edge with the legend beside it. A shared
|
|
* edge that one of the pair honours and the other does not is worse than no alignment: it reads as
|
|
* a broken layout rather than as a choice.
|
|
*
|
|
* ── 2. THE LABEL IS PROJECTED; THE VALUE IS NOT ──────────────────────────────────────────────
|
|
* The English in that legend was NOT chrome. `s.key` is a facet VALUE, and for the religion donut
|
|
* it is a family name DERIVED in English by library/colors/religion.js — the same table
|
|
* people-groups-grid re-derives per row so the two ends byte-match. So the obvious fix, translating
|
|
* the strings, is the dangerous one: it would work in English and match nothing in Arabic,
|
|
* SILENTLY, because an empty intersection reads as "no results". That is the language-link defect
|
|
* one facet over, and it cost a card to learn the first time.
|
|
* So this gate asserts BOTH halves: that the visible text is projected, AND that the projection did
|
|
* not leak into the value — data-key still carries the raw key, and RELIGION_FAMILIES is still the
|
|
* English table every consumer compares against.
|
|
*
|
|
* NEGATIVE-TESTED: a physical inset on either surface fails 1; binding data-key to the projection,
|
|
* or dropping the projection from the visible text, fails 2; translating a RELIGION_FAMILIES value
|
|
* fails 2 naming the link it would break.
|
|
*/
|
|
import { readFileSync, existsSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const TAG = 'LEGEND-MIRRORS'
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
|
|
const fails = []
|
|
const notes = []
|
|
const read = (rel) => { const p = join(ROOT, rel); return existsSync(p) ? readFileSync(p, 'utf8') : null }
|
|
|
|
// ── 1. logical insets on the two surfaces the survey named ───────────────────────────────────
|
|
const SURFACES = [
|
|
{ rel: 'plugin-designs/research-dashboard/src/components/dashboards/DashboardMapPanel.vue',
|
|
sel: '.rd-legend-map', want: 'inset-inline-end', what: 'the map legend' },
|
|
{ rel: 'plugin-designs/doxa-research-map/research-map.vue',
|
|
sel: '.mapboxgl-ctrl-top-left', want: 'inset-inline-start',
|
|
what: 'the geocoder column, which exists to share an edge with the legend below it' },
|
|
]
|
|
for (const { rel, sel, want, what } of SURFACES) {
|
|
const src = read(rel)
|
|
if (!src) { fails.push(`1 ${rel} is missing — this gate is blind to ${what}.`); continue }
|
|
const rule = new RegExp(`\\${sel}\\s*\\{([^}]*)\\}`).exec(src)
|
|
if (!rule) { fails.push(`1 ${rel}: no ${sel} rule found — it moved or was renamed, and this gate cannot see ${what}.`); continue }
|
|
const body = rule[1]
|
|
const physical = [...body.matchAll(/(^|[;{\s])(left|right)\s*:/g)].map((m) => m[2])
|
|
if (physical.length) {
|
|
fails.push(`1 ${rel}: ${sel} uses the PHYSICAL inset(s) ${[...new Set(physical)].join(', ')} — ${what} will not mirror under ar, so it lands on the edge an English reader expects while everything around it flips.`)
|
|
}
|
|
if (!body.includes(want)) {
|
|
fails.push(`1 ${rel}: ${sel} has no ${want} — ${what} has nothing anchoring it to the reading direction.`)
|
|
}
|
|
const other = want === 'inset-inline-end' ? 'inset-inline-start' : 'inset-inline-end'
|
|
if (!new RegExp(`${other}\\s*:\\s*auto`).test(body)) {
|
|
fails.push(`1 ${rel}: ${sel} sets ${want} without ${other}:auto. The pair is what cancels a physical inset inherited from a control container; without it the box stretches across the map once the anchored edge moves.`)
|
|
}
|
|
}
|
|
if (!fails.some((f) => f.startsWith('1'))) notes.push(`insets: ${SURFACES.length} surface(s) anchored to the reading direction, both halves of the pair present`)
|
|
|
|
// ── 2a. the visible text is projected, the wire is not ───────────────────────────────────────
|
|
{
|
|
const rel = 'plugin-designs/research-dashboard/src/components/dashboards/DashboardChartsRail.vue'
|
|
const src = read(rel)
|
|
if (!src) fails.push(`2 ${rel} is missing.`)
|
|
else {
|
|
const li = /<span v-for="s in chart\.geometry"[\s\S]*?<\/span>\n/.exec(src)
|
|
if (!li) fails.push(`2 ${rel}: the legend item markup moved — this gate is blind to what it renders.`)
|
|
else {
|
|
const m = li[0]
|
|
if (!/class="rd-lt">\{\{ legendLabel\(/.test(m)) {
|
|
fails.push(`2 ${rel}: the legend's visible text is not projected through legendLabel() — a derived English facet value renders untranslated in all eleven locales.`)
|
|
}
|
|
// ★ EVERY data-key IN THE FILE, not just this one. A first version checked only the legend
|
|
// item, and a mutation test aimed at it silently hit a DIFFERENT :data-key — the donut
|
|
// segment — and the gate stayed green while a projection sat on a wire. There are three
|
|
// (segment, legend item, bar), all of them values the bus compares, and a check that guards
|
|
// one of three is a check whose own negative test can miss.
|
|
const keyBinds = [...src.matchAll(/:data-(?:key|facet)="([^"]*)"/g)]
|
|
if (keyBinds.length < 3) fails.push(`2 ${rel}: expected at least 3 :data-key/:data-facet bindings (segment, legend item, bar) and found ${keyBinds.length} — the markup moved and this gate is guarding less than it thinks.`)
|
|
for (const b of keyBinds) {
|
|
if (/legendLabel\(|religionFamilyLabel\(|tStatic\(/.test(b[1])) {
|
|
fails.push(`2 ${rel}: a PROJECTION is bound to a wire attribute (${b[0]}). The value is what the bus and the grid compare; a localized one works in English and matches NOTHING in Arabic, silently, because an empty intersection reads as "no results". Labels are for readers; values cross boundaries.`)
|
|
}
|
|
}
|
|
if (!/:data-key="s\.key"/.test(m)) {
|
|
fails.push(`2 ${rel}: the legend item's data-key no longer carries the RAW key.`)
|
|
}
|
|
if (!fails.some((f) => f.startsWith('2'))) notes.push('charts legend: text projected, data-key still the canonical value')
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── 2b. the table itself was not translated ──────────────────────────────────────────────────
|
|
{
|
|
const rel = 'library/colors/religion.js'
|
|
const src = read(rel)
|
|
if (!src) fails.push(`2 ${rel} is missing.`)
|
|
else {
|
|
const tbl = /export const RELIGION_FAMILIES = \{([\s\S]*?)\}/.exec(src)
|
|
if (!tbl) fails.push(`2 ${rel}: RELIGION_FAMILIES not found.`)
|
|
else {
|
|
const values = [...tbl[1].matchAll(/'[A-Z]':\s*'([^']+)'/g)].map((m) => m[1])
|
|
const nonAscii = values.filter((v) => /[^\x20-\x7E]/.test(v))
|
|
if (nonAscii.length) {
|
|
fails.push(`2 ${rel}: RELIGION_FAMILIES has been TRANSLATED (${nonAscii.join(', ')}). These are VALUES: the dashboard puts them on the selection bus and people-groups-grid re-derives them per row. Localized, the link works in English and matches nothing elsewhere. Translate religionFamilyLabel's catalogue entries instead.`)
|
|
}
|
|
if (!/export function religionFamilyLabel/.test(src)) {
|
|
fails.push(`2 ${rel}: religionFamilyLabel() is gone, so there is no way to show a family name to a reader without translating the value.`)
|
|
}
|
|
if (!fails.some((f) => f.includes('RELIGION_FAMILIES has been'))) notes.push(`religion table: ${values.length} values still canonical English, projection available`)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── 2c. the projection has something to project ──────────────────────────────────────────────
|
|
{
|
|
const LOCALES = ['ar', 'de', 'en', 'es', 'fr', 'hi', 'it', 'pt', 'ro', 'ru', 'zh']
|
|
const en = read('library/i18n/locales/en/religion.json')
|
|
if (!en) fails.push('2 library/i18n/locales/en/religion.json is missing — religionFamilyLabel() would fall back to English everywhere, which is the defect it exists to fix.')
|
|
else {
|
|
const names = Object.keys(JSON.parse(en).family || {})
|
|
if (!names.length) fails.push('2 en/religion.json has no `family` entries.')
|
|
for (const loc of LOCALES) {
|
|
const j = read(`library/i18n/locales/${loc}/religion.json`)
|
|
if (!j) { fails.push(`2 ${loc}/religion.json is missing.`); continue }
|
|
const missing = names.filter((n) => !(JSON.parse(j).family || {})[n])
|
|
if (missing.length) fails.push(`2 ${loc}/religion.json is missing ${missing.length} family name(s): ${missing.join(', ')}`)
|
|
}
|
|
if (!fails.some((f) => f.includes('religion.json'))) notes.push(`catalogue: ${names.length} family names in all ${LOCALES.length} locales`)
|
|
}
|
|
}
|
|
|
|
for (const n of notes) console.log(' · ' + n)
|
|
if (fails.length) {
|
|
console.error(`\n${TAG}: FAIL`)
|
|
for (const f of fails) console.error(' ✗ ' + f)
|
|
process.exit(1)
|
|
}
|
|
console.log(`${TAG}: PASS — the legends mirror, and their labels never became their values.`)
|