mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 17:02:17 +00:00
fix(i18n): Translate the remaining UI text (Git, hierarchy, pull requests, dialogs, aria labels) (#114)
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]>
This commit is contained in:
121 files changed
+854
-457
No files matched your search
@@ -35,14 +35,9 @@ import {
|
||||
import { addComment, checkoutPullRequest, openCommentLocation, openFileDiff, openUrl, submitReview } from "./ops";
|
||||
import { cancelBusy, loadDetails, refreshAll, select, setQuery, useGithub, type DetailTab } from "./store";
|
||||
import styles from "./Github.module.css";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { t, tx } from "@/platform/i18n";
|
||||
|
||||
const STATES: Array<{ value: PrStateFilter; label: string }> = [
|
||||
{ value: "open", label: "Open" },
|
||||
{ value: "merged", label: "Merged" },
|
||||
{ value: "closed", label: "Closed" },
|
||||
{ value: "all", label: "All" },
|
||||
];
|
||||
const STATES: PrStateFilter[] = ["open", "merged", "closed", "all"];
|
||||
|
||||
function Unavailable({ availability }: { availability: GhAvailability }) {
|
||||
const retry = (
|
||||
@@ -138,7 +133,10 @@ function Filters() {
|
||||
className={styles.search}
|
||||
/>
|
||||
<div className={styles.filterRow}>
|
||||
<Select value={query.state} options={STATES} onChange={(state) => setQuery({ state })} aria-label="State" />
|
||||
<Select
|
||||
value={query.state}
|
||||
options={STATES.map((value) => ({ value, label: t(`github.prState.${value}`) }))}
|
||||
onChange={(state) => setQuery({ state })} aria-label="State" />
|
||||
<Checkbox checked={query.authorMe} onChange={(authorMe) => setQuery({ authorMe })} label="Mine" />
|
||||
<Checkbox
|
||||
checked={query.reviewRequested}
|
||||
@@ -247,7 +245,7 @@ function Overview({ pr }: { pr: PullRequestDetails }) {
|
||||
<div className={styles.scroll}>
|
||||
<Markdown text={pr.body} />
|
||||
<section className={styles.section}>
|
||||
<h3 className={styles.sectionTitle}>Checks · {checksText(summary)}</h3>
|
||||
<h3 className={styles.sectionTitle}>{tx`Checks · ${checksText(summary)}`}</h3>
|
||||
{pr.checks.map((c, i) => (
|
||||
<div key={i} className={styles.checkRow}>
|
||||
<Icon name={CHECK_ICON[c.status]} size={14} className={styles[`check-${c.status}`]} />
|
||||
@@ -303,7 +301,7 @@ function Overview({ pr }: { pr: PullRequestDetails }) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{pr.assignees.length > 0 && <div className={styles.dim}>Assignees: {pr.assignees.join(", ")}</div>}
|
||||
{pr.assignees.length > 0 && <div className={styles.dim}>{tx`Assignees: ${pr.assignees.join(", ")}`}</div>}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
@@ -318,7 +316,7 @@ function Files({ pr }: { pr: PullRequestDetails }) {
|
||||
return m;
|
||||
}, [comments]);
|
||||
return (
|
||||
<div className={styles.scroll} role="list" aria-label="Changed files">
|
||||
<div className={styles.scroll} role="list" aria-label={tx("Changed files")}>
|
||||
{pr.files.map((f) => {
|
||||
const slash = f.path.lastIndexOf("/");
|
||||
return (
|
||||
@@ -334,7 +332,7 @@ function Files({ pr }: { pr: PullRequestDetails }) {
|
||||
<span className={clsx(styles.fileName, styles[`change-${f.changeType}`])}>{f.path.slice(slash + 1)}</span>
|
||||
<span className={clsx(styles.dim, styles.ellipsis)}>{slash > 0 ? f.path.slice(0, slash) : ""}</span>
|
||||
<span className={styles.flex} />
|
||||
{counts.get(f.path) ? <span className={styles.dim}>{counts.get(f.path)} comments</span> : null}
|
||||
{counts.get(f.path) ? <span className={styles.dim}>{tx`${counts.get(f.path)} comments`}</span> : null}
|
||||
<span className={styles.added}>+{f.additions}</span>
|
||||
<span className={styles.deleted}>−{f.deletions}</span>
|
||||
</button>
|
||||
@@ -346,7 +344,7 @@ function Files({ pr }: { pr: PullRequestDetails }) {
|
||||
|
||||
function Commits({ pr }: { pr: PullRequestDetails }) {
|
||||
return (
|
||||
<div className={styles.scroll} role="list" aria-label="Commits">
|
||||
<div className={styles.scroll} role="list" aria-label={tx("Commits")}>
|
||||
{pr.commits.map((c) => (
|
||||
<div
|
||||
key={c.oid}
|
||||
@@ -406,7 +404,11 @@ function Conversation({ pr }: { pr: PullRequestDetails }) {
|
||||
<button type="button" className={styles.link} onClick={() => void openCommentLocation(pr, t.root.path)}>
|
||||
{t.root.path}:{t.root.line ?? t.root.originalLine ?? "?"}
|
||||
</button>
|
||||
{t.root.line == null && <span className={styles.dim}>outdated</span>}
|
||||
{t.root.line == null && (
|
||||
<span className={styles.dim}>
|
||||
<T>outdated</T>
|
||||
</span>
|
||||
)}
|
||||
</header>
|
||||
{[t.root, ...t.replies].map((c) => (
|
||||
<div key={c.id} className={styles.threadItem}>
|
||||
@@ -421,8 +423,8 @@ function Conversation({ pr }: { pr: PullRequestDetails }) {
|
||||
))}
|
||||
<div className={styles.newComment}>
|
||||
<textarea
|
||||
aria-label="New comment"
|
||||
placeholder="Add a comment (Markdown)"
|
||||
aria-label={tx("New comment")}
|
||||
placeholder={tx("Add a comment (Markdown)")}
|
||||
className={styles.textarea}
|
||||
rows={4}
|
||||
value={draft}
|
||||
@@ -565,7 +567,11 @@ export function PullRequestsView() {
|
||||
if (!useGithub.getState().availability) void refreshAll();
|
||||
}, []);
|
||||
if (!availability)
|
||||
return <div className={styles.loading}>{checking && <Spinner size={14} />} Connecting to GitHub…</div>;
|
||||
return (
|
||||
<div className={styles.loading}>
|
||||
{checking && <Spinner size={14} />} {tx("Connecting to GitHub…")}
|
||||
</div>
|
||||
);
|
||||
if (availability.kind !== "ready")
|
||||
return (
|
||||
<div className={styles.view}>
|
||||
|
||||
@@ -126,8 +126,8 @@ function CreateDialog(props: {
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
<textarea
|
||||
aria-label="Description"
|
||||
placeholder="Description (Markdown)"
|
||||
aria-label={tx("Description")}
|
||||
placeholder={tx("Description (Markdown)")}
|
||||
className={styles.textarea}
|
||||
rows={10}
|
||||
value={body}
|
||||
@@ -188,7 +188,7 @@ function ReviewDialog(props: {
|
||||
/>
|
||||
<textarea
|
||||
data-autofocus
|
||||
aria-label="Review comment"
|
||||
aria-label={tx("Review comment")}
|
||||
placeholder={needsBody ? "Leave a comment (required)" : "Leave a comment (optional)"}
|
||||
className={styles.textarea}
|
||||
rows={8}
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("pull request model", () => {
|
||||
const c = (status: PrCheck["status"]): PrCheck => ({ name: status, workflow: null, status, url: null });
|
||||
const s = checksSummary([c("failure"), c("success"), c("success"), c("pending"), c("skipped")]);
|
||||
expect(s).toEqual({ failed: 1, pending: 1, passed: 2, other: 1 });
|
||||
expect(checksText(s)).toBe("1 failing, 1 pending, 2 successful, 1 skipped");
|
||||
expect(checksText(s)).toBe("1 failing, 1 pending, 2 passed, 1 skipped");
|
||||
expect(checksText(checksSummary([]))).toBe("No checks");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
/** 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 "Merged";
|
||||
if (pr.state === "closed") return "Closed";
|
||||
return pr.isDraft ? "Draft" : "Open";
|
||||
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 "Approved";
|
||||
return tx("Approved");
|
||||
case "CHANGES_REQUESTED":
|
||||
return "Changes requested";
|
||||
return tx("Changes requested");
|
||||
case "REVIEW_REQUIRED":
|
||||
return "Review required";
|
||||
return tx("Review required");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -23,15 +25,15 @@ export function reviewDecisionLabel(decision: string | null): string | null {
|
||||
export function reviewStateLabel(state: string): string {
|
||||
switch (state) {
|
||||
case "APPROVED":
|
||||
return "approved";
|
||||
return tx("approved");
|
||||
case "CHANGES_REQUESTED":
|
||||
return "requested changes";
|
||||
return tx("requested changes");
|
||||
case "COMMENTED":
|
||||
return "reviewed";
|
||||
return tx("reviewed");
|
||||
case "DISMISSED":
|
||||
return "review dismissed";
|
||||
return tx("review dismissed");
|
||||
case "REQUESTED":
|
||||
return "review requested";
|
||||
return tx("review requested");
|
||||
default:
|
||||
return state.toLowerCase().replace(/_/g, " ");
|
||||
}
|
||||
@@ -58,11 +60,11 @@ export function checksSummary(checks: readonly PrCheck[]): ChecksSummary {
|
||||
|
||||
export function checksText(s: ChecksSummary): string {
|
||||
const parts: string[] = [];
|
||||
if (s.failed) parts.push(`${s.failed} failing`);
|
||||
if (s.pending) parts.push(`${s.pending} pending`);
|
||||
if (s.passed) parts.push(`${s.passed} successful`);
|
||||
if (s.other) parts.push(`${s.other} skipped`);
|
||||
return parts.length ? parts.join(", ") : "No checks";
|
||||
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. */
|
||||
@@ -70,18 +72,18 @@ 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, string]> = [
|
||||
[60 * 60 * 24 * 365, "year"],
|
||||
[60 * 60 * 24 * 30, "month"],
|
||||
[60 * 60 * 24, "day"],
|
||||
[60 * 60, "hour"],
|
||||
[60, "minute"],
|
||||
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, name] of units) {
|
||||
for (const [size, text] of units) {
|
||||
const n = Math.floor(s / size);
|
||||
if (n >= 1) return `${n} ${name}${n === 1 ? "" : "s"} ago`;
|
||||
if (n >= 1) return text(n);
|
||||
}
|
||||
return "just now";
|
||||
return tx("just now");
|
||||
}
|
||||
|
||||
export interface ReviewThread {
|
||||
|
||||
Reference in new issue
Block a user