diff --git a/plugin-designs/research-dashboard/research-dashboard.vue b/plugin-designs/research-dashboard/research-dashboard.vue index c902ab30f..9973f7bf8 100644 --- a/plugin-designs/research-dashboard/research-dashboard.vue +++ b/plugin-designs/research-dashboard/research-dashboard.vue @@ -23,7 +23,7 @@ * DashboardSearchHeader (DOXA-LIB-3) remains available in the kit but is not mounted here. */ import { computed, watch, inject, ref, unref, onMounted, onBeforeUnmount } 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 @@ -183,7 +183,10 @@ function announce() { // chips that carry that answer are exactly what a screen-reader user cannot glance at. const active = Object.keys(bus.filterSet) if (active.length) { - const names = active.map((k) => mergedConfig.value.facets?.[k]?.label || k).join(', ') + // CONJUNCTION: these are separate facets that all hold at once — Religion AND Affinity Bloc AND + // Country — each one narrowing the set further. Read aloud, so the connector is the difference + // between a list and a sentence in most locales. + const names = formatList(active.map((k) => mergedConfig.value.facets?.[k]?.label || k), { type: 'conjunction' }) msg += ' · ' + T('dashboard.filters_live', 'Filters: {list}').replace('{list}', names) } const q = (bus.searchTerm?.value || '').trim() diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardResearchFilters.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardResearchFilters.vue index 6541b3d62..be7f6b69d 100644 --- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardResearchFilters.vue +++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardResearchFilters.vue @@ -37,7 +37,7 @@ */ import { inject, computed } from 'vue' import { useI18n } from 'vue-i18n' -import { tStatic } from '@map/i18n/index.js' +import { tStatic, formatList } from '@map/i18n/index.js' import { useShadowStyles } from '@map/composables/useShadowStyles.js' import SearchFilterHeader from '@map/components/search/SearchFilterHeader.vue' import { getPrayerLevel, LABELS as PRAYER_LABELS } from '@map/colors/prayer-progress.js' @@ -188,7 +188,11 @@ const activeChips = computed(() => { // a two-value pick renders as "A,B" with no spacing. LATENT, not live: nothing in this plugin // calls toggleFilterValue, so no caller produces a prayer array today. Closed here because the // next caller that does would not think to look in a chip label for the reason. - const shown = k === PRAYER_KEY ? (Array.isArray(v) ? v.join(', ') : v) : labelFor(k, v) + // DISJUNCTION, not a plain list: several values of ONE facet are ALTERNATIVES. The bus matches a + // row if ANY of them matches (useSelectionBus some(), DashboardDataTable includes()), so a + // comma-joined "Islam, Hinduism" reads as "and" in any language with an explicit connector — + // stating the opposite of what the filter does. + const shown = k === PRAYER_KEY ? (Array.isArray(v) ? formatList(v, { type: 'disjunction' }) : v) : labelFor(k, v) return { name: k, label: `${FACETS.value[k]?.label || k}: ${shown}` } }) // ★ THE TYPED SEARCH IS A FILTER AND MUST LOOK LIKE ONE (reported 2026-09-23: "I searched Bhutan diff --git a/plugin-designs/research-dashboard/src/composables/useTreeLegendLabels.js b/plugin-designs/research-dashboard/src/composables/useTreeLegendLabels.js index 6e2c6b2d3..4467338d9 100644 --- a/plugin-designs/research-dashboard/src/composables/useTreeLegendLabels.js +++ b/plugin-designs/research-dashboard/src/composables/useTreeLegendLabels.js @@ -29,6 +29,7 @@ import { computed } from 'vue' import { useAffinityBlockLegendData } from '@map/composables/legends/useAffinityBlockLegendData.js' +import { formatList } from '@map/i18n/index.js' /** * @param {object} cfg the injected dashboardConfig (read for facetRail) @@ -83,7 +84,11 @@ export function useTreeLegendLabels(cfg, dataStore, searchSourceId) { // thing. Without it an array fell through to its default toString and a chip read // "Religion: Islam,Hinduism": the codes joined by a bare comma, with no spacing and no code // resolved to its human name. - if (Array.isArray(value)) return value.map((v) => (map && map.get(v)) || v).join(', ') + // DISJUNCTION: a multi-value filter holds ALTERNATIVES, and the bus matches a row if ANY of them + // matches. Every surface that turns a filter into words comes through here, so the connector is + // resolved once and the chip bar, the research-filter chips and the CSV provenance line all say + // the same thing in the reader's language. + if (Array.isArray(value)) return formatList(value.map((v) => (map && map.get(v)) || v), { type: 'disjunction' }) if (!map) return value return map.get(value) || value }