- New editor-shell tab (lang): translatable languages as clickable flag tiles (shared langs.js mapping of the 28 Seed-X languages to flags), saved via the settings round-trip; translator service URL(s) shown. - Bootstrap seeds translate_langs with Spanish and Chinese (the original language is never a target). - GET /_api/settings also returns primary_lang. - PageEditor's flag/name helpers move to the shared langs.js.
211 lines
5.3 KiB
Vue
211 lines
5.3 KiB
Vue
<script setup>
|
|
// Lang tab: the site-wide translation target languages (translate_langs)
|
|
// and the translator service WebSocket URL(s) (translate_keys). The primary
|
|
// language is configured per site hierarchy, not here. Flag clicks toggle
|
|
// and save immediately; the settings round-trip re-reads the payload, so
|
|
// this tab only ever changes translate_langs. The settings write's
|
|
// invalidation hook kicks the translation dispatcher.
|
|
import { computed, onActivated, onMounted, onUnmounted, ref } from 'vue'
|
|
import { TRANSLATABLE, flagFor, langName } from './langs'
|
|
import { dropPageCache } from './swapdoc'
|
|
|
|
defineProps({ pagePath: { type: String, default: '' } })
|
|
// close/path-change are wired by EditorShell; this tab never emits them.
|
|
defineEmits(['close', 'pathChange'])
|
|
|
|
const saveError = ref('')
|
|
const primaryLang = ref('en')
|
|
const selected = ref(new Set())
|
|
const keyUrls = ref([])
|
|
|
|
// The toggleable targets: every translatable language but the primary,
|
|
// alphabetized by display name.
|
|
const options = computed(() =>
|
|
Object.keys(TRANSLATABLE)
|
|
.filter((code) => code !== primaryLang.value)
|
|
.map((code) => ({ code, name: langName(code), flag: flagFor(code) }))
|
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
)
|
|
|
|
function updateWindowTitle() {
|
|
document.title = 'lang 🖊️'
|
|
}
|
|
|
|
onActivated(updateWindowTitle)
|
|
|
|
// The shell stays mounted while hidden: when it is re-shown with this tab
|
|
// active, restore the window title.
|
|
function onEditorShown() {
|
|
if (document.body.dataset.editorMode === 'localization') updateWindowTitle()
|
|
}
|
|
|
|
onMounted(async () => {
|
|
addEventListener('pagerite:editor-shown', onEditorShown)
|
|
try {
|
|
const s = await (await fetch('/_api/settings')).json()
|
|
primaryLang.value = s.primary_lang || 'en'
|
|
selected.value = new Set(s.translate_langs || [])
|
|
const wsBase = location.origin.replace(/^http/, 'ws')
|
|
keyUrls.value = Object.entries(s.translate_keys || {})
|
|
.map(([key, name]) => ({ name, url: `${wsBase}/_translate/${key}` }))
|
|
} catch { /* keep defaults */ }
|
|
})
|
|
|
|
onUnmounted(() => removeEventListener('pagerite:editor-shown', onEditorShown))
|
|
|
|
async function toggle(code) {
|
|
const next = new Set(selected.value)
|
|
if (next.has(code)) next.delete(code)
|
|
else next.add(code)
|
|
selected.value = next
|
|
try {
|
|
const s = await (await fetch('/_api/settings')).json()
|
|
const res = await fetch('/_api/settings', {
|
|
method: 'PUT',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ ...s, translate_langs: [...next] }),
|
|
})
|
|
if (res.ok) {
|
|
saveError.value = ''
|
|
dropPageCache()
|
|
} else {
|
|
saveError.value = '⚠️ changes could not be saved'
|
|
}
|
|
} catch {
|
|
saveError.value = '⚠️ changes could not be saved'
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="localization-editor">
|
|
<div v-if="saveError">{{ saveError }}</div>
|
|
|
|
<section class="block">
|
|
<div class="block-head">
|
|
<span class="field-label">languages</span>
|
|
</div>
|
|
<div class="flags">
|
|
<button
|
|
v-for="o in options"
|
|
:key="o.code"
|
|
type="button"
|
|
class="flag-tile"
|
|
:class="{ selected: selected.has(o.code) }"
|
|
:title="`${o.name} (${o.code})`"
|
|
@click="toggle(o.code)"
|
|
>
|
|
<span class="flag" v-html="o.flag" />
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section v-if="keyUrls.length" class="block">
|
|
<div class="block-head">
|
|
<span class="field-label">translator service</span>
|
|
<small class="muted">connect scripts/translator.py to</small>
|
|
</div>
|
|
<div v-for="k in keyUrls" :key="k.url" class="key-row">
|
|
<code>{{ k.url }}</code>
|
|
<small class="muted">{{ k.name }}</small>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.localization-editor {
|
|
overflow-y: auto;
|
|
background: var(--surface);
|
|
}
|
|
|
|
.block {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.4rem;
|
|
padding: 0.5rem 1rem;
|
|
border-bottom: 1px solid var(--line);
|
|
background: var(--surface);
|
|
}
|
|
|
|
.block-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
.field-label {
|
|
color: var(--muted);
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.muted {
|
|
color: var(--muted);
|
|
}
|
|
|
|
/* Flag grid: deselected flags sit dimmed and grayed; a click brings one to
|
|
full color with an accent ring (selected = a translation target). */
|
|
.flags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
padding: 0.2rem 0;
|
|
}
|
|
|
|
.flag-tile {
|
|
padding: 3px;
|
|
background: none;
|
|
border: 2px solid transparent;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
opacity: 0.4;
|
|
filter: grayscale(0.8);
|
|
transition: opacity 0.15s, filter 0.15s, border-color 0.15s;
|
|
}
|
|
|
|
.flag-tile:hover {
|
|
opacity: 0.8;
|
|
filter: none;
|
|
}
|
|
|
|
.flag-tile.selected {
|
|
opacity: 1;
|
|
filter: none;
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 1px var(--accent);
|
|
}
|
|
|
|
/* Same flag chips as the PageEditor language picker / analytics cells. */
|
|
.flag {
|
|
display: inline-flex;
|
|
width: 18px;
|
|
height: 12px;
|
|
flex: 0 0 auto;
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
border: 1px solid var(--line);
|
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2) inset;
|
|
}
|
|
|
|
.flag-tile .flag {
|
|
width: 36px;
|
|
height: 24px;
|
|
}
|
|
|
|
.flag :deep(svg) {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
}
|
|
|
|
.key-row {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
.key-row code {
|
|
user-select: all;
|
|
}
|
|
</style>
|