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' } })