diff --git a/library/composables/useDashboardData.js b/library/composables/useDashboardData.js index 62a1c86a7..a5f514123 100644 --- a/library/composables/useDashboardData.js +++ b/library/composables/useDashboardData.js @@ -17,6 +17,7 @@ import { ref, shallowRef, inject } from 'vue' import { getApiBaseUrl } from '@map/api/apiBaseUrl.js' +import { getRuntimeLocale } from '@map/i18n/index.js' import sourcesConfig from '@map/api/sources.json' import { applyPeopleGroupList, resolvePeopleGroupList, LIST_DEFAULT } from '@map/lists/peopleGroupList.js' @@ -38,6 +39,25 @@ export function useDashboardData(dataOpts = {}) { let profileConfig = null try { profileConfig = inject('profileConfig', null) } catch (_) { /* called outside setup */ } + // ★ THE READER'S LOCALE, READ IN SETUP WHERE inject WORKS (the same reason profileConfig is). + // This dashboard asked the API for lang=en on every request, because sources.json freezes that + // value and nothing here ever overrode it. The effect was not a uniformly English page — it was + // WORSE: DashboardChartsRail localizes its country labels through countryName(), so at ?lang=ar + // the top-countries chart read الصين while the table row beneath it read "People's Republic of + // China". A page that is half-translated looks like a translation bug in the half that works. + // + // DataSourceManager.js:261 has done exactly this for its own callers all along — + // `if (this.locale) extraParams.lang = this.locale` — and doxa-research-map, doxa-simple-map and + // country-posters all pass a locale in. This composable simply never got the same treatment. + // + // BOTH CHANNELS, because the locale genuinely arrives by either: `lang` is provided by + // ProfileLoader for an element configured through profile-config, and window[PAGE_LOCALE_KEY] is + // what a generated leaf sets from ?lang= — which is the URL a person actually shares. Measured on + // the regions dashboard, where reading only one of the two left every data label English on the + // shared-link path while the chrome translated. + let injectedLang = null + try { injectedLang = inject('lang', null) } catch (_) { /* called outside setup */ } + async function load() { loading.value = true error.value = null @@ -46,7 +66,11 @@ export function useDashboardData(dataOpts = {}) { const base = getApiBaseUrl() const bulk = src.endpoints?.bulk || '/api/people-groups/list' const fields = (src.defaultFields || []).join(',') - const lang = src.queryParams?.lang || 'en' + // The runtime locale WINS over the source's frozen value; 'en' remains the floor. Resolved at + // REQUEST time, not at setup: a locale switch refetches through this same function, and a + // value captured once would pin the dashboard to whichever locale happened to load first. + const runtime = (injectedLang && injectedLang.value) || getRuntimeLocale() || '' + const lang = runtime || src.queryParams?.lang || 'en' // ── 1) bulk list (shared config: endpoint + defaultFields + lang + cache-bust) ── const listUrl = `${base}${bulk}?${fields ? `fields=${fields}&` : ''}lang=${lang}&_=${Date.now()}` diff --git a/plugin-designs/research-dashboard/src/composables/useDashboardShell.js b/plugin-designs/research-dashboard/src/composables/useDashboardShell.js index 7664d8636..35e0f24b6 100644 --- a/plugin-designs/research-dashboard/src/composables/useDashboardShell.js +++ b/plugin-designs/research-dashboard/src/composables/useDashboardShell.js @@ -35,7 +35,7 @@ import { useTreeLegendLabels } from './useTreeLegendLabels.js' import { useTreeLegendColors } from './useTreeLegendColors.js' import { DataSourceManager } from '@map/api/DataSourceManager.js' import sourcesConfig from '@map/api/sources.json' -import { tStatic } from '@map/i18n/index.js' +import { tStatic, getRuntimeLocale } from '@map/i18n/index.js' // tiny lodash-free mapValues: { k: v } -> { k: fn(v, k) } const mapValues = (obj, fn) => @@ -265,7 +265,24 @@ export function useDashboardShell(config) { provide('dataStore', dataStore) provide('searchSourceId', searchSourceId) - onMounted(() => data.load()) + // ★ A LOCALE CHANGE MUST REFETCH, not just re-render — the country, religion and region labels + // come from the API's ?lang=, so without this the chrome switches language and every DATA label + // keeps the language of first paint. Same rule and same shape as region-dashboards.vue:680, + // which learned it the same way. Guarded on a real change so the mount fetch is not doubled. + let _lastLang = null + // inject() ONLY works during setup — calling it inside the watch callback would warn on every + // tick and return null, so the ref is captured HERE and only READ later. This file already does + // the same for profileConfig, for the same reason. + let _langRef = null + try { _langRef = inject('lang', null) } catch (_) { /* called outside setup */ } + const _langNow = () => (_langRef && _langRef.value) || getRuntimeLocale() || '' + watch(_langNow, (next) => { + if (next === _lastLang) return + _lastLang = next + data.load() + }) + + onMounted(() => { _lastLang = _langNow(); data.load() }) return { bus, data, FACET_ACC, filteredRows, kpis, exporter, responsive } }