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
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-28 12:38:25 +08:00
1 джерело 443bb51bba
коміт 429e8ffc82
22 files changed
+237 -77

No files matched your search

+1 -1
Переглянути файл
@@ -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>
);
}
+3 -1
Переглянути файл
@@ -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
+2 -2
Переглянути файл
@@ -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>
+1 -1
Переглянути файл
@@ -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}
+2 -2
Переглянути файл
@@ -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} />;
+21 -15
Переглянути файл
@@ -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()}
+10 -5
Переглянути файл
@@ -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);
+8 -4
Переглянути файл
@@ -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>
) : (
+2 -6
Переглянути файл
@@ -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>
+1 -1
Переглянути файл
@@ -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>
+13
Переглянути файл
@@ -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", () => {
+2 -1
Переглянути файл
@@ -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>}
+1 -1
Переглянути файл
@@ -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>
))}
+52 -14
Переглянути файл
@@ -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")
: ""}
+7 -4
Переглянути файл
@@ -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 }],
});
+5 -3
Переглянути файл
@@ -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>
)}
+1 -1
Переглянути файл
@@ -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>
+9 -8
Переглянути файл
@@ -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);
+1 -1
Переглянути файл
@@ -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 ? (
<>
+5 -4
Переглянути файл
@@ -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)}`;
+90 -1
Переглянути файл
@@ -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} 创建"
}
-1
Переглянути файл
@@ -196,7 +196,6 @@ const UNTRANSLATED = new Set<string>([
"info",
"languageServer",
"process",
"read-only",
"Running processes requires the desktop app.\n",
"text",
]);