feat(i18n): grosser Sweep -- Toasts, Confirms, Notification-Center, Map, Empty-States, Lizenz-Hinweise

29 Stellen im Frontend lokalisiert (Toasts: Lage aktualisiert/geloescht/
archiviert/wiederhergestellt, Recherche abgebrochen, Daten aktualisiert,
Quelle hinzugefuegt/aktualisiert, Bericht heruntergeladen, kein RSS;
Confirms: Lage loeschen, Recherche abbrechen; Button-States: Wird
gestartet/abgebrochen/erstellt/gesendet, Suche Feeds, Quelle speichern;
Lizenz: abgelaufen/keine/Org-deaktiviert -- Nur Lesezugriff;
Notification-Center: Titel, Alle gelesen, Keine Benachrichtigungen;
Empty-States: Kein Vorfall ausgewaehlt; Map: Orte einlesen + Tooltip,
Keine Orte erkannt; Modal-Hint: Nur deutschsprachige Quellen). 30+
neue i18n-Keys. Cache-Buster app.js auf v=20260514c.
Dieser Commit ist enthalten in:
Claude Code
2026-05-13 22:16:42 +00:00
Ursprung 4fc3212e2c
Commit ea630cd31b
4 geänderte Dateien mit 4257 neuen und 4191 gelöschten Zeilen

Datei anzeigen

@@ -144,8 +144,8 @@
<main class="main-content" id="main-content">
<div class="empty-state" id="empty-state">
<div class="empty-state-icon">&#9737;</div>
<div class="empty-state-title">Kein Vorfall ausgewählt</div>
<div class="empty-state-text">Erstelle einen neuen Fall oder wähle einen bestehenden aus der Seitenleiste.</div>
<div class="empty-state-title" data-i18n="empty.no_incident_title">Kein Vorfall ausgewählt</div>
<div class="empty-state-text" data-i18n="empty.no_incident_text">Erstelle einen neuen Fall oder wähle einen bestehenden aus der Seitenleiste.</div>
</div>
@@ -270,11 +270,11 @@
<div class="card-title" data-i18n="card.map">Geografische Verteilung</div>
<span class="map-stats" id="map-stats"></span>
<div class="card-header-actions">
<button class="btn btn-secondary btn-small" id="geoparse-btn" onclick="App.triggerGeoparse()" title="Orte aus Artikeln einlesen">Orte einlesen</button>
<button class="btn btn-secondary btn-small" id="geoparse-btn" onclick="App.triggerGeoparse()" title="Orte aus Artikeln einlesen" data-i18n="map.import_locations" data-i18n-attr="title:map.import_locations_title">Orte einlesen</button>
</div>
</div>
<div class="map-container" id="map-container">
<div class="map-empty" id="map-empty">Keine Orte erkannt</div>
<div class="map-empty" id="map-empty" data-i18n="map.empty">Keine Orte erkannt</div>
</div>
</div>
</div>
@@ -729,7 +729,7 @@
<script src="/static/js/components.js?v=20260513d"></script>
<script src="/static/js/layout.js?v=20260513f"></script>
<script src="/static/js/pipeline.js?v=20260513d"></script>
<script src="/static/js/app.js?v=20260514b"></script>
<script src="/static/js/app.js?v=20260514c"></script>
<script src="/static/js/cluster-data.js?v=20260322f"></script>
<script src="/static/js/tutorial.js?v=20260316z"></script>
<script src="/static/js/chat.js?v=20260422a"></script>

Datei anzeigen

