feat: pair community post translations

This commit is contained in:
hajimi
2026-08-02 02:27:04 +08:00
parent ac20d5d914
commit 18b7f843ae
3 changed files with 204 additions and 65 deletions
@@ -17,9 +17,45 @@
- [ ] **Step 1: Upgrade the local text splitter**
Replace the newline-only `splitContentLines` helper with the article-detail behavior: normalize line endings, prefer explicit non-empty lines, and otherwise split the single line by Chinese or English sentence punctuation.
Replace the newline-only `splitContentLines` helper with the article-detail behavior: normalize line endings, preserve explicit line order, and split every line by Chinese or English sentence punctuation. Protect decimals, abbreviations such as `U.S.`, and name initials such as `J.` so sentence indexes do not drift.
```ts
const shouldKeepEnglishPeriod = (text: string, index: number) => {
const previous = text[index - 1] || ''
const next = text[index + 1] || ''
if (/\d/.test(previous) && /\d/.test(next)) return true
if (/[A-Za-z]/.test(previous) && /[A-Za-z]/.test(next)) return true
const token = text.slice(0, index).match(/([A-Za-z](?:\.[A-Za-z])*)$/)?.[1] || ''
const nextNonSpace = text.slice(index + 1).match(/\S/)?.[0] || ''
if (/^(?:[A-Za-z]\.)+[A-Za-z]$/.test(token)) return true
return /^[A-Z]$/.test(token) && /[A-Za-z]/.test(nextNonSpace)
}
const splitSentenceLine = (text: string) => {
const sentences: string[] = []
let start = 0
for (let index = 0; index < text.length; index += 1) {
const character = text[index]
const isStop = /[。!?!?]/.test(character) || character === '.'
if (!isStop || (character === '.' && shouldKeepEnglishPeriod(text, index))) continue
let end = index + 1
while (end < text.length && /[。!?.!?]/.test(text[end])) end += 1
while (end < text.length && /[”’"')】\]]/.test(text[end])) end += 1
const sentence = text.slice(start, end).trim()
if (sentence) sentences.push(sentence)
start = end
index = end - 1
}
const tail = text.slice(start).trim()
if (tail) sentences.push(tail)
return sentences
}
const splitContentLines = (value: unknown) => {
const normalized = String(value || '')
.replace(/\r\n/g, '\n')
@@ -32,15 +68,9 @@ const splitContentLines = (value: unknown) => {
.map((line) => line.replace(/\s+/g, ' ').trim())
.filter(Boolean)
if (manualLines.length > 1) return manualLines
const sentenceLines = manualLines.flatMap((line) => splitSentenceLine(line))
const sentenceLines = normalized
.replace(/\s+/g, ' ')
.match(/[^。!?.!?]+[。!?.!?]?/g)
?.map((line) => line.trim())
.filter(Boolean)
return sentenceLines?.length ? sentenceLines : [normalized]
return sentenceLines.length ? sentenceLines : [normalized]
}
```
@@ -61,23 +91,22 @@ const bilingualBlocks = computed(() => {
const hasBilingualContent = computed(() => originalLines.value.length > 0 && translatedLines.value.length > 0)
```
- [ ] **Step 3: Make expansion evaluate the complete bilingual body**
- [ ] **Step 3: Collapse only at complete pair boundaries**
Use the combined bilingual text length and group count when bilingual content exists; keep the existing summary rule for ordinary posts.
Treat multiple bilingual pairs as expandable. When collapsed, render only the first complete pair instead of clipping the container by height; keep the existing summary rule for ordinary posts.
```ts
const previewCanExpand = computed(() => {
if (hasBilingualContent.value) {
const contentLength = bilingualBlocks.value.reduce(
(total, block) => total + block.original.length + block.translated.length,
0
)
return contentLength > 180 || bilingualBlocks.value.length > 3
}
if (hasBilingualContent.value) return bilingualBlocks.value.length > 1
const summary = postPreview.value.summary
return summary.length > 90 || summary.split('\n').length > 3
})
const visibleBilingualBlocks = computed(() => {
if (!previewCanExpand.value || previewExpanded.value) return bilingualBlocks.value
return bilingualBlocks.value.slice(0, 1)
})
```
### Task 2: Render one original sentence followed by one translation
@@ -90,9 +119,8 @@ const previewCanExpand = computed(() => {
Place this branch before the ordinary title/summary content. The branch replaces both the artificial title and the former standalone translation card.
```vue
<view v-if="hasBilingualContent" class="detail-post-card__bilingual"
:class="{ 'detail-post-card__bilingual--collapsed': previewCanExpand && !previewExpanded }">
<view v-for="(block, index) in bilingualBlocks" :key="`bilingual-${index}`"
<view v-if="hasBilingualContent" class="detail-post-card__bilingual">
<view v-for="(block, index) in visibleBilingualBlocks" :key="`bilingual-${index}`"
class="detail-post-card__bilingual-pair">
<text v-if="block.original" class="detail-post-card__bilingual-original">{{ block.original }}</text>
<text v-if="block.translated" class="detail-post-card__bilingual-translated">{{ block.translated }}</text>
@@ -139,11 +167,6 @@ Use a single-column, mobile-first hierarchy. Keep the original dominant and the
display: flex;
flex-direction: column;
gap: 24rpx;
&--collapsed {
max-height: 430rpx;
overflow: hidden;
}
}
&__bilingual-pair {
@@ -178,7 +201,32 @@ Use a single-column, mobile-first hierarchy. Keep the original dominant and the
Delete the old `&__translation`, `&__translation-label`, and `&__translation-text` rules so the stylesheet matches the template.
### Task 4: Review and commit the source change
### Task 4: Restore translation after a paid post is unlocked
**Files:**
- Modify: `uniapp/src/packages_community/pages/post_detail.vue`
- [ ] **Step 1: Trigger the existing translation flow after unlocking**
After applying the purchased content, start the existing non-blocking translation request only for a Trump post that has no translation and has not already started translation. Keep `loadUserPoints()` unchanged.
```ts
const applyUnlockedContent = async (result: any) => {
if (!post.value || !result?.content) return
post.value.content = result.content
post.value.is_locked = false
post.value.is_purchased = true
if (!result.from_cache) post.value.paid_count = Number(post.value.paid_count || 0) + 1
previewExpanded.value = true
if (isTrumpPost.value && !translatedText.value && !autoTranslateStarted.value) {
autoTranslateStarted.value = true
void requestTranslate()
}
await loadUserPoints()
}
```
### Task 5: Review and commit the source change
**Files:**
- Review: `uniapp/src/packages_community/pages/post_detail.vue`
@@ -212,7 +260,8 @@ Confirm from the diff that:
- bilingual mode requires both original content and translated content;
- unmatched trailing sentences remain rendered;
- no API or paid-content logic changed;
- no API contract, payment confirmation, or points calculation changed;
- the only unlock-path change is the guarded non-blocking translation trigger;
- normal posts retain the old title/summary branch;
- old translation-card selectors have no remaining references.