Religion families follow the reader; the value they filter on does not
REGREL. Two library sites rendered RELIGION_FAMILIES values as English in every locale: the Religion legend's rows (useLegendData.js) and the search aggregate that collapses sub-types into their family (useSearch.js). Both now project the LABEL through religionFamilyLabel(), which resolves through the catalogue and keeps English as the last resort. The half worth a gate is the half that did NOT change. A translated value matches nothing — the moment a localized string lands in a filter, a bus payload or an element attribute, the link works in English and fails silently in Arabic. So `key` and `filterKey` stay the canonical family letter, useSearch's key function stays bare getReligionFamily(), and the check asserts all of that as well as the projection. RELIGION_FAMILIES is no longer imported in either file: the label was its only local use, and religionFamilyLabel reads the canonical map internally. ★ A COMMENT IN useSearch.js ARGUED AGAINST THIS FIX, and it was wrong. It read "label = family name so selection can resolve the family letter back (see research-map onGeocoderAggregateResult religion branch)" — presenting an English label as a REQUIREMENT of selection. Measured: onGeocoderAggregateResult does not exist anywhere in this repo (three comments name it, nothing defines it), and selection resolves by value — aggregate() emits `key` and `label` as separate fields, and the consumer reads detail.expectedValue (countries-map.vue:1611). Retracted in place with the measurement, not deleted, because a reader who has seen the old claim needs to know it was tested. check:religion-label-not-value is registered and negative-tested. It executes religionFamilyLabel's REAL source against the SHIPPED ar/ and en/ religion.json rather than a stub, because religion.js imports tStatic through an i18n index that calls import.meta.glob and cannot load in plain Node. Under ar the label is المسيحية while RELIGION_FAMILIES.C is still "Christianity", and a control asserts the two really differ — an English-only projection fails that assertion. One assertion of mine failed on its own retraction first: it required the stale phrase to be ABSENT, but the corrected comment quotes it in order to record it. Requiring the absence of a phrase punishes documenting the error, so it now asserts the retraction is present instead.
This commit is contained in:
parent
9df086e5f0
commit
2525c61151
4 changed files with 128 additions and 7 deletions
|
|
@ -22,7 +22,7 @@ import { ENGAGEMENT_COLORS } from '../../colors/engagement.js'
|
|||
import { ADOPTION_COLORS } from '../../colors/adoption.js'
|
||||
import { getLanguageFamilyColor } from '../../colors/language-family.js'
|
||||
import { getColorStrategy, COLOR_MODES } from '../../colors/_registry.js'
|
||||
import { PALETTE as RELIGION_COLORS, RELIGION_FAMILIES, getReligionFamily } from '../../colors/religion.js'
|
||||
import { PALETTE as RELIGION_COLORS, getReligionFamily, religionFamilyLabel } from '../../colors/religion.js'
|
||||
import { useLanguageFamilyLegendData } from './useLanguageFamilyLegendData.js'
|
||||
|
||||
const COLUMNS_SIMPLE = [] // e.g. affinity-blocs — no numeric columns
|
||||
|
|
@ -319,7 +319,11 @@ export function useLegendData(legendTypeRef, opts = {}) {
|
|||
})
|
||||
return order.map(fam => ({
|
||||
key: fam,
|
||||
label: RELIGION_FAMILIES[fam] || 'Unknown',
|
||||
// REGREL: the LABEL is localized, the VALUE is not. `key` and `filterKey` below stay the
|
||||
// canonical family letter, which is what the filter and the map compare on; only this
|
||||
// display string follows the reader's language. RELIGION_FAMILIES is no longer imported here:
|
||||
// religionFamilyLabel() reads it internally, so this file's only use of it was this label.
|
||||
label: religionFamilyLabel(fam) || 'Unknown',
|
||||
color: RELIGION_COLORS[fam] || '#94a3b8',
|
||||
count: familyAggregates[fam].count,
|
||||
population: familyAggregates[fam].population,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,10 @@ import langFamilyByLanguage from '../data/langFamilyByLanguage.json'
|
|||
// Religion search must group by FAMILY (same 10 families the Religion legend shows),
|
||||
// not by raw sub-type names — otherwise search surfaces 'Animism'/'Romanism' rows
|
||||
// the legend can't select. Reuse the legend's exact family logic.
|
||||
import { RELIGION_FAMILIES, getReligionFamily } from '../colors/religion.js'
|
||||
// RELIGION_FAMILIES is no longer imported: its only use here was the label that now goes through
|
||||
// religionFamilyLabel(), which reads the canonical map internally. The KEY still comes from
|
||||
// getReligionFamily, so nothing that is compared or sent changed.
|
||||
import { getReligionFamily, religionFamilyLabel } from '../colors/religion.js'
|
||||
|
||||
// ── Result category ordering by active-tab context (Round 16) ────────────────
|
||||
// The search bar is unified — every category is always searched — but the
|
||||
|
|
@ -319,11 +322,19 @@ function buildAggregates(entries) {
|
|||
// SAME extraction the legend uses (getReligionFamily on the religion code),
|
||||
// so every search result corresponds to a selectable Religion-legend row.
|
||||
// The raw sub-type names ('Animism') still MATCH via religionHay, but collapse
|
||||
// into their family row. label = family name so selection can resolve the
|
||||
// family letter back (see research-map onGeocoderAggregateResult religion branch).
|
||||
// into their family row.
|
||||
// ✗ THIS COMMENT USED TO SAY: "label = family name so selection can resolve the family letter back
|
||||
// (see research-map onGeocoderAggregateResult religion branch)". It was WRONG, and wrong in the
|
||||
// direction that forbids the fix below: it presented an English label as a REQUIREMENT of
|
||||
// selection. Measured 2026-09-27 — onGeocoderAggregateResult does not exist anywhere in this repo
|
||||
// (three comments name it, nothing defines it), and selection resolves by VALUE: aggregate() emits
|
||||
// `key` and `label` as SEPARATE fields (:262-263) with key from getReligionFamily, and the consumer
|
||||
// reads detail.expectedValue, never the label (country-posters/countries-map.vue:1611). So the label
|
||||
// is display-only and safe to localize. A stale comment that argues against a correct change is
|
||||
// worse than no comment: the next reader either reverts it or refuses the next one like it.
|
||||
religions: aggregate(
|
||||
(e) => getReligionFamily(e.feature.religion || e.feature._raw?.religion?.value || e.feature._raw?.religion || ''),
|
||||
(e) => RELIGION_FAMILIES[getReligionFamily(e.feature.religion || e.feature._raw?.religion?.value || e.feature._raw?.religion || '')] || 'Unknown'
|
||||
(e) => religionFamilyLabel(getReligionFamily(e.feature.religion || e.feature._raw?.religion?.value || e.feature._raw?.religion || '')) || 'Unknown'
|
||||
),
|
||||
// WAGF / DOXA regions (doxaRegion) — keyed by the region value so a search
|
||||
// for "Asia" collapses every pin in that region into one row. The click
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@
|
|||
"check:script-flags": "node internal/check-script-flags-reachable.mjs",
|
||||
"check:settled": "node internal/settled-tree-gate.mjs",
|
||||
"check:sfc-parse": "node internal/check-sfc-script-parses.mjs",
|
||||
"check:religion-label-not-value": "node plugin-designs/regions-dashboard/scripts/check-religion-label-not-value.mjs",
|
||||
"check:share-policy": "node internal/check-share-policy.mjs",
|
||||
"check:shared-reached": "node internal/check-shared-component-reached-bundles.mjs",
|
||||
"check:shell-scroll": "node internal/verify-shell-scroll.mjs",
|
||||
|
|
@ -229,7 +230,7 @@
|
|||
"dev": "bun x vite",
|
||||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
#!/usr/bin/env node
|
||||
/* check-religion-label-not-value.mjs — `bun run check:religion-label-not-value`. Source-only, headless.
|
||||
*
|
||||
* REGREL: two LIBRARY sites showed RELIGION_FAMILIES values as English in every locale. The fix is to
|
||||
* project the LABEL through religionFamilyLabel() and leave the VALUE canonical English — because a
|
||||
* TRANSLATED VALUE MATCHES NOTHING. That is the language-link bug class: the moment a localized string
|
||||
* lands in a filter, a bus payload or an element attribute, the link works in English and silently
|
||||
* fails in Arabic.
|
||||
*
|
||||
* So this gate asserts BOTH halves. Projecting the label is the easy half; the half worth a gate is
|
||||
* that the value did NOT move with it.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { codeOnly } from './strip-code.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = resolve(HERE, '..', '..', '..')
|
||||
let fails = 0
|
||||
const ok = (c, m) => { console.log(` ${c ? 'ok ' : 'FAIL'} - ${m}`); if (!c) fails++ }
|
||||
const read = (p) => readFileSync(resolve(ROOT, p), 'utf8')
|
||||
|
||||
// ── 1. BEHAVIOUR, against the SHIPPED catalogues ────────────────────────────────────────────────
|
||||
// religion.js imports tStatic from library/i18n/index.js, which calls import.meta.glob — a Vite
|
||||
// transform plain Node cannot run. So religionFamilyLabel's REAL SOURCE is extracted and executed with
|
||||
// tStatic stubbed over the actual ar/ and en/ religion.json, rather than re-typed here: a copy would
|
||||
// test the copy. Same approach as check-provider-for.mjs and check-optional-t-equivalence.mjs.
|
||||
const relSrc = read('library/colors/religion.js')
|
||||
const famBlock = /export const RELIGION_FAMILIES = \{[\s\S]*?\n\}/.exec(relSrc)
|
||||
const labelFn = /export function religionFamilyLabel\(familyOrName\) \{[\s\S]*?\n\}/.exec(relSrc)
|
||||
if (!famBlock || !labelFn) {
|
||||
console.error(' FAIL - could not extract RELIGION_FAMILIES / religionFamilyLabel: NOT MEASURED, not passing')
|
||||
process.exit(2)
|
||||
}
|
||||
const en = JSON.parse(read('library/i18n/locales/en/religion.json'))
|
||||
const ar = JSON.parse(read('library/i18n/locales/ar/religion.json'))
|
||||
const messages = { en: { religion: en }, ar: { religion: ar } }
|
||||
const tStaticFor = (want) => (path) => {
|
||||
const walk = (loc) => String(path).split('.').reduce((o, k) => (o == null ? o : o[k]), messages[loc])
|
||||
const v = walk(want)
|
||||
return typeof v === 'string' ? v : walk('en')
|
||||
}
|
||||
const build = (want) => new Function('tStatic',
|
||||
famBlock[0].replace(/^export /, '') + ';\n' + labelFn[0].replace(/^export /, '')
|
||||
+ ';\nreturn { RELIGION_FAMILIES, religionFamilyLabel };')(tStaticFor(want))
|
||||
|
||||
const AR = build('ar'), EN = build('en')
|
||||
ok(AR.religionFamilyLabel('C') === ar.family.Christianity,
|
||||
`under ar the LABEL is Arabic ("${ar.family.Christianity}")`)
|
||||
ok(AR.RELIGION_FAMILIES.C === 'Christianity',
|
||||
'while the VALUE stays canonical English ("Christianity") -- a translated value would match nothing')
|
||||
ok(AR.religionFamilyLabel('C') !== AR.RELIGION_FAMILIES.C,
|
||||
'CONTROL: label and value really DIFFER under ar, so the projection is doing something')
|
||||
ok(EN.religionFamilyLabel('C') === 'Christianity',
|
||||
'under en the label equals the canonical name, so nothing changes for an English reader')
|
||||
ok(AR.religionFamilyLabel('M') === ar.family.Islam && AR.religionFamilyLabel('H') === ar.family.Hinduism,
|
||||
'and it holds for more than one family (Islam, Hinduism)')
|
||||
ok(AR.religionFamilyLabel('') === '', 'an empty family gives an empty label, not the word "undefined"')
|
||||
ok(AR.religionFamilyLabel('Christianity') === ar.family.Christianity,
|
||||
'it accepts a NAME as well as a letter, as its signature promises (familyOrName)')
|
||||
ok(AR.religionFamilyLabel('ZZZ') === 'ZZZ',
|
||||
'an unknown family falls through to the input rather than blanking the row')
|
||||
|
||||
// ── 2. THE HALF WORTH A GATE: the VALUE did not move with the label ─────────────────────────────
|
||||
const legend = codeOnly(read('library/composables/legends/useLegendData.js'))
|
||||
ok(/label:\s*religionFamilyLabel\(fam\)/.test(legend), 'useLegendData projects the LABEL through religionFamilyLabel(fam)')
|
||||
ok(/key:\s*fam\b/.test(legend), 'and `key` is still the canonical family letter')
|
||||
ok(/filterKey:\s*fam\b/.test(legend), 'and `filterKey` is still the canonical family letter -- this is what the filter compares')
|
||||
ok(!/key:\s*religionFamilyLabel|filterKey:\s*religionFamilyLabel/.test(legend),
|
||||
'NEG: neither key nor filterKey is derived from the localized label')
|
||||
|
||||
const search = codeOnly(read('library/composables/useSearch.js'))
|
||||
ok(/religions:\s*aggregate\(/.test(search), 'useSearch still aggregates religions')
|
||||
ok(/\(e\) => religionFamilyLabel\(getReligionFamily\(/.test(search),
|
||||
'its LABEL function goes through religionFamilyLabel()')
|
||||
ok(/\(e\) => getReligionFamily\(/.test(search),
|
||||
'and its KEY function is still bare getReligionFamily() -- the value is untouched')
|
||||
ok(/key,\n\s*label:/.test(search) || /\bkey,\s*$/m.test(search),
|
||||
'aggregate() still emits key and label as SEPARATE fields, so a consumer can choose')
|
||||
|
||||
// ── 3. the consumer really does resolve by VALUE, which is why (2) is sufficient ────────────────
|
||||
const consumer = codeOnly(read('plugin-designs/country-posters/countries-map.vue'))
|
||||
ok(/kind === 'religion'/.test(consumer) && /expectedValue = detail\.expectedValue/.test(consumer),
|
||||
'the religion consumer resolves by detail.expectedValue, never by the label (countries-map)')
|
||||
|
||||
// ── 4. the stale comment that argued AGAINST this fix is gone ───────────────────────────────────
|
||||
// ★ THIS ASSERTION FAILED ON MY OWN RETRACTION, which is worth recording. The first version required
|
||||
// that "onGeocoderAggregateResult religion branch" be ABSENT from the file — but the corrected comment
|
||||
// QUOTES that phrase in order to record what it used to claim, so the string is still there, inside the
|
||||
// retraction that fixes it. Requiring the absence of a phrase punishes documenting the error. What I
|
||||
// actually care about is that the claim is no longer presented as CURRENT, so assert the retraction.
|
||||
const searchRaw = read('library/composables/useSearch.js')
|
||||
ok(/USED TO SAY/.test(searchRaw) && /does not exist anywhere in this repo/.test(searchRaw),
|
||||
'the stale claim is RETRACTED in place, with the measurement that falsified it (the cited handler does not exist)')
|
||||
ok(!/\/\/ into their family row\. label = family name so selection/.test(searchRaw),
|
||||
'and it no longer reads as a live requirement -- the original standalone sentence is gone')
|
||||
|
||||
// ── 5. NEG CONTROL: the behavioural half must be capable of failing ─────────────────────────────
|
||||
const brokenLabel = (f) => ({ C: 'Christianity' })[f] || f // an English-only "projection"
|
||||
ok(brokenLabel('C') !== ar.family.Christianity,
|
||||
'NEG CONTROL: an English-only projection FAILS the ar assertion, so passing it means something')
|
||||
|
||||
console.log(fails ? `\nRELIGION-LABEL: ${fails} FAIL(s)` : '\nRELIGION-LABEL: the label follows the reader, the value stays canonical, and the filter still compares the value')
|
||||
process.exit(fails ? 1 : 0)
|
||||
Loading…
Reference in a new issue