@@ -168,5 +168,38 @@
"toast.data_refreshed": "Daten aktualisiert.",
"toast.source_updated": "Quelle aktualisiert.",
"toast.session_expires": "Session läuft in {min} Minute(n) ab. Bitte erneut anmelden.",
"confirm.delete_incident": "Lage wirklich löschen? Alle gesammelten Daten gehen verloren."
"confirm.delete_incident": "Lage wirklich löschen? Alle gesammelten Daten gehen verloren.",
"toast.incident_updated": "Lage aktualisiert.",
"toast.refresh_started": "Aktualisierung gestartet.",
"toast.incident_deleted": "Lage gelöscht.",
"toast.incident_archived": "Lage archiviert.",
"toast.incident_restored": "Lage wiederhergestellt.",
"toast.research_cancelled": "Recherche abgebrochen.",
"toast.no_active_refresh": "Kein aktiver Refresh zum Abbrechen gefunden.",
"toast.report_downloaded": "Bericht heruntergeladen",
"toast.data_updated": "Daten aktualisiert.",
"toast.no_rss_save_as_web": "Kein RSS-Feed gefunden. Als Web-Quelle speichern?",
"toast.source_added": "Quelle hinzugefügt.",
"confirm.cancel_running_research": "Laufende Recherche abbrechen?",
"action.starting": "Wird gestartet...",
"action.cancelling": "Wird abgebrochen...",
"action.creating": "Wird erstellt...",
"action.sending": "Wird gesendet...",
"action.searching_feeds": "Suche Feeds...",
"action.save_source": "Quelle speichern",
"license.expired_readonly": "Lizenz abgelaufen – nur Lesezugriff",
"license.none_readonly": "Keine aktive Lizenz – nur Lesezugriff",
"license.org_disabled_readonly": "Organisation deaktiviert – nur Lesezugriff",
"notifications.title": "Benachrichtigungen",
"notifications.mark_all_read": "Alle gelesen",
"notifications.empty": "Keine Benachrichtigungen",
"empty.no_incident_title": "Kein Vorfall ausgewählt",
"empty.no_incident_text": "Erstelle einen neuen Fall oder wähle einen bestehenden aus der Seitenleiste.",
"map.import_locations": "Orte einlesen",
"map.import_locations_title": "Orte aus Artikeln einlesen",
"map.empty": "Keine Orte erkannt",
"source.type.rss_feed": "RSS-Feed",
"source.type.telegram": "Telegram",
"source.type.web": "Web-Quelle",
"modal.hint.sources_german_only": "Nur deutschsprachige Quellen (DE, AT, CH)"
}

Datei anzeigen

@@ -168,5 +168,38 @@
"toast.data_refreshed": "Data refreshed.",
"toast.source_updated": "Source updated.",
"toast.session_expires": "Session expires in {min} minute(s). Please sign in again.",
"confirm.delete_incident": "Really delete this situation? All collected data will be lost."
"confirm.delete_incident": "Really delete this situation? All collected data will be lost.",
"toast.incident_updated": "Situation refreshed.",
"toast.refresh_started": "Refresh started.",
"toast.incident_deleted": "Situation deleted.",
"toast.incident_archived": "Situation archived.",
"toast.incident_restored": "Situation restored.",
"toast.research_cancelled": "Research cancelled.",
"toast.no_active_refresh": "No active refresh found to cancel.",
"toast.report_downloaded": "Report downloaded",
"toast.data_updated": "Data refreshed.",
"toast.no_rss_save_as_web": "No RSS feed found. Save as web source?",
"toast.source_added": "Source added.",
"confirm.cancel_running_research": "Cancel running research?",
"action.starting": "Starting...",
"action.cancelling": "Cancelling...",
"action.creating": "Generating...",
"action.sending": "Sending...",
"action.searching_feeds": "Searching feeds...",
"action.save_source": "Save source",
"license.expired_readonly": "License expired – read-only",
"license.none_readonly": "No active license – read-only",
"license.org_disabled_readonly": "Organization disabled – read-only",
"notifications.title": "Notifications",
"notifications.mark_all_read": "Mark all read",
"notifications.empty": "No notifications",
"empty.no_incident_title": "No situation selected",
"empty.no_incident_text": "Create a new case or pick an existing one from the sidebar.",
"map.import_locations": "Import locations",
"map.import_locations_title": "Import locations from articles",
"map.empty": "No locations detected",
"source.type.rss_feed": "RSS feed",
"source.type.telegram": "Telegram",
"source.type.web": "Web source",
"modal.hint.sources_german_only": "Primary-language sources only"
}

Datei anzeigen

