From 6729d61dca6e08bc84483e28e0669c21c0120f2c Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Sat, 11 Jul 2026 15:03:01 +0200 Subject: [PATCH] feat(language): implement internationalization and help overlay This update introduces comprehensive language support (English/German) across the application, enabling localization for UI elements and settings dialogs. It also adds a dedicated Help Overlay component with detailed guides on using Gitty's core Git features. - Adds language selection to App Settings - Implements German translations in key areas - Introduces global help documentation accessible via Ctrl+/ --- src/App.svelte | 58 +- src/app.css | 4 + src/lib/TitleBar.svelte | 20 +- src/lib/components/AppSettingsDialog.svelte | 61 +- src/lib/components/HelpOverlay.svelte | 798 ++++++++++++++++++++ src/lib/types.ts | 1 + 6 files changed, 917 insertions(+), 25 deletions(-) create mode 100644 src/lib/components/HelpOverlay.svelte diff --git a/src/App.svelte b/src/App.svelte index a29bd62..a68be1b 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -21,6 +21,7 @@ import ExplorerPanel from "./lib/components/ExplorerPanel.svelte"; import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte"; import GlobalSearchDialog from "./lib/components/GlobalSearchDialog.svelte"; + import HelpOverlay from "./lib/components/HelpOverlay.svelte"; import HistoryPanel from "./lib/components/HistoryPanel.svelte"; import InteractiveRebaseDialog from "./lib/components/InteractiveRebaseDialog.svelte"; import LinePatchDialog from "./lib/components/LinePatchDialog.svelte"; @@ -104,6 +105,7 @@ import type { AiSettings, + AppLanguage, AppTheme, AnalyticsSettings, CommitAiPhase, @@ -176,6 +178,7 @@ const AI_SETTINGS_KEY = "gitlite.aiSettings.v1"; const ANALYTICS_SETTINGS_KEY = "gitlite.analyticsSettings.v1"; const APP_THEME_KEY = "gitlite.theme.v1"; + const APP_LANGUAGE_KEY = "gitlite.language.v1"; const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1"; const LEFT_SIDEBAR_WIDTH_KEY = "gitlite.leftSidebarWidth.v1"; const LEFT_BRANCH_PANEL_HEIGHT_KEY = "gitlite.leftBranchPanelHeight.v1"; @@ -248,9 +251,11 @@ let aiSettings: AiSettings = defaultAiSettings(); let aiSettingsOpen = false; let appSettingsOpen = false; + let helpOpen = false; let analyticsNoticeOpen = false; let analyticsSettings: AnalyticsSettings = defaultAnalyticsSettings(); let appTheme: AppTheme = loadThemePreference(); + let appLanguage: AppLanguage = loadLanguagePreference(); let localModelOptions: LocalModelOption[] = []; let errorMessage = ""; let operation = ""; @@ -412,6 +417,7 @@ $: allLeftPanelsCollapsed = branchPanelCollapsed && stashPanelCollapsed && explorerPanelCollapsed; $: applyThemePreference(appTheme); + $: applyLanguagePreference(appLanguage); // ── Lifecycle ────────────────────────────────────────────────────────────── @@ -672,7 +678,7 @@ } async function autoRefreshTick() { - if (!autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || newBranchCommit || globalSearchOpen) return; + if (!autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || newBranchCommit || globalSearchOpen || helpOpen) return; const path = activeRepoPath; autoRefreshInFlight = true; try { @@ -813,6 +819,29 @@ } } + function loadLanguagePreference(): AppLanguage { + try { + const stored = localStorage.getItem(APP_LANGUAGE_KEY); + if (stored === "en" || stored === "de") return stored; + } catch { + // Local storage is optional; English is the first-start default. + } + return "en"; + } + + function persistLanguagePreference(next: AppLanguage) { + try { + localStorage.setItem(APP_LANGUAGE_KEY, next); + } catch { + // Ignore storage quota/private-mode errors. + } + } + + function applyLanguagePreference(next: AppLanguage) { + document.documentElement.lang = next; + document.documentElement.dataset.language = next; + } + function applyThemePreference(next: AppTheme) { const prefersLight = themeMediaQuery?.matches ?? window.matchMedia("(prefers-color-scheme: light)").matches; const resolved = next === "system" @@ -828,13 +857,15 @@ if (appTheme === "system") applyThemePreference(appTheme); } - function saveAppSettings(next: AnalyticsSettings, nextTheme: AppTheme) { + function saveAppSettings(next: AnalyticsSettings, nextTheme: AppTheme, nextLanguage: AppLanguage) { analyticsSettings = next; appTheme = nextTheme; + appLanguage = nextLanguage; persistAnalyticsSettings(next); persistThemePreference(nextTheme); + persistLanguagePreference(nextLanguage); appSettingsOpen = false; - if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1, theme: nextTheme }); + if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1, theme: nextTheme, language: nextLanguage }); } function updateCommitMessage(message: string) { @@ -3259,6 +3290,11 @@ trackEvent("global_search_opened"); } + function openHelp() { + helpOpen = true; + trackEvent("help_opened"); + } + async function compareSelectedCommits() { if (!canCompare) return; await runOperation("Comparing commits", async () => { @@ -3449,6 +3485,15 @@ // ── Event handlers ───────────────────────────────────────────────────────── function handleWindowKeydown(event: KeyboardEvent) { + if ((event.ctrlKey || event.metaKey) && event.key === "/") { + event.preventDefault(); + openHelp(); + return; + } + if (event.key === "Escape" && helpOpen) { + helpOpen = false; + return; + } if (event.key === "Escape" && repoTabContextMenu) closeRepoTabContextMenu(); else if (event.key === "Escape" && pendingDiscard && !isBusy) closeDiscardConfirm(); else if (event.key === "Escape" && compareDialogOpen) closeCompareDialog(); @@ -3499,6 +3544,8 @@ onOpenInExplorer={openActiveRepoInExplorer} onToggleAutoRefresh={toggleAutoRefresh} onOpenSettings={() => { appSettingsOpen = true; }} + onOpenHelp={openHelp} + language={appLanguage} />
@@ -4113,11 +4160,16 @@ { appSettingsOpen = false; }} /> {/if} +{#if helpOpen} + { helpOpen = false; }} /> +{/if} + {#if linePatchOpen && linePatchFile} void = () => {}; export let onOpenInExplorer: () => void = () => {}; export let onToggleAutoRefresh: () => void = () => {}; + export let onOpenHelp: () => void = () => {}; export let onOpenSettings: () => void = () => {}; + export let language: "en" | "de" = "en"; let win: ReturnType | null = null; let isMaximized = false; @@ -238,14 +240,24 @@ Auto + +
diff --git a/src/lib/components/AppSettingsDialog.svelte b/src/lib/components/AppSettingsDialog.svelte index 345d936..d3053d0 100644 --- a/src/lib/components/AppSettingsDialog.svelte +++ b/src/lib/components/AppSettingsDialog.svelte @@ -1,22 +1,26 @@