Fix unstyled editor language selector, order all language menus logically

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.
This commit is contained in:
2026-09-04 18:52:22 +00:00
parent b30d909a23
commit 3f27a0a292
5 changed files with 64 additions and 28 deletions
+16 -11
View File
@@ -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)
+7 -5
View File
@@ -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,
+5 -4
View File
@@ -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) {
+14
View File
@@ -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 {