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 @@ + + + + + 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.