diff --git a/frontend/src/components/modal/ProvenancePanel.vue b/frontend/src/components/modal/ProvenancePanel.vue index 2bd8309..3b8f9e5 100644 --- a/frontend/src/components/modal/ProvenancePanel.vue +++ b/frontend/src/components/modal/ProvenancePanel.vue @@ -42,13 +42,23 @@ · {{ e.post.attachment_count }} files +
+ {{ showOrig[e.provenance_id] ? 'Show translation' : `Show original${langLabel(e)}` }} +
{{ expanded[e.provenance_id] ? 'Hide description ▴' : 'Show description ▾' }}
+ +

{{ e.post.description_translated }}

@@ -117,8 +127,12 @@ const effectiveImage = computed(() => props.image ?? modal.current) // 2026-05-28. Reset when the viewed image changes. const expanded = reactive({}) function toggleDesc(id) { expanded[id] = !expanded[id] } +// Per-entry "show the original (untranslated) text" toggle (#143). +const showOrig = reactive({}) +function toggleOrig(id) { showOrig[id] = !showOrig[id] } watch(() => effectiveId.value, () => { for (const k of Object.keys(expanded)) delete expanded[k] + for (const k of Object.keys(showOrig)) delete showOrig[k] }) watch( @@ -157,10 +171,23 @@ const show = computed(() => { const attachments = computed(() => state.value?.attachments || []) function postDate(e) { return formatPostDate(e.post.date) } +// Translation-forward (#143): show the English by default when present. +function hasTranslation(e) { + return !!(e.post.title_translated || e.post.description_translated) +} +function showTranslated(e) { + return hasTranslation(e) && !showOrig[e.provenance_id] +} +function langLabel(e) { + return e.post.translated_source_lang ? ` (${e.post.translated_source_lang})` : '' +} function postTitle(e) { // Titles can arrive as stored HTML (e.g. ""); render // as plain text (the CSS makes it bold). - return toPlainText(e.post.title) || `Post ${e.post.external_post_id}` + const t = showTranslated(e) && e.post.title_translated + ? e.post.title_translated + : e.post.title + return toPlainText(t) || `Post ${e.post.external_post_id}` } function openPost(postId, artistId) { diff --git a/frontend/src/components/posts/PostCard.vue b/frontend/src/components/posts/PostCard.vue index 4fdce08..1b7837b 100644 --- a/frontend/src/components/posts/PostCard.vue +++ b/frontend/src/components/posts/PostCard.vue @@ -58,16 +58,23 @@
-

{{ plainTitle }}

+

{{ displayTitle }}

Post {{ post.external_post_id }}

+ +
@@ -75,7 +82,7 @@ v-else-if="hasDescription" ref="descEl" class="fc-post-card__desc" :class="{ 'fc-post-card__desc--clamped': !descExpanded }" - >{{ descText }}

+ >{{ descTextDisplay }}

(no description)

@@ -231,6 +238,32 @@ const canExpand = computed( () => props.post.description_truncated === true || cssOverflow.value, ) +// --- translation-forward (#143): the English is shown by default when a +// translation exists; a per-card toggle reveals the original. Translations are +// plain text, so showTranslated also decides HTML-vs-plain body rendering. +const showOriginal = ref(false) +const hasTranslation = computed( + () => !!(props.post.post_title_translated || props.post.description_translated), +) +const showTranslated = computed(() => hasTranslation.value && !showOriginal.value) +const sourceLangLabel = computed(() => + props.post.translated_source_lang ? ` (${props.post.translated_source_lang})` : '', +) +const displayTitle = computed(() => + showTranslated.value && props.post.post_title_translated + ? toPlainText(props.post.post_title_translated) + : plainTitle.value, +) +const descTextDisplay = computed(() => { + if (!showTranslated.value) return descText.value + return descExpanded.value + ? (detail.value?.description_translated_full || props.post.description_translated) + : props.post.description_translated +}) +const showHtmlBody = computed( + () => !showTranslated.value && descExpanded.value && !!descHtml.value, +) + function measureOverflow () { const el = descEl.value cssOverflow.value = !!el && el.scrollHeight > el.clientHeight + 1 @@ -467,6 +500,19 @@ function formatBytes (n) { font-size: 0.85rem; font-weight: 600; } .fc-post-card__more:hover { text-decoration: underline; } +/* Translation toggle (#143) — quiet secondary affordance under the title. */ +.fc-post-card__lang { + display: block; margin: 2px 0 6px; padding: 0; + background: none; border: 0; cursor: pointer; + color: rgb(var(--v-theme-on-surface-variant)); + font-size: 0.75rem; font-weight: 600; +} +.fc-post-card__lang:hover { + text-decoration: underline; color: rgb(var(--v-theme-accent)); +} +.fc-post-card__lang:focus-visible { + outline: 2px solid rgb(var(--v-theme-accent)); outline-offset: 1px; +} .fc-post-card__atts { display: flex; flex-wrap: wrap; gap: 8px; diff --git a/frontend/src/components/settings/MaintenancePanel.vue b/frontend/src/components/settings/MaintenancePanel.vue index dfc5693..b379015 100644 --- a/frontend/src/components/settings/MaintenancePanel.vue +++ b/frontend/src/components/settings/MaintenancePanel.vue @@ -13,6 +13,7 @@

+
@@ -69,6 +70,7 @@ import { onMounted, onUnmounted } from 'vue' import ImportFiltersForm from './ImportFiltersForm.vue' +import TranslationCard from './TranslationCard.vue' import MLBackfillCard from './MLBackfillCard.vue' import ThumbnailBackfillCard from './ThumbnailBackfillCard.vue' import ArchiveReextractCard from './ArchiveReextractCard.vue' diff --git a/frontend/src/components/settings/TranslationCard.vue b/frontend/src/components/settings/TranslationCard.vue new file mode 100644 index 0000000..5629267 --- /dev/null +++ b/frontend/src/components/settings/TranslationCard.vue @@ -0,0 +1,133 @@ + + +