Posterverse: the shell language menu in the phone bar and at the bottom of the rail

Fills VerticalTabsLayout's #phone-end and #rail-end slots with the shared
ShellLanguageMenu. A pick first asks the host (doxa-language-request); only an
uncancelled pick reaches setLocale, which keeps the pick with the host language
value it was made over and saves it. The pick wins only while that value is
unchanged; a new value from the host takes over and drops the pick. The saved
choice is never read as a source, since it falls back to the browser's
language and would override the host page.
This commit is contained in:
Doxa Maps 2026-10-01 05:50:22 +00:00
parent 448ef0ac70
commit 067979ae71

View file

@ -57,6 +57,11 @@
</template>
</section>
<!-- The language menu: the end of the phone bar, and the bottom of the desktop rail. A pick asks the
host first (doxa-language-request); only an uncancelled pick reaches setLocale. -->
<template #phone-end><ShellLanguageMenu :current="uiLocale" @switch="setLocale" /></template>
<template #rail-end="{ collapsed }"><ShellLanguageMenu :current="uiLocale" :collapsed="collapsed" @switch="setLocale" /></template>
<!-- The pane is the scroller (overflow-y:auto), so back-to-top follows the active pane. -->
<BackToTopButton :target="activePaneEl" :labels="{ backToTop: tr('backToTop') }" />
</VerticalTabsLayout>
@ -66,6 +71,8 @@
import { defineComponent, inject } from 'vue'
import VerticalTabsLayout from '@map/layouts/VerticalTabsLayout.vue'
import BackToTopButton from '@map/components/BackToTopButton.vue'
import ShellLanguageMenu from '@map/components/ShellLanguageMenu.vue'
import { saveLocale } from '@map/layouts/app-wrapper-material/languages.js'
import { loadScriptOnce, whenTagDefined } from '@map/utils/embedScript.js'
import { translate, LOCALES } from './src/strings.js'
import { SECTIONS, TABS, resolveTabs } from './src/tabs.js'
@ -93,7 +100,7 @@ export default defineComponent({
// The host's attributes stay on the host: an inline style copied onto the layout root beats the
// layout's own rules (a styled host once collapsed a nested map to 76px).
inheritAttrs: false,
components: { VerticalTabsLayout, BackToTopButton },
components: { VerticalTabsLayout, BackToTopButton, ShellLanguageMenu },
props: {
treeBase: { type: String, default: '' },
profileConfig: { type: [String, Object], default: '' },
@ -128,6 +135,11 @@ export default defineComponent({
// The manifest's `runtime` block (the build's apiBaseUrl + public token), for foreign-page embeds.
runtime: {},
hostLang: detectHostLocale(),
// A language picked in THIS session through the menu, and the host's language value it was picked
// over. The pick wins only while that value is unchanged; the saved choice (loadLocale) is never
// read, since it falls back to the browser's language and would override the host page.
localPick: '',
pickBase: null,
// Until the manifest lands, the rail shows the rows that are expected to exist; provisional rows
// appear only once the manifest proves their bundle is in this tree.
tabs: TABS.filter((d) => !d.provisional).map(freshTab),
@ -153,10 +165,13 @@ export default defineComponent({
return this.manifestBase ? this.manifestBase.replace(/\/+$/, '') + '/assets' : ''
},
headingText() { return String(this.cfg.heading || this.heading || '').trim() || 'Posterverse' },
// Language precedence: what the host says, then the host page's own language, then English.
// The language the host passed: the `language` prop, else profile-config language / locale / lang.
hostLanguage() { return String(this.language || this.cfg.language || this.cfg.locale || this.cfg.lang || '').trim() },
// Language precedence: this session's menu pick (while the host's value is the one it was made over),
// then what the host says, then the host page's own language, then English.
effectiveLocale() {
const fromProp = String(this.language || this.cfg.language || this.cfg.locale || this.cfg.lang || '').trim()
return (fromProp || this.hostLang || 'en').split('-')[0].toLowerCase()
if (this.localPick && this.hostLanguage === this.pickBase) return this.localPick
return (this.hostLanguage || this.hostLang || 'en').split('-')[0].toLowerCase()
},
uiLocale() { return LOCALES.includes(this.effectiveLocale) ? this.effectiveLocale : 'en' },
uiDir() { return this.uiLocale === 'ar' ? 'rtl' : 'ltr' },
@ -201,6 +216,8 @@ export default defineComponent({
this.mountElement(this.activeIndex)
},
tab(id) { if (id) this.onSelect(id) },
// A host that sets a new language takes over again: the session pick is dropped.
hostLanguage(v) { if (v !== this.pickBase) { this.localPick = ''; this.pickBase = null } },
},
mounted() {
this.watchHostLang()
@ -213,6 +230,12 @@ export default defineComponent({
},
methods: {
tr(key, params) { return translate(this.uiLocale, key, params) },
// The menu's 'switch': no host cancelled the request, so the shell switches itself.
setLocale(code) {
this.pickBase = this.hostLanguage
this.localPick = code
saveLocale(code)
},
tabLabel(t) { return this.tr('tab.' + t.id) },
tabSub(t) { return t.section === 'experimental' ? this.tr('experimentalSub') : this.tr('sub.' + t.id) },
setSlot(i, el) { if (el) this._slots = Object.assign(this._slots || {}, { [i]: el }) },