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:
Christoph Brandau
2026-07-02 22:12:20 +02:00
parent 6b7186d040
commit 70de45e1ba
11 changed files with 537 additions and 187 deletions
+4 -4
View File
@@ -153,9 +153,9 @@
<div class="cred-token-hint">
<AlertCircle size={13} aria-hidden="true" />
<span>
Beim Wechsel wird das Modell{selectedLocalModel ? ` (${formatSize(selectedLocalModel.approx_size_mb)})` : ""}
im Hintergrund heruntergeladen — je nach Internetverbindung kann das mehrere Minuten dauern.
Danach bleibt es lokal zwischengespeichert und lädt beim nächsten Start sofort.
Switching downloads the model{selectedLocalModel ? ` (${formatSize(selectedLocalModel.approx_size_mb)})` : ""}
in the background — depending on your internet connection this can take several minutes.
After that it stays cached locally and loads instantly on the next start.
</span>
</div>
{:else if provider === "openai"}
@@ -230,7 +230,7 @@
</div>
<div class="cred-token-hint">
<Globe size={13} aria-hidden="true" />
<span>Für lokale OpenAI-kompatible Server wie Ollama oder LM Studio. Die Basis-URL sollte auf /v1 enden.</span>
<span>For local OpenAI-compatible servers like Ollama or LM Studio. The base URL should end in /v1.</span>
</div>
{/if}
+1 -1
View File
@@ -58,7 +58,7 @@
);
</script>
<section class="panel flex flex-col" aria-label="Commit">
<section class="panel commit-panel" aria-label="Commit">
<div class="section-head">
<div>
<span class="eyebrow">Commit</span>
+18 -18
View File
@@ -44,10 +44,10 @@
(mode === "token" || username.trim().length > 0),
);
let actionLabel = $derived(action === "push" ? "Push" : "Pull");
let actionTitle = $derived(action === "push" ? "Push authentifizieren" : "Pull authentifizieren");
let actionTitle = $derived(action === "push" ? "Authenticate push" : "Authenticate pull");
let actionHint = $derived(action === "push"
? "Der Remote braucht Schreibrechte. Nutze ein Passwort oder einen Token mit passenden Repository-Rechten."
: "Der Remote braucht Zugriff auf das Repository. Nutze deine Git-Zugangsdaten oder einen Personal Access Token.");
? "The remote needs write access. Use a password or a token with the appropriate repository permissions."
: "The remote needs access to the repository. Use your Git credentials or a personal access token.");
function handleSubmit(e: SubmitEvent) {
e.preventDefault();
@@ -66,7 +66,7 @@
role="presentation"
onclick={(e) => { if (e.target === e.currentTarget) onCancel(); }}
>
<div class="cred-card" role="dialog" aria-modal="true" aria-label="Git-Zugangsdaten" tabindex="-1">
<div class="cred-card" role="dialog" aria-modal="true" aria-label="Git credentials" tabindex="-1">
<div class="cred-hero">
<div class="cred-hero-top">
<div class="cred-hero-icon">
@@ -80,7 +80,7 @@
<p class="cred-hero-label">{actionLabel} Remote</p>
<h2 class="cred-hero-title">{actionTitle}</h2>
</div>
<button class="cred-close" type="button" onclick={onCancel} title="Abbrechen" aria-label="Abbrechen">
<button class="cred-close" type="button" onclick={onCancel} title="Cancel" aria-label="Cancel">
<X size={16} aria-hidden="true" />
</button>
</div>
@@ -89,12 +89,12 @@
<div class="cred-security-note">
<ShieldCheck size={14} aria-hidden="true" />
<span>Beim Speichern landet der Token verschluesselt im Schluesselbund des Betriebssystems nie im Klartext.</span>
<span>When saved, the token is stored encrypted in the operating system's keychain — never in plain text.</span>
</div>
</div>
<form class="cred-body" onsubmit={handleSubmit}>
<div class="cred-segment" role="group" aria-label="Authentifizierungsart">
<div class="cred-segment" role="group" aria-label="Authentication method">
<button
type="button"
class="cred-seg-btn"
@@ -103,7 +103,7 @@
aria-pressed={mode === "credentials"}
>
<User size={13} aria-hidden="true" />
Username + Passwort
Username + password
</button>
<button
type="button"
@@ -127,7 +127,7 @@
id="cred-username"
type="text"
bind:value={username}
placeholder="z. B. mein-github-username"
placeholder="e.g. my-github-username"
autocomplete="username"
disabled={isBusy}
/>
@@ -137,7 +137,7 @@
<div class="cred-field">
<label class="cred-field-label" for="cred-password">
{mode === "token" ? "Token" : "Passwort"}
{mode === "token" ? "Token" : "Password"}
</label>
<div class="cred-input">
<Lock size={15} class="cred-field-icon" aria-hidden="true" />
@@ -146,8 +146,8 @@
type={showPassword ? "text" : "password"}
bind:value={password}
placeholder={mode === "token"
? "ghp_... oder anderer Zugangstoken"
: "Passwort oder Personal Access Token"}
? "ghp_... or another access token"
: "Password or personal access token"}
autocomplete="current-password"
disabled={isBusy}
/>
@@ -156,7 +156,7 @@
class="cred-reveal"
onclick={() => { showPassword = !showPassword; }}
tabindex="-1"
aria-label={showPassword ? "Verbergen" : "Anzeigen"}
aria-label={showPassword ? "Hide" : "Show"}
>
{#if showPassword}
<EyeOff size={14} aria-hidden="true" />
@@ -171,7 +171,7 @@
{#if mode === "token"}
<div class="cred-token-hint">
<Key size={13} aria-hidden="true" />
<span>Username wird automatisch auf <code>oauth2</code> gesetzt. Das funktioniert mit GitHub, GitLab und Bitbucket.</span>
<span>Username is automatically set to <code>oauth2</code>. This works with GitHub, GitLab, and Bitbucket.</span>
</div>
{/if}
@@ -184,26 +184,26 @@
{#if saveSession}
<div class="cred-expiry">
<label class="cred-field-label" for="cred-expiry">Ablaufdatum (optional)</label>
<label class="cred-field-label" for="cred-expiry">Expiration date (optional)</label>
<input
id="cred-expiry"
type="date"
bind:value={expiresAt}
disabled={isBusy}
/>
<span class="cred-expiry-hint">Nach diesem Datum wird automatisch erneut nach dem Login gefragt.</span>
<span class="cred-expiry-hint">After this date you'll automatically be asked to log in again.</span>
</div>
{/if}
<div class="cred-footer">
<label class="cred-save">
<input type="checkbox" bind:checked={saveSession} disabled={isBusy} />
<span>Im Schluesselbund speichern</span>
<span>Save in keychain</span>
</label>
<div class="cred-btns">
<button type="button" class="cred-cancel" onclick={onCancel} disabled={isBusy}>
Abbrechen
Cancel
</button>
<button class="cred-submit" type="submit" disabled={!canSubmit}>
{#if isBusy}
@@ -0,0 +1,74 @@
<script lang="ts">
import { AlertTriangle, LoaderCircle, RotateCcw, X } from "@lucide/svelte";
import type { GitFileStatus } from "../types";
interface Props {
file: GitFileStatus;
staged: boolean;
scope: "file" | "hunk";
isBusy: boolean;
onConfirm: () => void | Promise<void>;
onClose: () => void;
}
let {
file,
staged = false,
scope = "file",
isBusy = false,
onConfirm = () => {},
onClose = () => {},
}: Props = $props();
let targetPath = $derived(file.old_path ? `${file.old_path} -> ${file.path}` : file.path);
let title = $derived(scope === "hunk" ? "Discard hunk?" : "Discard file changes?");
let scopeLabel = $derived(scope === "hunk" ? "Selected hunk" : "File changes");
let sourceLabel = $derived(staged ? "staged changes" : "unstaged changes");
function closeFromBackdrop(event: MouseEvent) {
if (isBusy || event.target !== event.currentTarget) return;
onClose();
}
</script>
<div class="dialog-backdrop" role="presentation" onclick={closeFromBackdrop}>
<div class="dialog discard-confirm-dialog" role="dialog" aria-modal="true" aria-label={title}>
<header class="dialog-header">
<div>
<span class="eyebrow">Confirm discard</span>
<p class="dialog-title">{title}</p>
</div>
<button class="btn-sm dialog-close" type="button" onclick={onClose} disabled={isBusy} aria-label="Close">
<X size={16} aria-hidden="true" />
</button>
</header>
<div class="discard-confirm-body">
<div class="discard-warning-icon" aria-hidden="true">
<AlertTriangle size={22} />
</div>
<div class="discard-confirm-copy">
<p>
This will reset the {sourceLabel} for the {scopeLabel.toLowerCase()} below.
</p>
<code class="discard-target" title={targetPath}>{targetPath}</code>
<p class="discard-warning-text">
This cannot be undone. If the file only exists in your working tree, it can be deleted entirely.
</p>
</div>
</div>
<footer class="discard-confirm-actions">
<button class="btn-secondary" type="button" onclick={onClose} disabled={isBusy}>Cancel</button>
<button class="btn-danger" type="button" onclick={onConfirm} disabled={isBusy}>
{#if isBusy}
<LoaderCircle class="spin" size={15} aria-hidden="true" />
{:else}
<RotateCcw size={15} aria-hidden="true" />
{/if}
Discard
</button>
</footer>
</div>
</div>
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
export let label = "Repository wird geöffnet";
export let label = "Opening repository";
export let repoName = "";
</script>