feat(ui): localize commit AI and git error messages to English
Translate commit AI prompts, model labels, and git/keychain errors to English so the app and generated messages are consistent. Also add a discard confirmation dialog and update the UI text/styles to match the new flow. - src-tauri/crates/commit_ai/src/cloud.rs - Translate HTTP and API error messages to English. - Keep request timeout and token sizing behavior unchanged. - src-tauri/crates/commit_ai/src/lib.rs - Translate model labels, prompt text, and validation errors. - Keep diff truncation and message sanitization logic intact. - src-tauri/src/git.rs - Translate git, credential, merge, and history errors. - Update AI provider validation messages to English. - src/lib/components/* - Update AI settings, commit panel, credential, and loading UI text. - Add discard confirmation dialog for destructive actions. - src/App.svelte, src/app.css - Adjust app layout and styling for the new dialog and text changes.
This commit is contained in:
+153
-16
@@ -11,6 +11,7 @@
|
||||
import CompareDialog from "./lib/components/CompareDialog.svelte";
|
||||
import CompareSelectDialog from "./lib/components/CompareSelectDialog.svelte";
|
||||
import CredentialDialog from "./lib/components/CredentialDialog.svelte";
|
||||
import DiscardConfirmDialog from "./lib/components/DiscardConfirmDialog.svelte";
|
||||
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
||||
import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte";
|
||||
import GlobalSearchDialog from "./lib/components/GlobalSearchDialog.svelte";
|
||||
@@ -96,6 +97,9 @@
|
||||
|
||||
type UpdateToastState = "available" | "downloading" | "installed" | "error";
|
||||
type AppView = "management" | "repository";
|
||||
type PendingDiscard =
|
||||
| { kind: "file"; file: GitFileStatus; staged: boolean }
|
||||
| { kind: "hunk"; file: GitFileStatus; staged: boolean; action: PatchApplyAction; patch: string };
|
||||
|
||||
interface RepoTab {
|
||||
path: string;
|
||||
@@ -110,6 +114,10 @@
|
||||
const OPEN_REPOS_KEY = "gitlite.openRepos.v1";
|
||||
const RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
|
||||
const AI_SETTINGS_KEY = "gitlite.aiSettings.v1";
|
||||
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
||||
const COMMIT_PANEL_DEFAULT_HEIGHT = 220;
|
||||
const COMMIT_PANEL_MIN_HEIGHT = COMMIT_PANEL_DEFAULT_HEIGHT;
|
||||
const COMMIT_PANEL_MAX_HEIGHT = 640;
|
||||
|
||||
// ── State ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -157,6 +165,7 @@
|
||||
let linePatchText = "";
|
||||
let linePatchLoading = false;
|
||||
let linePatchError = "";
|
||||
let pendingDiscard: PendingDiscard | null = null;
|
||||
let globalSearchOpen = false;
|
||||
let lastSearchQuery = "";
|
||||
let globalSearchResults: GitSearchHit[] = [];
|
||||
@@ -186,6 +195,10 @@
|
||||
let updateCheckInFlight = false;
|
||||
let updateDownloadTotal = 0;
|
||||
let updateDownloadedBytes = 0;
|
||||
let commitPanelHeight = loadCommitPanelHeight();
|
||||
let resizingCommitPanel = false;
|
||||
let resizeStartY = 0;
|
||||
let resizeStartHeight = 0;
|
||||
|
||||
// ── Derived ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -542,6 +555,56 @@
|
||||
}
|
||||
}
|
||||
|
||||
function clampCommitPanelHeight(value: number): number {
|
||||
return Math.min(COMMIT_PANEL_MAX_HEIGHT, Math.max(COMMIT_PANEL_MIN_HEIGHT, Math.round(value)));
|
||||
}
|
||||
|
||||
function loadCommitPanelHeight(): number {
|
||||
try {
|
||||
const stored = Number(localStorage.getItem(COMMIT_PANEL_HEIGHT_KEY));
|
||||
if (Number.isFinite(stored) && stored > 0) return clampCommitPanelHeight(stored);
|
||||
} catch {
|
||||
// Fall through to the default below.
|
||||
}
|
||||
return COMMIT_PANEL_DEFAULT_HEIGHT;
|
||||
}
|
||||
|
||||
function persistCommitPanelHeight(value: number) {
|
||||
try {
|
||||
localStorage.setItem(COMMIT_PANEL_HEIGHT_KEY, String(value));
|
||||
} catch {
|
||||
// Local storage is best-effort only; resizing must keep working without it.
|
||||
}
|
||||
}
|
||||
|
||||
function startCommitPanelResize(event: PointerEvent) {
|
||||
event.preventDefault();
|
||||
resizingCommitPanel = true;
|
||||
resizeStartY = event.clientY;
|
||||
resizeStartHeight = commitPanelHeight;
|
||||
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
function onCommitPanelResizeMove(event: PointerEvent) {
|
||||
if (!resizingCommitPanel) return;
|
||||
commitPanelHeight = clampCommitPanelHeight(resizeStartHeight + (resizeStartY - event.clientY));
|
||||
}
|
||||
|
||||
function endCommitPanelResize(event: PointerEvent) {
|
||||
if (!resizingCommitPanel) return;
|
||||
resizingCommitPanel = false;
|
||||
persistCommitPanelHeight(commitPanelHeight);
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
function onCommitPanelResizeKeydown(event: KeyboardEvent) {
|
||||
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
|
||||
event.preventDefault();
|
||||
commitPanelHeight = clampCommitPanelHeight(commitPanelHeight + (event.key === "ArrowUp" ? 20 : -20));
|
||||
persistCommitPanelHeight(commitPanelHeight);
|
||||
}
|
||||
|
||||
function rememberRecentRepo(path: string) {
|
||||
recentRepoPaths = uniqueRepoPaths([path, ...recentRepoPaths]).slice(0, 40);
|
||||
persistRepoLists();
|
||||
@@ -692,7 +755,7 @@
|
||||
}
|
||||
|
||||
function isCancellationMessage(message: string): boolean {
|
||||
return message.toLowerCase().includes("abgebrochen");
|
||||
return message.toLowerCase().includes("cancelled");
|
||||
}
|
||||
|
||||
function cancelActiveFileHistoryLoad() {
|
||||
@@ -766,7 +829,7 @@
|
||||
if (isBusy) return;
|
||||
try {
|
||||
const selected = await openDialog({
|
||||
title: "Repository folder auswaehlen",
|
||||
title: "Select repository folder",
|
||||
directory: true,
|
||||
multiple: false,
|
||||
defaultPath: repoPath.trim() || activeRepoPath || undefined,
|
||||
@@ -975,7 +1038,7 @@
|
||||
if (auth) {
|
||||
if (key) void credDelete(key).catch(() => {});
|
||||
credDialogError =
|
||||
"Zugangsdaten wurden abgelehnt oder sind abgelaufen. Bitte erneut anmelden.";
|
||||
"Credentials were rejected or have expired. Please sign in again.";
|
||||
credDialogAction = action;
|
||||
credDialogKey = key;
|
||||
credDialogOpen = true;
|
||||
@@ -984,7 +1047,7 @@
|
||||
errorMessage = message;
|
||||
}
|
||||
} else {
|
||||
credDialogError = message || "Anmeldung fehlgeschlagen.";
|
||||
credDialogError = message || "Sign-in failed.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1022,11 +1085,11 @@
|
||||
if (errorMessage && !isAuthError(errorMessage) && isNonFastForwardPushError(errorMessage)) {
|
||||
errorMessage = "";
|
||||
const shouldSync = window.confirm(
|
||||
"Der Remote hat neuere Commits, deshalb wurde der Push abgelehnt.\n\nJetzt Pull/Merge ausfuehren und danach den Push erneut versuchen?",
|
||||
"The remote has newer commits, so the push was rejected.\n\nRun Pull/Merge now and try pushing again afterwards?",
|
||||
);
|
||||
|
||||
if (!shouldSync) {
|
||||
const message = "Push abgelehnt: Der Remote hat neuere Commits. Pull zuerst ausfuehren, dann erneut pushen.";
|
||||
const message = "Push rejected: the remote has newer commits. Pull first, then push again.";
|
||||
if (fromStore) errorMessage = message;
|
||||
else credDialogError = message;
|
||||
return;
|
||||
@@ -1050,7 +1113,7 @@
|
||||
if (statusHasConflicts(status)) {
|
||||
credDialogOpen = false;
|
||||
credDialogAction = null;
|
||||
errorMessage = "Pull hat Merge-Konflikte erzeugt. Loese die Konflikte, committe den Merge und pushe danach erneut.";
|
||||
errorMessage = "Pull produced merge conflicts. Resolve the conflicts, commit the merge, and then push again.";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1125,7 +1188,12 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function discardFile(file: GitFileStatus, staged: boolean) {
|
||||
function discardFile(file: GitFileStatus, staged: boolean) {
|
||||
if (!activeRepoPath || isBusy) return;
|
||||
pendingDiscard = { kind: "file", file, staged };
|
||||
}
|
||||
|
||||
async function runDiscardFile(file: GitFileStatus, staged: boolean) {
|
||||
await runOperation(`Discarding ${file.path}`, async () => {
|
||||
applyStatus(await restoreFiles(activeRepoPath, [file.path], staged));
|
||||
await refreshExplorerFiles(activeRepoPath);
|
||||
@@ -1176,9 +1244,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function applyLinePatch(action: PatchApplyAction, patch: string) {
|
||||
if (!activeRepoPath || !linePatchFile || isBusy) return;
|
||||
const file = linePatchFile;
|
||||
function isDiscardPatchAction(action: PatchApplyAction): boolean {
|
||||
return action === "discard-staged" || action === "discard-unstaged";
|
||||
}
|
||||
|
||||
async function runLinePatchAction(
|
||||
action: PatchApplyAction,
|
||||
patch: string,
|
||||
file: GitFileStatus,
|
||||
staged: boolean,
|
||||
) {
|
||||
if (!activeRepoPath || isBusy) return;
|
||||
operation = patchOperationLabel(action, file);
|
||||
errorMessage = "";
|
||||
linePatchError = "";
|
||||
@@ -1188,7 +1264,7 @@
|
||||
await refreshExplorerFiles(activeRepoPath);
|
||||
await refreshFileHistory(activeRepoPath);
|
||||
|
||||
const updatedPatch = await getFilePatch(activeRepoPath, file.path, linePatchStaged);
|
||||
const updatedPatch = await getFilePatch(activeRepoPath, file.path, staged);
|
||||
if (updatedPatch.trim()) {
|
||||
linePatchText = updatedPatch;
|
||||
} else {
|
||||
@@ -1204,6 +1280,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function applyLinePatch(action: PatchApplyAction, patch: string) {
|
||||
if (!activeRepoPath || !linePatchFile || isBusy) return;
|
||||
const file = linePatchFile;
|
||||
const staged = linePatchStaged;
|
||||
|
||||
if (isDiscardPatchAction(action)) {
|
||||
pendingDiscard = { kind: "hunk", file, staged, action, patch };
|
||||
return;
|
||||
}
|
||||
|
||||
await runLinePatchAction(action, patch, file, staged);
|
||||
}
|
||||
|
||||
async function confirmDiscard() {
|
||||
const discard = pendingDiscard;
|
||||
if (!discard || !activeRepoPath || isBusy) return;
|
||||
|
||||
if (discard.kind === "file") {
|
||||
await runDiscardFile(discard.file, discard.staged);
|
||||
} else {
|
||||
await runLinePatchAction(discard.action, discard.patch, discard.file, discard.staged);
|
||||
}
|
||||
|
||||
pendingDiscard = null;
|
||||
}
|
||||
|
||||
function closeDiscardConfirm() {
|
||||
if (isBusy) return;
|
||||
pendingDiscard = null;
|
||||
}
|
||||
|
||||
async function stageAllFiles() {
|
||||
const paths = changedFiles.filter((f) => f.unstaged !== null).map((f) => f.path);
|
||||
if (paths.length === 0) return;
|
||||
@@ -1441,7 +1548,7 @@
|
||||
} catch (error) {
|
||||
if (globalSearchId === searchId) {
|
||||
const message = errorToMessage(error);
|
||||
globalSearchError = message.includes("abgebrochen") ? "Suche wurde abgebrochen." : message;
|
||||
globalSearchError = message.includes("cancelled") ? "Search was cancelled." : message;
|
||||
}
|
||||
} finally {
|
||||
if (globalSearchId === searchId) {
|
||||
@@ -1454,7 +1561,7 @@
|
||||
async function cancelGlobalSearch() {
|
||||
if (!globalSearchId) return;
|
||||
const searchId = globalSearchId;
|
||||
globalSearchError = "Abbruch wird angefordert...";
|
||||
globalSearchError = "Requesting cancellation...";
|
||||
try {
|
||||
await cancelCodeSearch(searchId);
|
||||
} catch (error) {
|
||||
@@ -1533,7 +1640,8 @@
|
||||
// ── Event handlers ─────────────────────────────────────────────────────────
|
||||
|
||||
function handleWindowKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape" && compareDialogOpen) closeCompareDialog();
|
||||
if (event.key === "Escape" && pendingDiscard && !isBusy) closeDiscardConfirm();
|
||||
else if (event.key === "Escape" && compareDialogOpen) closeCompareDialog();
|
||||
else if (event.key === "Escape" && newBranchCommit) newBranchCommit = null;
|
||||
else if (event.key === "Escape" && renameBranchTarget) renameBranchTarget = null;
|
||||
else if (event.key === "Escape" && compareSelectOpen) compareSelectOpen = false;
|
||||
@@ -1813,7 +1921,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="top-section">
|
||||
<div class="top-section" style="--commit-panel-height: {commitPanelHeight}px;">
|
||||
<StatusPanel
|
||||
{changedFiles}
|
||||
{stagedCount}
|
||||
@@ -1830,6 +1938,24 @@
|
||||
onStageAll={stageAllFiles}
|
||||
onUnstageAll={unstageAllFiles}
|
||||
/>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class="panel-resize-handle"
|
||||
class:resizing={resizingCommitPanel}
|
||||
role="separator"
|
||||
aria-orientation="horizontal"
|
||||
aria-label="Resize commit panel height"
|
||||
aria-valuenow={commitPanelHeight}
|
||||
aria-valuemin={COMMIT_PANEL_MIN_HEIGHT}
|
||||
aria-valuemax={COMMIT_PANEL_MAX_HEIGHT}
|
||||
tabindex="0"
|
||||
onpointerdown={startCommitPanelResize}
|
||||
onpointermove={onCommitPanelResizeMove}
|
||||
onpointerup={endCommitPanelResize}
|
||||
onpointercancel={endCommitPanelResize}
|
||||
onkeydown={onCommitPanelResizeKeydown}
|
||||
></div>
|
||||
<CommitPanel
|
||||
{commitMessage}
|
||||
{canCommit}
|
||||
@@ -1908,6 +2034,17 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if pendingDiscard}
|
||||
<DiscardConfirmDialog
|
||||
file={pendingDiscard.file}
|
||||
staged={pendingDiscard.staged}
|
||||
scope={pendingDiscard.kind === "hunk" ? "hunk" : "file"}
|
||||
{isBusy}
|
||||
onConfirm={confirmDiscard}
|
||||
onClose={closeDiscardConfirm}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if globalSearchOpen}
|
||||
<GlobalSearchDialog
|
||||
{hasRepository}
|
||||
|
||||
Reference in New Issue
Block a user