diff --git a/AGENTS.md b/AGENTS.md
index 2674238..46b056c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -29,6 +29,8 @@ Pagerite is a CMS. See `docs` for the full design and implementation details. Ke
- `frontend/src/` — Vue editor and public-page JS entries.
- `main.js` — Vue editor app entry.
- `analytics-main.js` — analytics page entry (mounts `AnalyticsView` at `/_a`).
+ - `langselect-main.js` + `LangSelector.vue` — public language selector, imported on demand by pagerite.js on pages with more than one hreflang alternate (the editors' `LangSelect` flag dropdown).
+ - `store.js` — the shared Pinia store (`useStore`, id `pagerite`) for cross-bundle UI state.
- `pagerite.js` — public page entry.
- `editorLang.js` + `LangSelect.vue` — the editor shell's shared language selection and its selector component (page + structure tabs; drives the page preview while the panel is open, via `swapdoc.setLangOverride`).
- `reconnect.js` — shared WebSocket pacing for all sockets (staggered connect slots, stuck-CONNECTING watchdog, exponential backoff): bursts and rapid retries trip the browser's WebSocket throttling.
diff --git a/docs/localization.md b/docs/localization.md
index 62c593b..e691342 100644
--- a/docs/localization.md
+++ b/docs/localization.md
@@ -53,11 +53,13 @@ Region tags normalize to their base subtag (`fi-FI` → `fi`).
article's own language), `?lang=xx` when serving a translation — however
the language was arrived at (query or header).
- `` entries follow the canonical
- directly (before the social meta tags) and are the same set on every
- page — the site-wide configured languages (`translate_langs`, which the
- translator works to fill in): `x-default` first, pointing at the plain
- autodetecting URL, then every language explicitly with `?lang=`, the
- page's own primary language included.
+ directly (before the social meta tags) and list the languages the page
+ is **actually available in**: `x-default` first, pointing at the plain
+ autodetecting URL, then every available language — the original again by
+ its plain URL, translations by `?lang=`. The public language selector
+ keys off these: pagerite.js mounts the editors' flag dropdown in the
+ top-right corner when the head advertises x-default plus more than one
+ language, loading its bundle (Vue + the flag SVG set) on demand.
- The override sticks for the session of clicks: a page requested with
`?lang=` replicates the query onto the navigation links it renders (nav,
sidebar, cards, brand — in-article links are content and stay as
@@ -66,6 +68,11 @@ Region tags normalize to their base subtag (`fi-FI` → `fi`).
`history.replaceState` (pretty, shareable URLs), remembers the language,
and adds it to every internal fetch that lacks one (preloads,
fetch-navigations, history traversals); history entries stay query-less.
+- The public selector's pick is the same override, pure JS state
+ (`pagerite:set-session-lang`): the session language changes and the page
+ swaps in place — no `?lang=` in the address bar, no reload. The choice is
+ linked with the editor panel's language dropdown both ways; closing the
+ panel keeps the chosen language instead of reverting.
- A full page refresh or a shared link resets to automatic selection (header
only). This gives a clean one-time override without cookies.
diff --git a/frontend/package.json b/frontend/package.json
index 50f02b2..c942881 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -20,6 +20,7 @@
"codemirror": "^6.0.2",
"country-flag-icons": "^1.6.20",
"overlayscrollbars": "^2.16.0",
+ "pinia": "^4.0.3",
"transliteration": "^2.6.1",
"vue": "^3.5.26",
"vuedraggable": "^4.1.0"
diff --git a/frontend/src/EditorShell.vue b/frontend/src/EditorShell.vue
index a53d152..471da31 100644
--- a/frontend/src/EditorShell.vue
+++ b/frontend/src/EditorShell.vue
@@ -21,11 +21,11 @@ const currentPath = ref(props.pagePath)
const activeMode = ref(props.initialMode)
// The shared language selection (./editorLang, v-modeled by the tabs'
-// LangSelects) also drives the page preview: while the shell is open it
-// overrides the normal language preferences (?lang= / Accept-Language),
-// so the page renders in the language being edited; closing restores.
-// The primary selection pins by the CURRENT PAGE's own primary language
-// (pages may differ — Node.language is inherited down the tree).
+// LangSelects) is linked to the whole-page language: while the shell is
+// open it drives the page preview (overrides ?lang= / Accept-Language),
+// and closing keeps the pick as the session language. The primary
+// selection pins by the CURRENT PAGE's own primary language (pages may
+// differ — Node.language is inherited down the tree).
let pinned = false
function pinPreviewLang() {
pinned = true
@@ -34,6 +34,15 @@ function pinPreviewLang() {
setLangOverride(editorLang.value || pagePrimary.value || 'en')
loadPlain(currentPath.value)
}
+// Opening the panel must not switch the page's language: adopt the
+// session's chosen language (public selector / earlier pick) once, then
+// pin. Runs only on (re)open — after that the selection is the user's.
+function openShell() {
+ const session = window.__pageriteLang
+ if (!editorLang.value && session && session !== (pagePrimary.value || 'en'))
+ editorLang.value = session
+ pinPreviewLang()
+}
function unpinPreviewLang() {
if (!pinned) return
pinned = false
@@ -89,13 +98,13 @@ function onSwitchEvent(ev) {
onMounted(() => {
document.body.dataset.editorMode = activeMode.value
addEventListener('pagerite:switch-editor', onSwitchEvent)
- addEventListener('pagerite:editor-shown', pinPreviewLang)
+ addEventListener('pagerite:editor-shown', openShell)
addEventListener('pagerite:editor-hidden', unpinPreviewLang)
- // The shell mounts visible (openEditor), so pin immediately. The site
+ // The shell mounts visible (openEditor), so open immediately. The site
// default primary language comes from the settings — it only fills the
// unknown; the page/structure tabs refine pagePrimary per page as they
// learn it (their knowledge is strictly better).
- pinPreviewLang()
+ openShell()
fetch('/_api/settings').then((r) => r.json()).then((s) => {
if (!pagePrimary.value) pagePrimary.value = s.primary_lang || 'en'
}).catch(() => { /* keep the fallback */ })
@@ -103,7 +112,7 @@ onMounted(() => {
onUnmounted(() => {
removeEventListener('pagerite:switch-editor', onSwitchEvent)
- removeEventListener('pagerite:editor-shown', pinPreviewLang)
+ removeEventListener('pagerite:editor-shown', openShell)
removeEventListener('pagerite:editor-hidden', unpinPreviewLang)
})
diff --git a/frontend/src/LangSelect.vue b/frontend/src/LangSelect.vue
index 6c0ba28..ac51db8 100644
--- a/frontend/src/LangSelect.vue
+++ b/frontend/src/LangSelect.vue
@@ -3,7 +3,8 @@
// flag button opening a clean dropdown, v-modeled on the shared editorLang
// ('' = the primary language). The lang tab's flag grid is a different
// control (toggles, not a select) and stays as it is.
-import { computed, ref } from 'vue'
+import { computed, nextTick, ref } from 'vue'
+import { usePopup } from './dropdown'
const props = defineProps({
modelValue: { type: String, default: '' },
@@ -13,8 +14,12 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const open = ref(false)
+const root = ref(null)
const toggleBtn = ref(null)
+const pop = ref(null)
const popStyle = ref({})
+// Closes on outside click / Escape (./dropdown), not on mouseleave.
+usePopup(open, root)
const current = computed(
() => props.options.find((o) => o.tag === props.modelValue) ?? props.options[0],
)
@@ -26,6 +31,17 @@ function toggle() {
// onto the page area instead of being clipped by it.
const r = toggleBtn.value.getBoundingClientRect()
popStyle.value = { top: `${r.bottom + 2}px`, left: `${r.left}px` }
+ // A toggle mounted near the right window edge (the public page
+ // selector sits top-right) opens the popup flush against that edge.
+ nextTick(() => {
+ const p = pop.value?.getBoundingClientRect()
+ if (p && p.right > innerWidth - 4) {
+ popStyle.value = {
+ ...popStyle.value,
+ left: `${Math.max(4, innerWidth - 4 - p.width)}px`,
+ }
+ }
+ })
}
}
@@ -36,7 +52,7 @@ function select(tag) {
-
+
-
+