feat: align bilingual post phrases

This commit is contained in:
hajimi
2026-08-02 02:56:49 +08:00
parent 163df0cf85
commit 65889b88eb
2 changed files with 286 additions and 20 deletions
@@ -0,0 +1,214 @@
# 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`.