Localize UI to English across all 22 source files

Translates all German user-facing strings (command names, notices,
settings, modal labels, template names/descriptions, error messages,
status bar, and code comments) to English.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca Oelfke 2026-02-12 12:17:24 +01:00
parent 878b144ccc
commit 3c8c22ee07
23 changed files with 377 additions and 270 deletions

107
CLAUDE.md Normal file
View file

@ -0,0 +1,107 @@
# Logfire — Obsidian Plugin
Kombiniertes Plugin aus **Basefire** (SQLite-Query-Engine) und **Logfire** (Event-Logging).
Trackt alle Vault-Aktivitaeten, speichert in SQLite, macht per SQL abfragbar, visualisiert mit Charts/Dashboards.
## Technischer Stack
- **SQLite**: `better-sqlite3` (nativ, synchron, schnell)
- **Build**: esbuild mit `nativeModulePlugin` fuer Electron-Kompatibilitaet
- **Desktop-only** (FileSystemAdapter erforderlich)
- **Autor**: tolvitty
## Architektur
```
src/
├── main.ts # Plugin-Einstieg, Lifecycle
├── types.ts # Event-Typen, Settings, Query-Interfaces
├── core/
│ ├── database.ts # better-sqlite3, Schema, Retention, Maintenance
│ ├── event-bus.ts # Circular Buffer, Pub/Sub, Auto-Flush
│ ├── session-manager.ts # Session-Start/End, Dauer-Tracking
│ ├── content-analyzer.ts # Snapshot-Cache, Wort-/Link-/Tag-Diffs
│ └── query-builder.ts # QueryConfig → parametrisiertes SQL
├── collectors/
│ ├── file-collector.ts # File CRUD Events
│ ├── content-collector.ts # Semantische Content-Analyse
│ ├── nav-collector.ts # Navigation-Tracking
│ ├── editor-collector.ts # CM6 ViewPlugin, Debouncing
│ └── system-collector.ts # Command-Tracking
├── query/
│ ├── processor.ts # Code-Block-Prozessoren (logfire, logfire-sql, logfire-dashboard)
│ ├── query-modal.ts # Interaktiver SQL-Editor (Shorthand + SQL)
│ └── virtual-tables.ts # _files, _links, _tags, _headings
├── viz/
│ ├── table-renderer.ts # Table, Timeline, Summary, Metric, List, Heatmap
│ ├── chart-renderer.ts # 10 SVG-Chart-Typen (Bar, Line, Pie, Gauge, ...)
│ └── dashboard.ts # Multi-Widget-Dashboards, Grid-Layout
├── management/
│ ├── history.ts # Automatische Query-History mit Metriken
│ ├── favorites.ts # Gespeicherte Queries, Kategorien, Tags
│ └── templates.ts # Built-in + Custom Templates, Parameter-Substitution
├── projection/
│ ├── formatters.ts # Query-Results → Markdown (Timeline, Table, Summary, Metric, Heatmap)
│ ├── template-registry.ts # Built-in + Custom ProjectionTemplate Verwaltung
│ ├── projection-engine.ts # Kern-Engine: Scheduling, Session-End-Listener, PickerModal
│ └── presets/
│ ├── daily-log.ts # Tagesprotokoll-Preset
│ ├── session-log.ts # Session-Protokoll-Preset
│ └── weekly-digest.ts # Wochenuebersicht-Preset
├── ui/
│ ├── settings-tab.ts # Obsidian-native Settings
│ ├── status-bar.ts # Live-Status-Widget (Recording/Paused)
│ ├── event-stream-view.ts # Echtzeit-Event-Sidebar
│ ├── schema-view.ts # Schema-Browser (Tabellen, Spalten, Indizes)
│ └── initial-scan-modal.ts # Initialer Vault-Scan mit Fortschritt
```
## DB-Schema
**Kern-Tabellen** (database.ts):
- `events` — id, timestamp, type, category, source, target, payload, session
- `sessions` — id, start_time, end_time, vault_name
- `baseline` — file_path, word_count, char_count, links, tags, headings, ...
- `daily_stats` — date, file_path, events_count, words_added/removed, time_active_ms
- `monthly_stats` — wie daily_stats, aggregiert pro Monat
**Virtual Tables** (virtual-tables.ts):
- `_files` — path, name, basename, extension, size, created, modified, folder
- `_links` — from_path, to_path, display_text, link_type
- `_tags` — path, tag
- `_headings` — path, level, heading
## Konventionen
- **Sprache**: Code und Variablennamen auf Englisch, UI-Texte und Commits auf Deutsch
- **Commits**: Kleinschrittig, atomar, deutsch. Niemals pushen (Nutzer pusht manuell)
- **Branching**: Feature-Branches (`feature/<name>`), Merge mit `--no-ff` in `main`
- **CSS**: Ausschliesslich Obsidian-Variablen, Monospace, "Utilitarian System Monitor" Aesthetic
- **Charts**: Reines SVG, keine externen Bibliotheken
- **Queries**: `Record<string, unknown>[]` Format (better-sqlite3 Rueckgabe)
- **Storage**: History/Favorites/Templates in localStorage, Plugin-Daten via loadData/saveData
## Feature-Roadmap
### Abgeschlossen
- [x] **Feature 1**: Projekt-Grundgeruest & Datenbank (`feature/grundgeruest`)
- [x] **Feature 2**: Event-System & Collectors (`feature/event-system`)
- [x] **Feature 3**: Echtzeit-UI (`feature/echtzeit-ui`)
- [x] **Feature 4**: SQL-Query-Engine (`feature/sql-engine`)
- [x] **Feature 5**: Virtual Tables (`feature/virtual-tables`)
- [x] **Feature 6**: Datenvisualisierung (`feature/visualisierung`)
- [x] **Feature 7**: Query-Management (`feature/query-management`)
- [x] **Feature 8**: Projections & Reports (`feature/projections`)
- [x] **Feature 9**: Polish & Extras (`feature/polish`)
## Build & Test
```bash
npm run build # Production-Build (esbuild)
npm run dev # Watch-Mode
```
Build-Output: `main.js` (aktuell ~86KB), `styles.css`, `manifest.json`
Zum Testen: Plugin-Ordner in `.obsidian/plugins/logfire/` eines Vaults verlinken/kopieren.

View file

@ -92,7 +92,7 @@ export class ContentCollector {
});
}
} catch (err) {
console.error('[Logfire] ContentCollector-Fehler:', err);
console.error('[Logfire] ContentCollector error:', err);
}
}

View file

@ -49,7 +49,7 @@ export class EventBus {
try {
this.db.insertEvents(batch);
} catch (err) {
console.error('[Logfire] Flush fehlgeschlagen:', err);
console.error('[Logfire] Flush failed:', err);
this.buffer.unshift(...batch);
}
}
@ -93,7 +93,7 @@ export class EventBus {
try {
cb(event);
} catch (err) {
console.error('[Logfire] Subscriber-Fehler:', err);
console.error('[Logfire] Subscriber error:', err);
}
}
}

View file

