feat(compare): add German localization and revamp compare UI

Pass the application language into compare dialogs and add German
translations so titles, buttons and helper text render appropriately.
Refactor dialog markup to introduce a compact header with a GitCompare
icon, show changed-file counts, and surface contextual help and warnings.
Adjust and extend CSS for spacing, layout and responsive behavior.

- Pass app language into compare components and derive isGerman flag
- Replace icons and restructure headers; add counts, help and warnings
- Update styles for sizing, spacing, responsive rules and new classes
This commit is contained in:
2026-08-15 18:39:10 +02:00
parent 412e8d19b5
commit a7558e457d
4 changed files with 139 additions and 62 deletions
+3 -1
View File
@@ -5537,6 +5537,7 @@
{canCompare}
{isBusy}
{operation}
language={appLanguage}
onCompareFromChange={(val) => { compareFrom = val; }}
onCompareToChange={(val) => { compareTo = val; }}
onCompare={compareSelectedTargets}
@@ -5551,10 +5552,11 @@
{comparison}
{selectedDiffPath}
{isBusy}
language={appLanguage}
fromLabel={comparisonFromLabel}
toLabel={comparisonToLabel}
highlightQuery={diffHighlightQuery}
restoreLabel={pendingRestoreFile ? "Restore file" : ""}
restoreLabel={pendingRestoreFile ? (appLanguage === "de" ? "Datei wiederherstellen" : "Restore file") : ""}
onClose={closeCompareDialog}
onRestore={restorePreviewedCommitFile}
onSelectFile={selectDiffFile}
+54 -10
View File
@@ -324,7 +324,14 @@
.select-menu-option.selected { color: var(--color-ink); }
.select-menu-option:disabled { cursor: not-allowed; opacity: 0.42; }
.compare-target-select .select-menu-trigger { height: 40px; min-height: 40px; }
.compare-target-select .select-menu-trigger {
height: 38px;
min-height: 38px;
padding: 0 11px;
border-color: var(--color-border);
font-size: 12px;
font-weight: 700;
}
.btn-primary {
border-color: rgba(111, 140, 255, 0.72);
@@ -3323,17 +3330,16 @@
.compare-form {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto;
grid-template-columns: minmax(0, 1fr) 40px minmax(0, 1fr);
align-items: end;
gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid var(--color-border-subtle);
gap: 10px;
}
.compare-field { display: grid; gap: 4px; min-width: 0; }
.compare-field span { color: var(--color-ink-faint); font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; }
.compare-arrow { margin-bottom: 6px; color: var(--color-ink-faint); }
.compare-target-help {
margin: 12px;
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 12px;
border: 1px solid color-mix(in srgb, var(--color-accent) 20%, var(--color-border-subtle));
border-radius: 8px;
@@ -3342,6 +3348,9 @@
font-size: 11.5px;
line-height: 1.5;
}
.compare-target-help svg { flex: 0 0 auto; margin-top: 1px; color: var(--color-accent); }
.compare-target-warning { border-color: color-mix(in srgb, #e0a040 30%, var(--color-border-subtle)); background: color-mix(in srgb, #e0a040 7%, var(--color-surface-raised)); }
.compare-target-warning svg { color: #e0a040; }
.compare-summary { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; padding: 10px 12px; }
.compare-range { display: flex; align-items: center; gap: 7px; color: var(--color-ink-muted); }
@@ -3487,6 +3496,21 @@
width: min(1560px, calc(100vw - 32px));
height: min(940px, calc(100vh - 32px));
}
.compare-dialog-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
min-height: 70px;
padding: 14px 18px;
border-bottom: 1px solid var(--color-border-subtle);
background: var(--app-dialog-chrome);
}
.compare-dialog-title { display: flex; align-items: center; gap: 12px; min-width: 0; }
.compare-dialog-title > div { min-width: 0; }
.compare-dialog-mark { display: grid; place-items: center; flex: 0 0 auto; width: 38px; height: 38px; border: 1px solid color-mix(in srgb, var(--color-accent) 28%, var(--color-border)); border-radius: 10px; color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 9%, transparent); }
.compare-dialog-title h2 { margin: 0; color: var(--color-accent); font-size: 18px; font-weight: 700; line-height: 1.2; }
.compare-dialog-title p { margin: 3px 0 0; color: var(--color-ink); font-size: 11px; font-weight: 400; line-height: 1.35; }
.global-search-dialog {
width: min(1180px, calc(100vw - 32px));
height: min(840px, calc(100vh - 32px));
@@ -3502,12 +3526,22 @@
height: min(860px, calc(100vh - 32px));
}
.compare-select-dialog {
display: block;
grid-template-rows: auto minmax(0, 1fr);
width: min(720px, calc(100vw - 32px));
height: auto;
max-height: calc(100vh - 32px);
overflow: auto;
overflow: hidden;
}
.compare-select-shell { display: grid; grid-template-rows: minmax(0, 1fr) auto; min-height: 0; }
.compare-select-body { display: grid; align-content: start; gap: 12px; min-height: 0; padding: 18px 20px 20px; overflow: auto; }
.compare-target-panel { display: grid; gap: 16px; padding: 16px; border: 1px solid var(--color-border-subtle); border-radius: 12px; background: color-mix(in srgb, var(--app-settings-row-bg) 72%, transparent); }
.compare-target-heading h3 { margin: 0; color: var(--color-accent); font-size: 18px; font-weight: 700; line-height: 1.2; }
.compare-target-heading p { margin: 4px 0 0; color: var(--color-ink); font-size: 11px; font-weight: 400; line-height: 1.45; }
.compare-arrow-shell { display: grid; place-items: center; width: 40px; height: 40px; border: 1px solid var(--color-border-subtle); border-radius: 9px; color: var(--color-accent); background: var(--color-surface-raised); }
.compare-select-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 62px; padding: 11px 16px; border-top: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
.compare-select-footer > span { color: var(--color-ink-faint); font-size: 9.5px; }
.compare-select-footer > div { display: flex; gap: 8px; }
.compare-select-footer button { min-height: 32px; }
.interactive-rebase-dialog,
.reflog-dialog {
grid-template-rows: auto minmax(0, 1fr) auto;
@@ -4066,6 +4100,8 @@
.compare-dialog .dialog-body { grid-template-columns: minmax(260px, 320px) minmax(0, 1fr); }
.dialog-files { display: grid; align-content: start; gap: 4px; padding: 8px; overflow: auto; border-right: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
.compare-files-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 36px; margin: -8px -8px 4px; padding: 8px 12px; border-bottom: 1px solid var(--color-border-subtle); color: var(--color-ink-dim); font-size: 10px; font-weight: 850; letter-spacing: .05em; text-transform: uppercase; }
.compare-files-head strong { display: grid; place-items: center; min-width: 22px; height: 20px; padding-inline: 5px; border-radius: 10px; color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 12%, transparent); font-size: 9px; }
.dialog-file-row {
display: grid;
@@ -6927,7 +6963,15 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
.repo-summary { height: 40px; }
.change-lanes { grid-template-columns: 1fr; }
.compare-form { grid-template-columns: 1fr; }
.compare-arrow { display: none; }
.compare-arrow-shell { width: 100%; height: 30px; }
.compare-arrow-shell svg { transform: rotate(90deg); }
.compare-dialog-head { min-height: 60px; padding: 10px 12px; }
.compare-dialog-mark { width: 34px; height: 34px; }
.compare-dialog-title h2 { font-size: 15px; }
.compare-select-body { padding: 14px; }
.compare-target-panel { padding: 13px; }
.compare-select-footer > span { display: none; }
.compare-select-footer { justify-content: flex-end; }
.dialog-body { grid-template-columns: 1fr; grid-template-rows: minmax(120px, 0.4fr) minmax(0, 1fr); }
.compare-dialog .dialog-body { grid-template-columns: 1fr; grid-template-rows: minmax(120px, 0.4fr) minmax(0, 1fr); }
.global-search-options { grid-template-columns: 1fr; }
+27 -16
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { ArrowRight, FileCode, RotateCcw, X } from "@lucide/svelte";
import { ArrowRight, FileCode, GitCompare, RotateCcw, X } from "@lucide/svelte";
import type { GitCommitComparison, GitDiffFile, FileStatusKind } from "../types";
type SplitRow =
@@ -25,6 +25,7 @@
restoreLabel?: string;
/** When opened from a search hit, the term to highlight on matching lines. */
highlightQuery?: string;
language?: "en" | "de";
onClose: () => void;
onRestore?: () => void;
onSelectFile: (file: GitDiffFile) => void;
@@ -38,11 +39,14 @@
toLabel = "",
restoreLabel = "",
highlightQuery = "",
language = "en",
onClose = () => {},
onRestore = undefined,
onSelectFile = () => {},
}: Props = $props();
let isGerman = $derived(language === "de");
// Needle = first non-empty line of the search query, lowercased for matching.
let highlightNeedle = $derived(
highlightQuery
@@ -225,14 +229,17 @@
>
<div class="dialog compare-dialog" role="dialog" aria-modal="true" aria-label="Branch or commit comparison" tabindex="-1">
<header class="dialog-header">
<div>
<span class="eyebrow">Compare</span>
<h2 class="dialog-range">
<span class="hash" title={comparison.from_hash}>{fromLabel || comparison.from_short}</span>
<ArrowRight size={14} aria-hidden="true" />
<span class="hash" title={comparison.to_hash}>{toLabel || comparison.to_short}</span>
</h2>
<header class="compare-dialog-head">
<div class="compare-dialog-title">
<span class="compare-dialog-mark"><GitCompare size={18} aria-hidden="true" /></span>
<div>
<h2>{isGerman ? "Änderungen vergleichen" : "Compare changes"}</h2>
<p class="dialog-range">
<span class="hash" title={comparison.from_hash}>{fromLabel || comparison.from_short}</span>
<ArrowRight size={14} aria-hidden="true" />
<span class="hash" title={comparison.to_hash}>{toLabel || comparison.to_short}</span>
</p>
</div>
</div>
<div class="dialog-header-actions">
{#if restoreLabel && onRestore}
@@ -241,19 +248,23 @@
<span>{restoreLabel}</span>
</button>
{/if}
<button class="dialog-close" type="button" onclick={onClose} title="Close">
<button class="dialog-close" type="button" onclick={onClose} title={isGerman ? "Schließen" : "Close"} aria-label={isGerman ? "Vergleich schließen" : "Close comparison"}>
<X size={18} aria-hidden="true" />
</button>
</div>
</header>
{#if comparison.files.length === 0}
<div class="blank-state">No differences these versions are identical.</div>
<div class="blank-state">{isGerman ? "Keine Unterschiede - diese Versionen sind identisch." : "No differences - these versions are identical."}</div>
{:else}
<div class="dialog-body">
<!-- File list -->
<aside class="dialog-files" aria-label="Changed files">
<aside class="dialog-files" aria-label={isGerman ? "Geänderte Dateien" : "Changed files"}>
<div class="compare-files-head">
<span>{isGerman ? "Geänderte Dateien" : "Changed files"}</span>
<strong>{comparison.files.length}</strong>
</div>
{#each comparison.files as file (`${file.old_path ?? ""}:${file.path}`)}
<button
class="dialog-file-row"
@@ -275,9 +286,9 @@
<!-- Diff pane -->
<div class="dialog-diff">
{#if !selectedFile}
<div class="blank-state">Select a file to see its changes.</div>
<div class="blank-state">{isGerman ? "Wähle eine Datei aus, um ihre Änderungen zu sehen." : "Select a file to see its changes."}</div>
{:else if splitRows.length === 0}
<div class="blank-state">No textual changes for this file.</div>
<div class="blank-state">{isGerman ? "Keine textuellen Änderungen für diese Datei." : "No textual changes for this file."}</div>
{:else}
<!-- Path bar -->
<div class="diff-header">
@@ -292,11 +303,11 @@
<!-- Column headers -->
<div class="split-col-headers">
<div class="split-col-label">
<span>Before</span>
<span>{isGerman ? "Vorher" : "Before"}</span>
<span class="split-col-hash" title={comparison.from_hash}>{fromLabel || comparison.from_short}</span>
</div>
<div class="split-col-label">
<span>After</span>
<span>{isGerman ? "Nachher" : "After"}</span>
<span class="split-col-hash" title={comparison.to_hash}>{toLabel || comparison.to_short}</span>
</div>
</div>
+55 -35
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { ArrowRight, GitCompare, LoaderCircle, X } from "@lucide/svelte";
import { ArrowRight, GitCompare, Info, LoaderCircle, X } from "@lucide/svelte";
import type { GitBranch, GitCommit } from "../types";
import SelectMenu from "./SelectMenu.svelte";
@@ -11,6 +11,7 @@
canCompare: boolean;
isBusy: boolean;
operation: string;
language?: "en" | "de";
onCompareFromChange: (val: string) => void;
onCompareToChange: (val: string) => void;
onCompare: () => void;
@@ -25,12 +26,15 @@
canCompare = false,
isBusy = false,
operation = "",
language = "en",
onCompareFromChange = () => {},
onCompareToChange = () => {},
onCompare = () => {},
onClose = () => {},
}: Props = $props();
let isGerman = $derived(language === "de");
function commitOptionLabel(item: GitCommit): string {
return `${item.short_hash} - ${item.summary}`;
}
@@ -58,50 +62,66 @@
class="dialog-backdrop"
role="presentation"
>
<div class="dialog compare-select-dialog" role="dialog" aria-modal="true" aria-label="Select branches or commits to compare" tabindex="-1">
<header class="dialog-header">
<div>
<span class="eyebrow">Compare</span>
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Compare branches or commits</h2>
<div class="dialog compare-select-dialog" role="dialog" aria-modal="true" aria-label={isGerman ? "Branches oder Commits vergleichen" : "Compare branches or commits"} tabindex="-1">
<header class="compare-dialog-head">
<div class="compare-dialog-title">
<span class="compare-dialog-mark"><GitCompare size={18} aria-hidden="true" /></span>
<div>
<h2>{isGerman ? "Branches oder Commits vergleichen" : "Compare branches or commits"}</h2>
<p>{isGerman ? "Zwei Repository-Stände direkt gegenüberstellen" : "Review two repository states side by side"}</p>
</div>
</div>
<button class="dialog-close" type="button" onclick={onClose} title="Close">
<button class="dialog-close" type="button" onclick={onClose} title={isGerman ? "Schließen" : "Close"} aria-label={isGerman ? "Vergleich schließen" : "Close comparison"}>
<X size={18} aria-hidden="true" />
</button>
</header>
{#if targetCount < 2}
<div class="blank-state">At least two branches or commits are needed to compare.</div>
{:else}
<form class="compare-form" onsubmit={handleSubmit}>
<label class="compare-field">
<span>Base</span>
<SelectMenu class="compare-target-select" value={compareFrom} options={compareOptions} placeholder="Select a branch or commit" showSelectedGroup disabled={isBusy} onChange={onCompareFromChange} />
</label>
<form class="compare-select-shell" onsubmit={handleSubmit}>
<div class="compare-select-body">
{#if targetCount < 2}
<div class="blank-state">{isGerman ? "Für einen Vergleich werden mindestens zwei Branches oder Commits benötigt." : "At least two branches or commits are needed to compare."}</div>
{:else}
<section class="compare-target-panel">
<div class="compare-target-heading">
<h3>{isGerman ? "Vergleichsbereich" : "Comparison range"}</h3>
<p>{isGerman ? "Wähle Ausgangspunkt und Ziel des Vergleichs." : "Choose the starting point and target for the comparison."}</p>
</div>
<div class="compare-form">
<label class="compare-field">
<span>{isGerman ? "Ausgangsbasis" : "Base"}</span>
<SelectMenu class="compare-target-select" value={compareFrom} options={compareOptions} placeholder={isGerman ? "Branch oder Commit wählen" : "Select a branch or commit"} showSelectedGroup disabled={isBusy} onChange={onCompareFromChange} />
</label>
<ArrowRight class="compare-arrow" size={18} aria-hidden="true" />
<span class="compare-arrow-shell"><ArrowRight size={18} aria-hidden="true" /></span>
<label class="compare-field">
<span>Compare with</span>
<SelectMenu class="compare-target-select" value={compareTo} options={compareOptions} placeholder="Select a branch or commit" showSelectedGroup disabled={isBusy} onChange={onCompareToChange} />
</label>
<label class="compare-field">
<span>{isGerman ? "Vergleichen mit" : "Compare with"}</span>
<SelectMenu class="compare-target-select" value={compareTo} options={compareOptions} placeholder={isGerman ? "Branch oder Commit wählen" : "Select a branch or commit"} showSelectedGroup disabled={isBusy} onChange={onCompareToChange} />
</label>
</div>
</section>
<button class="btn-primary" type="submit" disabled={!canCompare}>
{#if operation === "Comparing revisions"}
<LoaderCircle class="spin" size={16} aria-hidden="true" />
{#if compareFrom && compareTo && compareFrom === compareTo}
<div class="compare-target-help compare-target-warning"><Info size={15} aria-hidden="true" /><span>{isGerman ? "Wähle zwei unterschiedliche Branches oder Commits." : "Select two different branches or commits to compare."}</span></div>
{:else}
<GitCompare size={16} aria-hidden="true" />
<div class="compare-target-help"><Info size={15} aria-hidden="true" /><span>{isGerman ? "Verglichen werden die vollständigen Repository-Stände. Nicht committete Änderungen im Arbeitsverzeichnis sind nicht enthalten." : "The complete repository states are compared. Uncommitted working-tree changes are not included."}</span></div>
{/if}
Compare
</button>
</form>
{#if compareFrom && compareTo && compareFrom === compareTo}
<div class="blank-state">Select two different branches or commits to compare.</div>
{:else}
<div class="compare-target-help">
The two branch tips are compared across the entire repository. Uncommitted working-tree changes are not included.
{/if}
</div>
<footer class="compare-select-footer">
<span>{isGerman ? `${targetCount} Ziele verfügbar` : `${targetCount} targets available`}</span>
<div>
<button class="btn-secondary" type="button" onclick={onClose}>{isGerman ? "Abbrechen" : "Cancel"}</button>
<button class="btn-primary" type="submit" disabled={!canCompare || targetCount < 2}>
{#if operation === "Comparing revisions"}
<LoaderCircle class="spin" size={16} aria-hidden="true" />
{:else}
<GitCompare size={16} aria-hidden="true" />
{/if}
{isGerman ? "Vergleich öffnen" : "Open comparison"}
</button>
</div>
{/if}
{/if}
</footer>
</form>
</div>
</div>