Studio and stack-demo hand the host's language down to what they mount
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

A host that embeds either composite with profile-config.locale was ignored (check:host-locale-contract:
0 Arabic characters with locale "ar", against 2121 / 2249 at ?lang=ar).

- stack-demo mounts its children by tag and already threaded tk and provider; locale now rides the
  same way to the research dashboard, and the grid (which reads cfg.locale itself) gets it only when
  the host gave one. 0 -> 358: the grid half. The dashboard half waits on its own bundle providing
  appI18n.
- plugin-studio seeded its language from localStorage / the browser and handed that to every pane.
  The ladder is now host profile-config locale/lang -> ?lang= / ?locale= -> saved choice -> browser;
  a host- or URL-given language is not persisted as the reader's. 0 -> 893 (its own chrome); the rest
  is the Research Gallery pane it opens on, which does not pass the locale on yet.
- plugin-studio's language switch rebuilt a pane's profile-config by hand without provider, tokenUrl
  and treeBase, so switching language turned a maplibre pane back into a mapbox one. Mount and
  switch now share one paneConfig().
This commit is contained in:
doxa-maps-88655c70 2026-09-25 13:25:01 +00:00
parent aed352f7b6
commit 2c83cce407
11 changed files with 73 additions and 42 deletions

View file

@ -161,7 +161,7 @@ import { defineComponent, inject } from 'vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import VerticalTabsLayout from '@map/layouts/VerticalTabsLayout.vue'
import StudioTree from './src/StudioTree.vue'
import { SWITCHER_LANGUAGES, loadLocale, saveLocale, getLanguage } from '@map/layouts/app-wrapper-material/languages.js'
import { SWITCHER_LANGUAGES, loadLocale, saveLocale, getLanguage, normaliseLocale } from '@map/layouts/app-wrapper-material/languages.js'
// Only the studio's CONTENT-AREA styles remain here; the shell (top bar + rail + body) is now the
// shared VerticalTabsLayout (which self-injects its own CSS). Kept as an injected string so the studio
@ -409,7 +409,13 @@ export default defineComponent({
activeKey: '',
embedOpen: false,
// i18n locale switcher (reuses app-wrapper-material/languages.js — persisted, offline, no dep).
locale: loadLocale(),
// ★ THE HOST'S LOCALE FIRST. A host that embeds the studio with profile-config.locale (the
// library's contract, read by ProfileLoader) was ignored: the switcher seeded from localStorage /
// the browser, and every pane was handed THAT — check:host-locale-contract measured 0 Arabic
// characters with locale:"ar" against 2077 at ?lang=ar. Ladder, as in i18n/index.js detectLocale:
// profile-config locale/lang → ?lang= / ?locale= → the reader's saved switcher choice → browser.
// A host- or URL-given locale is NOT persisted: it is the host's choice, not the reader's.
locale: this.initialLocale(),
localeOpen: false,
switcherLangs: SWITCHER_LANGUAGES,
// rows keyed by "<bundle>/<profile>"; each carries its resolved nesting state.
@ -584,6 +590,26 @@ export default defineComponent({
// Switch preview language. languages.js's contract: switching = re-setting profile-config.locale
// on the mounted element. Persist, then re-set it on the element this pane already holds. Every
// pane now carries a real element, so there is ONE path here — no per-mode branch.
initialLocale() {
let pc = this.profileConfig
if (typeof pc === 'string') { try { pc = pc ? JSON.parse(pc) : {} } catch { pc = {} } }
let q = ''
try { const u = new URLSearchParams(window.location.search); q = u.get('lang') || u.get('locale') || '' } catch { /* no window */ }
const given = (pc && (pc.locale || pc.lang)) || q
return given ? normaliseLocale(given) : loadLocale()
},
// ONE builder for a pane's profile-config, used by the mount AND by a language switch. The switch
// used to rebuild it by hand with only profile/instanceId/locale/tk — dropping tokenUrl, provider
// and treeBase, so switching language flipped a maplibre pane back to mapbox.
paneConfig(row, locale) {
const pc = { profile: row.profile, instanceId: 'ps-' + row.key.replace(/\//g, '-'), locale }
if (this.tk || this.cfg.tk) pc.tk = this.tk || this.cfg.tk
if (this.tokenUrl || this.cfg.tokenUrl) pc.tokenUrl = this.tokenUrl || this.cfg.tokenUrl
const wantProvider = this.provider || this.cfg.provider
if (wantProvider === 'maplibre' || wantProvider === 'mapbox') pc.provider = wantProvider
if (this.base) pc.treeBase = this.base
return pc
},
setLocale(code) {
this.locale = code
this.localeOpen = false
@ -593,9 +619,7 @@ export default defineComponent({
const slot = this._slots && this._slots[this.activeKey]
const el = slot && slot.firstElementChild
if (el && el.setAttribute && row) {
const pc = { profile: row.profile, instanceId: 'ps-' + this.activeKey.replace(/\//g, '-'), locale: code }
if (this.tk || this.cfg.tk) pc.tk = this.tk || this.cfg.tk
el.setAttribute('profile-config', JSON.stringify(pc))
el.setAttribute('profile-config', JSON.stringify(this.paneConfig(row, code)))
}
} catch { /* non-fatal: the switcher still persists + updates the UI */ }
},
@ -782,16 +806,11 @@ export default defineComponent({
// renders in the studio's active language (ProfileLoader reads profile-config.locale).
// tk/tokenUrl ride along here now that map rows mount directly — this is the token ladder
// the removed iframe src used to carry as a query param.
const pc = { profile: row.profile, instanceId: 'ps-' + row.key.replace(/\//g, '-'), locale: this.locale }
if (this.tk || this.cfg.tk) pc.tk = this.tk || this.cfg.tk
if (this.tokenUrl || this.cfg.tokenUrl) pc.tokenUrl = this.tokenUrl || this.cfg.tokenUrl
// ★ AND THE ENGINE, for the same reason tk rides along. resolveMapProvider's ladder starts
// at profile-config.provider, and a pane reads the config THIS file writes — so a studio
// embedded with provider=maplibre showed mapbox panes and, on a keyless host, panes that
// asked for a token. MEASURED on a foreign origin: 19 requests to api.mapbox.com for a
// maplibre request. Validated to the two known engines so a typo cannot select a third.
const wantProvider = this.provider || this.cfg.provider
if (wantProvider === 'maplibre' || wantProvider === 'mapbox') pc.provider = wantProvider
// PASS THE TREE BASE DOWN — a nested cap-5 shell cannot guess it.
// doxa-maps-spa carries its OWN hardcoded, TREE-RELATIVE tab list (doxa-maps-spa.vue:265-312,
// e.g. 'doxa-research-map/research-map/index.html') and resolves it against ITS treeBase,
@ -804,8 +823,8 @@ export default defineComponent({
// We know the correct base because it is the one WE resolved against, so hand it over rather
// than letting the child re-derive it. doxa-maps-spa reads cfg.treeBase (:519) ahead of its
// own guess, so this lands through the existing config path with no new prop.
if (this.base) pc.treeBase = this.base
el.setAttribute('profile-config', JSON.stringify(pc))
// (All of the above is built in ONE place, paneConfig(), shared with setLocale.)
el.setAttribute('profile-config', JSON.stringify(this.paneConfig(row, this.locale)))
// …and as a plain `tree-base` ATTRIBUTE, for elements that take the base that way instead of
// through profile-config (golden-path-check does, deliberately — it has no ProfileLoader).
// Without it that check guessed the tree from the PAGE URL, which in the studio is the page

View file

@ -76,6 +76,14 @@ const hostProvider = computed(() => {
const p = hostConfig.value?.provider
return p === 'maplibre' || p === 'mapbox' ? p : ''
})
// ★ AND THE HOST'S LANGUAGE, the third knob a host passes the same way. The children are mounted BY
// TAG, each with its own ProfileLoader / config reader, so the only thing missing was handing the
// locale down — check:host-locale-contract measured 0 Arabic characters with profile-config
// locale:"ar" against 2205 at ?lang=ar. Passed through as given; each child validates it.
const hostLocale = computed(() => {
const l = hostConfig.value?.locale || hostConfig.value?.lang
return typeof l === 'string' && l ? l : ''
})
const sections = computed(() => [
{
@ -101,6 +109,7 @@ const sections = computed(() => [
// what its standalone leaf asks for.
...(hostToken.value ? { tk: hostToken.value } : {}),
...(hostProvider.value ? { provider: hostProvider.value } : {}),
...(hostLocale.value ? { locale: hostLocale.value } : {}),
assetsBase: '../../assets'
})
}
@ -110,7 +119,10 @@ const sections = computed(() => [
title: 'People Groups',
sub: 'the full searchable list (live)',
element: 'people-groups-grid',
src: bundleSrc('people-groups-grid', 'people-groups-grid')
src: bundleSrc('people-groups-grid', 'people-groups-grid'),
// The grid reads profile-config.locale itself (people-groups-grid.vue, cfg.locale). Only set
// when the host gave one, so without it the grid mounts exactly as its standalone leaf does.
...(hostLocale.value ? { attrs: { 'profile-config': JSON.stringify({ locale: hostLocale.value }) } } : {})
}
])
</script>

View file

@ -782,7 +782,7 @@
"bundle": "plugin-studio",
"scriptUrl": "./plugin-studio/plugin-studio.js",
"output": "doxa-maps/plugin-studio/plugin-studio.js",
"integrity": "sha384-JhqC9WgYSipRHC8JK60x+VOeRp4gg1USvIDUXWb9aLx3gHdOCeLgmtV6HkN8mfgM",
"integrity": "sha384-JkHKL07MX7U5S8CLVSqL5FoDxuS25rpJygq75ozwNJuq8w9TVeZCw5DCZw82CTTA",
"elements": [
"plugin-studio"
],
@ -810,7 +810,7 @@
"scriptUrl": "./plugin-studio/plugin-studio.js",
"embed": {
"iframe": "<iframe src=\"./plugin-studio/plugin-studio/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-JhqC9WgYSipRHC8JK60x+VOeRp4gg1USvIDUXWb9aLx3gHdOCeLgmtV6HkN8mfgM\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-JkHKL07MX7U5S8CLVSqL5FoDxuS25rpJygq75ozwNJuq8w9TVeZCw5DCZw82CTTA\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
},
"instances": [
{
@ -1015,7 +1015,7 @@
"bundle": "stack-demo",
"scriptUrl": "./stack-demo/stack-demo.js",
"output": "doxa-maps/stack-demo/stack-demo.js",
"integrity": "sha384-3Rjk4pZDcTABDfBIAbT2tu+D6J9/U3jx3UJDkiPynH+oeiwzCGFotSX7hbhdT48G",
"integrity": "sha384-UkW6xuFr5wd1F6WJagiWzDoGxBXN6V89ESxbUu6AsRkcQOXTMGxBdRs84EpOAV6A",
"elements": [
"stack-demo"
],
@ -1035,7 +1035,7 @@
"scriptUrl": "./stack-demo/stack-demo.js",
"embed": {
"iframe": "<iframe src=\"./stack-demo/stack-demo/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./stack-demo/stack-demo.js\" integrity=\"sha384-3Rjk4pZDcTABDfBIAbT2tu+D6J9/U3jx3UJDkiPynH+oeiwzCGFotSX7hbhdT48G\" crossorigin=\"anonymous\"></script>\n<stack-demo profile-config='{\"profile\":\"stack-demo\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></stack-demo>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./stack-demo/stack-demo.js\" integrity=\"sha384-UkW6xuFr5wd1F6WJagiWzDoGxBXN6V89ESxbUu6AsRkcQOXTMGxBdRs84EpOAV6A\" crossorigin=\"anonymous\"></script>\n<stack-demo profile-config='{\"profile\":\"stack-demo\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></stack-demo>"
},
"instances": [
{

File diff suppressed because one or more lines are too long

View file

@ -782,7 +782,7 @@
"bundle": "plugin-studio",
"scriptUrl": "./plugin-studio/plugin-studio.js",
"output": "doxa-maps/plugin-studio/plugin-studio.js",
"integrity": "sha384-JhqC9WgYSipRHC8JK60x+VOeRp4gg1USvIDUXWb9aLx3gHdOCeLgmtV6HkN8mfgM",
"integrity": "sha384-JkHKL07MX7U5S8CLVSqL5FoDxuS25rpJygq75ozwNJuq8w9TVeZCw5DCZw82CTTA",
"elements": [
"plugin-studio"
],
@ -810,7 +810,7 @@
"scriptUrl": "./plugin-studio/plugin-studio.js",
"embed": {
"iframe": "<iframe src=\"./plugin-studio/plugin-studio/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-JhqC9WgYSipRHC8JK60x+VOeRp4gg1USvIDUXWb9aLx3gHdOCeLgmtV6HkN8mfgM\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-JkHKL07MX7U5S8CLVSqL5FoDxuS25rpJygq75ozwNJuq8w9TVeZCw5DCZw82CTTA\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
},
"instances": [
{
@ -1015,7 +1015,7 @@
"bundle": "stack-demo",
"scriptUrl": "./stack-demo/stack-demo.js",
"output": "doxa-maps/stack-demo/stack-demo.js",
"integrity": "sha384-3Rjk4pZDcTABDfBIAbT2tu+D6J9/U3jx3UJDkiPynH+oeiwzCGFotSX7hbhdT48G",
"integrity": "sha384-UkW6xuFr5wd1F6WJagiWzDoGxBXN6V89ESxbUu6AsRkcQOXTMGxBdRs84EpOAV6A",
"elements": [
"stack-demo"
],
@ -1035,7 +1035,7 @@
"scriptUrl": "./stack-demo/stack-demo.js",
"embed": {
"iframe": "<iframe src=\"./stack-demo/stack-demo/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./stack-demo/stack-demo.js\" integrity=\"sha384-3Rjk4pZDcTABDfBIAbT2tu+D6J9/U3jx3UJDkiPynH+oeiwzCGFotSX7hbhdT48G\" crossorigin=\"anonymous\"></script>\n<stack-demo profile-config='{\"profile\":\"stack-demo\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></stack-demo>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./stack-demo/stack-demo.js\" integrity=\"sha384-UkW6xuFr5wd1F6WJagiWzDoGxBXN6V89ESxbUu6AsRkcQOXTMGxBdRs84EpOAV6A\" crossorigin=\"anonymous\"></script>\n<stack-demo profile-config='{\"profile\":\"stack-demo\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></stack-demo>"
},
"instances": [
{

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <plugin-studio> custom element. -->
<script src="../plugin-studio.js" integrity="sha384-JhqC9WgYSipRHC8JK60x+VOeRp4gg1USvIDUXWb9aLx3gHdOCeLgmtV6HkN8mfgM" crossorigin="anonymous"></script>
<script src="../plugin-studio.js" integrity="sha384-JkHKL07MX7U5S8CLVSqL5FoDxuS25rpJygq75ozwNJuq8w9TVeZCw5DCZw82CTTA" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <plugin-studio> custom element. -->
<script src="../../plugin-studio.js" integrity="sha384-JhqC9WgYSipRHC8JK60x+VOeRp4gg1USvIDUXWb9aLx3gHdOCeLgmtV6HkN8mfgM" crossorigin="anonymous"></script>
<script src="../../plugin-studio.js" integrity="sha384-JkHKL07MX7U5S8CLVSqL5FoDxuS25rpJygq75ozwNJuq8w9TVeZCw5DCZw82CTTA" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -37,7 +37,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <stack-demo> custom element. -->
<script src="../stack-demo.js" integrity="sha384-3Rjk4pZDcTABDfBIAbT2tu+D6J9/U3jx3UJDkiPynH+oeiwzCGFotSX7hbhdT48G" crossorigin="anonymous"></script>
<script src="../stack-demo.js" integrity="sha384-UkW6xuFr5wd1F6WJagiWzDoGxBXN6V89ESxbUu6AsRkcQOXTMGxBdRs84EpOAV6A" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -37,7 +37,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <stack-demo> custom element. -->
<script src="../../stack-demo.js" integrity="sha384-3Rjk4pZDcTABDfBIAbT2tu+D6J9/U3jx3UJDkiPynH+oeiwzCGFotSX7hbhdT48G" crossorigin="anonymous"></script>
<script src="../../stack-demo.js" integrity="sha384-UkW6xuFr5wd1F6WJagiWzDoGxBXN6V89ESxbUu6AsRkcQOXTMGxBdRs84EpOAV6A" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: