mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 17:02:17 +00:00
Hard-coded UI text, titles and aria-labels in about 110 components (Git panels, hierarchy, pull requests, dialogs, settings, welcome) now go through tx; spliced fragments became whole sentences (fileCount, noneFoundLabel, hierarchyTitle) and pull request states use keyed messages. 225 zh entries added, stale fragment entries removed.
Also: tx used as a tag without interpolations (tx`No checks`) now looks the text up exactly instead of as a template, so it is translated like tx("No checks"); the text-catalog test checks such tags as plain literals. Check and test counts reuse the existing "{count} passed" / "{failed} failing" entries.
Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Co-authored-by: jaw52 <[email protected]>
153 lines
5.7 KiB
TypeScript
153 lines
5.7 KiB
TypeScript
/** Pure helpers of the Pull Requests view (labels, check summaries, review threads, diff inputs). */
|
|
import type { ChangedFile, PrCheck, PrDiffRevisions, PrFile, PrReviewComment, PullRequestDetails, PullRequestSummary } from "@/platform/ipc";
|
|
import { t, tx } from "@/platform/i18n";
|
|
|
|
/** Keyed, not source text: "Open" as a state reads differently from the "Open" action in some locales. */
|
|
export function stateLabel(pr: PullRequestSummary): string {
|
|
if (pr.state === "merged") return t("github.prState.merged");
|
|
if (pr.state === "closed") return t("github.prState.closed");
|
|
return pr.isDraft ? t("github.prState.draft") : t("github.prState.open");
|
|
}
|
|
|
|
export function reviewDecisionLabel(decision: string | null): string | null {
|
|
switch (decision) {
|
|
case "APPROVED":
|
|
return tx("Approved");
|
|
case "CHANGES_REQUESTED":
|
|
return tx("Changes requested");
|
|
case "REVIEW_REQUIRED":
|
|
return tx("Review required");
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function reviewStateLabel(state: string): string {
|
|
switch (state) {
|
|
case "APPROVED":
|
|
return tx("approved");
|
|
case "CHANGES_REQUESTED":
|
|
return tx("requested changes");
|
|
case "COMMENTED":
|
|
return tx("reviewed");
|
|
case "DISMISSED":
|
|
return tx("review dismissed");
|
|
case "REQUESTED":
|
|
return tx("review requested");
|
|
default:
|
|
return state.toLowerCase().replace(/_/g, " ");
|
|
}
|
|
}
|
|
|
|
export interface ChecksSummary {
|
|
failed: number;
|
|
pending: number;
|
|
passed: number;
|
|
/** Skipped / neutral / cancelled. */
|
|
other: number;
|
|
}
|
|
|
|
export function checksSummary(checks: readonly PrCheck[]): ChecksSummary {
|
|
const s: ChecksSummary = { failed: 0, pending: 0, passed: 0, other: 0 };
|
|
for (const c of checks) {
|
|
if (c.status === "failure") s.failed++;
|
|
else if (c.status === "pending") s.pending++;
|
|
else if (c.status === "success") s.passed++;
|
|
else s.other++;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
export function checksText(s: ChecksSummary): string {
|
|
const parts: string[] = [];
|
|
if (s.failed) parts.push(tx`${s.failed} failing`);
|
|
if (s.pending) parts.push(tx`${s.pending} pending`);
|
|
if (s.passed) parts.push(tx`${s.passed} passed`);
|
|
if (s.other) parts.push(tx`${s.other} skipped`);
|
|
return parts.length ? parts.join(", ") : tx`No checks`;
|
|
}
|
|
|
|
/** "3 minutes ago"-style text for an ISO date. */
|
|
export function relativeTime(iso: string, now = Date.now()): string {
|
|
const t = Date.parse(iso);
|
|
if (Number.isNaN(t)) return "";
|
|
const s = Math.max(0, Math.round((now - t) / 1000));
|
|
const units: Array<[number, (n: number) => string]> = [
|
|
[60 * 60 * 24 * 365, (n) => (n === 1 ? tx`${n} year ago` : tx`${n} years ago`)],
|
|
[60 * 60 * 24 * 30, (n) => (n === 1 ? tx`${n} month ago` : tx`${n} months ago`)],
|
|
[60 * 60 * 24, (n) => (n === 1 ? tx`${n} day ago` : tx`${n} days ago`)],
|
|
[60 * 60, (n) => (n === 1 ? tx`${n} hour ago` : tx`${n} hours ago`)],
|
|
[60, (n) => (n === 1 ? tx`${n} minute ago` : tx`${n} minutes ago`)],
|
|
];
|
|
for (const [size, text] of units) {
|
|
const n = Math.floor(s / size);
|
|
if (n >= 1) return text(n);
|
|
}
|
|
return tx("just now");
|
|
}
|
|
|
|
export interface ReviewThread {
|
|
root: PrReviewComment;
|
|
replies: PrReviewComment[];
|
|
}
|
|
|
|
/** Groups inline comments into threads (replies under their root), roots in file / line order. */
|
|
export function reviewThreads(comments: readonly PrReviewComment[]): ReviewThread[] {
|
|
const byId = new Map<number, ReviewThread>();
|
|
const threads: ReviewThread[] = [];
|
|
for (const c of comments) {
|
|
const parent = c.inReplyTo != null ? byId.get(c.inReplyTo) : undefined;
|
|
if (parent) {
|
|
parent.replies.push(c);
|
|
byId.set(c.id, parent);
|
|
} else {
|
|
const t = { root: c, replies: [] };
|
|
threads.push(t);
|
|
byId.set(c.id, t);
|
|
}
|
|
}
|
|
return threads.sort(
|
|
(a, b) => a.root.path.localeCompare(b.root.path) || (a.root.line ?? a.root.originalLine ?? 0) - (b.root.line ?? b.root.originalLine ?? 0),
|
|
);
|
|
}
|
|
|
|
/** Annotation shape of the diff viewer (`vcs.openDiff` → DiffData.annotations). */
|
|
export interface DiffNote {
|
|
side: "left" | "right";
|
|
line: number;
|
|
author: string;
|
|
body: string;
|
|
date?: string;
|
|
}
|
|
|
|
/** Current (not outdated) review comments of `path`, as diff notes. */
|
|
export function diffNotes(path: string, comments: readonly PrReviewComment[]): DiffNote[] {
|
|
return comments
|
|
.filter((c) => c.path === path && c.line != null)
|
|
.map((c) => ({ side: c.side === "LEFT" ? "left" : "right", line: c.line! - 1, author: c.author, body: c.body, date: c.createdAt }));
|
|
}
|
|
|
|
type Side =
|
|
| { type: "rev"; rev: string; path?: string; label?: string }
|
|
| { type: "text"; text: string; label: string };
|
|
|
|
/** `DiffData` for one changed file of a pull request (merge base ↔ head), with its review comments. */
|
|
export function fileDiffData(pr: PullRequestDetails, file: PrFile, revs: PrDiffRevisions, comments: readonly PrReviewComment[]) {
|
|
const root = revs.root;
|
|
const changed: ChangedFile | undefined = revs.files.find((f) => f.path === file.path);
|
|
const added = file.changeType === "ADDED" || changed?.kind === "added";
|
|
const deleted = file.changeType === "DELETED" || changed?.kind === "deleted";
|
|
const n = pr.summary.number;
|
|
const left: Side = added
|
|
? { type: "text", text: "", label: "(does not exist)" }
|
|
: {
|
|
type: "rev",
|
|
rev: revs.base,
|
|
path: changed?.oldPath ? `${root}/${changed.oldPath}` : undefined,
|
|
label: `${pr.summary.baseRef} (${revs.base.slice(0, 8)})`,
|
|
};
|
|
const right: Side = deleted ? { type: "text", text: "", label: "(deleted)" } : { type: "rev", rev: revs.head, label: `#${n} ${pr.summary.headRef}` };
|
|
const name = file.path.slice(file.path.lastIndexOf("/") + 1);
|
|
return { path: `${root}/${file.path}`, left, right, title: `#${n}: ${name}`, annotations: diffNotes(file.path, comments) };
|
|
}
|