diff --git a/docs/localization.md b/docs/localization.md
index c97506f..3880779 100644
--- a/docs/localization.md
+++ b/docs/localization.md
@@ -282,8 +282,10 @@ Frames are JSON-encoded tagged msgspec structs (`pagerite/translate.py`;
translation of the connection's current job, matching it by (lang, key).
Which languages get translated is **server-configured**:
-`Data.translate_langs` (presence-key dict, read/set via `/_api/settings`
-as `translate_langs`; no editing UI yet). The dispatcher offers a
+`Data.translate_langs` (presence-key dict, bootstrapped to Spanish and
+Chinese — the original language is never a target — edited in the editor
+shell's localization tab or set via `/_api/settings` as `translate_langs`).
+The dispatcher offers a
connection jobs only in `wanted ∩ capable`; a connection without overlap
simply stays idle.
diff --git a/frontend/src/EditorShell.vue b/frontend/src/EditorShell.vue
index 064a23c..79f83ef 100644
--- a/frontend/src/EditorShell.vue
+++ b/frontend/src/EditorShell.vue
@@ -7,6 +7,7 @@ import PageEditor from './PageEditor.vue'
import BannerEditor from './BannerEditor.vue'
import SiteEditor from './SiteEditor.vue'
import StructureEditor from './StructureEditor.vue'
+import LocalizationEditor from './LocalizationEditor.vue'
const props = defineProps({
pagePath: { type: String, default: '' },
@@ -17,11 +18,12 @@ const emit = defineEmits(['close'])
const currentPath = ref(props.pagePath)
const activeMode = ref(props.initialMode)
-// Tab order: site-wide settings first (site, structure), then — after a
-// visual break — the per-page editors (article, banner).
+// Tab order: site-wide settings first (site, structure, localization), then
+// — after a visual break — the per-page editors (article, banner).
const MODES = [
{ key: 'site', label: 'site', component: SiteEditor },
{ key: 'structure', label: 'structure', component: StructureEditor },
+ { key: 'localization', label: 'lang', component: LocalizationEditor },
{ key: 'page', label: 'article', component: PageEditor, breakBefore: true },
{ key: 'banner', label: 'banner', component: BannerEditor },
]
diff --git a/frontend/src/LocalizationEditor.vue b/frontend/src/LocalizationEditor.vue
new file mode 100644
index 0000000..da1cc7f
--- /dev/null
+++ b/frontend/src/LocalizationEditor.vue
@@ -0,0 +1,210 @@
+
+
+
+
+
{{ saveError }}
+
+
+
+ languages
+
+
+
+
+
+
+
+
+ translator service
+ connect scripts/translator.py to
+
+
+ {{ k.url }}
+ {{ k.name }}
+
+
+
+
+
+
diff --git a/frontend/src/PageEditor.vue b/frontend/src/PageEditor.vue
index 1ed309e..903b1cb 100644
--- a/frontend/src/PageEditor.vue
+++ b/frontend/src/PageEditor.vue
@@ -31,8 +31,8 @@ import { EditorState } from '@codemirror/state'
import { keymap } from '@codemirror/view'
import { indentWithTab } from '@codemirror/commands'
import { markdown } from '@codemirror/lang-markdown'
-import * as flagSvgs from 'country-flag-icons/string/3x2'
import { cmHighlight, cmTheme } from './cmtheme'
+import { flagFor, langName } from './langs'
import { dropPageCache, loadPlain } from './swapdoc'
const props = defineProps({
@@ -91,25 +91,8 @@ function normPath(p) {
}
// --- Language picker -------------------------------------------------------
-// Flag icons from the same country-flag-icons set the analytics visitor
-// cells use, resolved from the language tag's most likely region.
-const displayNames = new Intl.DisplayNames(['en'], { type: 'language' })
-
-function langName(tag) {
- try {
- return displayNames.of(tag) || tag
- } catch {
- return tag
- }
-}
-
-function flagFor(tag) {
- try {
- return flagSvgs[new Intl.Locale(tag).maximize().region] || ''
- } catch {
- return ''
- }
-}
+// Flag icons and display names come from ./langs (shared with the
+// 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.
diff --git a/frontend/src/langs.js b/frontend/src/langs.js
new file mode 100644
index 0000000..a1bd049
--- /dev/null
+++ b/frontend/src/langs.js
@@ -0,0 +1,37 @@
+// Language helpers shared by the editors (the PageEditor language picker,
+// the localization settings tab). Flags come from the country-flag-icons
+// set, same as the analytics visitor cells.
+import * as flagSvgs from 'country-flag-icons/string/3x2'
+
+// The Seed-X reference translator's languages (scripts/translator.py) — the
+// translation-target ceiling — each mapped to the country whose flag stands
+// for it (CLDR likely-subtag regions: the country with the most speakers).
+export const TRANSLATABLE = {
+ ar: 'EG', cs: 'CZ', da: 'DK', de: 'DE', el: 'GR', en: 'US', es: 'ES',
+ fa: 'IR', fi: 'FI', fr: 'FR', hu: 'HU', id: 'ID', it: 'IT', ja: 'JP',
+ ko: 'KR', ms: 'MY', nl: 'NL', no: 'NO', pl: 'PL', pt: 'BR', ro: 'RO',
+ ru: 'RU', sv: 'SE', th: 'TH', tr: 'TR', uk: 'UA', vi: 'VN', zh: 'CN',
+}
+
+const displayNames = new Intl.DisplayNames(['en'], { type: 'language' })
+
+// English display name for a language tag ("fi" -> "Finnish").
+export function langName(tag) {
+ try {
+ return displayNames.of(tag) || tag
+ } catch {
+ return tag
+ }
+}
+
+// Flag SVG string for a language tag: the explicit mapping first, then the
+// tag's most likely region (for languages outside the translatable list).
+export function flagFor(tag) {
+ const country = TRANSLATABLE[tag]
+ if (country) return flagSvgs[country] || ''
+ try {
+ return flagSvgs[new Intl.Locale(tag).maximize().region] || ''
+ } catch {
+ return ''
+ }
+}
diff --git a/pagerite/app.py b/pagerite/app.py
index 3868031..c454138 100644
--- a/pagerite/app.py
+++ b/pagerite/app.py
@@ -292,13 +292,16 @@ _KEY_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"
@kanta.bootstrap
-def _translate_key(data: Data) -> None:
- """Generate the first translator service key on database creation.
+def _translator_defaults(data: Data) -> None:
+ """Translator defaults on database creation: the first service key and
+ the wanted target languages (Spanish and Chinese — English is the
+ original language, never a translation target).
Keys are a dict (key -> display name) with the future reservation that
multiple keys could be managed (e.g. via a web interface)."""
key = "".join(secrets.choice(_KEY_ALPHABET) for _ in range(12))
data.translate_keys[key] = "default"
+ data.translate_langs = {"es": True, "zh": True}
@asynccontextmanager
@@ -662,6 +665,7 @@ async def get_settings() -> dict:
"transition": data.transition,
"transitions": views._transition_names(),
"translate_keys": data.translate_keys,
+ "primary_lang": i18n.ORIGINAL_LANGUAGE,
"translate_langs": sorted(data.translate_langs),
}
diff --git a/pagerite/data.py b/pagerite/data.py
index 6d2a206..0b6b010 100644
--- a/pagerite/data.py
+++ b/pagerite/data.py
@@ -110,7 +110,8 @@ class Data(msgspec.Struct):
#: Wanted target languages for the translator service (presence-keys,
#: value always True). The dispatcher offers jobs only in the
#: intersection of these and a connection's announced capabilities.
- #: Read/set via /_api/settings (no editing UI yet).
+ #: Bootstrapped to es+zh; edited in the editor shell's localization
+ #: tab (or via /_api/settings).
translate_langs: dict[str, bool] = {}
#: All original-language page text, content-addressed:
#: chunk_key (9 bytes; base64 at the JSON level) -> Markdown chunk.