From 3f27a0a2921c3b4f16a7070f214e8f275bbcea8e Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 4 Sep 2026 18:52:22 +0000 Subject: [PATCH] Fix unstyled editor language selector, order all language menus logically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LangSelect's scoped CSS landed on the shared store chunk, whose stylesheet the editor never loaded (only the public selector path injected it), so the editor's language selector rendered unstyled on untranslated pages. Collect editor stylesheets from the entry's imported chunks too (same traversal as the langselect assets). Also: hreflang alternates now skip languages disabled site-wide, and all selectors (page editor, structure tab, public selector) order languages the same way — primary first, then the lang tab's geographic grouping. --- frontend/src/LangSelector.vue | 27 ++++++++++++++++----------- frontend/src/PageEditor.vue | 12 +++++++----- frontend/src/StructureEditor.vue | 9 +++++---- frontend/src/langs.js | 14 ++++++++++++++ pagerite/views.py | 30 ++++++++++++++++++++++-------- 5 files changed, 64 insertions(+), 28 deletions(-) diff --git a/frontend/src/LangSelector.vue b/frontend/src/LangSelector.vue index a7e5183..60ea552 100644 --- a/frontend/src/LangSelector.vue +++ b/frontend/src/LangSelector.vue @@ -9,23 +9,28 @@ // store and re-renders it). import { computed } from 'vue' import LangSelect from './LangSelect.vue' -import { flagFor, langName } from './langs' +import { flagFor, langName, langSort } from './langs' import { useStore } from './store' const store = useStore() // The "(primary)" marker is admin-panel information; the public selector -// lists plain languages. -const options = computed(() => - store.langAlternates.map((a) => ({ - tag: a.tag, - code: a.tag, - name: langName(a.tag), - flag: flagFor(a.tag), - primary: false, - })), -) +// lists plain languages. Order: the primary language first, then the rest +// in the lang tab's geographic grouping (./langs langSort) — the head's +// hreflang order is just alphabetical. const primaryTag = computed(() => store.langAlternates.find((a) => a.primary)?.tag ?? '') +const options = computed(() => { + const rest = langSort( + store.langAlternates.map((a) => a.tag).filter((t) => t !== primaryTag.value), + ) + return [primaryTag.value, ...rest].filter(Boolean).map((tag) => ({ + tag, + code: tag, + name: langName(tag), + flag: flagFor(tag), + primary: false, + })) +}) // The explicit pick, else the served language (header-autodetected pages // may have neither), else the primary. const model = computed(() => store.lang || store.servedLang || primaryTag.value) diff --git a/frontend/src/PageEditor.vue b/frontend/src/PageEditor.vue index 525bb34..5400b02 100644 --- a/frontend/src/PageEditor.vue +++ b/frontend/src/PageEditor.vue @@ -33,7 +33,7 @@ import { keymap } from '@codemirror/view' import { indentWithTab } from '@codemirror/commands' import { markdown } from '@codemirror/lang-markdown' import { cmHighlight, cmTheme } from './cmtheme' -import { flagFor, langName } from './langs' +import { flagFor, langName, langSort } from './langs' import { editorLang, pagePrimary } from './editorLang' import LangSelect from './LangSelect.vue' import ConnNote from './ConnNote.vue' @@ -121,11 +121,13 @@ function normPath(p) { // localization settings tab). // The picker's options: the primary language first, then the union of the -// page's translations and the site-wide configured targets, sorted. +// page's translations and the site-wide configured targets in the lang +// tab's geographic grouping (./langs langSort). const langOptions = computed(() => { - const others = [...new Set([...siteLangs.value, ...pageLangs.value])] - .filter((l) => l && l !== primaryLang.value) - .sort() + const others = langSort( + [...new Set([...siteLangs.value, ...pageLangs.value])] + .filter((l) => l && l !== primaryLang.value), + ) return [primaryLang.value, ...others].map((code) => ({ tag: code === primaryLang.value ? '' : code, code, diff --git a/frontend/src/StructureEditor.vue b/frontend/src/StructureEditor.vue index fec45eb..b5b21ab 100644 --- a/frontend/src/StructureEditor.vue +++ b/frontend/src/StructureEditor.vue @@ -19,7 +19,7 @@ import { computed, inject, onActivated, onMounted, onUnmounted, provide, ref, wa import StructureTree from './StructureTree.vue' import LangSelect from './LangSelect.vue' import { slugify } from './slugify' -import { flagFor, langName } from './langs' +import { flagFor, langName, langSort } from './langs' import { editorLang, pagePrimary } from './editorLang' import { dropPageCache, loadPlain } from './swapdoc' @@ -40,9 +40,10 @@ const primaryLang = ref('en') const siteLangs = ref([]) // The strip's options: the primary language first, then the configured -// translation targets (the lang tab manages that set). +// translation targets (the lang tab manages that set) in the lang tab's +// geographic grouping (./langs langSort). const langOptions = computed(() => - [primaryLang.value, ...siteLangs.value.filter((l) => l !== primaryLang.value)] + [primaryLang.value, ...langSort(siteLangs.value.filter((l) => l !== primaryLang.value))] .map((code) => ({ tag: code === primaryLang.value ? '' : code, code, @@ -63,7 +64,7 @@ watch(lang, () => refreshPages()) // dropdown lists "inherit" first (naming what it resolves to), then every // site language. Setting it on a section covers its whole subtree. const rowLangChoices = computed(() => - [primaryLang.value, ...siteLangs.value.filter((l) => l !== primaryLang.value)] + [primaryLang.value, ...langSort(siteLangs.value.filter((l) => l !== primaryLang.value))] .map((code) => ({ tag: code, code, name: langName(code), flag: flagFor(code), primary: false })), ) function rowLangOptions(el) { diff --git a/frontend/src/langs.js b/frontend/src/langs.js index 356385a..8fc097a 100644 --- a/frontend/src/langs.js +++ b/frontend/src/langs.js @@ -30,6 +30,20 @@ export const LANG_GROUPS = [ const displayNames = new Intl.DisplayNames(['en'], { type: 'language' }) +// Consistent menu ordering for language selectors: the geographic/cultural +// grouping above (similar languages sit together, and it does not vary with +// the display language the way alphabetical-by-name would). Tags outside +// the groups trail, ordered by tag. The primary language is not special +// here — callers put it first themselves. +const groupOrder = new Map(LANG_GROUPS.flat().map((c, i) => [c, i])) +export function langSort(codes) { + return [...codes].sort( + (a, b) => + (groupOrder.get(a) ?? groupOrder.size) - (groupOrder.get(b) ?? groupOrder.size) + || a.localeCompare(b), + ) +} + // English display name for a language tag ("fi" -> "Finnish"). export function langName(tag) { try { diff --git a/pagerite/views.py b/pagerite/views.py index c74c983..4e8475b 100644 --- a/pagerite/views.py +++ b/pagerite/views.py @@ -263,20 +263,31 @@ def _transition_css_url(transition: str) -> str | None: def _editor_css_url(vite_url: str | None) -> str | None: - """URL for the editor-specific stylesheet (Vue component styles). + """URLs (comma-joined) for the editor-specific stylesheets (Vue + component styles). This is linked by the public-page edit pen so the editor styles are - loaded before the editor JS dynamic-import resolves. + loaded before the editor JS dynamic-import resolves. Component styles + can land on shared chunks rather than the entry's own stylesheet — + LangSelect's ride on the shared store chunk, as it is also used by the + on-demand public language selector — so collect the stylesheets of the + entry and its imported chunks (the same traversal _langselect_assets + does). """ if vite_url: return None manifest = _manifest() - entry = manifest["src/main.js"] base = manifest.get(_BASE_CSS_KEY, {}).get("file") - for css in entry.get("css", []): - if css != base: - return f"/{css}" - return None + stylesheets, seen = [], set() + queue = ["src/main.js"] + for key in queue: # grows with imported chunks + if key in seen: + continue + seen.add(key) + entry = manifest[key] + stylesheets += [f"/{css}" for css in entry.get("css", []) if css != base] + queue += entry.get("imports", []) + return ",".join(stylesheets) or None def _inline_asset(url: str) -> str: @@ -1047,9 +1058,12 @@ def _language_urls( canonical = url if lang == original else f"{url}?lang={lang}" alternates = [] if data.translate_langs: + # Only languages the page actually has AND that are still enabled + # site-wide (a disabled target stops being advertised). + enabled = {original, *data.translate_langs} alternates = [("x-default", url)] + [ (tag, url if tag == original else f"{url}?lang={tag}") - for tag in sorted({original, *node.langs}) + for tag in sorted({original, *node.langs} & enabled) ] return canonical, alternates