@@ -229,8 +229,8 @@ const NotificationCenter = {
</button>
<div class="notification-panel" id="notification-panel" style="display:none;">
<div class="notification-panel-header">
<span class="notification-panel-title">Benachrichtigungen</span>
<button class="notification-mark-read" id="notification-mark-read">Alle gelesen</button>
<span class="notification-panel-title">${(typeof T === 'function' ? T('notifications.title', 'Benachrichtigungen') : 'Benachrichtigungen')}</span>
<button class="notification-mark-read" id="notification-mark-read">${(typeof T === 'function' ? T('notifications.mark_all_read', 'Alle gelesen') : 'Alle gelesen')}</button>
</div>
<div class="notification-panel-list" id="notification-panel-list">
<div class="notification-empty">Keine Benachrichtigungen</div>
@@ -328,7 +328,7 @@ const NotificationCenter = {
if (!list) return;
if (this._notifications.length === 0) {
list.innerHTML = '<div class="notification-empty">Keine Benachrichtigungen</div>';
list.innerHTML = ('<div class="notification-empty">' + (typeof T === 'function' ? T('notifications.empty', 'Keine Benachrichtigungen') : 'Keine Benachrichtigungen') + '</div>');
return;
}
@@ -533,11 +533,11 @@ const App = {
if (reason === 'budget_exceeded') {
text = 'Token-Budget aufgebraucht – nur Lesezugriff. Für Aufstockung oder Upgrade bitte info@aegis-sight.de kontaktieren.';
} else if (reason === 'expired') {
text = 'Lizenz abgelaufen – nur Lesezugriff';
text = (typeof T === 'function' ? T('license.expired_readonly', 'Lizenz abgelaufen – nur Lesezugriff') : 'Lizenz abgelaufen – nur Lesezugriff');
} else if (reason === 'no_license') {
text = 'Keine aktive Lizenz – nur Lesezugriff';
text = (typeof T === 'function' ? T('license.none_readonly', 'Keine aktive Lizenz – nur Lesezugriff') : 'Keine aktive Lizenz – nur Lesezugriff');
} else if (reason === 'org_disabled') {
text = 'Organisation deaktiviert – nur Lesezugriff';
text = (typeof T === 'function' ? T('license.org_disabled_readonly', 'Organisation deaktiviert – nur Lesezugriff') : 'Organisation deaktiviert – nur Lesezugriff');
}
warningEl.textContent = text;
warningEl.classList.add('visible');
@@ -1869,7 +1869,7 @@ const App = {
closeModal('modal-new');
await this.loadIncidents();
await this.loadIncidentDetail(editId);
UI.showToast('Lage aktualisiert.', 'success');
UI.showToast((typeof T === 'function' ? T('toast.incident_updated', 'Lage aktualisiert.') : 'Lage aktualisiert.'), 'success');
} else {
// Create-Modus
const incident = await API.createIncident(data);
@@ -1973,7 +1973,7 @@ async handleRefresh() {
if (result && result.status === 'skipped') {
UI.showToast('Aktualisierung ist in der Warteschlange und wird ausgefuehrt, sobald die aktuelle Recherche abgeschlossen ist.', 'info');
} else {
UI.showToast('Aktualisierung gestartet.', 'success');
UI.showToast((typeof T === 'function' ? T('toast.refresh_started', 'Aktualisierung gestartet.') : 'Aktualisierung gestartet.'), 'success');
var _inc2 = this.incidents.find(function(i) { return i.id === this.currentIncidentId; }.bind(this));
UI.showProgress('queued', {}, this.currentIncidentId, _inc2 && !_inc2.has_summary);
}
@@ -1990,7 +1990,7 @@ async handleRefresh() {
async triggerGeoparse() {
if (!this.currentIncidentId) return;
const btn = document.getElementById('geoparse-btn');
if (btn) { btn.disabled = true; btn.textContent = 'Wird gestartet...'; }
if (btn) { btn.disabled = true; btn.textContent = (typeof T === 'function' ? T('action.starting', 'Wird gestartet...') : 'Wird gestartet...'); }
try {
const result = await API.triggerGeoparse(this.currentIncidentId);
if (result.status === 'done') {
@@ -2222,7 +2222,7 @@ async handleRefresh() {
document.getElementById('incident-view').style.display = 'none';
document.getElementById('empty-state').style.display = 'flex';
await this.loadIncidents();
UI.showToast('Lage gelöscht.', 'success');
UI.showToast((typeof T === 'function' ? T('toast.incident_deleted', 'Lage gelöscht.') : 'Lage gelöscht.'), 'success');
} catch (err) {
UI.showToast('Fehler: ' + err.message, 'error');
}
@@ -2288,7 +2288,7 @@ async handleRefresh() {
await this.loadIncidents();
await this.loadIncidentDetail(this.currentIncidentId);
this._updateArchiveButton(newStatus);
UI.showToast(isArchived ? 'Lage wiederhergestellt.' : 'Lage archiviert.', 'success');
UI.showToast(isArchived ? (typeof T === 'function' ? T('toast.incident_restored', 'Lage wiederhergestellt.') : 'Lage wiederhergestellt.') : (typeof T === 'function' ? T('toast.incident_archived', 'Lage archiviert.') : 'Lage archiviert.'), 'success');
} catch (err) {
UI.showToast('Fehler: ' + err.message, 'error');
}
@@ -2490,7 +2490,7 @@ async handleRefresh() {
this._pendingComplete = null;
UI.hideProgress(msg.incident_id);
}
UI.showToast('Recherche abgebrochen.', 'info');
UI.showToast((typeof T === 'function' ? T('toast.research_cancelled', 'Recherche abgebrochen.') : 'Recherche abgebrochen.'), 'info');
},
/**
@@ -2546,7 +2546,7 @@ async handleRefresh() {
const progressOverlay = document.getElementById('progress-overlay');
if (progressOverlay) progressOverlay.style.display = 'none';
const ok = await confirmDialog('Laufende Recherche abbrechen?');
const ok = await confirmDialog((typeof T === 'function' ? T('confirm.cancel_running_research', 'Laufende Recherche abbrechen?') : 'Laufende Recherche abbrechen?'));
// Restore progress popup if not confirmed
if (!ok) {
@@ -2559,18 +2559,18 @@ async handleRefresh() {
if (progressOverlay) progressOverlay.style.display = 'flex';
const btn = document.getElementById('progress-cancel-btn');
if (btn) {
btn.textContent = 'Wird abgebrochen...';
btn.textContent = (typeof T === 'function' ? T('action.cancelling', 'Wird abgebrochen...') : 'Wird abgebrochen...');
btn.disabled = true;
}
const titleEl = document.getElementById('progress-popup-title');
if (titleEl) titleEl.textContent = 'Wird abgebrochen...';
if (titleEl) titleEl.textContent = (typeof T === 'function' ? T('action.cancelling', 'Wird abgebrochen...') : 'Wird abgebrochen...');
try {
const result = await API.cancelRefresh(this.currentIncidentId);
if (!result) {
UI.showToast('Kein aktiver Refresh zum Abbrechen gefunden.', 'info');
UI.showToast((typeof T === 'function' ? T('toast.no_active_refresh', 'Kein aktiver Refresh zum Abbrechen gefunden.') : 'Kein aktiver Refresh zum Abbrechen gefunden.'), 'info');
if (btn) { btn.textContent = 'Abbrechen'; btn.disabled = false; }
if (titleEl) titleEl.textContent = 'Aktualisierung l\u00e4uft';
if (titleEl) titleEl.textContent = (typeof T === 'function' ? T('progress.title.refresh', 'Aktualisierung läuft') : 'Aktualisierung läuft');
}
} catch (err) {
UI.showToast('Abbrechen fehlgeschlagen: ' + err.message, 'error');
@@ -2599,7 +2599,7 @@ async handleRefresh() {
const btn = document.getElementById('export-submit-btn');
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Wird erstellt...';
btn.textContent = (typeof T === 'function' ? T('action.creating', 'Wird erstellt...') : 'Wird erstellt...');
try {
const response = await API.exportReport(this.currentIncidentId, format, null, sections);
@@ -2621,7 +2621,7 @@ async handleRefresh() {
document.body.removeChild(a);
URL.revokeObjectURL(url);
closeModal('modal-export');
UI.showToast('Bericht heruntergeladen', 'success');
UI.showToast((typeof T === 'function' ? T('toast.report_downloaded', 'Bericht heruntergeladen') : 'Bericht heruntergeladen'), 'success');
} catch (err) {
UI.showToast('Export fehlgeschlagen: ' + err.message, 'error');
} finally {
@@ -2661,7 +2661,7 @@ async handleRefresh() {
if (this.currentIncidentId) {
await this.selectIncident(this.currentIncidentId);
}
UI.showToast('Daten aktualisiert.', 'success', 2000);
UI.showToast((typeof T === 'function' ? T('toast.data_updated', 'Daten aktualisiert.') : 'Daten aktualisiert.'), 'success', 2000);
} catch (err) {
UI.showToast('Aktualisierung fehlgeschlagen: ' + err.message, 'error');
}
@@ -2708,7 +2708,7 @@ async handleRefresh() {
}
btn.disabled = true;
btn.textContent = 'Wird gesendet...';
btn.textContent = (typeof T === 'function' ? T('action.sending', 'Wird gesendet...') : 'Wird gesendet...');
try {
const formData = new FormData();
formData.append('category', category);
@@ -3179,7 +3179,7 @@ async handleRefresh() {
const btn = document.getElementById('src-discover-btn');
btn.disabled = true;
btn.textContent = 'Suche Feeds...';
btn.textContent = (typeof T === 'function' ? T('action.searching_feeds', 'Suche Feeds...') : 'Suche Feeds...');
try {
const result = await API.discoverMulti(url);
@@ -3223,7 +3223,7 @@ async handleRefresh() {
this.toggleSourceForm(false);
await this.loadSources();
} else if (result.total_found === 0) {
UI.showToast('Kein RSS-Feed gefunden. Als Web-Quelle speichern?', 'info');
UI.showToast((typeof T === 'function' ? T('toast.no_rss_save_as_web', 'Kein RSS-Feed gefunden. Als Web-Quelle speichern?') : 'Kein RSS-Feed gefunden. Als Web-Quelle speichern?'), 'info');
} else {
UI.showToast('Feed bereits vorhanden.', 'info');
}
@@ -3307,7 +3307,7 @@ async handleRefresh() {
// Submit-Button-Text ändern
const saveBtn = document.querySelector('#src-discovery-result .sources-discovery-actions .btn-primary');
if (saveBtn) saveBtn.textContent = 'Quelle speichern';
if (saveBtn) saveBtn.textContent = (typeof T === 'function' ? T('action.save_source', 'Quelle speichern') : 'Quelle speichern');
// Zum Formular scrollen
if (form) form.scrollIntoView({ behavior: 'smooth', block: 'start' });
@@ -3337,10 +3337,10 @@ async handleRefresh() {
try {
if (this._editingSourceId) {
await API.updateSource(this._editingSourceId, data);
UI.showToast('Quelle aktualisiert.', 'success');
UI.showToast((typeof T === 'function' ? T('toast.source_updated', 'Quelle aktualisiert.') : 'Quelle aktualisiert.'), 'success');
} else {
await API.createSource(data);
UI.showToast('Quelle hinzugefügt.', 'success');
UI.showToast((typeof T === 'function' ? T('toast.source_added', 'Quelle hinzugefügt.') : 'Quelle hinzugefügt.'), 'success');
}
this.toggleSourceForm(false);
@@ -3709,7 +3709,7 @@ function updateSourcesHint() {
if (hint) {
hint.textContent = intl
? 'DE + internationale Feeds (Reuters, BBC, Al Jazeera etc.)'
: 'Nur deutschsprachige Quellen (DE, AT, CH)';
: (typeof T === 'function' ? T('modal.hint.sources_german_only', 'Nur deutschsprachige Quellen (DE, AT, CH)') : 'Nur deutschsprachige Quellen (DE, AT, CH)');
}
}