A filter chip with several values says "or", in the reader's language

With no resolver configured, the kit chip bar joined a multi-value filter with ', ', so "Africa,
Asia" read as ALL to most people and carried English commas in every locale. The selection bus
matches several values on one facet with includes() (useSelectionBus isFilterActive), so adding a
value WIDENS the result: they are alternatives. The chip now uses formatList(value, { type:
'disjunction' }): "Africa or Asia", "Africa أو Asia", "Africa或Asia". gospel traced the matcher
and made the call as the only consumer's owner (gospel-internet's GiCombos). The English on this
chip changes from a comma to "or".

check:legend-mirrors lifts this exact expression and drives it with no resolver. It now lifts
formatList from library/i18n the same way (the shipped grammar, not a copy) and expects
"Islam or Hinduism". Poisoned with the old join it fails; restored it passes. formatList's doc now
says to choose the type by what the list MEANS, and never 'unit' for names.
This commit is contained in:
Doxa Maps 2026-09-29 13:29:07 +00:00
parent 62c8a2120c
commit 201baa3d60
3 changed files with 17 additions and 4 deletions

View file

@ -23,7 +23,7 @@
* already showing chips — two chip rows over one filterSet is the bug the dedup removed.
*/
import { inject, computed } from 'vue'
import { tStatic } from '@map/i18n/index.js'
import { tStatic, formatList } from '@map/i18n/index.js'
// This component's chrome, read out of the catalogue. tStatic and not useI18n: these mount inside
// custom elements that install no i18n plugin, and an unguarded useI18n() throws on the way in and
@ -54,7 +54,9 @@ const filterSet = bus?.filterSet || {}
// punctuation: the second difference reads as a rendering bug rather than as missing configuration.
// Kept as one liftable expression on purpose — check:legend-mirrors pulls it out of this source and
// drives it, so "no resolver yields the raw value" is measured instead of asserted.
const labelFor = (facet, value) => (cfg?.legend?.labelFor ? cfg.legend.labelFor(facet, value) : (Array.isArray(value) ? value.join(', ') : value))
// Several values on ONE facet are ALTERNATIVES: the selection bus matches with includes(), so adding a value
// widens the result. 'or' in the reader's language says that; a bare comma read as ALL to most people.
const labelFor = (facet, value) => (cfg?.legend?.labelFor ? cfg.legend.labelFor(facet, value) : (Array.isArray(value) ? formatList(value, { type: 'disjunction' }) : value))
const chips = computed(() => Object.entries(filterSet).map(([k, v]) => ({
k,
text: String(labelFor(k, v)),

View file

@ -464,6 +464,10 @@ export function tStatic(path, locale) {
* type 'conjunction' (and, the default) · 'disjunction' (or) · 'unit' (no conjunction)
* style 'long' (default) · 'short' · 'narrow'
* Items are stringified. Where a browser has no Intl.ListFormat it falls back to ', ', today's behaviour.
* PICK THE TYPE FROM WHAT THE LIST MEANS. Several values on one filter facet are alternatives, so 'disjunction'
* (the chip bar: the bus matches with includes()). Things that all hold are a 'conjunction'. If a facet ever needs
* ALL its values, the type belongs in that facet's config, not fixed at the call site. Never 'unit' for names:
* in zh it joins them with no separator at all.
*/
const listFormats = new Map()
export function formatList(items, { type = 'conjunction', style = 'long', locale } = {}) {

View file

@ -330,12 +330,19 @@ if (!fails.some((f) => f.startsWith('1'))) notes.push(`insets: ${SURFACES.length
if (!lf) {
fails.push('5 could not lift `const labelFor` out of the kit chip bar, so its unconfigured behaviour is whatever this gate can no longer see. Keep it one expression.')
} else {
const make = (cfg) => new Function('cfg', `return ${lf[1]}`)(cfg)
// The chip joins a multi-value filter with library/i18n's formatList. It is LIFTED from index.js exactly
// as the chip's own expression is, so the gate drives the shipped list grammar, not a re-typed copy.
const i18nSrc = read('library/i18n/index.js') || ''
const flStart = i18nSrc.indexOf('const listFormats = new Map()')
const flEnd = i18nSrc.indexOf('\n}\n', i18nSrc.indexOf('export function formatList')) + 2
const formatList = new Function('getRuntimeLocale', 'detectLocale',
`${i18nSrc.slice(flStart, flEnd).replace('export function', 'function')}\nreturn formatList`)(() => '', () => 'en')
const make = (cfg) => new Function('cfg', 'formatList', `return ${lf[1]}`)(cfg, formatList)
const bare = make({})
if (bare('affinity_bloc', 'A012') !== 'A012') {
fails.push('5 with NO resolver the kit chip bar does not pass a scalar value through — a dashboard that has not wired its code tables must see the code, not blank.')
}
if (bare('religion', ['Islam', 'Hinduism']) !== 'Islam, Hinduism') {
if (bare('religion', ['Islam', 'Hinduism']) !== 'Islam or Hinduism') {
fails.push(`5 with NO resolver a MULTI-VALUE filter (spec D1) renders as ${JSON.stringify(String(bare('religion', ['Islam', 'Hinduism'])))} instead of a joined list. Configured and unconfigured must differ in whether CODES RESOLVE, never in punctuation — a comma-jammed "A012,A013" reads as a rendering bug rather than as missing configuration.`)
}
const wired = make({ legend: { labelFor: () => 'SUPPLIED' } })