10 KiB
Community Post Bilingual Pairing Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Display translated community posts as vertically interleaved original/translation sentence pairs instead of separate long text blocks.
Architecture: Keep the change local to the community post detail page. Reuse the news detail page's newline-first, sentence-second splitting rule, derive a bilingual view model with a computed property, and render it through a dedicated template branch without changing API contracts.
Tech Stack: uni-app, Vue 3 <script setup>, TypeScript, SCSS
Task 1: Add the bilingual sentence view model
Files:
-
Modify:
uniapp/src/packages_community/pages/post_detail.vue -
Step 1: Upgrade the local text splitter
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.
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')
.replace(/\r/g, '\n')
.trim()
if (!normalized) return []
const manualLines = normalized
.split('\n')
.map((line) => line.replace(/\s+/g, ' ').trim())
.filter(Boolean)
const sentenceLines = manualLines.flatMap((line) => splitSentenceLine(line))
return sentenceLines.length ? sentenceLines : [normalized]
}
- Step 2: Derive paired original and translated blocks
Add computed original lines and a computed array whose length is the maximum of both sides. Filter only fully empty rows so an unmatched trailing sentence remains visible.
const originalLines = computed(() => splitContentLines(post.value?.content))
const translatedLines = computed(() => splitContentLines(translatedText.value))
const bilingualBlocks = computed(() => {
const size = Math.max(originalLines.value.length, translatedLines.value.length)
return Array.from({ length: size }, (_, index) => ({
original: originalLines.value[index] || '',
translated: translatedLines.value[index] || ''
})).filter((block) => block.original || block.translated)
})
const hasBilingualContent = computed(() => originalLines.value.length > 0 && translatedLines.value.length > 0)
- Step 3: Collapse only at complete pair boundaries
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.
const previewCanExpand = computed(() => {
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
Files:
-
Modify:
uniapp/src/packages_community/pages/post_detail.vue -
Step 1: Add the bilingual template branch
Place this branch before the ordinary title/summary content. The branch replaces both the artificial title and the former standalone translation card.
<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>
</view>
</view>
<template v-else>
<text v-if="postPreview.title" class="detail-post-card__title">{{ postPreview.title }}</text>
<view v-if="postPreview.summary" class="detail-post-card__summary-wrap">
<text class="detail-post-card__summary"
:class="{ 'detail-post-card__summary--collapsed': previewCanExpand && !previewExpanded }">
{{ postPreview.summary }}
</text>
<text v-if="previewCanExpand" class="detail-post-card__expand"
@tap="previewExpanded = !previewExpanded">
{{ previewExpanded ? '收起' : '展开' }}
</text>
</view>
</template>
<text v-if="hasBilingualContent && previewCanExpand" class="detail-post-card__expand detail-post-card__expand--bilingual"
@tap="previewExpanded = !previewExpanded">
{{ previewExpanded ? '收起' : '展开' }}
</text>
- Step 2: Remove the obsolete standalone translation block
Delete the old detail-post-card__translation, detail-post-card__translation-label, and detail-post-card__translation-text template markup because translation is now rendered inside each bilingual pair.
Task 3: Style the bilingual reading hierarchy
Files:
-
Modify:
uniapp/src/packages_community/pages/post_detail.vue -
Step 1: Add bilingual layout styles
Use a single-column, mobile-first hierarchy. Keep the original dominant and the translation visually subordinate without wrapping every pair in a heavy card.
&__bilingual {
margin-top: 20rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
&__bilingual-pair {
display: flex;
flex-direction: column;
gap: 8rpx;
}
&__bilingual-original {
color: #20242b;
font-size: 29rpx;
font-weight: 500;
line-height: 1.72;
word-break: break-word;
}
&__bilingual-translated {
color: #647187;
font-size: 27rpx;
line-height: 1.72;
word-break: break-word;
}
&__expand--bilingual {
display: block;
margin-top: 12rpx;
text-align: right;
}
- Step 2: Remove unused translation-card styles
Delete the old &__translation, &__translation-label, and &__translation-text rules so the stylesheet matches the template.
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.
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 -
Step 1: Inspect the scoped diff
Run:
git diff -- uniapp/src/packages_community/pages/post_detail.vue
Expected: only the bilingual template, computed pairing logic, expansion rule, and related styles change.
- Step 2: Check whitespace and conflict markers
Run:
git diff --check -- uniapp/src/packages_community/pages/post_detail.vue
rg -n "<<<<<<<|=======|>>>>>>>" uniapp/src/packages_community/pages/post_detail.vue
if ($LASTEXITCODE -eq 0) { throw 'Conflict markers found' }
if ($LASTEXITCODE -gt 1) { throw 'Conflict marker scan failed' }
Expected: git diff --check exits successfully and rg returns no matches.
- Step 3: Review logic against edge cases
Confirm from the diff that:
-
bilingual mode requires both original content and translated content;
-
unmatched trailing sentences remain rendered;
-
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.
-
Step 4: Commit only the intended files
Run:
git add -- docs/superpowers/plans/2026-08-02-community-post-bilingual-pairing.md uniapp/src/packages_community/pages/post_detail.vue
git commit -m "feat: pair community post translations"
Expected: one commit containing the plan and the uni-app page source. Do not include existing .playwright-mcp or modified shared docs.
Verification boundary
The repository AGENTS.md prohibits automatically running builds or tests unless the user explicitly requests them. This plan therefore stops at scoped static diff checks and code review; suggested optional verification is npm run build:h5 plus browser inspection of post 23361 after separate authorization.