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

215 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
```ts
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.
```ts
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.
```ts
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.
```ts
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:
```markdown
- 特朗普等已有译文的帖子详情按逗号、分号、句号、感叹号和问号切成短语,采用“一段原文紧跟一段译文”显示;两侧数量不一致时自动合并较多一侧的相邻短片段,避免空侧和内容丢失,同时保护英文缩写、小数及数字千分位。
```
In `docs/设计稿映射.md`, replace the community post detail row with:
```markdown
| 社区帖子详情 | `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:
```powershell
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:
```powershell
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:
```powershell
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`.