дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
fix(i18n): Translate the remaining Git and pull request text (#116)
Multi-repo branch popup matches actions by their translated label (highlights and Chinese search now work) and translates its group headers. Conflict counts, relative times (shared dateFormat), in-progress operation titles, git operation progress/results, branch widget tooltips, diff/hunk counts, worktree markers, PR author line and review placeholders go through tx; the default "Changes" changelist shows its translated name. HexViewer read-only leaves the untranslated allowlist. 92 zh entries. Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
1 джерело
443bb51bba
коміт
429e8ffc82
22 files changed
+237
-77
No files matched your search
@@ -293,7 +293,7 @@ function ModifyTableDialog(props: ModifyProps) {
|
||||
width={440}
|
||||
buttons={[{ label: "Cancel", role: "cancel" }]}
|
||||
>
|
||||
<div className={styles.dialogText}>{details?.status === "error" ? details.message : "Loading…"}</div>
|
||||
<div className={styles.dialogText}>{details?.status === "error" ? details.message : tx("Loading…")}</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -276,7 +276,9 @@ function FramesPane({ session }: { session: DebugSession }) {
|
||||
}
|
||||
onBlur={() => blurDebugView("frames")}
|
||||
>
|
||||
{!paused && <div className={styles.note}>{session.status === "terminated" ? tx("Finished") : "Running…"}</div>}
|
||||
{!paused && (
|
||||
<div className={styles.note}>{session.status === "terminated" ? tx("Finished") : tx("Running…")}</div>
|
||||
)}
|
||||
{items.map((item) =>
|
||||
item.kind === "hidden" ? (
|
||||
<button
|
||||
|
||||
@@ -160,7 +160,7 @@ function ListRow({ pr }: { pr: PullRequestSummary }) {
|
||||
<StateBadge pr={pr} />
|
||||
</div>
|
||||
<div className={styles.listMeta}>
|
||||
#{pr.number} by {pr.author} · {relativeTime(pr.updatedAt)}
|
||||
#{pr.number} {tx`by ${pr.author}`} · {relativeTime(pr.updatedAt)}
|
||||
{decision ? ` · ${decision}` : ""}
|
||||
{pr.labels.length ? ` · ${pr.labels.map((l) => l.name).join(", ")}` : ""}
|
||||
</div>
|
||||
@@ -486,7 +486,7 @@ function Details() {
|
||||
<div className={styles.metaRow}>
|
||||
<StateBadge pr={s} />
|
||||
<span>
|
||||
#{s.number} by <strong>{s.author}</strong> · {s.baseRef} ← {s.headRef}
|
||||
#{s.number} {tx`by ${s.author}`} · {s.baseRef} ← {s.headRef}
|
||||
</span>
|
||||
{reviewDecisionLabel(s.reviewDecision) && (
|
||||
<span className={styles.dim}>· {reviewDecisionLabel(s.reviewDecision)}</span>
|
||||
|
||||
@@ -189,7 +189,7 @@ function ReviewDialog(props: {
|
||||
<textarea
|
||||
data-autofocus
|
||||
aria-label={tx("Review comment")}
|
||||
placeholder={needsBody ? "Leave a comment (required)" : "Leave a comment (optional)"}
|
||||
placeholder={needsBody ? tx("Leave a comment (required)") : tx("Leave a comment (optional)")}
|
||||
className={styles.textarea}
|
||||
rows={8}
|
||||
value={body}
|
||||
|
||||
@@ -171,7 +171,7 @@ export function StructureView() {
|
||||
return error ? (
|
||||
<EmptyState title="Structure unavailable">{error}</EmptyState>
|
||||
) : (
|
||||
<div className={styles.loading}>{loading ? "Loading…" : null}</div>
|
||||
<div className={styles.loading}>{loading ? tx("Loading…") : null}</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -491,7 +491,7 @@ function FileStructureDialog() {
|
||||
onActivate={(n) => choose(n)}
|
||||
rowHeight={POPUP_ROW}
|
||||
indent={16}
|
||||
empty={<span className={styles.popupEmpty}>{!path ? tx("Open a file first") : loading && !items ? "Loading…" : "No symbols"}</span>}
|
||||
empty={<span className={styles.popupEmpty}>{!path ? tx("Open a file first") : loading && !items ? tx("Loading…") : tx("No symbols")}</span>}
|
||||
renderNode={(n, s) => {
|
||||
const matched = filter.matches.get(n.key);
|
||||
return <SymbolRow node={n} indices={matched} selected={s.selected} dim={!!query.trim() && !matched} />;
|
||||
|
||||
@@ -45,7 +45,7 @@ export const showBranchesPopup = () => useBranchesPopup.setState({ open: true })
|
||||
export function headLabel(branch: { head: string | null; headCommit: string | null } | null): string {
|
||||
if (!branch) return "";
|
||||
if (branch.head) return branch.head;
|
||||
return branch.headCommit ? branch.headCommit.slice(0, 8) : "No commits";
|
||||
return branch.headCommit ? branch.headCommit.slice(0, 8) : tx("No commits");
|
||||
}
|
||||
|
||||
/** The current repository (see `currentRepoFor`): the widget and the popup show the same one. */
|
||||
@@ -71,18 +71,24 @@ export function BranchWidget() {
|
||||
const branch: BranchInfo = other
|
||||
? { head: other.head, headCommit: other.headCommit, upstream: null, ahead: 0, behind: 0, operation: otherOp }
|
||||
: selected;
|
||||
const op = branch.operation ? ` (${branch.operation})` : "";
|
||||
const sync = [
|
||||
branch.behind > 0 ? `${branch.behind} incoming` : "",
|
||||
branch.ahead > 0 ? `${branch.ahead} outgoing` : "",
|
||||
].filter(Boolean);
|
||||
const tooltip =
|
||||
(repo ? `Repository: ${repo}\n` : "") +
|
||||
(branch.upstream
|
||||
? `Git branch: ${headLabel(branch)}${op}\nTracking ${branch.upstream}`
|
||||
: `Git branch: ${headLabel(branch)}${op}`) +
|
||||
(count > 1 && !repo ? `\nIn all ${count} repositories` : "") +
|
||||
(sync.length ? `\n${sync.join(", ")} commits` : "");
|
||||
const head = branch.operation ? `${headLabel(branch)} (${branch.operation})` : headLabel(branch);
|
||||
const sync =
|
||||
branch.behind > 0 && branch.ahead > 0
|
||||
? tx`${branch.behind} incoming, ${branch.ahead} outgoing commits`
|
||||
: branch.behind > 0
|
||||
? tx`${branch.behind} incoming commits`
|
||||
: branch.ahead > 0
|
||||
? tx`${branch.ahead} outgoing commits`
|
||||
: "";
|
||||
const tooltip = [
|
||||
repo ? tx`Repository: ${repo}` : "",
|
||||
tx`Git branch: ${head}`,
|
||||
branch.upstream ? tx`Tracking ${branch.upstream}` : "",
|
||||
count > 1 && !repo ? tx`In all ${count} repositories` : "",
|
||||
sync,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return (
|
||||
<>
|
||||
<BarButton
|
||||
@@ -341,7 +347,7 @@ function BranchesPanel({ onClose, actions: baseActions, root, fallback, onBack }
|
||||
rowHeight={ROW_HEIGHT}
|
||||
aria-label="Branches"
|
||||
className={styles.list}
|
||||
empty={<span className={styles.empty}>{loaded ? tx("No matching branches") : "Loading…"}</span>}
|
||||
empty={<span className={styles.empty}>{loaded ? tx("No matching branches") : tx("Loading…")}</span>}
|
||||
renderItem={(r) => <BranchRowView row={r} />}
|
||||
/>
|
||||
</div>
|
||||
@@ -746,7 +752,7 @@ function BranchRowView({ row }: { row: BranchRow | Extract<RepoRow, { type: "act
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(styles.star, row.favorite && styles.starOn)}
|
||||
aria-label={row.favorite ? "Remove from Favorites" : "Add to Favorites"}
|
||||
aria-label={row.favorite ? tx("Remove from Favorites") : tx("Add to Favorites")}
|
||||
aria-pressed={row.favorite}
|
||||
tabIndex={-1}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
} from "./changes";
|
||||
import { applyStash, commitChanges, dropStash, showUnstashDialog, stashChanges } from "./changeOps";
|
||||
import { CommitOptionsPopup } from "./CommitOptions";
|
||||
import { useChangelists, withListDefaults } from "./changelists";
|
||||
import { DEFAULT_LIST_NAME, useChangelists, withListDefaults } from "./changelists";
|
||||
import { moveToChangelist } from "./changelistsUi";
|
||||
import { messageHistory, setAmend, setGroupByDir, useCommit, type CommitTab } from "./commitStore";
|
||||
import { openDiff, withChain, workingDiffData } from "./diffInput";
|
||||
@@ -53,6 +53,11 @@ import styles from "./CommitView.module.css";
|
||||
const CHANGES_MIME = "application/x-openidea-changes";
|
||||
|
||||
/** "Commit" / "Shelf" / "Stash" content tabs; they stand in for the tool window title. */
|
||||
/** Shown text of a node: group names are translated, except the names of user change lists. */
|
||||
function nodeText(n: ChangeNode): string {
|
||||
return n.type === "group" && (!n.changelist || n.label === DEFAULT_LIST_NAME) ? tx(n.label) : n.label;
|
||||
}
|
||||
|
||||
export function CommitTabs() {
|
||||
const available = useVcs((s) => s.available);
|
||||
const tab = useCommit((s) => s.tab);
|
||||
@@ -373,7 +378,7 @@ function ChangesPanel() {
|
||||
handleRef={treeRef}
|
||||
roots={roots}
|
||||
getKey={(n) => n.key}
|
||||
getLabel={(n) => n.label}
|
||||
getLabel={nodeText}
|
||||
getChildren={(n) => n.children}
|
||||
isExpandable={(n) => n.type !== "file"}
|
||||
expanded={expanded}
|
||||
@@ -421,9 +426,9 @@ function ChangesPanel() {
|
||||
n.type === "group" && n.changelist && !n.active && styles.inactiveList,
|
||||
)}
|
||||
style={n.type === "file" && n.kind ? { color: KIND_COLOR[n.kind] } : undefined}
|
||||
title={n.type === "file" && n.kind ? KIND_LABEL[n.kind] : undefined}
|
||||
title={n.type === "file" && n.kind ? tx(KIND_LABEL[n.kind]) : undefined}
|
||||
>
|
||||
{n.type === "group" && !n.changelist ? tx(n.label) : n.label}
|
||||
{nodeText(n)}
|
||||
</span>
|
||||
);
|
||||
// Pointer-driven (HTML5 drag and drop never reaches the Tauri webview):
|
||||
@@ -765,7 +770,7 @@ function StashesPanel() {
|
||||
rowHeight={22}
|
||||
aria-label="Stashes"
|
||||
className={styles.stashList}
|
||||
empty={<span className={styles.empty}>{loaded ? "No stashes" : "Loading…"}</span>}
|
||||
empty={<span className={styles.empty}>{loaded ? tx("No stashes") : tx("Loading…")}</span>}
|
||||
onActivate={(s, _i, e) => {
|
||||
if (e.type === "click" && (e as MouseEvent).detail < 2) return;
|
||||
void applyStash(s.index, false);
|
||||
|
||||
@@ -592,10 +592,12 @@ export function DiffEditor({ input }: { input: EditorInput }) {
|
||||
: chunkInfo.count === 0
|
||||
? identical
|
||||
? tx("Contents are identical")
|
||||
: "No differences"
|
||||
: tx("No differences")
|
||||
: chunkInfo.current >= 0
|
||||
? `${chunkInfo.current + 1} of ${chunkInfo.count} differences`
|
||||
: `${chunkInfo.count} difference${chunkInfo.count === 1 ? "" : "s"}`}
|
||||
? tx`${chunkInfo.current + 1} of ${chunkInfo.count} differences`
|
||||
: chunkInfo.count === 1
|
||||
? tx`${chunkInfo.count} difference`
|
||||
: tx`${chunkInfo.count} differences`}
|
||||
</span>
|
||||
</div>
|
||||
{opts.mode === "split" ? (
|
||||
@@ -608,7 +610,9 @@ export function DiffEditor({ input }: { input: EditorInput }) {
|
||||
</span>
|
||||
<span className={styles.label} title={sideLabel(right)}>
|
||||
{sideLabel(right)}
|
||||
{editableB && <span className={styles.badge}>{left.type === "rev" ? tx("working copy") : "editable"}</span>}
|
||||
{editableB && (
|
||||
<span className={styles.badge}>{left.type === "rev" ? tx("working copy") : tx("editable")}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -146,12 +146,8 @@ function HunkPopupView({ state }: { state: PopupState }) {
|
||||
}}
|
||||
/>
|
||||
<span className={styles.title}>
|
||||
{KIND_TITLE[hunk.kind]}
|
||||
{index >= 0 && all.length > 1 && (
|
||||
<span className={styles.counter}>
|
||||
{index + 1} of {all.length}
|
||||
</span>
|
||||
)}
|
||||
{tx(KIND_TITLE[hunk.kind])}
|
||||
{index >= 0 && all.length > 1 && <span className={styles.counter}>{tx`${index + 1} of ${all.length}`}</span>}
|
||||
</span>
|
||||
<IconButton icon="close" title="Close" size="small" onClick={closeHunkPopup} />
|
||||
</div>
|
||||
|
||||
@@ -317,7 +317,7 @@ export function MergeEditor({ input }: { input: EditorInput }) {
|
||||
</Button>
|
||||
<IconButton icon="refresh" title="Reload from Disk" size="small" onClick={() => void load()} />
|
||||
<span className={clsx(styles.status, !left && styles.statusDone)}>
|
||||
{left ? `${left} conflict${left === 1 ? "" : "s"} left` : tx("All conflicts resolved")}
|
||||
{left === 1 ? tx`${left} conflict left` : left ? tx`${left} conflicts left` : tx("All conflicts resolved")}
|
||||
</span>
|
||||
<Button size="small" variant="primary" onClick={() => void apply()}>
|
||||
<T>Apply</T>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { Branch } from "@/bindings";
|
||||
import { useI18n } from "@/platform/i18n";
|
||||
import {
|
||||
buildMultiRepoRows,
|
||||
commonBranches,
|
||||
@@ -193,6 +194,18 @@ describe("buildMultiRepoRows", () => {
|
||||
"repo ology-gateway [dev] c3",
|
||||
);
|
||||
});
|
||||
|
||||
it("matches and shows actions and groups in the UI language", () => {
|
||||
const texts = { "Update Project…": "更新项目…", "Common Local Branches": "公共本地分支" };
|
||||
useI18n.setState({ texts });
|
||||
try {
|
||||
const rows = buildMultiRepoRows({ ...input, query: "更新" });
|
||||
expect(rows.map((r) => (r.type === "action" ? r.matches : null))).toEqual([[0, 1]]);
|
||||
expect(summary(buildMultiRepoRows({ ...input, query: "" }))).toContain("v 公共本地分支");
|
||||
} finally {
|
||||
useI18n.setState({ texts: {} });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildMultiRepoRows with repositories on different branches", () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Get from VCS: clone a repository into a new folder and open it as a project. */
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Channel } from "@tauri-apps/api/core";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { call, commands, isCancelled, isTauri, nextRequestId } from "@/platform/ipc";
|
||||
import { openProject } from "@/platform/project";
|
||||
import { Button, Dialog, Input, ProgressBar, T } from "@/ui";
|
||||
@@ -207,7 +208,7 @@ function CloneDialog({ onClose, initialUrl }: { onClose: () => void; initialUrl:
|
||||
{busy && (
|
||||
<div className={styles.cloneProgress} aria-live="polite">
|
||||
<ProgressBar value={progress.fraction} aria-label="Clone progress" />
|
||||
<span className={styles.hint}>{progress.line || "Cloning…"}</span>
|
||||
<span className={styles.hint}>{progress.line || tx("Cloning…")}</span>
|
||||
</div>
|
||||
)}
|
||||
{error && <div className={styles.error}>{error}</div>}
|
||||
|
||||
@@ -596,7 +596,7 @@ function WorktreesDialog({ onClose }: { onClose: () => void }) {
|
||||
<span className={styles.fileName}>{w.branch ?? (w.head ? w.head.slice(0, 8) : <T>(bare)</T>)}</span>
|
||||
<span className={clsx(styles.fileDir, styles.mono)}>{w.path}</span>
|
||||
{(w.main || w.locked || w.prunable) && (
|
||||
<span className={styles.fileDir}>{w.main ? "main" : w.locked ? "locked" : "prunable"}</span>
|
||||
<span className={styles.fileDir}>{w.main ? "main" : w.locked ? tx("locked") : tx("prunable")}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -31,20 +31,35 @@ const OP_NOUN: Record<InProgress, string> = {
|
||||
am: "Patch Application",
|
||||
};
|
||||
|
||||
const IN_PROGRESS: Record<InProgress, string> = {
|
||||
rebase: "Rebase in progress",
|
||||
merge: "Merge in progress",
|
||||
"cherry-pick": "Cherry-pick in progress",
|
||||
revert: "Revert in progress",
|
||||
am: "Patch application in progress",
|
||||
};
|
||||
|
||||
export function inProgress(op: string | null | undefined = useVcs.getState().branch?.operation): InProgress | null {
|
||||
return op === "rebase" || op === "merge" || op === "cherry-pick" || op === "revert" || op === "am" ? op : null;
|
||||
}
|
||||
|
||||
/** "Rebasing main", "Merging", "Cherry-picking"… for the branch widget and banner. */
|
||||
export function operationTitle(op: InProgress, head: string | null): string {
|
||||
const verb = {
|
||||
rebase: "Rebasing",
|
||||
merge: "Merging",
|
||||
"cherry-pick": "Cherry-picking",
|
||||
revert: "Reverting",
|
||||
am: "Applying patches",
|
||||
if (!head)
|
||||
return {
|
||||
rebase: tx("Rebasing"),
|
||||
merge: tx("Merging"),
|
||||
"cherry-pick": tx("Cherry-picking"),
|
||||
revert: tx("Reverting"),
|
||||
am: tx("Applying patches"),
|
||||
}[op];
|
||||
return {
|
||||
rebase: tx`Rebasing ${head}`,
|
||||
merge: tx`Merging ${head}`,
|
||||
"cherry-pick": tx`Cherry-picking ${head}`,
|
||||
revert: tx`Reverting ${head}`,
|
||||
am: tx`Applying patches to ${head}`,
|
||||
}[op];
|
||||
return head ? `${verb} ${head}` : verb;
|
||||
}
|
||||
|
||||
/** What a stopped operation offers, in IDEA's order. A merge is concluded by committing. */
|
||||
@@ -106,16 +121,39 @@ export async function controlOperation(control: "continue" | "skip" | "abort", e
|
||||
void executeAction("vcs.resolveConflicts");
|
||||
return;
|
||||
}
|
||||
const noun = OP_NOUN[op];
|
||||
const progress = {
|
||||
continue: `Continuing ${noun.toLowerCase()}`,
|
||||
continue: {
|
||||
rebase: "Continuing rebase",
|
||||
merge: "Continuing merge",
|
||||
"cherry-pick": "Continuing cherry-pick",
|
||||
revert: "Continuing revert",
|
||||
am: "Continuing patch application",
|
||||
}[op],
|
||||
skip: "Skipping commit",
|
||||
abort: `Aborting ${noun.toLowerCase()}`,
|
||||
abort: {
|
||||
rebase: "Aborting rebase",
|
||||
merge: "Aborting merge",
|
||||
"cherry-pick": "Aborting cherry-pick",
|
||||
revert: "Aborting revert",
|
||||
am: "Aborting patch application",
|
||||
}[op],
|
||||
}[control];
|
||||
const success = {
|
||||
continue: `${noun} continued`,
|
||||
continue: {
|
||||
rebase: "Rebase continued",
|
||||
merge: "Merge continued",
|
||||
"cherry-pick": "Cherry-pick continued",
|
||||
revert: "Revert continued",
|
||||
am: "Patch application continued",
|
||||
}[op],
|
||||
skip: "Commit skipped",
|
||||
abort: `${noun} aborted`,
|
||||
abort: {
|
||||
rebase: "Rebase aborted",
|
||||
merge: "Merge aborted",
|
||||
"cherry-pick": "Cherry-pick aborted",
|
||||
revert: "Revert aborted",
|
||||
am: "Patch application aborted",
|
||||
}[op],
|
||||
}[control];
|
||||
await runGitOperation(
|
||||
{ op: control },
|
||||
@@ -138,12 +176,12 @@ export function OperationBanner() {
|
||||
const op = inProgress(branch?.operation);
|
||||
if (!op) return null;
|
||||
return (
|
||||
<div className={styles.banner} role="status" aria-label={`${OP_NOUN[op]} in progress`}>
|
||||
<div className={styles.banner} role="status" aria-label={tx(IN_PROGRESS[op])}>
|
||||
<Icon name={conflicts ? "warning" : "branch"} size={14} />
|
||||
<span className={styles.bannerText}>
|
||||
{operationTitle(op, branch?.head ?? null)}
|
||||
{conflicts
|
||||
? ` · ${conflicts} conflict${conflicts === 1 ? "" : "s"}`
|
||||
? ` · ${conflicts === 1 ? tx`${conflicts} conflict` : tx`${conflicts} conflicts`}`
|
||||
: op === "merge"
|
||||
? tx(" · commit to conclude")
|
||||
: ""}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { Channel } from "@tauri-apps/api/core";
|
||||
import { create } from "zustand";
|
||||
import type { GitOperation, RemoteOp } from "@/bindings";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { call, commands, emitLocalIdeEvent, isCancelled, nextRequestId } from "@/platform/ipc";
|
||||
import { editorsFor } from "@/editor/api";
|
||||
import { maybeEditorService, notifications } from "@/workbench/contributions";
|
||||
@@ -150,7 +151,8 @@ export async function runRemote(
|
||||
notifications().notify({ group: "vcs", level: "warning", title: "Another remote operation is in progress" });
|
||||
return false;
|
||||
}
|
||||
const [progressTitle, doneTitle] = REMOTE_TITLE[op.op];
|
||||
const [progressText, doneTitle] = REMOTE_TITLE[op.op];
|
||||
const progressTitle = tx(progressText);
|
||||
const request = nextRequestId();
|
||||
const taskId = 900_000 + request;
|
||||
activeRemote = request;
|
||||
@@ -320,15 +322,16 @@ export async function runGitOperation(op: GitOperation, opts: GitOperationOption
|
||||
const request = nextRequestId();
|
||||
const taskId = 900_000 + request;
|
||||
activeRemote = request;
|
||||
useVcs.setState({ remoteBusy: opts.progress });
|
||||
const progress = tx(opts.progress);
|
||||
useVcs.setState({ remoteBusy: progress });
|
||||
const task = (message: string | null, done: boolean) =>
|
||||
emitLocalIdeEvent({ type: "task", id: taskId, title: opts.progress, message, fraction: done ? 1 : null, done });
|
||||
emitLocalIdeEvent({ type: "task", id: taskId, title: progress, message, fraction: done ? 1 : null, done });
|
||||
task(null, false);
|
||||
notifications().notify({
|
||||
group: "vcs",
|
||||
level: "info",
|
||||
key: "vcs.operation",
|
||||
title: `${opts.progress}…`,
|
||||
title: `${progress}…`,
|
||||
sticky: true,
|
||||
actions: [{ label: "Cancel", run: cancelRemote }],
|
||||
});
|
||||
|
||||
@@ -257,7 +257,9 @@ function ApplyPatchDialog({ patch, source, onClose }: { patch: string; source: s
|
||||
<span
|
||||
className={styles.fileRow}
|
||||
style={{ flex: 1, padding: 0 }}
|
||||
title={f.applies ? "Applies cleanly" : "Does not apply cleanly: a 3-way merge will be attempted"}
|
||||
title={
|
||||
f.applies ? tx("Applies cleanly") : tx("Does not apply cleanly: a 3-way merge will be attempted")
|
||||
}
|
||||
>
|
||||
<Icon name={fileIconName(baseName(f.path))} size={14} />
|
||||
<span
|
||||
@@ -269,9 +271,9 @@ function ApplyPatchDialog({ patch, source, onClose }: { patch: string; source: s
|
||||
>
|
||||
{baseName(f.path)}
|
||||
</span>
|
||||
<span className={styles.fileDir}>{f.oldPath ? `from ${f.oldPath}` : dirName(f.path)}</span>
|
||||
<span className={styles.fileDir}>{f.oldPath ? tx`from ${f.oldPath}` : dirName(f.path)}</span>
|
||||
<span className={styles.grow} />
|
||||
<span className={styles.commitMeta}>{f.binary ? "binary" : `+${f.added} −${f.removed}`}</span>
|
||||
<span className={styles.commitMeta}>{f.binary ? tx("binary") : `+${f.added} −${f.removed}`}</span>
|
||||
{!f.applies && <Icon name="warning" size={14} label="Does not apply cleanly" />}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -425,7 +425,7 @@ function PushDialog({ target: pushTarget, onClose }: { target: PushTarget; onClo
|
||||
rowHeight={22}
|
||||
aria-label="Outgoing commits"
|
||||
className={styles.boxList}
|
||||
empty={<span>{upstream ? tx("Everything is up to date") : "No commits"}</span>}
|
||||
empty={<span>{upstream ? tx("Everything is up to date") : tx("No commits")}</span>}
|
||||
renderItem={(c) => (
|
||||
<span className={styles.commitRow} style={{ flex: 1, padding: 0 }}>
|
||||
<span className={styles.commitHash}>{c.shortId}</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Branches popup model for a project with several repositories (pure). */
|
||||
import type { Branch } from "@/bindings";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { fuzzyMatch } from "@/ui/match";
|
||||
import { rootOf } from "./model";
|
||||
import { isFavoriteBranch, MAX_RECENT, sortLocal, sortRemote, type PopupAction } from "./branches";
|
||||
@@ -23,7 +24,7 @@ export function repoColorIndex(index: number): number {
|
||||
/** Display name of a repository's HEAD: its branch, else the short commit. */
|
||||
export function repoHeadLabel(r: Pick<RepoEntry, "head" | "headCommit">): string {
|
||||
if (r.head) return r.head;
|
||||
return r.headCommit ? r.headCommit.slice(0, 8) : "No commits";
|
||||
return r.headCommit ? r.headCommit.slice(0, 8) : tx("No commits");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,7 +168,7 @@ export function buildMultiRepoRows({
|
||||
|
||||
const actionRows: RepoRow[] = [];
|
||||
for (const a of actions) {
|
||||
const m = match(a.label);
|
||||
const m = match(tx(a.label));
|
||||
if (m !== false) actionRows.push({ type: "action", key: `action:${a.id}`, action: a, matches: m });
|
||||
}
|
||||
blocks.push(actionRows);
|
||||
@@ -225,7 +226,7 @@ export function buildMultiRepoRows({
|
||||
? []
|
||||
: [{ type: "common", key: `common:${b.name}`, branch: b, label: b.name, depth: 1, matches: m }];
|
||||
});
|
||||
group(groups, "common:local", "Common Local Branches", 0, local);
|
||||
group(groups, "common:local", tx("Common Local Branches"), 0, local);
|
||||
const byRemote = new Map<string, CommonBranch[]>();
|
||||
for (const b of common.remote) {
|
||||
const remote = b.name.slice(0, Math.max(0, b.name.indexOf("/")));
|
||||
@@ -243,7 +244,7 @@ export function buildMultiRepoRows({
|
||||
});
|
||||
group(remote, `common:folder:${name}`, name, 1, leaves);
|
||||
}
|
||||
group(groups, "common:remote", "Common Remote Branches", 0, remote);
|
||||
group(groups, "common:remote", tx("Common Remote Branches"), 0, remote);
|
||||
} else if (currentRepo) {
|
||||
const list = branches[currentRepo.root] ?? [];
|
||||
const local = sortLocal(list, favorites);
|
||||
@@ -255,12 +256,12 @@ export function buildMultiRepoRows({
|
||||
.filter((b): b is Branch => !!b && !b.current)
|
||||
.slice(0, MAX_RECENT);
|
||||
const name = currentRepo.name;
|
||||
group(groups, "repo:recent", `Recent Branches in ${name}`, 0, branchRows(currentRepo, recentList, "recent", 1));
|
||||
group(groups, "repo:local", `Local Branches in ${name}`, 0, branchRows(currentRepo, local, "local", 1));
|
||||
group(groups, "repo:recent", tx`Recent Branches in ${name}`, 0, branchRows(currentRepo, recentList, "recent", 1));
|
||||
group(groups, "repo:local", tx`Local Branches in ${name}`, 0, branchRows(currentRepo, local, "local", 1));
|
||||
group(
|
||||
groups,
|
||||
"repo:remote",
|
||||
`Remote Branches in ${name}`,
|
||||
tx`Remote Branches in ${name}`,
|
||||
0,
|
||||
branchRows(currentRepo, sortRemote(list, favorites), "remote", 1),
|
||||
);
|
||||
@@ -272,7 +273,7 @@ export function buildMultiRepoRows({
|
||||
? []
|
||||
: [{ type: "tag", key: `tag:${currentRepo.root}:${t}`, repo: currentRepo, tag: t, depth: 1, matches: m }];
|
||||
});
|
||||
group(groups, "repo:tags", `Tags in ${name}`, 0, tagRows);
|
||||
group(groups, "repo:tags", tx`Tags in ${name}`, 0, tagRows);
|
||||
}
|
||||
blocks.push(groups);
|
||||
|
||||
|
||||
@@ -355,7 +355,7 @@ export function ShelfPanel() {
|
||||
}}
|
||||
onContextMenu={(node, _sel, e: MouseEvent) => node && showContextMenu(e, shelfMenu(node.entry))}
|
||||
aria-label="Shelved changes"
|
||||
empty={<span className={viewStyles.empty}>{loaded ? tx("No shelved changes") : "Loading…"}</span>}
|
||||
empty={<span className={viewStyles.empty}>{loaded ? tx("No shelved changes") : tx("Loading…")}</span>}
|
||||
renderNode={(n) =>
|
||||
n.file ? (
|
||||
<>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* "Use pretty formatting" (Today / Yesterday / N min ago). Used by the Git
|
||||
* log, Local History, commit details and blame.
|
||||
*/
|
||||
import { tx } from "./i18n";
|
||||
import { useProject } from "./project";
|
||||
|
||||
export interface DateFormatOptions {
|
||||
@@ -69,11 +70,11 @@ export function formatTimestamp(ms: number, now = Date.now(), o: DateFormatOptio
|
||||
if (o.pretty) {
|
||||
const diff = now - ms;
|
||||
const min = 60_000;
|
||||
if (diff >= 0 && diff < min) return "Moments ago";
|
||||
if (diff >= 0 && diff < 60 * min) return `${Math.floor(diff / min)} min ago`;
|
||||
if (diff >= 0 && diff < min) return tx("Moments ago");
|
||||
if (diff >= 0 && diff < 60 * min) return tx`${Math.floor(diff / min)} min ago`;
|
||||
const today = new Date(now);
|
||||
if (d.toDateString() === today.toDateString()) return `Today ${formatClock(d, o)}`;
|
||||
if (d.toDateString() === new Date(now - 86_400_000).toDateString()) return `Yesterday ${formatClock(d, o)}`;
|
||||
if (d.toDateString() === today.toDateString()) return tx`Today ${formatClock(d, o)}`;
|
||||
if (d.toDateString() === new Date(now - 86_400_000).toDateString()) return tx`Yesterday ${formatClock(d, o)}`;
|
||||
return formatDay(d, o);
|
||||
}
|
||||
return `${formatDay(d, o)} ${formatClock(d, o)}`;
|
||||
|
||||
@@ -4383,5 +4383,94 @@
|
||||
"{n} hours ago": "{n} 小时前",
|
||||
"{n} minute ago": "{n} 分钟前",
|
||||
"{n} minutes ago": "{n} 分钟前",
|
||||
"just now": "刚刚"
|
||||
"just now": "刚刚",
|
||||
"No symbols": "没有符号",
|
||||
"{behind} incoming, {ahead} outgoing commits": "{behind} 个传入提交,{ahead} 个传出提交",
|
||||
"{behind} incoming commits": "{behind} 个传入提交",
|
||||
"{ahead} outgoing commits": "{ahead} 个传出提交",
|
||||
"Repository: {repo}": "仓库:{repo}",
|
||||
"Git branch: {head}": "Git 分支:{head}",
|
||||
"Tracking {upstream}": "跟踪 {upstream}",
|
||||
"In all {count} repositories": "在全部 {count} 个仓库中",
|
||||
"{current} of {count} differences": "第 {current} 处差异,共 {count} 处",
|
||||
"{count} difference": "{count} 处差异",
|
||||
"{count} differences": "{count} 处差异",
|
||||
"{left} conflict left": "剩余 {left} 个冲突",
|
||||
"{left} conflicts left": "剩余 {left} 个冲突",
|
||||
"Cloning…": "正在克隆…",
|
||||
"locked": "已锁定",
|
||||
"prunable": "可清理",
|
||||
"Rebasing": "正在变基",
|
||||
"Merging": "正在合并",
|
||||
"Cherry-picking": "正在拣选",
|
||||
"Reverting": "正在还原",
|
||||
"Applying patches": "正在应用补丁",
|
||||
"Rebasing {head}": "正在变基 {head}",
|
||||
"Merging {head}": "正在合并到 {head}",
|
||||
"Cherry-picking {head}": "正在拣选到 {head}",
|
||||
"Reverting {head}": "正在 {head} 上还原",
|
||||
"Applying patches to {head}": "正在向 {head} 应用补丁",
|
||||
"Rebase in progress": "变基进行中",
|
||||
"Merge in progress": "合并进行中",
|
||||
"Cherry-pick in progress": "拣选进行中",
|
||||
"Revert in progress": "还原进行中",
|
||||
"Patch application in progress": "补丁应用进行中",
|
||||
"{conflicts} conflict": "{conflicts} 个冲突",
|
||||
"{conflicts} conflicts": "{conflicts} 个冲突",
|
||||
"Continuing rebase": "正在继续变基",
|
||||
"Continuing merge": "正在继续合并",
|
||||
"Continuing cherry-pick": "正在继续拣选",
|
||||
"Continuing revert": "正在继续还原",
|
||||
"Continuing patch application": "正在继续应用补丁",
|
||||
"Skipping commit": "正在跳过提交",
|
||||
"Aborting rebase": "正在中止变基",
|
||||
"Aborting merge": "正在中止合并",
|
||||
"Aborting cherry-pick": "正在中止拣选",
|
||||
"Aborting revert": "正在中止还原",
|
||||
"Aborting patch application": "正在中止补丁应用",
|
||||
"Rebase continued": "变基已继续",
|
||||
"Merge continued": "合并已继续",
|
||||
"Cherry-pick continued": "拣选已继续",
|
||||
"Revert continued": "还原已继续",
|
||||
"Patch application continued": "补丁应用已继续",
|
||||
"Commit skipped": "已跳过提交",
|
||||
"Rebase aborted": "变基已中止",
|
||||
"Merge aborted": "合并已中止",
|
||||
"Cherry-pick aborted": "拣选已中止",
|
||||
"Revert aborted": "还原已中止",
|
||||
"Patch application aborted": "补丁应用已中止",
|
||||
"Rebase failed": "变基失败",
|
||||
"Cherry-pick failed": "拣选失败",
|
||||
"Revert failed": "还原失败",
|
||||
"Reset failed": "重置失败",
|
||||
"Rebase stopped for editing; continue the rebase when done": "变基已停止以便编辑;完成后请继续变基",
|
||||
"Cherry-picked {label}": "已拣选 {label}",
|
||||
"Reverted {label}": "已还原 {label}",
|
||||
"Reset {head} to {rev} ({mode})": "已将 {head} 重置到 {rev}({mode})",
|
||||
"Resetting": "正在重置",
|
||||
"Fetching": "正在获取",
|
||||
"Updating project": "正在更新项目",
|
||||
"Pulling": "正在拉取",
|
||||
"Pushing": "正在推送",
|
||||
"Deleting remote branch": "正在删除远程分支",
|
||||
"Updating branch": "正在更新分支",
|
||||
"Delete remote branch": "删除远程分支",
|
||||
"binary": "二进制",
|
||||
"Common Local Branches": "公共本地分支",
|
||||
"Common Remote Branches": "公共远程分支",
|
||||
"Recent Branches in {name}": "{name} 中的最近分支",
|
||||
"Local Branches in {name}": "{name} 中的本地分支",
|
||||
"Remote Branches in {name}": "{name} 中的远程分支",
|
||||
"Tags in {name}": "{name} 中的标签",
|
||||
"Moments ago": "刚刚",
|
||||
"{min} min ago": "{min} 分钟前",
|
||||
"Today {time}": "今天 {time}",
|
||||
"Yesterday {time}": "昨天 {time}",
|
||||
"Added lines": "新增的行",
|
||||
"Deleted lines": "删除的行",
|
||||
"Added": "已添加",
|
||||
"Renamed": "已重命名",
|
||||
"Unversioned": "未跟踪",
|
||||
"Merge conflict": "合并冲突",
|
||||
"by {author}": "由 {author} 创建"
|
||||
}
|
||||
@@ -196,7 +196,6 @@ const UNTRANSLATED = new Set<string>([
|
||||
"info",
|
||||
"languageServer",
|
||||
"process",
|
||||
"read-only",
|
||||
"Running processes requires the desktop app.\n",
|
||||
"text",
|
||||
]);
|
||||
|
||||
Reference in new issue
Заблокувати користувача