mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
feat(studio): switch a release's notes between Chinese and English
This commit is contained in:
1 parent
574f69bdde
commit
5436dc9ae1
13 files changed
+233
-18
No files matched your search
@@ -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" },
|
||||
|
||||
@@ -839,6 +839,7 @@ export const EN: Record<string, string> = {
|
||||
"更新内容": "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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<!-- lang:en -->\n\n## New\n\n- English item\n";
|
||||
|
||||
it("show the window's language first and switch to the other on request", async () => {
|
||||
render(<Versions port={portWith(doc("2.21.0", both))} />);
|
||||
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(<Versions port={portWith(doc("2.21.0", "## 新增\n\n- 只有中文\n"))} />);
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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 <LazyMarkdown text={first} images={false} />;
|
||||
return (
|
||||
<>
|
||||
<div className="vnotes-lang seg" role="group" aria-label={t("更新内容的语言")}>
|
||||
<button data-action="versions.notes-lang" data-value="first" aria-pressed={shown === "first"} onClick={() => setShown("first")}>{NOTES_LANGUAGE_NAMES.first}</button>
|
||||
<button data-action="versions.notes-lang" data-value="english" aria-pressed={shown === "english"} onClick={() => setShown("english")}>{NOTES_LANGUAGE_NAMES.english}</button>
|
||||
</div>
|
||||
<LazyMarkdown text={shown === "english" ? english : first} images={false} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NotesPanel({ id, version, state, retry, onKeyDown }: { id: string; version: string; state: NotesState | undefined; retry: () => void; onKeyDown: (e: KeyboardEvent<HTMLElement>) => void }) {
|
||||
const loading = !state || state.phase === "loading";
|
||||
return (
|
||||
<section id={id} className="vnotes" data-action="versions.notes-close" onKeyDown={onKeyDown} aria-label={t("{v} 的更新内容", { v: version })} aria-busy={loading}>
|
||||
{loading && <p className="acct-note">{t("正在读取更新内容…")}</p>}
|
||||
{state?.phase === "ok" && <LazyMarkdown text={state.markdown} images={false} />}
|
||||
{state?.phase === "ok" && <NotesBody markdown={state.markdown} />}
|
||||
{state?.phase === "err" && (
|
||||
<div className="find" data-lvl="warn" role="alert">
|
||||
<span className="t">{t("更新内容读取失败")}</span>
|
||||
|
||||
@@ -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 = "<!-- lang:en -->";
|
||||
|
||||
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;
|
||||
@@ -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 `<!-- lang:en -->` 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`.
|
||||
|
||||
|
||||
@@ -58,3 +58,68 @@
|
||||
- 子代理状态帧在客户端落后时不再丢失 (#12888)
|
||||
- 文件预览会先检查目标是否允许访问,并先经过 PreToolUse 钩子 (#12115)
|
||||
- 记忆保存失败时会撤回,迁移过的记忆再次修改时会修复索引 (#12938, #12939, #12940)
|
||||
|
||||
---
|
||||
|
||||
<!-- lang:en -->
|
||||
|
||||
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)
|
||||
@@ -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.
|
||||
@@ -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 = "<!-- lang:en -->";
|
||||
|
||||
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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
<!-- lang:en -->
|
||||
|
||||
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("<!-- lang:en -->");
|
||||
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);
|
||||
});
|
||||
+32
-11
@@ -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 = "<!-- lang:en -->"
|
||||
|
||||
// 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})
|
||||
}
|
||||
|
||||
@@ -196,6 +196,29 @@ func TestAReleaseNoteIsGroupedReferencedAndShort(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseNoteHoldsEachLanguageToTheSameShape(t *testing.T) {
|
||||
ok := strings.Join([]string{
|
||||
"一句话概括本版。", "", "## 新增", "", "- 做了一件事 (#1)", "", "---", "", "<!-- lang:en -->", "",
|
||||
"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)", "", "<!-- lang:en -->", "",
|
||||
"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")
|
||||
|
||||
Reference in new issue
Block a user