CMS page translation
This commit is contained in:
parent
1eec64b45a
commit
847258f9ae
5 changed files with 42 additions and 12 deletions
|
|
@ -37,6 +37,11 @@ NUXT_TURNSTILE_SECRET_KEY=your-turnstile-secret-key
|
|||
NUXT_PUBLIC_FEEDBACK_API_BASE=https://support.gospelambition.org
|
||||
NUXT_PUBLIC_FEEDBACK_PROJECT_ID=your-feedback-project-id
|
||||
|
||||
# DeepL (used by the CMS "Translate from English" workflow on /admin/pages/:id)
|
||||
# Pro endpoint: https://api.deepl.com | Free-tier endpoint: https://api-free.deepl.com
|
||||
DEEPL_API_KEY=your-deepl-api-key
|
||||
DEEPL_API_URL=https://api.deepl.com
|
||||
|
||||
# OAuth 2.1 server
|
||||
# Issuer is derived from NUXT_PUBLIC_SITE_URL; resource is `${siteUrl}/mcp`.
|
||||
# Generate with: openssl rand -hex 32
|
||||
|
|
|
|||
|
|
@ -202,13 +202,13 @@ for (const l of ENABLED_LANGUAGES) {
|
|||
// active locale's translation (PUT /translations/:locale) in one click.
|
||||
// Passing a statusOverride also flips published/draft on the translation.
|
||||
const saving = ref(false)
|
||||
async function saveAll(statusOverride?: 'draft' | 'published') {
|
||||
const locale = activeLocale.value
|
||||
async function saveAll(statusOverride?: 'draft' | 'published', localeOverride?: string): Promise<boolean> {
|
||||
const locale = localeOverride ?? activeLocale.value
|
||||
const f = forms[locale]
|
||||
if (!f) return
|
||||
if (!f) return false
|
||||
if (!f.title.trim()) {
|
||||
toast.add({ title: 'Title is required', color: 'error' })
|
||||
return
|
||||
return false
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
|
|
@ -249,12 +249,14 @@ async function saveAll(statusOverride?: 'draft' | 'published') {
|
|||
: 'Saved'
|
||||
toast.add({ title: `${verb} ${locale}`, color: 'success' })
|
||||
await refresh()
|
||||
return true
|
||||
} catch (e: any) {
|
||||
toast.add({
|
||||
title: 'Save failed',
|
||||
description: e?.data?.statusMessage || e?.message,
|
||||
color: 'error'
|
||||
})
|
||||
return false
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
|
@ -300,16 +302,25 @@ const translateModalOpen = ref(false)
|
|||
const translateSource = ref('en')
|
||||
const translateTargets = ref<string[]>([])
|
||||
const translateOverwrite = ref(false)
|
||||
const translateStatus = ref<'draft' | 'published'>('draft')
|
||||
const translating = ref(false)
|
||||
|
||||
function openTranslateModal() {
|
||||
translateSource.value = 'en'
|
||||
translateTargets.value = ENABLED_LANGUAGES.map(l => l.code).filter(c => c !== 'en')
|
||||
translateOverwrite.value = false
|
||||
translateStatus.value = 'draft'
|
||||
translateModalOpen.value = true
|
||||
}
|
||||
|
||||
async function runTranslate() {
|
||||
// Persist any unsaved edits in the source-locale tab first — the
|
||||
// translate endpoint reads from the DB, not from the in-memory form.
|
||||
const sourceForm = forms[translateSource.value]
|
||||
if (sourceForm && (sourceForm.dirty || !sourceForm.loaded)) {
|
||||
const ok = await saveAll(undefined, translateSource.value)
|
||||
if (!ok) return
|
||||
}
|
||||
translating.value = true
|
||||
try {
|
||||
const res = await $fetch<{ results: Array<{ locale: string; skipped?: boolean; error?: string }> }>(
|
||||
|
|
@ -319,7 +330,8 @@ async function runTranslate() {
|
|||
body: {
|
||||
sourceLocale: translateSource.value,
|
||||
targetLocales: translateTargets.value,
|
||||
overwrite: translateOverwrite.value
|
||||
overwrite: translateOverwrite.value,
|
||||
status: translateStatus.value
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -355,7 +367,7 @@ const enabledLanguages = ENABLED_LANGUAGES
|
|||
<h1 class="text-xl font-semibold">Edit page</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton variant="outline" color="primary" icon="i-lucide-languages" disabled title="Workflow not yet tested" @click="openTranslateModal">Translate from English</UButton>
|
||||
<UButton variant="outline" color="primary" icon="i-lucide-languages" @click="openTranslateModal">Translate from English</UButton>
|
||||
<UButton variant="outline" color="error" icon="i-lucide-trash-2" @click="deleteModalOpen = true">Delete page</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -623,7 +635,8 @@ const enabledLanguages = ENABLED_LANGUAGES
|
|||
:items="enabledLanguages.map(l => ({ label: l.nativeName, value: l.code }))"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField label="Target locales">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-(--ui-text) mb-1">Target locales</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<UCheckbox
|
||||
v-for="l in enabledLanguages.filter(x => x.code !== translateSource)"
|
||||
|
|
@ -633,6 +646,15 @@ const enabledLanguages = ENABLED_LANGUAGES
|
|||
@update:model-value="checked => { if (checked) { translateTargets = Array.from(new Set([...translateTargets, l.code])) } else { translateTargets = translateTargets.filter(c => c !== l.code) } }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<UFormField label="Save translations as">
|
||||
<USelect
|
||||
v-model="translateStatus"
|
||||
:items="[
|
||||
{ label: 'Draft', value: 'draft' },
|
||||
{ label: 'Published', value: 'published' }
|
||||
]"
|
||||
/>
|
||||
</UFormField>
|
||||
<UCheckbox
|
||||
v-model="translateOverwrite"
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export default defineNuxtConfig({
|
|||
formApiKey: process.env.FORM_API_KEY || '',
|
||||
prayBaseUrl: process.env.NUXT_PRAY_BASE_URL || 'https://pray.doxa.life',
|
||||
deeplApiKey: process.env.DEEPL_API_KEY || '',
|
||||
deeplApiUrl: process.env.DEEPL_API_URL || 'https://api-free.deepl.com',
|
||||
deeplApiUrl: process.env.DEEPL_API_URL || 'https://api.deepl.com',
|
||||
public: {
|
||||
appName: process.env.APP_TITLE || 'My App',
|
||||
nodeEnv: process.env.NODE_ENV || '',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
// Admin: one-click translate an EN (or other source) translation of a
|
||||
// CMS page into one or more target locales. Uses DeepL for title,
|
||||
// excerpt, meta_title, meta_description, and body_json. Each target
|
||||
// translation is upserted as a draft (overwrite controllable via
|
||||
// `overwrite` flag); the editor reviews + publishes manually.
|
||||
// translation is upserted with the caller-chosen `status` (draft or
|
||||
// published, default draft). `overwrite` controls whether to replace
|
||||
// translations that already exist for a given locale.
|
||||
|
||||
import { defineEventHandler, getRouterParam, readBody, createError } from 'h3'
|
||||
import { requirePermission } from '../../../../utils/rbac'
|
||||
|
|
@ -17,6 +18,7 @@ interface Body {
|
|||
sourceLocale?: string
|
||||
targetLocales?: string[]
|
||||
overwrite?: boolean
|
||||
status?: 'draft' | 'published'
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
|
|
@ -34,6 +36,7 @@ export default defineEventHandler(async (event) => {
|
|||
if (targetLocales.length === 0) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'targetLocales is empty' })
|
||||
}
|
||||
const status: 'draft' | 'published' = body?.status === 'published' ? 'published' : 'draft'
|
||||
|
||||
const source = await db
|
||||
.selectFrom('page_translations')
|
||||
|
|
@ -83,7 +86,7 @@ export default defineEventHandler(async (event) => {
|
|||
meta_title: translatedMetaTitle || null,
|
||||
meta_description: translatedMetaDescription || null,
|
||||
og_image: source.og_image,
|
||||
status: 'draft'
|
||||
status
|
||||
})
|
||||
results.push({ locale: target })
|
||||
} catch (e: any) {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export async function translateTexts(
|
|||
|
||||
params.append('model_type', 'quality_optimized')
|
||||
|
||||
const apiUrl = config.deeplApiUrl || 'https://api-free.deepl.com'
|
||||
const apiUrl = config.deeplApiUrl || 'https://api.deepl.com'
|
||||
console.log(`[DeepL] Translating ${texts.length} text(s) → ${targetLang}${sourceLang ? ` from ${sourceLang}` : ''}${glossaryId ? ` (glossary: ${glossaryId})` : ''}`)
|
||||
|
||||
const response = await fetch(`${apiUrl}/v2/translate`, {
|
||||
|
|
|
|||
Loading…
Reference in a new issue