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

9.2 KiB
Raw Blame History

Community Post Bilingual Phrase 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: Split community post originals and translations at comma, semicolon, period, exclamation, and question punctuation, then render exact one-to-one groups by merging adjacent short fragments when counts differ.

Architecture: Keep the implementation local to post_detail.vue. Replace sentence-only splitting with punctuation phrase splitting, protect numeric and English abbreviation punctuation, normalize both sides to the same count with a deterministic shortest-fragment merge, and reuse the existing bilingual rendering computed property.

Tech Stack: uni-app, Vue 3 <script setup>, TypeScript


Task 1: Split text into protected punctuation phrases

Files:

  • Modify: uniapp/src/packages_community/pages/post_detail.vue:350-402

  • Step 1: Add delimiter protection and phrase splitting

Replace splitSentenceLine with the following helpers. The delimiter set includes Chinese and English comma, semicolon, period, exclamation, and question punctuation. shouldKeepEnglishPeriod remains unchanged.

const isPhraseDelimiter = (character: string) =>
    /[,;。!?!?]/.test(character) || character === '.'

const shouldKeepPhraseDelimiter = (text: string, index: number, character: string) => {
    if (character === '.' && shouldKeepEnglishPeriod(text, index)) return true

    const previous = text[index - 1] || ''
    const next = text[index + 1] || ''
    return /[,]/.test(character) && /\d/.test(previous) && /\d/.test(next)
}