@ -49,7 +49,7 @@ export default class LogfirePlugin extends Plugin {
private paused = false;
async onload(): Promise<void> {
console.log('[Logfire] Lade Plugin...');
console.log('[Logfire] Loading plugin...');
await this.loadSettings();
@ -125,7 +125,7 @@ export default class LogfirePlugin extends Plugin {
this.statusBar = new StatusBar(this);
this.statusBar.start();
// Query: Code-Block-Prozessoren
// Query: Code-block processors
registerLogfireBlock(this.db, (lang, handler) => {
this.registerMarkdownCodeBlockProcessor(lang, handler);
});
@ -137,7 +137,7 @@ export default class LogfirePlugin extends Plugin {
});
// Ribbon icons
this.addRibbonIcon('activity', 'Logfire: Event-Stream', () => {
this.addRibbonIcon('activity', 'Logfire: Event Stream', () => {
this.activateEventStream();
});
this.addRibbonIcon('layout-dashboard', 'Logfire: Dashboard', () => {
@ -163,7 +163,7 @@ export default class LogfirePlugin extends Plugin {
try {
this.db.runMaintenance(this.settings.advanced.retention);
} catch (err) {
console.error('[Logfire] Wartung beim Start fehlgeschlagen:', err);
console.error('[Logfire] Startup maintenance failed:', err);
}
}
@ -175,18 +175,18 @@ export default class LogfirePlugin extends Plugin {
this.projectionEngine = new ProjectionEngine(this.app, this.db, this.eventBus, this.settings);
this.projectionEngine.start();
// Autocomplete (nach Virtual Tables)
// Autocomplete (after virtual tables)
this.autocomplete = new SqlAutocomplete(this.db);
// Keyboard Navigator
this.keyboardNav = new KeyboardNavigator(this.app);
});
console.log('[Logfire] Plugin geladen. Session:', this.sessionManager.currentSessionId);
console.log('[Logfire] Plugin loaded. Session:', this.sessionManager.currentSessionId);
}
async onunload(): Promise<void> {
console.log('[Logfire] Entlade Plugin...');
console.log('[Logfire] Unloading plugin...');
cleanupAllRefreshTimers();
this.projectionEngine?.destroy();
@ -206,7 +206,7 @@ export default class LogfirePlugin extends Plugin {
this.db.close();
}
console.log('[Logfire] Plugin entladen.');
console.log('[Logfire] Plugin unloaded.');
}
// ---------------------------------------------------------------------------
@ -304,57 +304,57 @@ export default class LogfirePlugin extends Plugin {
private registerCommands(): void {
this.addCommand({
id: 'show-event-stream',
name: 'Event-Stream anzeigen',
name: 'Show event stream',
callback: () => this.activateEventStream(),
});
this.addCommand({
id: 'toggle-tracking',
name: 'Tracking pausieren/fortsetzen',
name: 'Toggle tracking',
callback: () => {
if (this.paused) {
this.resume();
new Notice('Logfire: Tracking fortgesetzt.');
new Notice('Logfire: Tracking resumed.');
} else {
this.pause();
new Notice('Logfire: Tracking pausiert.');
new Notice('Logfire: Tracking paused.');
}
},
});
this.addCommand({
id: 'rescan-vault',
name: 'Vault erneut scannen',
name: 'Rescan vault',
callback: () => this.runInitialScan(),
});
this.addCommand({
id: 'run-maintenance',
name: 'Wartung ausführen',
name: 'Run maintenance',
callback: () => {
this.db.runMaintenance(this.settings.advanced.retention);
new Notice('Logfire: Wartung abgeschlossen.');
new Notice('Logfire: Maintenance complete.');
},
});
this.addCommand({
id: 'refresh-virtual-tables',
name: 'Virtual Tables neu aufbauen',
name: 'Rebuild virtual tables',
callback: () => {
this.virtualTables?.rebuild();
new Notice('Logfire: Virtual Tables aktualisiert.');
new Notice('Logfire: Virtual tables updated.');
},
});
this.addCommand({
id: 'show-dashboard',
name: 'Dashboard anzeigen',
name: 'Show dashboard',
callback: () => this.activateDashboard(),
});
this.addCommand({
id: 'open-query',
name: 'Query-Editor \u00f6ffnen',
name: 'Open query editor',
callback: () => {
new QueryModal(this.app, this.db, this.historyManager, undefined, this.autocomplete).open();
},
@ -362,13 +362,13 @@ export default class LogfirePlugin extends Plugin {
this.addCommand({
id: 'show-schema',
name: 'Schema-Browser anzeigen',
name: 'Show schema browser',
callback: () => this.activateSchema(),
});
this.addCommand({
id: 'show-templates',
name: 'Query-Templates anzeigen',
name: 'Show query templates',
callback: () => {
new TemplatePickerModal(this, this.templateManager, (sql) => {
new QueryModal(this.app, this.db, this.historyManager, sql, this.autocomplete).open();
@ -378,7 +378,7 @@ export default class LogfirePlugin extends Plugin {
this.addCommand({
id: 'save-favorite',
name: 'Aktuelle Query als Favorit speichern',
name: 'Save current query as favorite',
callback: () => {
new SaveFavoriteModal(this, this.favoritesManager, '').open();
},
@ -402,7 +402,7 @@ export default class LogfirePlugin extends Plugin {
this.addCommand({
id: 'run-projection',
name: 'Projektion manuell ausführen',
name: 'Run projection',
callback: () => {
new ProjectionPickerModal(this.app, this.projectionEngine).open();
},
@ -410,7 +410,7 @@ export default class LogfirePlugin extends Plugin {
this.addCommand({
id: 'run-all-projections',
name: 'Alle Projektionen ausführen',
name: 'Run all projections',
callback: () => {
this.projectionEngine.runAllProjections();
},
@ -418,33 +418,33 @@ export default class LogfirePlugin extends Plugin {
this.addCommand({
id: 'export-csv',
name: 'Letzte Query als CSV exportieren',
name: 'Export last query as CSV',
callback: () => {
new Notice('CSV-Export: Bitte Query-Editor öffnen und dort exportieren.');
new Notice('CSV export: Please open the query editor and export from there.');
},
});
this.addCommand({
id: 'export-json',
name: 'Letzte Query als JSON exportieren',
name: 'Export last query as JSON',
callback: () => {
new Notice('JSON-Export: Bitte Query-Editor öffnen und dort exportieren.');
new Notice('JSON export: Please open the query editor and export from there.');
},
});
this.addCommand({
id: 'toggle-vim-nav',
name: 'Vim-Navigation umschalten',
name: 'Toggle Vim navigation',
callback: () => {
if (this.keyboardNav?.isActive()) {
this.keyboardNav.detach();
new Notice('Logfire: Vim-Navigation deaktiviert.');
new Notice('Logfire: Vim navigation disabled.');
} else {
const active = document.querySelector('.logfire-qm-results, .logfire-dash-widget-content');
if (active instanceof HTMLElement && this.keyboardNav?.attach(active)) {
new Notice('Logfire: Vim-Navigation aktiviert (j/k/h/l, gg/G, /, y, Enter, Esc).');
new Notice('Logfire: Vim navigation enabled (j/k/h/l, gg/G, /, y, Enter, Esc).');
} else {
new Notice('Logfire: Keine Tabelle gefunden.');
new Notice('Logfire: No table found.');
}
}
},
@ -522,7 +522,7 @@ export default class LogfirePlugin extends Plugin {
private getDatabasePath(): string {
const adapter = this.app.vault.adapter;
if (!(adapter instanceof FileSystemAdapter)) {
throw new Error('[Logfire] Benötigt einen Desktop-Vault mit Dateisystem-Zugriff.');
throw new Error('[Logfire] Requires a desktop vault with filesystem access.');
}
const basePath = adapter.getBasePath();
return `${basePath}/${this.app.vault.configDir}/plugins/logfire/logfire.db`;

View file

@ -25,9 +25,9 @@ export interface FavoriteCategory {
const STORAGE_KEY = 'logfire-favorites';
const DEFAULT_CATEGORIES: FavoriteCategory[] = [
{ id: 'allgemein', name: 'Allgemein', color: '#4C78A8' },
{ id: 'analyse', name: 'Analyse', color: '#F58518' },
{ id: 'wartung', name: 'Wartung', color: '#E45756' },
{ id: 'general', name: 'General', color: '#4C78A8' },
{ id: 'analysis', name: 'Analysis', color: '#F58518' },
{ id: 'maintenance', name: 'Maintenance', color: '#E45756' },
];
// ---------------------------------------------------------------------------
@ -75,7 +75,7 @@ export class FavoritesManager {
// Favorites CRUD
// ---------------------------------------------------------------------------
add(name: string, sql: string, category = 'allgemein', tags: string[] = []): Favorite {
add(name: string, sql: string, category = 'general', tags: string[] = []): Favorite {
const fav: Favorite = {
id: `fav-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name,
@ -154,7 +154,7 @@ export class SaveFavoriteModal extends Modal {
private manager: FavoritesManager;
private sql: string;
private name = '';
private category = 'allgemein';
private category = 'general';
private tags: string[] = [];
constructor(plugin: LogfirePlugin, manager: FavoritesManager, sql: string) {
@ -167,41 +167,41 @@ export class SaveFavoriteModal extends Modal {
onOpen(): void {
const { contentEl } = this;
contentEl.addClass('logfire-save-fav-modal');
contentEl.createEl('h3', { text: 'Als Favorit speichern' });
contentEl.createEl('h3', { text: 'Save as Favorite' });
new Setting(contentEl).setName('Name').addText(text =>
text.setPlaceholder('Meine Query').onChange(v => { this.name = v; }),
text.setPlaceholder('My Query').onChange(v => { this.name = v; }),
);
new Setting(contentEl).setName('Kategorie').addDropdown(dd => {
new Setting(contentEl).setName('Category').addDropdown(dd => {
for (const cat of this.manager.getCategories()) dd.addOption(cat.id, cat.name);
dd.setValue(this.category);
dd.onChange(v => { this.category = v; });
});
new Setting(contentEl).setName('Tags').setDesc('Komma-getrennt').addText(text =>
new Setting(contentEl).setName('Tags').setDesc('Comma-separated').addText(text =>
text.setPlaceholder('select, events').onChange(v => {
this.tags = v.split(',').map(s => s.trim()).filter(Boolean);
}),
);
contentEl.createEl('h4', { text: 'SQL-Vorschau' });
contentEl.createEl('h4', { text: 'SQL Preview' });
contentEl.createEl('pre', {
cls: 'logfire-sql-preview',
text: this.sql.length > 200 ? this.sql.slice(0, 200) + '...' : this.sql,
});
const btns = contentEl.createDiv({ cls: 'logfire-modal-buttons' });
const saveBtn = btns.createEl('button', { text: 'Speichern', cls: 'mod-cta' });
const saveBtn = btns.createEl('button', { text: 'Save', cls: 'mod-cta' });
saveBtn.addEventListener('click', () => this.doSave());
const cancelBtn = btns.createEl('button', { text: 'Abbrechen' });
const cancelBtn = btns.createEl('button', { text: 'Cancel' });
cancelBtn.addEventListener('click', () => this.close());
}
private doSave(): void {
if (!this.name.trim()) { new Notice('Bitte einen Namen eingeben.'); return; }
if (!this.name.trim()) { new Notice('Please enter a name.'); return; }
this.manager.add(this.name.trim(), this.sql, this.category, this.tags);
new Notice(`Favorit "${this.name}" gespeichert.`);
new Notice(`Favorite "${this.name}" saved.`);
this.close();
}

View file

@ -54,7 +54,7 @@ export class HistoryManager {
// ---------------------------------------------------------------------------
addEntry(sql: string, executionTimeMs?: number, rowCount?: number): HistoryEntry {
// Deduplizieren: gleiche SQL aktualisiert vorhandenen Eintrag
// Deduplicate: same SQL updates existing entry
for (const entry of this.entries.values()) {
if (entry.sql === sql) {
entry.executedAt = new Date().toISOString();
@ -75,7 +75,7 @@ export class HistoryManager {
};
this.entries.set(entry.id, entry);
// Limit: aelteste nicht-favorisierte loeschen
// Limit: delete oldest non-favorited entries
if (this.entries.size > MAX_ENTRIES) {
const sorted = this.getAll();
for (const e of sorted.slice(MAX_ENTRIES)) {

View file

@ -42,7 +42,7 @@ export class TemplateManager {
for (const t of arr) this.templates.set(t.id, t);
} catch { /* ignore */ }
}
// Immer Built-in Templates sicherstellen
// Ensure built-in templates are always present
for (const t of BUILTIN_TEMPLATES) {
if (!this.templates.has(t.id)) this.templates.set(t.id, t);
}
@ -123,15 +123,15 @@ function parseParameters(sql: string): TemplateParameter[] {
}
// ---------------------------------------------------------------------------
// Built-in Templates (angepasst an Logfire-Schema)
// Built-in Templates (adapted for Logfire schema)
// ---------------------------------------------------------------------------
const BUILTIN_TEMPLATES: QueryTemplate[] = [
{
id: 'builtin-events-today',
name: 'Events heute',
description: 'Alle Events seit Mitternacht',
sql: `SELECT type, source, datetime(timestamp/1000, 'unixepoch', 'localtime') as zeit
name: 'Events Today',
description: 'All events since midnight',
sql: `SELECT type, source, datetime(timestamp/1000, 'unixepoch', 'localtime') as time
FROM events
WHERE timestamp > (strftime('%s', 'now', 'start of day') * 1000)
ORDER BY timestamp DESC
@ -141,9 +141,9 @@ LIMIT {{limit:100}}`,
},
{
id: 'builtin-active-files',
name: 'Aktivste Dateien',
description: 'Dateien mit den meisten Events',
sql: `SELECT source as datei, COUNT(*) as events
name: 'Most Active Files',
description: 'Files with the most events',
sql: `SELECT source as file, COUNT(*) as events
FROM events
WHERE source IS NOT NULL
GROUP BY source
@ -155,14 +155,14 @@ LIMIT {{limit:20}}`,
{
id: 'builtin-session-overview',
name: 'Sessions',
description: 'Letzte Sessions mit Dauer',
description: 'Recent sessions with duration',
sql: `SELECT
id,
datetime(start_time/1000, 'unixepoch', 'localtime') as start,
CASE WHEN end_time IS NOT NULL
THEN ROUND((end_time - start_time) / 60000.0, 1) || ' min'
ELSE 'aktiv'
END as dauer
ELSE 'active'
END as duration
FROM sessions
ORDER BY start_time DESC
LIMIT {{limit:10}}`,
@ -171,23 +171,23 @@ LIMIT {{limit:10}}`,
},
{
id: 'builtin-daily-stats',
name: 'Tagesstatistik',
description: 'Aggregierte Statistiken pro Tag',
sql: `SELECT date as tag, SUM(events_count) as events,
SUM(words_added) as woerter_hinzu, SUM(words_removed) as woerter_entfernt
name: 'Daily Statistics',
description: 'Aggregated statistics per day',
sql: `SELECT date as day, SUM(events_count) as events,
SUM(words_added) as words_added, SUM(words_removed) as words_removed
FROM daily_stats
GROUP BY date
ORDER BY date DESC
LIMIT {{limit:14}}`,
parameters: [{ name: 'limit', label: 'Tage', defaultValue: '14', type: 'number' }],
parameters: [{ name: 'limit', label: 'Days', defaultValue: '14', type: 'number' }],
builtIn: true,
},
{
id: 'builtin-recent-files',
name: 'Zuletzt geaenderte Dateien',
description: 'Dateien nach Aenderungsdatum',
name: 'Recently Modified Files',
description: 'Files by modification date',
sql: `SELECT name, extension,
datetime(modified/1000, 'unixepoch', 'localtime') as geaendert,
datetime(modified/1000, 'unixepoch', 'localtime') as modified_at,
folder
FROM _files
WHERE extension = 'md'
@ -198,22 +198,22 @@ LIMIT {{limit:20}}`,
},
{
id: 'builtin-tag-stats',
name: 'Tag-Statistiken',
description: 'Notizen pro Tag zaehlen',
sql: `SELECT tag, COUNT(DISTINCT path) as notizen
name: 'Tag Statistics',
description: 'Count notes per tag',
sql: `SELECT tag, COUNT(DISTINCT path) as notes
FROM _tags
GROUP BY tag
ORDER BY notizen DESC
ORDER BY notes DESC
LIMIT {{limit:30}}`,
parameters: [{ name: 'limit', label: 'Limit', defaultValue: '30', type: 'number' }],
builtIn: true,
},
{
id: 'builtin-orphan-notes',
name: 'Verwaiste Notizen',
description: 'Notizen ohne eingehende Links',
name: 'Orphan Notes',
description: 'Notes without incoming links',
sql: `SELECT f.path, f.name,
datetime(f.modified/1000, 'unixepoch', 'localtime') as geaendert
datetime(f.modified/1000, 'unixepoch', 'localtime') as modified_at
FROM _files f
WHERE f.extension = 'md'
AND f.path NOT IN (SELECT DISTINCT to_path FROM _links)
@ -223,9 +223,9 @@ ORDER BY f.modified DESC`,
},
{
id: 'builtin-broken-links',
name: 'Defekte Links',
description: 'Links zu nicht-existierenden Notizen',
sql: `SELECT l.from_path as von, l.to_path as nach, l.display_text
name: 'Broken Links',
description: 'Links to non-existent notes',
sql: `SELECT l.from_path as from_file, l.to_path as to_file, l.display_text
FROM _links l
WHERE l.to_path NOT IN (SELECT path FROM _files)
AND l.link_type = 'link'`,
@ -234,12 +234,12 @@ WHERE l.to_path NOT IN (SELECT path FROM _files)
},
{
id: 'builtin-event-types',
name: 'Event-Typen',
description: 'Verteilung der Event-Typen',
sql: `SELECT type as typ, category as kategorie, COUNT(*) as anzahl
name: 'Event Types',
description: 'Distribution of event types',
sql: `SELECT type, category, COUNT(*) as count
FROM events
GROUP BY type, category
ORDER BY anzahl DESC`,
ORDER BY count DESC`,
parameters: [],
builtIn: true,
},
@ -262,7 +262,7 @@ export class TemplatePickerModal extends Modal {
onOpen(): void {
const { contentEl } = this;
contentEl.addClass('logfire-template-picker');
contentEl.createEl('h2', { text: 'Query-Templates' });
contentEl.createEl('h2', { text: 'Query Templates' });
const list = contentEl.createDiv({ cls: 'logfire-template-list' });
@ -285,7 +285,7 @@ export class TemplatePickerModal extends Modal {
});
const actions = item.createDiv({ cls: 'logfire-template-actions' });
const useBtn = actions.createEl('button', { text: 'Verwenden', cls: 'mod-cta' });
const useBtn = actions.createEl('button', { text: 'Use', cls: 'mod-cta' });
useBtn.addEventListener('click', () => {
if (tpl.parameters.length > 0) {
this.close();
@ -299,11 +299,11 @@ export class TemplatePickerModal extends Modal {
});
if (!tpl.builtIn) {
const delBtn = actions.createEl('button', { text: 'Entfernen' });
const delBtn = actions.createEl('button', { text: 'Remove' });
delBtn.addEventListener('click', () => {
this.manager.delete(tpl.id);
item.remove();
new Notice('Template entfernt.');
new Notice('Template removed.');
});
}
}
@ -334,7 +334,7 @@ class ParameterModal extends Modal {
onOpen(): void {
const { contentEl } = this;
contentEl.createEl('h3', { text: `Parameter: ${this.template.name}` });
contentEl.createEl('h3', { text: `Parameters: ${this.template.name}` });
for (const param of this.template.parameters) {
new Setting(contentEl).setName(param.label).addText(text => {
@ -346,9 +346,9 @@ class ParameterModal extends Modal {
}
const btns = contentEl.createDiv({ cls: 'logfire-modal-buttons' });
const runBtn = btns.createEl('button', { text: 'Ausfuehren', cls: 'mod-cta' });
const runBtn = btns.createEl('button', { text: 'Execute', cls: 'mod-cta' });
runBtn.addEventListener('click', () => { this.onSubmit(this.values); this.close(); });
const cancelBtn = btns.createEl('button', { text: 'Abbrechen' });
const cancelBtn = btns.createEl('button', { text: 'Cancel' });
cancelBtn.addEventListener('click', () => this.close());
}

View file

@ -6,7 +6,7 @@ import { toMarkdownTable } from '../viz/table-renderer';
// ---------------------------------------------------------------------------
export function formatSection(rows: Record<string, unknown>[], section: SectionConfig): string {
if (rows.length === 0) return `### ${section.heading}\n\n*Keine Daten.*\n`;
if (rows.length === 0) return `### ${section.heading}\n\n*No data.*\n`;
const heading = `### ${section.heading}\n\n`;

View file

@ -2,8 +2,8 @@ import { ProjectionTemplate } from '../../types';
export const dailyLogTemplate: ProjectionTemplate = {
id: 'builtin:daily-log',
name: 'Tagesprotokoll',
description: 'Sessions, aktive Dateien, Events und Zeitleiste des Tages.',
name: 'Daily Log',
description: 'Sessions, active files, events, and timeline of the day.',
enabled: false,
trigger: { type: 'manual' },
output: {
@ -30,8 +30,8 @@ export const dailyLogTemplate: ProjectionTemplate = {
name: 'table',
columns: [
{ header: 'Session', value: 'session' },
{ header: 'Typ', value: 'type' },
{ header: 'Zeit', value: 'timestamp' },
{ header: 'Type', value: 'type' },
{ header: 'Time', value: 'timestamp' },
],
},
},
@ -39,7 +39,7 @@ export const dailyLogTemplate: ProjectionTemplate = {
},
{
id: 'active-files',
heading: 'Aktive Dateien',
heading: 'Active Files',
type: 'table',
query: {
timeRange: { type: 'relative', value: 'today' },
@ -53,10 +53,10 @@ export const dailyLogTemplate: ProjectionTemplate = {
builtin: {
name: 'table',
columns: [
{ header: 'Datei', value: 'group' },
{ header: 'File', value: 'group' },
{ header: 'Events', value: 'count' },
{ header: 'Woerter+', value: 'words_added' },
{ header: 'Woerter-', value: 'words_removed' },
{ header: 'Words+', value: 'words_added' },
{ header: 'Words-', value: 'words_removed' },
],
},
},
@ -64,7 +64,7 @@ export const dailyLogTemplate: ProjectionTemplate = {
},
{
id: 'event-summary',
heading: 'Event-Uebersicht',
heading: 'Event Summary',
type: 'table',
query: {
timeRange: { type: 'relative', value: 'today' },
@ -77,8 +77,8 @@ export const dailyLogTemplate: ProjectionTemplate = {
builtin: {
name: 'table',
columns: [
{ header: 'Typ', value: 'group' },
{ header: 'Anzahl', value: 'count' },
{ header: 'Type', value: 'group' },
{ header: 'Count', value: 'count' },
],
},
},
@ -86,7 +86,7 @@ export const dailyLogTemplate: ProjectionTemplate = {
},
{
id: 'timeline',
heading: 'Zeitleiste',
heading: 'Timeline',
type: 'timeline',
query: {
timeRange: { type: 'relative', value: 'today' },

View file

@ -2,8 +2,8 @@ import { ProjectionTemplate } from '../../types';
export const sessionLogTemplate: ProjectionTemplate = {
id: 'builtin:session-log',
name: 'Session-Protokoll',
description: 'Einzelne Session: Dauer, bearbeitete Dateien, ausgefuehrte Befehle.',
name: 'Session Log',
description: 'Single session: duration, edited files, executed commands.',
enabled: false,
trigger: { type: 'on-session-end' },
output: {
@ -19,7 +19,7 @@ export const sessionLogTemplate: ProjectionTemplate = {
sections: [
{
id: 'overview',
heading: 'Session-Uebersicht',
heading: 'Session Overview',
type: 'summary',
query: {
timeRange: { type: 'session' },
@ -29,7 +29,7 @@ export const sessionLogTemplate: ProjectionTemplate = {
builtin: {
name: 'summary',
metrics: [
{ label: 'Events gesamt', aggregate: 'count' },
{ label: 'Total events', aggregate: 'count' },
],
},
},
@ -37,7 +37,7 @@ export const sessionLogTemplate: ProjectionTemplate = {
},
{
id: 'files',
heading: 'Bearbeitete Dateien',
heading: 'Edited Files',
type: 'table',
query: {
timeRange: { type: 'session' },
@ -51,10 +51,10 @@ export const sessionLogTemplate: ProjectionTemplate = {
builtin: {
name: 'table',
columns: [
{ header: 'Datei', value: 'group' },
{ header: 'File', value: 'group' },
{ header: 'Events', value: 'count' },
{ header: 'Woerter+', value: 'words_added' },
{ header: 'Woerter-', value: 'words_removed' },
{ header: 'Words+', value: 'words_added' },
{ header: 'Words-', value: 'words_removed' },
],
},
},
@ -62,7 +62,7 @@ export const sessionLogTemplate: ProjectionTemplate = {
},
{
id: 'commands',
heading: 'Ausgefuehrte Befehle',
heading: 'Executed Commands',
type: 'table',
query: {
timeRange: { type: 'session' },
@ -74,8 +74,8 @@ export const sessionLogTemplate: ProjectionTemplate = {
builtin: {
name: 'table',
columns: [
{ header: 'Zeit', value: 'timestamp' },
{ header: 'Befehl', value: 'source' },
{ header: 'Time', value: 'timestamp' },
{ header: 'Command', value: 'source' },
],
},
},

View file

@ -2,8 +2,8 @@ import { ProjectionTemplate } from '../../types';
export const weeklyDigestTemplate: ProjectionTemplate = {
id: 'builtin:weekly-digest',
name: 'Wochen-Digest',
description: 'Wochenuebersicht: Tages-Summary, Top-Dateien, Schreib-Statistiken, Heatmap.',
name: 'Weekly Digest',
description: 'Weekly overview: daily summary, top files, writing statistics, heatmap.',
enabled: false,
trigger: { type: 'manual' },
output: {
@ -18,7 +18,7 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
sections: [
{
id: 'daily-summary',
heading: 'Tages-Uebersicht',
heading: 'Daily Overview',
type: 'table',
query: {
timeRange: { type: 'relative', value: 'last-7-days' },
@ -31,10 +31,10 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
builtin: {
name: 'table',
columns: [
{ header: 'Tag', value: 'group' },
{ header: 'Day', value: 'group' },
{ header: 'Events', value: 'count' },
{ header: 'Woerter+', value: 'words_added' },
{ header: 'Woerter-', value: 'words_removed' },
{ header: 'Words+', value: 'words_added' },
{ header: 'Words-', value: 'words_removed' },
],
},
},
@ -42,7 +42,7 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
},
{
id: 'top-files',
heading: 'Top-Dateien',
heading: 'Top Files',
type: 'table',
query: {
timeRange: { type: 'relative', value: 'last-7-days' },
@ -56,9 +56,9 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
builtin: {
name: 'table',
columns: [
{ header: 'Datei', value: 'group' },
{ header: 'File', value: 'group' },
{ header: 'Events', value: 'count' },
{ header: 'Woerter+', value: 'words_added' },
{ header: 'Words+', value: 'words_added' },
],
},
},
@ -66,7 +66,7 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
},
{
id: 'write-stats',
heading: 'Schreib-Statistiken',
heading: 'Writing Statistics',
type: 'summary',
query: {
timeRange: { type: 'relative', value: 'last-7-days' },
@ -77,9 +77,9 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
builtin: {
name: 'summary',
metrics: [
{ label: 'Tage aktiv', aggregate: 'count' },
{ label: 'Woerter geschrieben', aggregate: 'sum', field: 'words_added' },
{ label: 'Woerter geloescht', aggregate: 'sum', field: 'words_removed' },
{ label: 'Days active', aggregate: 'count' },
{ label: 'Words written', aggregate: 'sum', field: 'words_added' },
{ label: 'Words deleted', aggregate: 'sum', field: 'words_removed' },
],
},
},
@ -87,7 +87,7 @@ export const weeklyDigestTemplate: ProjectionTemplate = {
},
{
id: 'activity-heatmap',
heading: 'Aktivitaets-Heatmap',
heading: 'Activity Heatmap',
type: 'chart-data',
query: {
timeRange: { type: 'relative', value: 'last-7-days' },

View file

@ -32,7 +32,7 @@ export class ProjectionEngine {
start(): void {
if (!this.settings.projections.enabled) return;
// Session-End-Listener fuer session-log
// Session-end listener for session-log
if (this.settings.projections.sessionLog.enabled) {
this.sessionEndUnsub = this.eventBus.onEvent('system:session-end', (event) => {
const templates = this.registry.getByTrigger('on-session-end');
@ -42,7 +42,7 @@ export class ProjectionEngine {
});
}
// Scheduler fuer daily + weekly (prueft alle 60s)
// Scheduler for daily + weekly (checks every 60s)
this.schedulerTimer = setInterval(() => this.checkSchedule(), 60_000);
}
@ -75,7 +75,7 @@ export class ProjectionEngine {
if (template) this.runProjection(template);
}
// Weekly Digest (dayOfWeek: 0=So, 1=Mo, ...)
// Weekly Digest (dayOfWeek: 0=Sun, 1=Mon, ...)
if (
this.settings.projections.weeklyDigest.enabled &&
now.getDay() === this.settings.projections.weeklyDigest.dayOfWeek &&
@ -115,7 +115,7 @@ export class ProjectionEngine {
const queryConfig = { ...section.query };
// Session-Kontext einsetzen
// Inject session context
if (queryConfig.timeRange.type === 'session' && context?.sessionId) {
queryConfig.timeRange = { ...queryConfig.timeRange, sessionId: context.sessionId };
}
@ -133,10 +133,10 @@ export class ProjectionEngine {
});
const filePath = `${outputFolder}/${fileName}`;
// Ordner erstellen falls noetig
// Create folder if needed
await this.ensureFolder(outputFolder);
// Datei schreiben
// Write file
const existing = this.app.vault.getAbstractFileByPath(filePath);
if (existing) {
if (template.output.mode === 'append') {
@ -151,8 +151,8 @@ export class ProjectionEngine {
return filePath;
} catch (err) {
console.error('[Logfire] Projektion fehlgeschlagen:', err);
new Notice(`Logfire: Projektion "${template.name}" fehlgeschlagen.`);
console.error('[Logfire] Projection failed:', err);
new Notice(`Logfire: Projection "${template.name}" failed.`);
return null;
}
}
@ -166,7 +166,7 @@ export class ProjectionEngine {
if (result) count++;
}
}
new Notice(`Logfire: ${count} Projektion(en) ausgefuehrt.`);
new Notice(`Logfire: ${count} projection(s) executed.`);
}
private async ensureFolder(path: string): Promise<void> {
@ -198,7 +198,7 @@ export class ProjectionPickerModal extends Modal {
contentEl.empty();
contentEl.addClass('logfire-projection-picker');
contentEl.createEl('h2', { text: 'Projektion ausfuehren' });
contentEl.createEl('h2', { text: 'Run Projection' });
const list = contentEl.createDiv({ cls: 'logfire-template-list' });
const templates = this.engine.getRegistry().getAll();
@ -221,11 +221,11 @@ export class ProjectionPickerModal extends Modal {
});
const actions = item.createDiv({ cls: 'logfire-template-actions' });
const runBtn = actions.createEl('button', { text: 'Ausfuehren', cls: 'mod-cta' });
const runBtn = actions.createEl('button', { text: 'Execute', cls: 'mod-cta' });
runBtn.addEventListener('click', async () => {
const path = await this.engine.runProjection(template);
if (path) {
new Notice(`Projektion geschrieben: ${path}`);
new Notice(`Projection written: ${path}`);
}
this.close();
});

View file

@ -63,7 +63,7 @@ export class SqlAutocomplete {
) as { name: string }[];
this.columnCache.set(table, rows.map(r => r.name));
} catch {
// Virtual tables oder nicht existente Tabellen ueberspringen
// Skip virtual tables or non-existent tables
}
}
}
@ -91,7 +91,7 @@ export class SqlAutocomplete {
case 'where':
case 'group-by':
case 'order-by': {
// Spalten aus aktiven Tabellen
// Columns from active tables
const tables = extractTables(textBefore);
for (const table of tables) {
const cols = this.columnCache.get(table) ?? [];

View file

@ -76,14 +76,14 @@ export function registerLogfireSqlBlock(
const { sql, refresh } = parseSqlBlock(source);
if (!sql) {
renderError(el, new Error('Leere Query.'));
renderError(el, new Error('Empty query.'));
return;
}
// Safety: only SELECT/WITH
const firstWord = sql.split(/\s+/)[0].toUpperCase();
if (firstWord !== 'SELECT' && firstWord !== 'WITH') {
renderError(el, new Error('Nur SELECT- und WITH-Queries sind erlaubt.'));
renderError(el, new Error('Only SELECT and WITH queries are allowed.'));
return;
}
@ -92,7 +92,7 @@ export function registerLogfireSqlBlock(
try {
const rows = db.queryReadOnly(sql) as Record<string, unknown>[];
if (!Array.isArray(rows) || rows.length === 0) {
el.createEl('p', { text: 'Keine Ergebnisse.', cls: 'logfire-empty' });
el.createEl('p', { text: 'No results.', cls: 'logfire-empty' });
return;
}
@ -108,7 +108,7 @@ export function registerLogfireSqlBlock(
try {
const freshRows = db.queryReadOnly(sql) as Record<string, unknown>[];
if (!Array.isArray(freshRows) || freshRows.length === 0) {
el.createEl('p', { text: 'Keine Ergebnisse.', cls: 'logfire-empty' });
el.createEl('p', { text: 'No results.', cls: 'logfire-empty' });
return;
}
if (chartConfig) {
@ -139,7 +139,7 @@ export function registerLogfireDashboardBlock(
registerFn('logfire-dashboard', (source, el, ctx) => {
const dashboard = parseDashboardBlock(source);
if (!dashboard) {
renderError(el, new Error('Ungültige Dashboard-Definition.'));
renderError(el, new Error('Invalid dashboard definition.'));
return;
}
@ -170,7 +170,7 @@ export function registerLogfireDashboardBlock(
} else if (widget.sql) {
const rows = db.queryReadOnly(widget.sql) as Record<string, unknown>[];
if (rows.length === 0) {
content.createDiv({ cls: 'logfire-empty', text: 'Keine Ergebnisse.' });
content.createDiv({ cls: 'logfire-empty', text: 'No results.' });
} else if (widget.type === 'chart' && widget.chartConfig) {
renderChart(content, rows, widget.chartConfig);
} else if (widget.type === 'stat') {
@ -182,7 +182,7 @@ export function registerLogfireDashboardBlock(
} catch (err) {
content.createDiv({
cls: 'logfire-error',
text: `Fehler: ${err instanceof Error ? err.message : String(err)}`,
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
});
}
}
@ -292,7 +292,7 @@ function parseSqlBlock(source: string): { sql: string; refresh?: number } {
function renderResult(el: HTMLElement, rows: Record<string, unknown>[], format: string, columns?: string[]): void {
if (rows.length === 0) {
el.createEl('p', { text: 'Keine Events gefunden.', cls: 'logfire-empty' });
el.createEl('p', { text: 'No events found.', cls: 'logfire-empty' });
return;
}

View file

@ -35,17 +35,17 @@ export class QueryModal extends Modal {
// Mode toggle
const toolbar = contentEl.createDiv({ cls: 'logfire-qm-toolbar' });
this.modeToggle = toolbar.createEl('button', { text: 'Modus: Shorthand' });
this.modeToggle = toolbar.createEl('button', { text: 'Mode: Shorthand' });
this.modeToggle.addEventListener('click', () => {
this.mode = this.mode === 'shorthand' ? 'sql' : 'shorthand';
this.modeToggle.textContent = this.mode === 'shorthand' ? 'Modus: Shorthand' : 'Modus: SQL';
this.modeToggle.textContent = this.mode === 'shorthand' ? 'Mode: Shorthand' : 'Mode: SQL';
this.editorEl.placeholder = this.mode === 'shorthand'
? 'events today\nstats this-week group by file\nfiles modified yesterday'
: 'SELECT * FROM events\nWHERE type = \'file:create\'\nORDER BY timestamp DESC\nLIMIT 50';
});
const helpSpan = toolbar.createEl('span', {
text: 'Ctrl+Enter: Ausf\u00fchren',
text: 'Ctrl+Enter: Execute',
cls: 'logfire-qm-hint',
});
@ -87,13 +87,13 @@ export class QueryModal extends Modal {
// Buttons
const buttonRow = contentEl.createDiv({ cls: 'logfire-qm-buttons' });
const runBtn = buttonRow.createEl('button', { text: 'Ausf\u00fchren', cls: 'mod-cta' });
const runBtn = buttonRow.createEl('button', { text: 'Execute', cls: 'mod-cta' });
runBtn.addEventListener('click', () => this.executeQuery());
const copyBtn = buttonRow.createEl('button', { text: 'Als Markdown kopieren' });
const copyBtn = buttonRow.createEl('button', { text: 'Copy as Markdown' });
copyBtn.addEventListener('click', () => this.copyAsMarkdown());
const insertBtn = buttonRow.createEl('button', { text: 'In Notiz einf\u00fcgen' });
const insertBtn = buttonRow.createEl('button', { text: 'Insert into note' });
insertBtn.addEventListener('click', () => this.insertInNote());
const csvBtn = buttonRow.createEl('button', { text: 'CSV Export' });
@ -102,7 +102,7 @@ export class QueryModal extends Modal {
const jsonBtn = buttonRow.createEl('button', { text: 'JSON Export' });
jsonBtn.addEventListener('click', () => this.exportJson());
const clearBtn = buttonRow.createEl('button', { text: 'Leeren' });
const clearBtn = buttonRow.createEl('button', { text: 'Clear' });
clearBtn.addEventListener('click', () => {
this.editorEl.value = '';
this.resultEl.empty();
@ -113,10 +113,10 @@ export class QueryModal extends Modal {
// Results
this.resultEl = contentEl.createDiv({ cls: 'logfire-qm-results' });
// Initial SQL setzen
// Set initial SQL
if (this.initialSql) {
this.mode = 'sql';
this.modeToggle.textContent = 'Modus: SQL';
this.modeToggle.textContent = 'Mode: SQL';
this.editorEl.value = this.initialSql;
this.editorEl.placeholder = 'SELECT * FROM events\nWHERE type = \'file:create\'\nORDER BY timestamp DESC\nLIMIT 50';
}
@ -147,7 +147,7 @@ export class QueryModal extends Modal {
const firstWord = input.split(/\s+/)[0].toUpperCase();
if (firstWord !== 'SELECT' && firstWord !== 'WITH') {
this.resultEl.createEl('pre', {
text: 'Nur SELECT- und WITH-Queries sind erlaubt.',
text: 'Only SELECT and WITH queries are allowed.',
cls: 'logfire-error',
});
return;
@ -170,7 +170,7 @@ export class QueryModal extends Modal {
this.renderResults(rows);
} catch (err) {
this.resultEl.createEl('pre', {
text: `Fehler: ${err instanceof Error ? err.message : String(err)}`,
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
cls: 'logfire-error',
});
}
@ -180,7 +180,7 @@ export class QueryModal extends Modal {
this.resultEl.empty();
if (rows.length === 0) {
this.resultEl.createEl('p', { text: 'Keine Ergebnisse.' });
this.resultEl.createEl('p', { text: 'No results.' });
return;
}
@ -189,7 +189,7 @@ export class QueryModal extends Modal {
if (rows.length > 200) {
this.resultEl.createEl('p', {
text: `${rows.length} Ergebnisse, 200 angezeigt.`,
text: `${rows.length} results, 200 shown.`,
cls: 'logfire-qm-truncated',
});
}
@ -197,22 +197,22 @@ export class QueryModal extends Modal {
private copyAsMarkdown(): void {
if (this.lastRows.length === 0) {
new Notice('Keine Ergebnisse zum Kopieren.');
new Notice('No results to copy.');
return;
}
const md = toMarkdownTable(this.lastKeys, this.lastRows);
navigator.clipboard.writeText(md);
new Notice('In Zwischenablage kopiert.');
new Notice('Copied to clipboard.');
}
private insertInNote(): void {
if (this.lastRows.length === 0) {
new Notice('Keine Ergebnisse zum Einf\u00fcgen.');
new Notice('No results to insert.');
return;
}
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) {
new Notice('Kein aktiver Editor zum Einf\u00fcgen.');
new Notice('No active editor to insert into.');
return;
}
const md = toMarkdownTable(this.lastKeys, this.lastRows);
@ -226,7 +226,7 @@ export class QueryModal extends Modal {
private exportCsv(): void {
if (this.lastRows.length === 0) {
new Notice('Keine Ergebnisse zum Exportieren.');
new Notice('No results to export.');
return;
}
const bom = '\uFEFF';
@ -235,17 +235,17 @@ export class QueryModal extends Modal {
this.lastKeys.map(k => csvEscape(row[k])).join(',')
).join('\n');
downloadBlob(bom + header + '\n' + body, 'logfire-export.csv', 'text/csv;charset=utf-8');
new Notice('CSV exportiert.');
new Notice('CSV exported.');
}
private exportJson(): void {
if (this.lastRows.length === 0) {
new Notice('Keine Ergebnisse zum Exportieren.');
new Notice('No results to export.');
return;
}
const json = JSON.stringify(this.lastRows, null, 2);
downloadBlob(json, 'logfire-export.json', 'application/json');
new Notice('JSON exportiert.');
new Notice('JSON exported.');
}
// ---------------------------------------------------------------------------
@ -303,7 +303,7 @@ export class QueryModal extends Modal {
const before = text.substring(0, cursorPos);
const after = text.substring(cursorPos);
// Letztes Wort ersetzen
// Replace last word
const lastWordMatch = before.match(/[\w._:-]+$/);
const replaceStart = lastWordMatch ? cursorPos - lastWordMatch[0].length : cursorPos;
const newBefore = text.substring(0, replaceStart) + suggestion.text;

View file

@ -41,7 +41,7 @@ export class EventStreamView extends ItemView {
const searchInput = toolbar.createEl('input', {
type: 'text',
placeholder: 'Nach Quelle filtern...',
placeholder: 'Filter by source...',
cls: 'logfire-stream-search',
});
searchInput.addEventListener('input', () => {
@ -54,11 +54,11 @@ export class EventStreamView extends ItemView {
const pauseBtn = btnGroup.createEl('button', { text: 'Pause' });
pauseBtn.addEventListener('click', () => {
this.viewPaused = !this.viewPaused;
pauseBtn.textContent = this.viewPaused ? 'Weiter' : 'Pause';
pauseBtn.textContent = this.viewPaused ? 'Resume' : 'Pause';
pauseBtn.toggleClass('is-active', this.viewPaused);
});
const clearBtn = btnGroup.createEl('button', { text: 'Leeren' });
const clearBtn = btnGroup.createEl('button', { text: 'Clear' });
clearBtn.addEventListener('click', () => {
this.entries = [];
this.renderList();
@ -132,9 +132,9 @@ export class EventStreamView extends ItemView {
const p = event.payload;
switch (event.type) {
case 'content:words-changed':
return `+${p.wordsAdded ?? 0}/-${p.wordsRemoved ?? 0} Wörter`;
return `+${p.wordsAdded ?? 0}/-${p.wordsRemoved ?? 0} words`;
case 'editor:change':
return `+${p.insertedChars ?? 0}/-${p.deletedChars ?? 0} Zeichen`;
return `+${p.insertedChars ?? 0}/-${p.deletedChars ?? 0} chars`;
case 'nav:file-close':
return typeof p.duration === 'number' ? `${Math.round(p.duration / 1000)}s` : '';
case 'file:rename':

View file

@ -39,9 +39,9 @@ export class InitialScanModal extends Modal {
const files = this.app.vault.getMarkdownFiles()
.filter(f => this.shouldTrack(f.path));
contentEl.createEl('h2', { text: 'Logfire Vault-Scan' });
contentEl.createEl('h2', { text: 'Logfire Vault Scan' });
contentEl.createEl('p', {
text: `${files.length} Markdown-Dateien gefunden. Der Scan erstellt die Baseline für Content-Tracking.`,
text: `${files.length} Markdown files found. The scan creates the baseline for content tracking.`,
});
const progressContainer = contentEl.createDiv({ cls: 'logfire-scan-progress' });
@ -62,15 +62,15 @@ export class InitialScanModal extends Modal {
buttonContainer.style.gap = '8px';
buttonContainer.style.marginTop = '16px';
const cancelBtn = buttonContainer.createEl('button', { text: 'Abbrechen' });
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel' });
cancelBtn.addEventListener('click', () => {
this.cancelled = true;
});
const startBtn = buttonContainer.createEl('button', { text: 'Scan starten', cls: 'mod-cta' });
const startBtn = buttonContainer.createEl('button', { text: 'Start scan', cls: 'mod-cta' });
startBtn.addEventListener('click', async () => {
startBtn.disabled = true;
cancelBtn.textContent = 'Stoppen';
cancelBtn.textContent = 'Stop';
let scanned = 0;
let totalWords = 0;
@ -103,9 +103,9 @@ export class InitialScanModal extends Modal {
progressBar.value = scanned;
currentFileEl.textContent = file.path;
totalsEl.textContent = `${scanned}/${files.length} Dateien | ${totalWords.toLocaleString()} Wörter | ${totalLinks} Links | ${totalTags} Tags`;
totalsEl.textContent = `${scanned}/${files.length} files | ${totalWords.toLocaleString()} words | ${totalLinks} links | ${totalTags} tags`;
} catch (err) {
console.error(`[Logfire] Scan-Fehler bei ${file.path}:`, err);
console.error(`[Logfire] Scan error at ${file.path}:`, err);
}
}
@ -129,11 +129,11 @@ export class InitialScanModal extends Modal {
});
currentFileEl.textContent = this.cancelled
? `Scan gestoppt. ${scanned} von ${files.length} Dateien gescannt.`
: `Scan abgeschlossen! ${scanned} Dateien gescannt.`;
? `Scan stopped. ${scanned} of ${files.length} files scanned.`
: `Scan complete! ${scanned} files scanned.`;
cancelBtn.style.display = 'none';
const closeBtn = buttonContainer.createEl('button', { text: 'Schließen', cls: 'mod-cta' });
const closeBtn = buttonContainer.createEl('button', { text: 'Close', cls: 'mod-cta' });
closeBtn.addEventListener('click', () => {
this.close();
this.resolve(true);

View file

@ -46,7 +46,7 @@ export class SchemaView extends ItemView {
}
getDisplayText(): string {
return 'Schema-Browser';
return 'Schema Browser';
}
getIcon(): string {
@ -63,7 +63,7 @@ export class SchemaView extends ItemView {
const refreshBtn = header.createEl('button', {
cls: 'logfire-dash-btn clickable-icon',
attr: { 'aria-label': 'Aktualisieren' },
attr: { 'aria-label': 'Refresh' },
text: '\u21bb',
});
refreshBtn.addEventListener('click', () => this.refresh());
@ -77,7 +77,7 @@ export class SchemaView extends ItemView {
const tables = this.introspect();
if (tables.length === 0) {
this.contentContainer.createDiv({ cls: 'logfire-empty', text: 'Keine Tabellen gefunden.' });
this.contentContainer.createDiv({ cls: 'logfire-empty', text: 'No tables found.' });
return;
}
@ -144,11 +144,11 @@ export class SchemaView extends ItemView {
this.refresh();
});
// Context menu: SQL kopieren
// Context menu: copy SQL
header.addEventListener('contextmenu', (e) => {
e.preventDefault();
navigator.clipboard.writeText(`SELECT * FROM "${table.name}" LIMIT 100;`);
new Notice(`SELECT auf "${table.name}" kopiert.`);
new Notice(`SELECT for "${table.name}" copied.`);
});
if (!isExpanded) return;
@ -166,7 +166,7 @@ export class SchemaView extends ItemView {
// Indexes
if (table.indexes.length > 0) {
details.createDiv({ cls: 'logfire-schema-section-header', text: 'Indizes' });
details.createDiv({ cls: 'logfire-schema-section-header', text: 'Indexes' });
for (const idx of table.indexes) {
const row = details.createDiv({ cls: 'logfire-schema-idx' });
row.createSpan({ text: `${idx.unique ? 'UNIQUE ' : ''}${idx.name}` });

View file

@ -11,11 +11,11 @@ export class LogfireSettingTab extends PluginSettingTab {
containerEl.empty();
// ----- General -----
containerEl.createEl('h2', { text: 'Allgemein' });
containerEl.createEl('h2', { text: 'General' });
new Setting(containerEl)
.setName('Tracking aktiviert')
.setDesc('Hauptschalter für alle Event-Collector.')
.setName('Tracking enabled')
.setDesc('Master switch for all event collectors.')
.addToggle(t => t
.setValue(this.plugin.settings.general.enabled)
.onChange(async (v) => {
@ -24,8 +24,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Log-Ordner')
.setDesc('Ordner für Markdown-Projektionen. Wird automatisch vom Tracking ausgeschlossen.')
.setName('Log folder')
.setDesc('Folder for Markdown projections. Automatically excluded from tracking.')
.addText(t => t
.setPlaceholder('Logfire')
.setValue(this.plugin.settings.general.logFolder)
@ -35,8 +35,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Beim Start pausieren')
.setDesc('Startet im pausierten Zustand — keine Events bis manuell fortgesetzt.')
.setName('Pause on startup')
.setDesc('Starts in paused state — no events until manually resumed.')
.addToggle(t => t
.setValue(this.plugin.settings.general.pauseOnStartup)
.onChange(async (v) => {
@ -48,8 +48,8 @@ export class LogfireSettingTab extends PluginSettingTab {
containerEl.createEl('h2', { text: 'Tracking' });
new Setting(containerEl)
.setName('Datei-Events')
.setDesc('Erstellen, Löschen, Umbenennen, Verschieben und Ändern von Dateien.')
.setName('File events')
.setDesc('Create, delete, rename, move, and modify files.')
.addToggle(t => t
.setValue(this.plugin.settings.tracking.fileEvents)
.onChange(async (v) => {
@ -58,8 +58,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Content-Analyse')
.setDesc('Semantische Diffs: Wortzählung, Links, Tags, Überschriften, Frontmatter.')
.setName('Content analysis')
.setDesc('Semantic diffs: word count, links, tags, headings, frontmatter.')
.addToggle(t => t
.setValue(this.plugin.settings.tracking.contentAnalysis)
.onChange(async (v) => {
@ -69,7 +69,7 @@ export class LogfireSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName('Navigation')
.setDesc('Aktive Datei-Wechsel, Datei öffnen/schließen mit Dauer.')
.setDesc('Active file switches, file open/close with duration.')
.addToggle(t => t
.setValue(this.plugin.settings.tracking.navigation)
.onChange(async (v) => {
@ -78,8 +78,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Editor-Änderungen')
.setDesc('Tastenanschläge (debounced und aggregiert).')
.setName('Editor changes')
.setDesc('Keystrokes (debounced and aggregated).')
.addToggle(t => t
.setValue(this.plugin.settings.tracking.editorChanges)
.onChange(async (v) => {
@ -88,8 +88,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Kommando-Tracking')
.setDesc('Ausführung von Befehlen über Palette und Hotkeys.')
.setName('Command tracking')
.setDesc('Execution of commands via palette and hotkeys.')
.addToggle(t => t
.setValue(this.plugin.settings.tracking.commandTracking)
.onChange(async (v) => {
@ -98,8 +98,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Ausgeschlossene Ordner')
.setDesc('Komma-getrennte Liste von Ordnern, die nicht getrackt werden.')
.setName('Excluded folders')
.setDesc('Comma-separated list of folders to exclude from tracking.')
.addText(t => t
.setPlaceholder('.obsidian, templates')
.setValue(this.plugin.settings.tracking.excludedFolders.join(', '))
@ -112,8 +112,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Ausgeschlossene Patterns')
.setDesc('Komma-getrennte Glob-Patterns (z.B. **/*.excalidraw.md).')
.setName('Excluded patterns')
.setDesc('Comma-separated glob patterns (e.g. **/*.excalidraw.md).')
.addText(t => t
.setPlaceholder('**/*.excalidraw.md')
.setValue(this.plugin.settings.tracking.excludedPatterns.join(', '))
@ -126,11 +126,11 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
// ----- Projections -----
containerEl.createEl('h2', { text: 'Projektionen' });
containerEl.createEl('h2', { text: 'Projections' });
new Setting(containerEl)
.setName('Projektionen aktiviert')
.setDesc('Automatische Markdown-Reports aus Event-Daten.')
.setName('Projections enabled')
.setDesc('Automatic Markdown reports from event data.')
.addToggle(t => t
.setValue(this.plugin.settings.projections.enabled)
.onChange(async (v) => {
@ -139,8 +139,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Output-Ordner')
.setDesc('Ordner für generierte Projektions-Dateien.')
.setName('Output folder')
.setDesc('Folder for generated projection files.')
.addText(t => t
.setPlaceholder('Logfire')
.setValue(this.plugin.settings.projections.outputFolder)
@ -150,8 +150,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Tagesprotokoll')
.setDesc('Automatisches Tagesprotokoll generieren.')
.setName('Daily log')
.setDesc('Generate automatic daily log.')
.addToggle(t => t
.setValue(this.plugin.settings.projections.dailyLog.enabled)
.onChange(async (v) => {
@ -169,8 +169,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Session-Protokoll')
.setDesc('Protokoll bei Session-Ende generieren.')
.setName('Session log')
.setDesc('Generate log on session end.')
.addToggle(t => t
.setValue(this.plugin.settings.projections.sessionLog.enabled)
.onChange(async (v) => {
@ -179,8 +179,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Wochen-Digest')
.setDesc('Wöchentliche Zusammenfassung generieren.')
.setName('Weekly digest')
.setDesc('Generate weekly summary.')
.addToggle(t => t
.setValue(this.plugin.settings.projections.weeklyDigest.enabled)
.onChange(async (v) => {
@ -189,8 +189,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}))
.addDropdown(d => d
.addOptions({
'0': 'Sonntag', '1': 'Montag', '2': 'Dienstag', '3': 'Mittwoch',
'4': 'Donnerstag', '5': 'Freitag', '6': 'Samstag',
'0': 'Sunday', '1': 'Monday', '2': 'Tuesday', '3': 'Wednesday',
'4': 'Thursday', '5': 'Friday', '6': 'Saturday',
})
.setValue(String(this.plugin.settings.projections.weeklyDigest.dayOfWeek))
.onChange(async (v) => {
@ -200,12 +200,12 @@ export class LogfireSettingTab extends PluginSettingTab {
// ----- Advanced (collapsible) -----
const advancedHeader = containerEl.createEl('details');
advancedHeader.createEl('summary', { text: 'Erweiterte Einstellungen' })
advancedHeader.createEl('summary', { text: 'Advanced Settings' })
.style.cursor = 'pointer';
const advEl = advancedHeader.createDiv();
advEl.createEl('p', {
text: 'Diese Einstellungen beeinflussen Performance und Speicher. Die Standardwerte sind für die meisten Vaults optimal.',
text: 'These settings affect performance and storage. The defaults are optimal for most vaults.',
cls: 'setting-item-description',
});
@ -213,8 +213,8 @@ export class LogfireSettingTab extends PluginSettingTab {
advEl.createEl('h3', { text: 'Performance' });
new Setting(advEl)
.setName('Editor-Debounce (ms)')
.setDesc('Wartezeit nach letztem Tastenanschlag vor editor:change Event.')
.setName('Editor debounce (ms)')
.setDesc('Wait time after last keystroke before editor:change event.')
.addText(t => t
.setValue(String(this.plugin.settings.advanced.debounceMs))
.onChange(async (v) => {
@ -226,8 +226,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(advEl)
.setName('Flush-Intervall (ms)')
.setDesc('Wie oft gepufferte Events in die Datenbank geschrieben werden.')
.setName('Flush interval (ms)')
.setDesc('How often buffered events are written to the database.')
.addText(t => t
.setValue(String(this.plugin.settings.advanced.flushIntervalMs))
.onChange(async (v) => {
@ -239,8 +239,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(advEl)
.setName('Flush-Schwellwert')
.setDesc('Maximale Events im Puffer vor erzwungenem Schreiben.')
.setName('Flush threshold')
.setDesc('Maximum events in buffer before forced write.')
.addText(t => t
.setValue(String(this.plugin.settings.advanced.flushThreshold))
.onChange(async (v) => {
@ -252,11 +252,11 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
// Retention
advEl.createEl('h3', { text: 'Aufbewahrung' });
advEl.createEl('h3', { text: 'Retention' });
new Setting(advEl)
.setName('Raw-Events aufbewahren (Tage)')
.setDesc('Danach werden Events in Tages-Statistiken aggregiert und gelöscht.')
.setName('Keep raw events (days)')
.setDesc('After this, events are aggregated into daily stats and deleted.')
.addText(t => t
.setValue(String(this.plugin.settings.advanced.retention.rawEventsDays))
.onChange(async (v) => {
@ -268,8 +268,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(advEl)
.setName('Tages-Statistiken aufbewahren (Tage)')
.setDesc('Danach werden Tages-Statistiken in Monats-Statistiken aggregiert.')
.setName('Keep daily stats (days)')
.setDesc('After this, daily stats are aggregated into monthly stats.')
.addText(t => t
.setValue(String(this.plugin.settings.advanced.retention.dailyStatsDays))
.onChange(async (v) => {
@ -281,8 +281,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(advEl)
.setName('Wartung beim Start')
.setDesc('Automatisch Retention-Cleanup beim Plugin-Start ausführen.')
.setName('Maintenance on startup')
.setDesc('Automatically run retention cleanup on plugin start.')
.addToggle(t => t
.setValue(this.plugin.settings.advanced.retention.maintenanceOnStartup)
.onChange(async (v) => {
@ -291,11 +291,11 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
// Database settings
advEl.createEl('h3', { text: 'Datenbank' });
advEl.createEl('h3', { text: 'Database' });
new Setting(advEl)
.setName('WAL-Modus')
.setDesc('Write-Ahead-Logging für bessere Parallelität (Neustart erforderlich).')
.setName('WAL mode')
.setDesc('Write-Ahead Logging for better concurrency (restart required).')
.addToggle(t => t
.setValue(this.plugin.settings.advanced.database.walMode)
.onChange(async (v) => {
@ -304,8 +304,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(advEl)
.setName('Cache-Größe (MB)')
.setDesc('SQLite Page-Cache (8256 MB). Mehr Cache = schnellere Queries.')
.setName('Cache size (MB)')
.setDesc('SQLite page cache (8256 MB). More cache = faster queries.')
.addSlider(s => s
.setLimits(8, 256, 8)
.setValue(this.plugin.settings.advanced.database.cacheSizeMb)
@ -316,8 +316,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(advEl)
.setName('MMap-Größe (MB)')
.setDesc('Memory-Mapped I/O (01024 MB). 0 deaktiviert MMap.')
.setName('MMap size (MB)')
.setDesc('Memory-Mapped I/O (01024 MB). 0 disables MMap.')
.addSlider(s => s
.setLimits(0, 1024, 32)
.setValue(this.plugin.settings.advanced.database.mmapSizeMb)
@ -328,8 +328,8 @@ export class LogfireSettingTab extends PluginSettingTab {
}));
new Setting(advEl)
.setName('Geschützte Event-Typen')
.setDesc('Event-Typen die nie gelöscht werden (komma-getrennt).')
.setName('Protected event types')
.setDesc('Event types that are never deleted (comma-separated).')
.addText(t => t
.setPlaceholder('file:create, file:delete, file:rename')
.setValue(this.plugin.settings.advanced.retention.neverDeleteTypes.join(', '))
@ -349,10 +349,10 @@ export class LogfireSettingTab extends PluginSettingTab {
const oldest = this.plugin.db.getOldestEventTimestamp();
dbInfoEl.createEl('p', {
text: `Events: ${eventCount.toLocaleString()} | Größe: ${formatBytes(dbSize)} | Ältestes: ${oldest ? new Date(oldest).toLocaleDateString() : 'k.A.'}`,
text: `Events: ${eventCount.toLocaleString()} | Size: ${formatBytes(dbSize)} | Oldest: ${oldest ? new Date(oldest).toLocaleDateString() : 'N/A'}`,
});
} catch {
dbInfoEl.createEl('p', { text: 'Datenbank-Info nicht verfügbar.' });
dbInfoEl.createEl('p', { text: 'Database info unavailable.' });
}
// Info section
@ -362,17 +362,17 @@ export class LogfireSettingTab extends PluginSettingTab {
});
new Setting(advEl)
.setName('Datenbank-Aktionen')
.setName('Database actions')
.addButton(b => b
.setButtonText('Wartung ausführen')
.setButtonText('Run maintenance')
.onClick(() => {
try {
this.plugin.db.runMaintenance(this.plugin.settings.advanced.retention);
new Notice('Logfire: Wartung abgeschlossen.');
new Notice('Logfire: Maintenance complete.');
this.display();
} catch (err) {
new Notice('Logfire: Wartung fehlgeschlagen.');
console.error('[Logfire] Wartung fehlgeschlagen:', err);
new Notice('Logfire: Maintenance failed.');
console.error('[Logfire] Maintenance failed:', err);
}
}));
}

View file

@ -36,7 +36,7 @@ export class StatusBar {
private update(): void {
const paused = this.plugin.isPaused();
const indicator = paused ? '\u23F8' : '\u{1F534}';
const state = paused ? 'Pausiert' : 'Aufnahme';
const state = paused ? 'Paused' : 'Recording';
const duration = this.formatDuration(this.plugin.sessionManager.sessionDurationMs);
const words = this.wordsAdded > 0 ? ` | +${this.wordsAdded}w` : '';

View file

@ -36,7 +36,7 @@ export function renderChart(
config: ChartConfig,
): void {
if (rows.length === 0) {
el.createEl('p', { text: 'Keine Daten.', cls: 'logfire-empty' });
el.createEl('p', { text: 'No data.', cls: 'logfire-empty' });
return;
}

View file

@ -71,7 +71,7 @@ export class DashboardView extends ItemView {
const refreshBtn = actions.createEl('button', {
cls: 'logfire-dash-btn clickable-icon',
attr: { 'aria-label': 'Aktualisieren' },
attr: { 'aria-label': 'Refresh' },
text: '\u21bb',
});
refreshBtn.addEventListener('click', () => this.refreshAll());
@ -121,7 +121,7 @@ export class DashboardView extends ItemView {
this.contentContainer.empty();
this.contentContainer.createDiv({
cls: 'logfire-empty',
text: 'Kein Dashboard geladen.',
text: 'No dashboard loaded.',
});
}
@ -170,19 +170,19 @@ export class DashboardView extends ItemView {
} catch (err) {
container.createDiv({
cls: 'logfire-error',
text: `Fehler: ${err instanceof Error ? err.message : String(err)}`,
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
});
}
}
private renderQueryWidget(container: HTMLElement, widget: DashboardWidget): void {
if (!widget.sql) {
container.createDiv({ cls: 'logfire-empty', text: 'Keine Query konfiguriert.' });
container.createDiv({ cls: 'logfire-empty', text: 'No query configured.' });
return;
}
const rows = this.db.queryReadOnly(widget.sql) as Record<string, unknown>[];
if (rows.length === 0) {
container.createDiv({ cls: 'logfire-empty', text: 'Keine Ergebnisse.' });
container.createDiv({ cls: 'logfire-empty', text: 'No results.' });
return;
}
renderTable(container, rows);
@ -190,7 +190,7 @@ export class DashboardView extends ItemView {
private renderChartWidget(container: HTMLElement, widget: DashboardWidget): void {
if (!widget.sql || !widget.chartConfig) {
container.createDiv({ cls: 'logfire-empty', text: 'Kein Chart konfiguriert.' });
container.createDiv({ cls: 'logfire-empty', text: 'No chart configured.' });
return;
}
const rows = this.db.queryReadOnly(widget.sql) as Record<string, unknown>[];
@ -199,7 +199,7 @@ export class DashboardView extends ItemView {
private renderStatWidget(container: HTMLElement, widget: DashboardWidget): void {
if (!widget.sql) {
container.createDiv({ cls: 'logfire-empty', text: 'Keine Query konfiguriert.' });
container.createDiv({ cls: 'logfire-empty', text: 'No query configured.' });
return;
}
const rows = this.db.queryReadOnly(widget.sql) as Record<string, unknown>[];

View file

@ -154,7 +154,7 @@ export class KeyboardNavigator {
navigator.clipboard.writeText(td.textContent ?? '');
td.classList.add('logfire-vim-yanked');
setTimeout(() => td.classList.remove('logfire-vim-yanked'), 600);
new Notice('In Zwischenablage kopiert.');
new Notice('Copied to clipboard.');
}
// ---------------------------------------------------------------------------
@ -225,7 +225,7 @@ export class KeyboardNavigator {
return;
}
}
new Notice('Nicht gefunden.');
new Notice('Not found.');
}
}