From 5436dc9ae1a9651b66fbd9fea24db8d2b8405f9e Mon Sep 17 00:00:00 2001 From: esengine <359807859@qq.com> Date: Wed, 14 Oct 2026 02:27:10 -0700 Subject: [PATCH] feat(studio): switch a release's notes between Chinese and English --- desktop/frontend-next/src/actions.ts | 1 + desktop/frontend-next/src/i18n/en.ts | 1 + desktop/frontend-next/src/styles/versions.css | 1 + .../ui/Versions.notes.interaction.test.tsx | 25 +++++++ desktop/frontend-next/src/ui/Versions.tsx | 22 ++++++- .../frontend-next/src/ui/notesLanguages.ts | 21 ++++++ docs/STUDIO_RELEASE.md | 5 +- release-notes/studio/2.39.0.md | 65 +++++++++++++++++++ release-notes/studio/unreleased/13081.md | 2 + scripts/studio-release-notes.mjs | 15 ++++- scripts/studio-release-notes.test.mjs | 27 ++++++++ tools/repolint/docs.go | 43 ++++++++---- tools/repolint/docs_test.go | 23 +++++++ 13 files changed, 233 insertions(+), 18 deletions(-) create mode 100644 desktop/frontend-next/src/ui/notesLanguages.ts create mode 100644 release-notes/studio/unreleased/13081.md diff --git a/desktop/frontend-next/src/actions.ts b/desktop/frontend-next/src/actions.ts index 0af44c800d..781fff9536 100644 --- a/desktop/frontend-next/src/actions.ts +++ b/desktop/frontend-next/src/actions.ts @@ -456,6 +456,7 @@ export const ACTIONS: UIAction[] = [ { id: "versions.notes", kind: "repeatable", target: "none", proof: "interaction" }, { id: "versions.notes-retry", kind: "repeatable", target: "none", proof: "interaction" }, { id: "versions.notes-close", kind: "view", target: "none", proof: "interaction" }, + { id: "versions.notes-lang", kind: "view", target: "none", proof: "interaction" }, { id: "shell.executor", kind: "kernel-mutation", target: "none", proof: "authority-effect" }, { id: "shell.custom-path", kind: "kernel-mutation", target: "none", proof: "authority-effect" }, { id: "hooks.recipe", kind: "kernel-mutation", target: "entity", proof: "authority-effect" }, diff --git a/desktop/frontend-next/src/i18n/en.ts b/desktop/frontend-next/src/i18n/en.ts index c4fcc0ba79..0c20bc1c36 100644 --- a/desktop/frontend-next/src/i18n/en.ts +++ b/desktop/frontend-next/src/i18n/en.ts @@ -839,6 +839,7 @@ export const EN: Record = { "更新内容": "What's new", "收起更新内容": "Hide what's new", "{v} 的更新内容": "What's new in {v}", + "更新内容的语言": "Language of the notes", "正在读取更新内容…": "Loading what's new…", "更新内容读取失败": "What's new could not be read", "在 GitHub 查看": "View on GitHub", diff --git a/desktop/frontend-next/src/styles/versions.css b/desktop/frontend-next/src/styles/versions.css index c9c4f5117c..05279e5a59 100644 --- a/desktop/frontend-next/src/styles/versions.css +++ b/desktop/frontend-next/src/styles/versions.css @@ -2,6 +2,7 @@ .vrow .vn { grid-column: 2; grid-row: 1; justify-self: end; text-decoration: none } .vnotes { grid-column: 1 / -1; margin: 0 0 8px; padding: 10px 12px; border-left: 2px solid var(--hair); font-size: var(--fs-ui); overflow-wrap: anywhere } .vnotes .md-noimg { color: var(--muted) } +.vnotes-lang { display: flex; margin-bottom: 8px } @container versions (max-width: 600px) { .vrow .vn { grid-column: 1 / -1; grid-row: auto; justify-self: start; margin-top: 4px } @media (hover: hover) { diff --git a/desktop/frontend-next/src/ui/Versions.notes.interaction.test.tsx b/desktop/frontend-next/src/ui/Versions.notes.interaction.test.tsx index 53130d3a9e..01f61d2ab7 100644 --- a/desktop/frontend-next/src/ui/Versions.notes.interaction.test.tsx +++ b/desktop/frontend-next/src/ui/Versions.notes.interaction.test.tsx @@ -178,3 +178,28 @@ describe("version notes on the version panel", () => { expect(screen.getByRole("button", { name: "固定在这里" })).toBeTruthy(); }); }); + +describe("notes that carry two languages", () => { + const both = "## 新增\n\n- 中文条目\n\n---\n\n\n\n## New\n\n- English item\n"; + + it("show the window's language first and switch to the other on request", async () => { + render(); + await screen.findByText("2.19.0"); + await userEvent.click(screen.getAllByRole("button", { name: "更新内容" })[0]); + expect(await screen.findByText("中文条目")).toBeTruthy(); + expect(screen.queryByText("English item")).toBeNull(); + expect(screen.queryByText(/lang:en/)).toBeNull(); + await userEvent.click(screen.getByRole("button", { name: "English" })); + expect(await screen.findByText("English item")).toBeTruthy(); + expect(screen.queryByText("中文条目")).toBeNull(); + expect(screen.getByRole("button", { name: "English" }).getAttribute("aria-pressed")).toBe("true"); + }); + + it("show no switch for notes in one language", async () => { + render(); + await screen.findByText("2.19.0"); + await userEvent.click(screen.getAllByRole("button", { name: "更新内容" })[0]); + expect(await screen.findByText("只有中文")).toBeTruthy(); + expect(screen.queryByRole("button", { name: "English" })).toBeNull(); + }); +}); diff --git a/desktop/frontend-next/src/ui/Versions.tsx b/desktop/frontend-next/src/ui/Versions.tsx index 3785e465f8..96ba38e2b1 100644 --- a/desktop/frontend-next/src/ui/Versions.tsx +++ b/desktop/frontend-next/src/ui/Versions.tsx @@ -1,10 +1,11 @@ import { Fragment, useCallback, useEffect, useState, type KeyboardEvent } from "react"; import { bytes } from "../i18n/format"; -import { t } from "../i18n"; +import { current, t } from "../i18n"; import type { UpdateProgress, VersionHub, VersionNotes } from "../port/port"; import { reason } from "../i18n/kernel"; import { HttpError } from "../port/http_error"; import { LazyMarkdown } from "./LazyMarkdown"; +import { NOTES_LANGUAGE_NAMES, splitNotes } from "./notesLanguages"; import { deltaSkippedCopy, failureCopy, releasePage } from "./versionFailure"; // A shell that declared no install: a build run from source, not a failure. @@ -404,12 +405,29 @@ export function Versions({ port }: { port: Port }) { ); } +// The notes in the language the window draws in, with a switch to the other one +// when the document carries both. +function NotesBody({ markdown }: { markdown: string }) { + const { first, english } = splitNotes(markdown); + const [shown, setShown] = useState<"first" | "english">(english !== null && current() === "en" ? "english" : "first"); + if (english === null) return ; + return ( + <> +
+ + +
+ + + ); +} + function NotesPanel({ id, version, state, retry, onKeyDown }: { id: string; version: string; state: NotesState | undefined; retry: () => void; onKeyDown: (e: KeyboardEvent) => void }) { const loading = !state || state.phase === "loading"; return (
{loading &&

{t("正在读取更新内容…")}

} - {state?.phase === "ok" && } + {state?.phase === "ok" && } {state?.phase === "err" && (
{t("更新内容读取失败")} diff --git a/desktop/frontend-next/src/ui/notesLanguages.ts b/desktop/frontend-next/src/ui/notesLanguages.ts new file mode 100644 index 0000000000..9559c5523a --- /dev/null +++ b/desktop/frontend-next/src/ui/notesLanguages.ts @@ -0,0 +1,21 @@ +// A release's notes may carry a second language in the same document, after +// this line. The marker is invisible on the GitHub release page, where both +// languages read one after the other; the Versions panel shows one at a time. +export const NOTES_ENGLISH_MARK = ""; + +export interface NotesLanguages { + first: string; + english: string | null; +} + +const rule = /\n+---\s*$/; + +export function splitNotes(markdown: string): NotesLanguages { + const at = markdown.indexOf(NOTES_ENGLISH_MARK); + if (at < 0) return { first: markdown, english: null }; + const english = markdown.slice(at + NOTES_ENGLISH_MARK.length).trim(); + return { first: markdown.slice(0, at).replace(rule, "").trimEnd(), english: english || null }; +} + +// Language names are written in their own language and are not translated. +export const NOTES_LANGUAGE_NAMES = { first: "中文", english: "English" } as const; diff --git a/docs/STUDIO_RELEASE.md b/docs/STUDIO_RELEASE.md index 041224150e..24cd3f66f0 100644 --- a/docs/STUDIO_RELEASE.md +++ b/docs/STUDIO_RELEASE.md @@ -135,7 +135,7 @@ Verify a published package with `npm view reasonix dist.attestations`: the attes ## 6. Release note format -File: `release-notes/studio/X.Y.Z.md`, without the tag's `v`. Language: Chinese. Enforced by `release-note` and `doc-prose`. +File: `release-notes/studio/X.Y.Z.md`, without the tag's `v`. Language: Chinese, then English (R6). Enforced by `release-note` and `doc-prose`. | ID | Rule | | --- | --- | @@ -144,6 +144,7 @@ File: `release-notes/studio/X.Y.Z.md`, without the tag's `v`. Language: Chinese. | R3 | One change is one list item within 200 display columns. | | R4 | Every item names its issue (`#123`), pull request or commit. | | R5 | Describe what the user observes. The explanation belongs in the linked commit. | +| R6 | The same notes follow in English after a `---` line and the `` marker, under `## New`, `## Changed`, `## Fixed`, `## Removed`, `## Upgrade notes`, with the same items and references; R1 to R5 apply to each language. | The published body is rendered, not copied: @@ -156,7 +157,7 @@ The published body is rendered, not copied: The repository owner and any login in `RELEASE_CREDIT_EXCLUDE` (comma separated) are treated like bots: never named in an item or in the list. -A `## 贡献者` list of the credited authors closes the notes. A `#N` counts only at a line start or after whitespace, `(`, `(`, `、`, `,` or `,`, and never in code, an HTML comment or a link target. Write colours such as `#333` in a code span. +A `## 贡献者` list of the credited authors closes the Chinese part, and a `## Contributors` list closes the English part. A `#N` counts only at a line start or after whitespace, `(`, `(`, `、`, `,` or `,`, and never in code, an HTML comment or a link target. Write colours such as `#333` in a code span. A lookup GitHub refuses, or cannot answer after retries, fails `publish` with a `release_credits.*` code. Preview with `node scripts/studio-release-notes.mjs release-notes/studio/X.Y.Z.md /tmp/notes.md`; it reads `GH_TOKEN`, else `gh auth token`. diff --git a/release-notes/studio/2.39.0.md b/release-notes/studio/2.39.0.md index e6845fdb79..2543b28c69 100644 --- a/release-notes/studio/2.39.0.md +++ b/release-notes/studio/2.39.0.md @@ -58,3 +58,68 @@ - 子代理状态帧在客户端落后时不再丢失 (#12888) - 文件预览会先检查目标是否允许访问,并先经过 PreToolUse 钩子 (#12115) - 记忆保存失败时会撤回,迁移过的记忆再次修改时会修复索引 (#12938, #12939, #12940) + +--- + + + +This release adds double-click rename for sessions, starting without any model service, a low-disk-space warning and maps and dashboards in extension surfaces, and tidies the sidebar and conversation display. + +## New + +- Double-click a session row in the sidebar to rename it in place; the rename dialog can generate a title on request (#12977, #11591) +- Reasonix starts without any model service configured and opens the connection guide directly (#12966) +- A warning appears when the disk holding a project or session is nearly full; on by default, it only warns and never deletes files (#12960) +- The terminal UI reports working, waiting-for-you and finished states to the terminal (#12976) +- A finished session can stay in the Running list for a chosen time; off by default (#12957) +- The sidebar marks sessions stored by 1.x, and the terminal resume list marks sessions you have not viewed (#12405, #12587) +- A skill invoked with / shows as a compact skill block, and the JSON input of tool calls is syntax-coloured (#12972, #12969) +- Files can be staged and unstaged from the changes list (#12923) +- The application icon shows the count of unread finished tasks, and a new flat icon replaces the old one (#12922, #12897) +- New "Clear blue" and "Tea green" colour schemes; theme packs can declare density and corner radius (#12932, #12513) +- The icon rail has a New session button; clicking the folder mark in the title bar opens the project folder (#12891, #12857, #12288) +- File links in a conversation open in the workbench, and the file menu can add a file or folder to the conversation (#12910, #12877) +- Remote machines are gathered into one collapsible row, and the levels of the session tree are easier to tell apart (#12878, #12905) +- Esc must be pressed a second time to stop a running turn (#12859) +- Extension surfaces support style fields, editable drafts, map-versus-dashboard views and non-blocking actions (#12830, #12854, #12876, #12896) +- Installed packages can be searched by capability, show their contribution types, copy their invocation, and the install preview shows the resolved commit (#12065, #12117, #12070, #12075, #12098) +- You can view a sub-agent's skill declarations and search an MCP server's tool list (#12102, #12085) +- WSL distributions are discovered and one of them is probed (#12595) +- The phone page can be added to the home screen; paired phones survive a restart, with a switch in settings (#12591, #12440) +- Linux arm64 .deb and .rpm packages are published (#12914) +- Local stdio MCP servers receive the working directory and the host's read-only state (#12967) +- The tool catalogue refreshes automatically when an MCP server adds or removes tools at runtime and sends a notification (#12138) +- The host's check of changed turns can be turned off (#12860) +- Right-to-left text blocks are displayed in their own direction (#12118) +- The workbench browser-tab menu gains "Close browser tabs to the right", reachable by keyboard (#12557) + +## Changed + +- The Versions page keeps only the latest few earlier versions and folds the rest behind a button (#12982) +- Session lists use single-line ellipsis everywhere, and bulk selection moved to the "This machine" more menu (#12935) +- The desktop sidebar uses divider bars for its actions, and the brand mark is the official wordmark throughout (#12918, #12936) +- Computer use now runs on the CUA Driver (#12937) +- The "Execution" block in a conversation is a faint single line, and its header sticks to the top when expanded (#12954, #12944) +- The session title in the title bar sits next to the project name, and the branch button's hover shape matches its neighbours (#12955, #12971) +- The model list keeps only the settings page's own scrollbar, and the display-alias input follows the theme (#12979) +- Installed skills are grouped by source (#12329) + +## Fixed + +- Folded user messages no longer vanish entirely when compaction cannot get a summary (#12963) +- The context-usage figure now uses the same number the compaction trigger uses, instead of reading lower than the provider reports (#12991, #12988) +- On Windows, clicking a task-completed notification brings the window to the front, and the click path is logged (#12907, #12956) +- Closing a browser tab no longer reads content the kernel never sends (#12886) +- A question you have started answering no longer times out and is skipped (#12884) +- A turn in progress is kept when the runtime is rebuilt or a turn-end event is lost (#11735) +- Saving settings now reports success, not applied, and failure separately, and a failure can be retried (#12350) +- The undo entry is restored from the backend, and an expired-undo state is kept (#11589) +- Unsaved file drafts in the workbench are kept (#12898) +- Context usage refreshes immediately after switching model settings (#12871) +- Cost is shown in the wallet's currency, and the folder picker can list hidden folders (#12874) +- On Windows, when the Git Bash probe hangs it is retried once, and the fallback shell path is shown correctly (#12887) +- When only /skill-name is sent, history records exactly what you typed (#12890) +- Duplicate MCP tool names are refused, and the original identifier stays the same on every path (#12889, #12893, #12892) +- Sub-agent status frames are no longer lost when the client falls behind (#12888) +- File preview checks that the target may be accessed, and passes through the PreToolUse hook first (#12115) +- A failed memory save is rolled back, and a migrated memory repairs its index when changed again (#12938, #12939, #12940) diff --git a/release-notes/studio/unreleased/13081.md b/release-notes/studio/unreleased/13081.md new file mode 100644 index 0000000000..5e5cf5416c --- /dev/null +++ b/release-notes/studio/unreleased/13081.md @@ -0,0 +1,2 @@ +- 版本页的更新内容可以在中文和英文之间切换,默认跟随界面语言;之后每个版本的发布说明都附带英文。关联 #13081。 +- The Versions page can switch a release's notes between Chinese and English, following the interface language by default; every release's notes now carry English. Refs #13081. diff --git a/scripts/studio-release-notes.mjs b/scripts/studio-release-notes.mjs index 3c20f7685f..3da2d8c296 100644 --- a/scripts/studio-release-notes.mjs +++ b/scripts/studio-release-notes.mjs @@ -17,11 +17,20 @@ import { unresolvedError, } from "./release-credits.mjs"; +// A second language rides in the same document after this line; the Versions +// panel and the GitHub page both read it (desktop/frontend-next notesLanguages.ts). +const ENGLISH_MARK = ""; + export function renderStudioNotes(markdown, credits) { const body = annotateMarkdown(markdown, (ref) => creditSuffix(credits.get(ref))).trimEnd(); - const contributors = contributorLogins(markdownRefs(markdown), credits); - if (!contributors.length) return `${body}\n`; - return `${body}\n\n## 贡献者\n\n感谢本版本的贡献者:${contributors.map((login) => `@${login}`).join("、")}\n`; + const logins = contributorLogins(markdownRefs(markdown), credits).map((login) => `@${login}`); + if (!logins.length) return `${body}\n`; + const chinese = `## 贡献者\n\n感谢本版本的贡献者:${logins.join("、")}`; + const at = body.indexOf(ENGLISH_MARK); + if (at < 0) return `${body}\n\n${chinese}\n`; + const first = body.slice(0, at).replace(/\n+---\s*$/, "").trimEnd(); + const english = `## Contributors\n\nThanks to this release's contributors: ${logins.join(", ")}`; + return `${first}\n\n${chinese}\n\n---\n\n${body.slice(at)}\n\n${english}\n`; } async function main() { diff --git a/scripts/studio-release-notes.test.mjs b/scripts/studio-release-notes.test.mjs index 6eabf95e87..21b8625a70 100644 --- a/scripts/studio-release-notes.test.mjs +++ b/scripts/studio-release-notes.test.mjs @@ -88,3 +88,30 @@ test("an item whose only author is excluded renders plain and lists no contribut ]); assert.equal(renderStudioNotes("摘要。\n\n## 修复\n\n- 一 #1\n- 二 #2\n", credits), "摘要。\n\n## 修复\n\n- 一 #1\n- 二 #2 fixed in #9\n"); }); + +test("notes with an English part credit contributors in each language and keep the marker between them", async () => { + const both = `本版修复一件事。 + +## 修复 + +- 会话不再丢失(#20) + +--- + + + +This release fixes one thing. + +## Fixed + +- Sessions are no longer lost (#20) +`; + const { credits } = await resolveCredits(markdownRefs(both), async (ref) => authors[ref]); + const rendered = renderStudioNotes(both, credits); + const [first, english] = rendered.split(""); + assert.match(first, /会话不再丢失(#20 by @alice)/); + assert.match(first, /## 贡献者\n\n感谢本版本的贡献者:@alice\n\n---\n\n$/); + assert.match(english, /Sessions are no longer lost \(#20 by @alice\)/); + assert.match(english, /## Contributors\n\nThanks to this release's contributors: @alice\n$/); + assert.equal(rendered.split("## 贡献者").length, 2); +}); diff --git a/tools/repolint/docs.go b/tools/repolint/docs.go index a9373ab567..ec0697e619 100644 --- a/tools/repolint/docs.go +++ b/tools/repolint/docs.go @@ -69,9 +69,11 @@ var translatedDocs = []string{ var ( docStatuses = []string{"active", "deprecated"} releaseNoteGroups = []string{"新增", "变更", "修复", "移除", "升级须知"} - isoDate = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) - listItem = regexp.MustCompile(`^\s*(?:[-*+]|\d+\.)\s+`) - changeReference = regexp.MustCompile(`#\d+|\b[0-9a-f]{7,40}\b|https?://`) + // A second language follows releaseNoteEnglishMark in the same file and is held to the same shape under these headings. + releaseNoteEnglishGroups = []string{"New", "Changed", "Fixed", "Removed", "Upgrade notes"} + isoDate = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) + listItem = regexp.MustCompile(`^\s*(?:[-*+]|\d+\.)\s+`) + changeReference = regexp.MustCompile(`#\d+|\b[0-9a-f]{7,40}\b|https?://`) ) func classifyDoc(rel string) (docClass, bool) { @@ -272,22 +274,41 @@ func blockKind(b proseBlock) string { return "paragraph" } +const releaseNoteEnglishMark = "" + // checkReleaseNote holds a version's notes to the published shape: one summary // line, changes grouped under the fixed headings, one short referenced line per -// change. The reasoning behind a change lives in its commit. +// change. The reasoning behind a change lives in its commit. A file may carry +// an English part after releaseNoteEnglishMark; each language is held to the +// same shape under its own headings. func checkReleaseNote(rel string, lines []string) []Finding { + for i, raw := range lines { + if strings.TrimSpace(raw) != releaseNoteEnglishMark { + continue + } + first := lines[:i] + for len(first) > 0 && slices.Contains([]string{"", "---"}, strings.TrimSpace(first[len(first)-1])) { + first = first[:len(first)-1] + } + out := checkReleaseNoteBody(rel, first, 0, releaseNoteGroups) + return append(out, checkReleaseNoteBody(rel, lines[i+1:], i+1, releaseNoteEnglishGroups)...) + } + return checkReleaseNoteBody(rel, lines, 0, releaseNoteGroups) +} + +func checkReleaseNoteBody(rel string, lines []string, offset int, groups []string) []Finding { var out []Finding paragraphs := 0 for i, raw := range lines { t := strings.TrimSpace(raw) switch { case strings.HasPrefix(t, "## "): - if !slices.Contains(releaseNoteGroups, strings.TrimSpace(t[3:])) { - out = append(out, Finding{rel, i + 1, ruleReleaseNote, - fmt.Sprintf("heading %q is not one of %v", t, releaseNoteGroups), 1}) + if !slices.Contains(groups, strings.TrimSpace(t[3:])) { + out = append(out, Finding{rel, offset + i + 1, ruleReleaseNote, + fmt.Sprintf("heading %q is not one of %v", t, groups), 1}) } case strings.HasPrefix(t, "#"): - out = append(out, Finding{rel, i + 1, ruleReleaseNote, "only ## group headings; a change is one list item, not a section", 1}) + out = append(out, Finding{rel, offset + i + 1, ruleReleaseNote, "only ## group headings; a change is one list item, not a section", 1}) } } for _, b := range proseBlocks(lines) { @@ -295,17 +316,17 @@ func checkReleaseNote(rel string, lines []string) []Finding { case !b.item: paragraphs++ if paragraphs > 1 { - out = append(out, Finding{rel, b.line, ruleReleaseNote, "only the first line may be a summary paragraph; state changes as list items", 1}) + out = append(out, Finding{rel, offset + b.line, ruleReleaseNote, "only the first line may be a summary paragraph; state changes as list items", 1}) } case !changeReference.MatchString(b.text): - out = append(out, Finding{rel, b.line, ruleReleaseNote, "a change names its issue, pull request or commit", 1}) + out = append(out, Finding{rel, offset + b.line, ruleReleaseNote, "a change names its issue, pull request or commit", 1}) } limit := docProseWidth if b.item { limit = releaseNoteItemWidth } if b.width > limit { - out = append(out, Finding{rel, b.line, ruleDocProse, + out = append(out, Finding{rel, offset + b.line, ruleDocProse, fmt.Sprintf("%s is %d columns, over %d", blockKind(b), b.width, limit), (b.width - limit + proseWeightUnit - 1) / proseWeightUnit}) } diff --git a/tools/repolint/docs_test.go b/tools/repolint/docs_test.go index 552e4cafc2..06aafaaa93 100644 --- a/tools/repolint/docs_test.go +++ b/tools/repolint/docs_test.go @@ -196,6 +196,29 @@ func TestAReleaseNoteIsGroupedReferencedAndShort(t *testing.T) { } } +func TestReleaseNoteHoldsEachLanguageToTheSameShape(t *testing.T) { + ok := strings.Join([]string{ + "一句话概括本版。", "", "## 新增", "", "- 做了一件事 (#1)", "", "---", "", "", "", + "One-line summary.", "", "## New", "", "- Did one thing (#1)", + }, "\n") + if got := checkReleaseNote("release-notes/studio/9.9.9.md", strings.Split(ok, "\n")); len(got) != 0 { + t.Fatalf("a well-formed bilingual note has findings: %+v", got) + } + bad := strings.Join([]string{ + "概括。", "", "## 新增", "", "- 做了一件事 (#1)", "", "", "", + "Summary.", "", "Second paragraph.", "", "## Highlights", "", "- No reference", + }, "\n") + got := checkReleaseNote("release-notes/studio/9.9.9.md", strings.Split(bad, "\n")) + if len(got) != 3 { + t.Fatalf("findings = %+v, want a second paragraph, an unknown English group and an unreferenced item", got) + } + for _, f := range got { + if f.Line < 9 { + t.Fatalf("a finding in the English part points at line %d, inside the first language", f.Line) + } + } +} + func TestClassifyDocSkipsMigrationBackup(t *testing.T) { if _, ok := classifyDoc(".migration-backup/legacy-fact.md"); ok { t.Fatal("a store-owned memory backup is not an authored doc")