Files
sbnews/docs/superpowers/plans/2026-08-02-community-post-bilingual-pairing.md
T

10 KiB
Raw Blame History

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.