From 201baa3d609dea6cc57c4c7d8c203da6789abee8 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Tue, 29 Sep 2026 13:29:07 +0000 Subject: [PATCH] A filter chip with several values says "or", in the reader's language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../components/dashboards/DashboardFilterChips.vue | 6 ++++-- library/i18n/index.js | 4 ++++ .../scripts/check-legend-mirrors.mjs | 11 +++++++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/library/components/dashboards/DashboardFilterChips.vue b/library/components/dashboards/DashboardFilterChips.vue index c44e98caf..4311a53cf 100644 --- a/library/components/dashboards/DashboardFilterChips.vue +++ b/library/components/dashboards/DashboardFilterChips.vue @@ -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)), diff --git a/library/i18n/index.js b/library/i18n/index.js index c4f1f888a..2b4466abd 100644 --- a/library/i18n/index.js +++ b/library/i18n/index.js @@ -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 } = {}) { diff --git a/plugin-designs/research-dashboard/scripts/check-legend-mirrors.mjs b/plugin-designs/research-dashboard/scripts/check-legend-mirrors.mjs index b76f873df..a8cd87094 100644 --- a/plugin-designs/research-dashboard/scripts/check-legend-mirrors.mjs +++ b/plugin-designs/research-dashboard/scripts/check-legend-mirrors.mjs @@ -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' } })