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+/
This commit is contained in:
Christoph Brandau
2026-07-11 15:03:01 +02:00
parent b181b384e7
commit 6729d61dca
6 changed files with 917 additions and 25 deletions
+55 -3
View File
@@ -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}
/>
<div class="shell-body">
@@ -4113,11 +4160,16 @@
<AppSettingsDialog
analytics={analyticsSettings}
theme={appTheme}
language={appLanguage}
onSave={saveAppSettings}
onClose={() => { appSettingsOpen = false; }}
/>
{/if}
{#if helpOpen}
<HelpOverlay language={appLanguage} onClose={() => { helpOpen = false; }} />
{/if}
{#if linePatchOpen && linePatchFile}
<LinePatchDialog
file={linePatchFile}