feat(confirm): centralize confirmation dialogs and add i18n
Introduce a generic ConfirmDialog and a promise-based requestConfirmation API in App.svelte so callers can await user responses instead of using window.confirm. Provide helper builders (branchDeleteConfirmRequest, discardConfirmRequest) to create dialog content for common cases. Many call sites were switched to use requestConfirmation and now render the in-app ConfirmDialog; the previous specialized confirm components (BranchDeleteConfirmDialog, DiscardConfirmDialog) were removed. Add lightweight i18n support (setLanguage, t()) and new messages/i18n modules, and replace hardcoded English strings in several components (e.g. AiSettingsPage, BlameDialog and many confirmation prompts) with translated keys. Summary of effects: - Replaces native window.confirm with awaitable in-app ConfirmDialog dialogs. - Centralizes confirmation UI and content construction in App.svelte. - Adds i18n plumbing and updates UI text to use t(). - Removes two specialized confirm dialog components and adds src/lib/components/ConfirmDialog.svelte.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { Cherry, ChevronDown, ChevronRight, CloudOff, EllipsisVertical, GitBranch, GitMerge, LoaderCircle, RotateCcw, StickyNote, Tag, X } from "@lucide/svelte";
|
||||
import { visibleParentResolver } from "../graphParents";
|
||||
import type { FileStatusKind, GitCommit, GitCommitFile } from "../types";
|
||||
import { t } from "../i18n.svelte";
|
||||
|
||||
interface GraphSegment {
|
||||
fromCol: number;
|
||||
@@ -310,11 +311,11 @@
|
||||
|
||||
function commitHoverTitle(commit: GitCommit, row: GraphRow | undefined): string {
|
||||
const directBranches = graphBranchRefs(commit).filter(branchIsVisible);
|
||||
if (directBranches.length > 0) return `Branches: ${directBranches.join(", ")}`;
|
||||
if (directBranches.length > 0) return t("history.hoverBranches", { list: directBranches.join(", ") });
|
||||
|
||||
const containingBranches = (row?.branchLabels ?? []).filter(branchIsVisible);
|
||||
if (containingBranches.length === 0) return commit.short_hash;
|
||||
return `Branches containing this commit: ${containingBranches.join(", ")}`;
|
||||
return t("history.hoverContaining", { list: containingBranches.join(", ") });
|
||||
}
|
||||
|
||||
function segmentIsVisible(segment: GraphSegment): boolean {
|
||||
@@ -516,7 +517,7 @@
|
||||
const note = await onLoadCommitNote(commit);
|
||||
notePreviews = {
|
||||
...notePreviews,
|
||||
[commit.hash]: note?.trim() || "This Git note is empty.",
|
||||
[commit.hash]: note?.trim() || t("history.noteEmpty"),
|
||||
};
|
||||
} catch {
|
||||
const nextErrors = new Set(notePreviewErrors);
|
||||
@@ -684,14 +685,14 @@
|
||||
function branchDecorationTitle(branch: CommitBranchDecoration): string {
|
||||
if (branch.localOnly) {
|
||||
const status = branchStatusLabel(branch);
|
||||
return `${branch.label} · Local only — not published yet${status ? ` · ${status}` : ""}`;
|
||||
return `${t("history.branchLocalOnlyTitle", { name: branch.label })}${status ? ` · ${status}` : ""}`;
|
||||
}
|
||||
const status = branchStatusLabel(branch);
|
||||
if (branch.trackedRemote) {
|
||||
return `${branch.label} · Tracks ${branch.trackedRemote}${status ? ` · ${status}` : ""}`;
|
||||
return `${t("history.branchTracksTitle", { name: branch.label, upstream: branch.trackedRemote })}${status ? ` · ${status}` : ""}`;
|
||||
}
|
||||
if (status) return `${branch.label} · ${status}`;
|
||||
return branch.kind === "remote" ? `Remote branch ${branch.label}` : `Local branch ${branch.label}`;
|
||||
return branch.kind === "remote" ? t("history.branchRemoteTitle", { name: branch.label }) : t("history.branchLocalTitle", { name: branch.label });
|
||||
}
|
||||
|
||||
function formatCommitDate(value: string): string {
|
||||
@@ -778,11 +779,11 @@
|
||||
|
||||
<svelte:window onclick={closeCommitContextMenu} onkeydown={handleWindowKeydown} on:contextmenu|capture={closeCommitContextMenu} />
|
||||
|
||||
<section bind:this={panelElement} class="panel history-panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Commit history">
|
||||
<section bind:this={panelElement} class="panel history-panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label={t("history.panelLabel")}>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<span class="eyebrow">History</span>
|
||||
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Commits</h2>
|
||||
<span class="eyebrow">{t("history.eyebrow")}</span>
|
||||
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">{t("history.title")}</h2>
|
||||
</div>
|
||||
{#if graphBranchNames.length > 0}
|
||||
<div class="section-head-actions">
|
||||
@@ -790,8 +791,8 @@
|
||||
class="graph-branch-dialog-button"
|
||||
type="button"
|
||||
onclick={openBranchDialog}
|
||||
title="Customize visible branches"
|
||||
aria-label={`${visibleBranchCount} of ${graphBranchNames.length} branches visible. Customize branches.`}
|
||||
title={t("history.customizeBranches")}
|
||||
aria-label={t("history.visibleBranches", { visible: visibleBranchCount, total: graphBranchNames.length })}
|
||||
>
|
||||
<GitBranch size={13} aria-hidden="true" />
|
||||
Branches
|
||||
@@ -802,13 +803,13 @@
|
||||
</div>
|
||||
|
||||
{#if !hasRepository}
|
||||
<div class="blank-state">No repository loaded.</div>
|
||||
<div class="blank-state">{t("history.noRepo")}</div>
|
||||
{:else if commits.length === 0}
|
||||
<div class="blank-state">No commits returned.</div>
|
||||
<div class="blank-state">{t("history.noCommits")}</div>
|
||||
{:else}
|
||||
<div class="history-list graph-list overflow-auto">
|
||||
{#if visibleCommits.length === 0}
|
||||
<div class="blank-state">No loaded commits match the selected branches.</div>
|
||||
<div class="blank-state">{t("history.noMatchingCommits")}</div>
|
||||
{/if}
|
||||
{#each visibleCommitEntries as entry, rowIndex (entry.commit.hash)}
|
||||
{@const item = entry.commit}
|
||||
@@ -884,7 +885,7 @@
|
||||
{/if}
|
||||
{#if refSummary.primaryBranch || refSummary.primaryTag || refSummary.overflowCount > 0}
|
||||
<div class="commit-ref-area">
|
||||
<div class="commit-ref-strip" aria-label="Commit references">
|
||||
<div class="commit-ref-strip" aria-label={t("history.refs")}>
|
||||
{#if refSummary.primaryBranch}
|
||||
<span class="branch-ref-cluster" class:local-only={refSummary.primaryBranch.localOnly}>
|
||||
<span
|
||||
@@ -900,14 +901,14 @@
|
||||
{/if}
|
||||
</span>
|
||||
{#if refSummary.primaryBranch.localOnly}
|
||||
<span class="compact-ref-local-marker" title="This branch exists only locally and has not been published yet">
|
||||
LOCAL
|
||||
<span class="compact-ref-local-marker" title={t("history.localOnlyHint")}>
|
||||
{t("history.localOnlyBadge")}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
{#if refSummary.primaryTag}
|
||||
<span class="compact-ref-chip tag" title={`Tag ${refSummary.primaryTag}`}>
|
||||
<span class="compact-ref-chip tag" title={t("history.tagTitle", { name: refSummary.primaryTag })}>
|
||||
<Tag size={10} aria-hidden="true" />
|
||||
<span>{refSummary.primaryTag}</span>
|
||||
</span>
|
||||
@@ -919,7 +920,7 @@
|
||||
onclick={() => toggleCommitRefs(item)}
|
||||
aria-expanded={expandedRefsCommitHash === item.hash}
|
||||
aria-controls={`commit-refs-${item.hash}`}
|
||||
title={`Show ${refSummary.overflowCount} more ${refSummary.overflowCount === 1 ? "reference" : "references"}`}
|
||||
title={refSummary.overflowCount === 1 ? t("history.showMoreRefsOne") : t("history.showMoreRefs", { count: refSummary.overflowCount })}
|
||||
>
|
||||
+{refSummary.overflowCount}
|
||||
</button>
|
||||
@@ -928,17 +929,17 @@
|
||||
|
||||
{#if refSummary.overflowCount > 0 && expandedRefsCommitHash === item.hash}
|
||||
<div class="commit-ref-details" id={`commit-refs-${item.hash}`}>
|
||||
<strong>References on this commit</strong>
|
||||
<strong>{t("history.refsOnCommit")}</strong>
|
||||
{#if refSummary.branches.some((branch) => branch.kind !== "remote")}
|
||||
<section>
|
||||
<span>Local</span>
|
||||
<span>{t("common.local")}</span>
|
||||
<div>
|
||||
{#each refSummary.branches.filter((branch) => branch.kind !== "remote") as branch}
|
||||
<span class="commit-ref-detail-item local" title={branchDecorationTitle(branch)}>
|
||||
<i aria-hidden="true"></i>{branch.label}
|
||||
{#if branch.current}<small>Current</small>{/if}
|
||||
{#if branch.current}<small>{t("history.current")}</small>{/if}
|
||||
{#if branch.trackedRemote}<small>{branch.trackedRemote}</small>{/if}
|
||||
{#if branch.localOnly}<small class="local-only"><CloudOff size={9} aria-hidden="true" />Local only</small>{/if}
|
||||
{#if branch.localOnly}<small class="local-only"><CloudOff size={9} aria-hidden="true" />{t("history.localOnly")}</small>{/if}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -946,7 +947,7 @@
|
||||
{/if}
|
||||
{#if refSummary.branches.some((branch) => branch.kind === "remote")}
|
||||
<section>
|
||||
<span>Remote</span>
|
||||
<span>{t("common.remote")}</span>
|
||||
<div>
|
||||
{#each refSummary.branches.filter((branch) => branch.kind === "remote") as branch}
|
||||
<span class="commit-ref-detail-item remote"><i aria-hidden="true"></i>{branch.label}</span>
|
||||
@@ -956,7 +957,7 @@
|
||||
{/if}
|
||||
{#if refSummary.tags.length > 0}
|
||||
<section>
|
||||
<span>Tags</span>
|
||||
<span>{t("history.tags")}</span>
|
||||
<div>
|
||||
{#each refSummary.tags as tag}
|
||||
<span class="commit-ref-detail-item tag"><Tag size={10} aria-hidden="true" />{tag}</span>
|
||||
@@ -966,7 +967,7 @@
|
||||
{/if}
|
||||
{#if refSummary.other.length > 0}
|
||||
<section>
|
||||
<span>Other</span>
|
||||
<span>{t("history.other")}</span>
|
||||
<div>
|
||||
{#each refSummary.other as ref}
|
||||
<span class="commit-ref-detail-item">{ref}</span>
|
||||
@@ -1005,11 +1006,11 @@
|
||||
onfocus={() => void loadCommitNotePreview(item)}
|
||||
onclick={() => openCommitNote(item)}
|
||||
disabled={isBusy}
|
||||
aria-label={`Open Git note for ${item.short_hash}`}
|
||||
aria-label={t("history.openNote", { hash: item.short_hash })}
|
||||
aria-describedby={`commit-note-preview-${item.hash}`}
|
||||
>
|
||||
<StickyNote size={11} aria-hidden="true" />
|
||||
<span>Note</span>
|
||||
<span>{t("history.note")}</span>
|
||||
</button>
|
||||
<span
|
||||
class="commit-note-tooltip"
|
||||
@@ -1018,8 +1019,8 @@
|
||||
>
|
||||
<span class="commit-note-tooltip-head">
|
||||
<StickyNote size={12} aria-hidden="true" />
|
||||
Git Note
|
||||
<small>Click to open</small>
|
||||
{t("history.gitNote")}
|
||||
<small>{t("history.clickToOpen")}</small>
|
||||
</span>
|
||||
<span class="commit-note-tooltip-body">
|
||||
{#if notePreviewLoading.has(item.hash)}
|
||||
@@ -1054,14 +1055,14 @@
|
||||
</button>
|
||||
|
||||
{#if expandedCommitHashes.has(item.hash)}
|
||||
<div class="commit-file-list" aria-label="Changed files">
|
||||
<div class="commit-file-list" aria-label={t("history.changedFiles")}>
|
||||
{#each item.files as file (`${item.hash}:${file.old_path ?? ""}:${file.path}`)}
|
||||
<button
|
||||
class="commit-file-button"
|
||||
type="button"
|
||||
onclick={() => onPreviewCommitFile(item, file)}
|
||||
disabled={isBusy}
|
||||
title={`Show differences before restoring - ${displayCommitFile(file)}`}
|
||||
title={t("history.diffBeforeRestore", { file: displayCommitFile(file) })}
|
||||
>
|
||||
<span class={`status-badge ${file.status}`}>{statusLabel(file.status)}</span>
|
||||
<strong>{commitFileName(file)}</strong>
|
||||
@@ -1081,8 +1082,8 @@
|
||||
type="button"
|
||||
onclick={() => openCommitNote(item)}
|
||||
disabled={isBusy}
|
||||
title={`Add a Git note to ${item.short_hash}`}
|
||||
aria-label={`Add a Git note to ${item.short_hash}`}
|
||||
title={t("history.addNote", { hash: item.short_hash })}
|
||||
aria-label={t("history.addNote", { hash: item.short_hash })}
|
||||
>
|
||||
<StickyNote size={14} aria-hidden="true" />
|
||||
</button>
|
||||
@@ -1092,8 +1093,8 @@
|
||||
type="button"
|
||||
onclick={(event) => openCommitActionMenu(event, item)}
|
||||
disabled={isBusy}
|
||||
title="Commit actions"
|
||||
aria-label={`Actions for ${item.short_hash}`}
|
||||
title={t("history.commitActions")}
|
||||
aria-label={t("history.actionsFor", { hash: item.short_hash })}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={contextCommit?.hash === item.hash}
|
||||
>
|
||||
@@ -1108,13 +1109,13 @@
|
||||
<div class="history-load-more" use:observeHistoryEnd aria-live="polite">
|
||||
{#if isLoadingMore}
|
||||
<LoaderCircle class="spin" size={15} aria-hidden="true" />
|
||||
<span>Loading older commits…</span>
|
||||
<span>{t("history.loadingOlder")}</span>
|
||||
{:else if loadMoreError}
|
||||
<span title={loadMoreError}>Older commits could not be loaded.</span>
|
||||
<button type="button" class="btn-sm" onclick={() => { void onLoadMore(); }} disabled={isBusy}>Retry</button>
|
||||
<span title={loadMoreError}>{t("history.loadOlderFailed")}</span>
|
||||
<button type="button" class="btn-sm" onclick={() => { void onLoadMore(); }} disabled={isBusy}>{t("history.retry")}</button>
|
||||
{:else}
|
||||
<button type="button" class="history-load-more-button" onclick={() => { void onLoadMore(); }} disabled={isBusy}>
|
||||
Load older commits
|
||||
{t("history.loadOlder")}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1128,32 +1129,32 @@
|
||||
style={`left: ${contextMenuX}px; top: ${contextMenuY}px;`}
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
aria-label={`Actions for ${contextCommit.short_hash}`}
|
||||
aria-label={t("history.actionsFor", { hash: contextCommit.short_hash })}
|
||||
>
|
||||
<button type="button" role="menuitem" onclick={createBranchFromContextCommit} disabled={isBusy}>
|
||||
<GitBranch size={14} aria-hidden="true" />
|
||||
Branch
|
||||
{t("history.menuBranch")}
|
||||
</button>
|
||||
<button type="button" role="menuitem" onclick={openContextCommitNote} disabled={isBusy}>
|
||||
<StickyNote size={14} aria-hidden="true" />
|
||||
Note
|
||||
{t("history.note")}
|
||||
</button>
|
||||
<button type="button" role="menuitem" onclick={restoreContextCommit} disabled={isBusy}>
|
||||
<RotateCcw size={14} aria-hidden="true" />
|
||||
Restore
|
||||
{t("history.menuRestore")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onclick={cherryPickContextCommit}
|
||||
disabled={isBusy}
|
||||
title="Apply this commit's changes on top of the current branch"
|
||||
title={t("history.menuCherryPickHint")}
|
||||
>
|
||||
<Cherry size={14} aria-hidden="true" />
|
||||
Cherry-pick
|
||||
{t("history.menuCherryPick")}
|
||||
</button>
|
||||
<button type="button" role="menuitem" onclick={revertContextCommit} disabled={isBusy} title="Create a new commit that reverses this commit">
|
||||
<RotateCcw size={14} aria-hidden="true" /> Revert
|
||||
<button type="button" role="menuitem" onclick={revertContextCommit} disabled={isBusy} title={t("history.menuRevertHint")}>
|
||||
<RotateCcw size={14} aria-hidden="true" /> {t("history.menuRevert")}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1165,25 +1166,25 @@
|
||||
class="branch-filter-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Select visible branches"
|
||||
aria-label={t("history.branchDialogLabel")}
|
||||
>
|
||||
<header class="branch-filter-dialog-head unified-dialog-header">
|
||||
<span class="unified-dialog-icon" aria-hidden="true"><GitBranch size={18} /></span>
|
||||
<div class="unified-dialog-text">
|
||||
<span class="eyebrow">Git graph</span>
|
||||
<h3>Visible branches</h3>
|
||||
<span class="eyebrow">{t("history.graphEyebrow")}</span>
|
||||
<h3>{t("history.graphTitle")}</h3>
|
||||
</div>
|
||||
<button class="dialog-icon-button" type="button" onclick={closeBranchDialog} aria-label="Close branch selection">
|
||||
<button class="dialog-icon-button" type="button" onclick={closeBranchDialog} aria-label={t("history.closeBranchDialog")}>
|
||||
<X size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="branch-filter-summary">
|
||||
<span>{visibleBranchCount} of {graphBranchNames.length} branches selected</span>
|
||||
<span>{t("history.branchesSelected", { visible: visibleBranchCount, total: graphBranchNames.length })}</span>
|
||||
<div class="branch-filter-actions">
|
||||
<button type="button" onclick={showFocusGraphBranches} disabled={branchVisibilityMode === "focus"}>Focus</button>
|
||||
<button type="button" onclick={showAllGraphBranches} disabled={visibleBranchCount === graphBranchNames.length}>Show all</button>
|
||||
<button type="button" onclick={hideAllGraphBranches} disabled={visibleBranchCount === 0}>Hide all</button>
|
||||
<button type="button" onclick={showFocusGraphBranches} disabled={branchVisibilityMode === "focus"}>{t("history.focus")}</button>
|
||||
<button type="button" onclick={showAllGraphBranches} disabled={visibleBranchCount === graphBranchNames.length}>{t("history.showAll")}</button>
|
||||
<button type="button" onclick={hideAllGraphBranches} disabled={visibleBranchCount === 0}>{t("history.hideAll")}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1197,7 +1198,7 @@
|
||||
onclick={() => { localBranchGroupOpen = !localBranchGroupOpen; }}
|
||||
>
|
||||
{#if localBranchGroupOpen}<ChevronDown size={14} aria-hidden="true" />{:else}<ChevronRight size={14} aria-hidden="true" />{/if}
|
||||
<span>Local</span>
|
||||
<span>{t("common.local")}</span>
|
||||
<em>{localBranchNames.filter(branchIsVisible).length}/{localBranchNames.length}</em>
|
||||
</button>
|
||||
{#if localBranchGroupOpen}
|
||||
@@ -1226,7 +1227,7 @@
|
||||
onclick={() => { remoteBranchGroupOpen = !remoteBranchGroupOpen; }}
|
||||
>
|
||||
{#if remoteBranchGroupOpen}<ChevronDown size={14} aria-hidden="true" />{:else}<ChevronRight size={14} aria-hidden="true" />{/if}
|
||||
<span>Remote</span>
|
||||
<span>{t("common.remote")}</span>
|
||||
<em>{remoteBranchNames.filter(branchIsVisible).length}/{remoteBranchNames.length}</em>
|
||||
</button>
|
||||
{#if remoteBranchGroupOpen}
|
||||
|
||||
Reference in New Issue
Block a user