const splitPhraseLine = (text: string) => {
    const phrases: string[] = []
    let start = 0

    for (let index = 0; index < text.length; index += 1) {
        const character = text[index]
        if (!isPhraseDelimiter(character) || shouldKeepPhraseDelimiter(text, index, character)) continue

        let end = index + 1
        while (end < text.length && isPhraseDelimiter(text[end])) end += 1
        while (end < text.length && /[”’"')】\]]/.test(text[end])) end += 1

        const phrase = text.slice(start, end).trim()
        if (phrase) phrases.push(phrase)
        start = end
        index = end - 1
    }

    const tail = text.slice(start).trim()
    if (tail) phrases.push(tail)
    return phrases
}
  • Step 2: Rename the content splitter and preserve manual-line order

Replace splitContentLines with this function and update the original/translation computed properties to use it.

const splitContentSegments = (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 segments = manualLines.flatMap((line) => splitPhraseLine(line))
    return segments.length ? segments : [normalized]
}

const originalSegments = computed(() => splitContentSegments(post.value?.content))
const translatedSegments = computed(() => splitContentSegments(translatedText.value))

Task 2: Normalize both sides to an exact one-to-one count

Files:

  • Modify: uniapp/src/packages_community/pages/post_detail.vue:402-424

  • Step 1: Add deterministic adjacent-fragment merging

Add helpers that identify the shortest body fragment, preserve punctuation, restore spacing between English fragments, and prefer forward merging for comma/semicolon clauses.

const getSegmentBodyLength = (segment: string) =>
    segment.replace(/[,;。!?.!?]/g, '').replace(/\s+/g, '').length

const joinSegments = (left: string, right: string) => {
    const normalizedLeft = left.trim()
    const normalizedRight = right.trim()
    const leftBodyEnd = normalizedLeft
        .replace(/[”’"')】\]]+$/g, '')
        .replace(/[,;。!?.!?]+$/g, '')
    const needsSpace = /[A-Za-z0-9]$/.test(leftBodyEnd) && /^[A-Za-z0-9]/.test(normalizedRight)
    return `${normalizedLeft}${needsSpace ? ' ' : ''}${normalizedRight}`
}

const mergeSegmentsToCount = (segments: string[], targetCount: number) => {
    const merged = segments.filter(Boolean).slice()
    if (targetCount <= 0) return []

    while (merged.length > targetCount) {
        let shortestIndex = 0
        for (let index = 1; index < merged.length; index += 1) {
            if (getSegmentBodyLength(merged[index]) < getSegmentBodyLength(merged[shortestIndex])) {
                shortestIndex = index
            }
        }

        const ending = merged[shortestIndex].replace(/[”’"')】\]]+$/g, '').slice(-1)
        const mergeForward = shortestIndex === 0
            || (shortestIndex < merged.length - 1 && /[,;]/.test(ending))

        if (mergeForward) {
            merged.splice(shortestIndex, 2, joinSegments(merged[shortestIndex], merged[shortestIndex + 1]))
        } else {
            merged.splice(shortestIndex - 1, 2, joinSegments(merged[shortestIndex - 1], merged[shortestIndex]))
        }
    }

    return merged
}
  • Step 2: Replace max-length pairing with equal-count pairing

Replace bilingualBlocks and update hasBilingualContent. The shorter side defines the final number of groups; only the longer side is merged down, so every rendered group contains both values.

const bilingualBlocks = computed(() => {
    const pairCount = Math.min(originalSegments.value.length, translatedSegments.value.length)
    if (!pairCount) return []

    const alignedOriginal = mergeSegmentsToCount(originalSegments.value, pairCount)
    const alignedTranslation = mergeSegmentsToCount(translatedSegments.value, pairCount)
    return Array.from({ length: pairCount }, (_, index) => ({
        original: alignedOriginal[index],
        translated: alignedTranslation[index]
    }))
})
const hasBilingualContent = computed(() => bilingualBlocks.value.length > 0)

Keep previewCanExpand and visibleBilingualBlocks unchanged because they already operate on complete bilingual groups.

Task 3: Synchronize project records and review the change

Files:

  • Modify: docs/业务进度管理.md

  • Modify: docs/设计稿映射.md

  • Review: uniapp/src/packages_community/pages/post_detail.vue

  • Step 1: Update the existing community post detail records

In docs/业务进度管理.md, replace the existing bilingual bullet with:

- 特朗普等已有译文的帖子详情按逗号、分号、句号、感叹号和问号切成短语,采用“一段原文紧跟一段译文”显示;两侧数量不一致时自动合并较多一侧的相邻短片段,避免空侧和内容丢失,同时保护英文缩写、小数及数字千分位。

In docs/设计稿映射.md, replace the community post detail row with:

| 社区帖子详情 | `uniapp/src/packages_community/pages/post_detail.vue` | 🚧 部分完成 | 2026-08-02 已按付费帖子详情设计稿重构作者内容卡、免费摘要、积分解锁卡、互动评论区和底部输入栏;已有译文的帖子按标点短语进行原文/译文一对一交错显示;430px H5 页面已复核,App/小程序与正式构建待验收 |

Preserve all unrelated pending edits in both shared documents.

  • Step 2: Inspect only the intended source diff

Run:

git diff -- uniapp/src/packages_community/pages/post_detail.vue
git diff --check -- uniapp/src/packages_community/pages/post_detail.vue docs/superpowers/plans/2026-08-02-community-post-bilingual-phrase-pairing.md

Expected: the source diff only renames the splitter, adds delimiter protection and merge helpers, and updates the computed pairing model. git diff --check exits successfully.

  • Step 3: Check required references and removed names

Run:

rg -n "isPhraseDelimiter|shouldKeepPhraseDelimiter|splitPhraseLine|mergeSegmentsToCount|originalSegments|translatedSegments" uniapp/src/packages_community/pages/post_detail.vue
rg -n "splitSentenceLine|splitContentLines|originalLines|translatedLines" uniapp/src/packages_community/pages/post_detail.vue
if ($LASTEXITCODE -eq 0) { throw 'Obsolete sentence-level names remain' }
if ($LASTEXITCODE -gt 1) { throw 'Obsolete-name scan failed' }

Expected: all new helpers are referenced and the obsolete sentence-level names return no matches.

  • Step 4: Commit only the implementation files

Run:

git add -- docs/superpowers/plans/2026-08-02-community-post-bilingual-phrase-pairing.md uniapp/src/packages_community/pages/post_detail.vue
git commit -m "feat: align bilingual post phrases"

Expected: the commit contains the implementation plan and post_detail.vue. Existing changes in AGENTS.md, server/admin/match files, docs/业务进度管理.md, and docs/设计稿映射.md remain outside the commit.

Verification boundary

The repository AGENTS.md prohibits automatically running builds or tests unless the user explicitly requests them. This plan therefore performs scoped static diff checks only. Suggested optional verification after separate authorization is npm run build:h5 followed by browser inspection of community post 23361.