From 090b4eeb167c09459c455fa09acca7dabb748210 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Tue, 29 Sep 2026 13:40:28 +0000 Subject: [PATCH] Research dashboard: filter lists read as lists in the reader's language Three places joined names with ", ", which is a list only in English. Arabic separates with a word, Chinese uses a different mark entirely and no comma at any position, and German and French put a word before the last item. The separator and the final connector are the grammar, so no catalogue key can fix them from outside. Two of the three carried a second, worse error. A multi-value filter holds ALTERNATIVES -- the bus matches a row if ANY of its values matches, in useSelectionBus with some() and in the data table with includes() -- so the chip means "Islam or Hinduism". Joined with a comma it reads as "and" in every language with an explicit connector, which states the opposite of what the filter does. Those two become a disjunction. The third is the screen-reader live region, which names the facets currently applied. Those hold together and narrow the set in turn, so it is a conjunction. It is also the one string here that some readers rely on entirely, having no chips to glance at. The composable is the single owner: every surface that turns a filter into words comes through it, so the chip bar, the research-filter chips and the CSV provenance line now say the same thing in the same language. Left alone deliberately: five joins in this plugin's gate scripts. Those are failure messages read in a terminal by whoever runs the gate, not text shown to a reader, and formatting them per locale would help nobody. Source only. This needs the bundle rebuilt before it ships. --- plugin-designs/research-dashboard/research-dashboard.vue | 7 +++++-- .../components/dashboards/DashboardResearchFilters.vue | 8 ++++++-- .../src/composables/useTreeLegendLabels.js | 7 ++++++- 3 files changed, 17 insertions(+), 5 deletions(-) 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 }