Revert: Englische Sprachintegration (i18n DE/EN) komplett entfernen
Frontend-Dateien auf Zustand vor i18n zurückgesetzt. lang.js entfernt, CSP bereinigt. Backend-Umlaut-Fix bleibt erhalten. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -235,7 +235,7 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|||||||
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
|
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
|
||||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; "
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; "
|
||||||
"font-src 'self' https://fonts.gstatic.com; "
|
"font-src 'self' https://fonts.gstatic.com; "
|
||||||
"img-src 'self' data: https://tile.openstreetmap.de https://tile.openstreetmap.org; "
|
"img-src 'self' data: https://tile.openstreetmap.de; "
|
||||||
"connect-src 'self' wss: ws:; "
|
"connect-src 'self' wss: ws:; "
|
||||||
"frame-ancestors 'none'"
|
"frame-ancestors 'none'"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3584,35 +3584,6 @@ a:hover {
|
|||||||
transition: background 0.2s, border-color 0.2s;
|
transition: background 0.2s, border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Language Switcher === */
|
|
||||||
.lang-switcher {
|
|
||||||
display: flex;
|
|
||||||
gap: 2px;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
.lang-btn {
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
border: none;
|
|
||||||
transition: background 0.15s, color 0.15s;
|
|
||||||
}
|
|
||||||
.lang-btn:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
background: var(--bg-hover);
|
|
||||||
}
|
|
||||||
.lang-btn.active {
|
|
||||||
background: var(--accent);
|
|
||||||
color: var(--bg-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Light Theme Sonderregeln === */
|
/* === Light Theme Sonderregeln === */
|
||||||
[data-theme="light"] .sidebar {
|
[data-theme="light"] .sidebar {
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<script>(function(){var t=localStorage.getItem('osint_theme');if(t)document.documentElement.setAttribute('data-theme',t);try{var a=JSON.parse(localStorage.getItem('osint_a11y')||'{}');Object.keys(a).forEach(function(k){if(a[k])document.documentElement.setAttribute('data-a11y-'+k,'true');});}catch(e){}var l=localStorage.getItem('osint_lang')||'de';document.documentElement.lang=l;})()</script>
|
<script>(function(){var t=localStorage.getItem('osint_theme');if(t)document.documentElement.setAttribute('data-theme',t);try{var a=JSON.parse(localStorage.getItem('osint_a11y')||'{}');Object.keys(a).forEach(function(k){if(a[k])document.documentElement.setAttribute('data-a11y-'+k,'true');});}catch(e){}})()</script>
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<link rel="stylesheet" href="/static/css/style.css?v=20260304h">
|
<link rel="stylesheet" href="/static/css/style.css?v=20260304h">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<a href="#main-content" class="skip-link" data-i18n="header.skip_link">Zum Hauptinhalt springen</a>
|
<a href="#main-content" class="skip-link">Zum Hauptinhalt springen</a>
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="header">
|
<header class="header">
|
||||||
@@ -28,11 +28,7 @@
|
|||||||
<h1 class="sr-only">AegisSight Monitor Dashboard</h1>
|
<h1 class="sr-only">AegisSight Monitor Dashboard</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="lang-switcher" id="lang-switcher">
|
<button class="btn btn-secondary btn-small theme-toggle-btn" id="theme-toggle" onclick="ThemeManager.toggle()" title="Theme wechseln" aria-label="Theme wechseln">☼</button>
|
||||||
<button class="lang-btn" data-lang="de" onclick="LangManager.setLang('de')" title="Deutsch">DE</button>
|
|
||||||
<button class="lang-btn" data-lang="en" onclick="LangManager.setLang('en')" title="English">EN</button>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-secondary btn-small theme-toggle-btn" id="theme-toggle" onclick="ThemeManager.toggle()" data-i18n-title="header.theme_toggle" title="Theme wechseln" aria-label="Theme wechseln">☼</button>
|
|
||||||
<div class="header-user-info">
|
<div class="header-user-info">
|
||||||
<div class="header-user-top">
|
<div class="header-user-top">
|
||||||
<span class="header-user" id="header-user"></span>
|
<span class="header-user" id="header-user"></span>
|
||||||
@@ -41,25 +37,25 @@
|
|||||||
<span class="header-org-name" id="header-org-name"></span>
|
<span class="header-org-name" id="header-org-name"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-license-warning" id="header-license-warning"></div>
|
<div class="header-license-warning" id="header-license-warning"></div>
|
||||||
<button class="btn btn-secondary btn-small" id="logout-btn" data-i18n="header.logout">Abmelden</button>
|
<button class="btn btn-secondary btn-small" id="logout-btn">Abmelden</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<nav class="sidebar" aria-label="Seitenleiste">
|
<nav class="sidebar" aria-label="Seitenleiste">
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<button class="btn btn-primary btn-full btn-small" id="new-incident-btn" data-i18n="nav.new_incident">+ Neue Lage / Recherche</button>
|
<button class="btn btn-primary btn-full btn-small" id="new-incident-btn">+ Neue Lage / Recherche</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-filter">
|
<div class="sidebar-filter">
|
||||||
<button class="sidebar-filter-btn active" data-filter="all" onclick="App.setSidebarFilter('all')" aria-pressed="true" data-i18n="nav.filter_all">Alle</button>
|
<button class="sidebar-filter-btn active" data-filter="all" onclick="App.setSidebarFilter('all')" aria-pressed="true">Alle</button>
|
||||||
<button class="sidebar-filter-btn" data-filter="mine" onclick="App.setSidebarFilter('mine')" aria-pressed="false" data-i18n="nav.filter_mine">Eigene</button>
|
<button class="sidebar-filter-btn" data-filter="mine" onclick="App.setSidebarFilter('mine')" aria-pressed="false">Eigene</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('active-incidents')" role="button" tabindex="0" aria-expanded="true">
|
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('active-incidents')" role="button" tabindex="0" aria-expanded="true">
|
||||||
<span class="sidebar-chevron" id="chevron-active-incidents" aria-hidden="true">▾</span>
|
<span class="sidebar-chevron" id="chevron-active-incidents" aria-hidden="true">▾</span>
|
||||||
<span data-i18n="nav.active_incidents">Aktive Lagen</span>
|
Aktive Lagen
|
||||||
<span class="sidebar-section-count" id="count-active-incidents"></span>
|
<span class="sidebar-section-count" id="count-active-incidents"></span>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="active-incidents" aria-live="polite"></div>
|
<div id="active-incidents" aria-live="polite"></div>
|
||||||
@@ -68,7 +64,7 @@
|
|||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('active-research')" role="button" tabindex="0" aria-expanded="true">
|
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('active-research')" role="button" tabindex="0" aria-expanded="true">
|
||||||
<span class="sidebar-chevron" id="chevron-active-research" aria-hidden="true">▾</span>
|
<span class="sidebar-chevron" id="chevron-active-research" aria-hidden="true">▾</span>
|
||||||
<span data-i18n="nav.active_research">Aktive Recherchen</span>
|
Aktive Recherchen
|
||||||
<span class="sidebar-section-count" id="count-active-research"></span>
|
<span class="sidebar-section-count" id="count-active-research"></span>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="active-research" aria-live="polite"></div>
|
<div id="active-research" aria-live="polite"></div>
|
||||||
@@ -77,14 +73,14 @@
|
|||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('archived-incidents')" role="button" tabindex="0" aria-expanded="false">
|
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('archived-incidents')" role="button" tabindex="0" aria-expanded="false">
|
||||||
<span class="sidebar-chevron" id="chevron-archived-incidents" aria-hidden="true">▾</span>
|
<span class="sidebar-chevron" id="chevron-archived-incidents" aria-hidden="true">▾</span>
|
||||||
<span data-i18n="nav.archive">Archiv</span>
|
Archiv
|
||||||
<span class="sidebar-section-count" id="count-archived-incidents"></span>
|
<span class="sidebar-section-count" id="count-archived-incidents"></span>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="archived-incidents" aria-live="polite" style="display:none;"></div>
|
<div id="archived-incidents" aria-live="polite" style="display:none;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-sources-link">
|
<div class="sidebar-sources-link">
|
||||||
<button class="btn btn-secondary btn-full btn-small" onclick="App.openSourceManagement()" data-i18n="nav.manage_sources">Quellen verwalten</button>
|
<button class="btn btn-secondary btn-full btn-small" onclick="App.openSourceManagement()">Quellen verwalten</button>
|
||||||
<button class="btn btn-secondary btn-full btn-small sidebar-feedback-btn" onclick="App.openFeedback()" data-i18n="nav.send_feedback">Feedback senden</button>
|
<button class="btn btn-secondary btn-full btn-small sidebar-feedback-btn" onclick="App.openFeedback()">Feedback senden</button>
|
||||||
<div class="sidebar-stats-mini">
|
<div class="sidebar-stats-mini">
|
||||||
<span id="stat-sources-count">0 Quellen</span> · <span id="stat-articles-count">0 Artikel</span>
|
<span id="stat-sources-count">0 Quellen</span> · <span id="stat-articles-count">0 Artikel</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,8 +91,8 @@
|
|||||||
<main class="main-content" id="main-content">
|
<main class="main-content" id="main-content">
|
||||||
<div class="empty-state" id="empty-state">
|
<div class="empty-state" id="empty-state">
|
||||||
<div class="empty-state-icon">☉</div>
|
<div class="empty-state-icon">☉</div>
|
||||||
<div class="empty-state-title" data-i18n="empty.no_incident">Kein Vorfall ausgewählt</div>
|
<div class="empty-state-title">Kein Vorfall ausgewählt</div>
|
||||||
<div class="empty-state-text" data-i18n="empty.no_incident_text">Erstelle eine neue Lage oder wähle einen bestehenden Vorfall aus der Seitenleiste.</div>
|
<div class="empty-state-text">Erstelle eine neue Lage oder wähle einen bestehenden Vorfall aus der Seitenleiste.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Lagebild (hidden by default) -->
|
<!-- Lagebild (hidden by default) -->
|
||||||
@@ -112,22 +108,22 @@
|
|||||||
<h2 class="incident-header-title" id="incident-title"></h2>
|
<h2 class="incident-header-title" id="incident-title"></h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="incident-header-actions">
|
<div class="incident-header-actions">
|
||||||
<button class="btn btn-primary btn-small" id="refresh-btn" data-i18n="btn.refresh">Aktualisieren</button>
|
<button class="btn btn-primary btn-small" id="refresh-btn">Aktualisieren</button>
|
||||||
<button class="btn btn-secondary btn-small" id="edit-incident-btn" data-i18n="btn.edit">Bearbeiten</button>
|
<button class="btn btn-secondary btn-small" id="edit-incident-btn">Bearbeiten</button>
|
||||||
<div class="export-dropdown" id="export-dropdown">
|
<div class="export-dropdown" id="export-dropdown">
|
||||||
<button class="btn btn-secondary btn-small" onclick="App.toggleExportDropdown(event)" aria-expanded="false" aria-haspopup="true" data-i18n="btn.export">Exportieren ▾</button>
|
<button class="btn btn-secondary btn-small" onclick="App.toggleExportDropdown(event)" aria-expanded="false" aria-haspopup="true">Exportieren ▾</button>
|
||||||
<div class="export-dropdown-menu" id="export-dropdown-menu" role="menu">
|
<div class="export-dropdown-menu" id="export-dropdown-menu" role="menu">
|
||||||
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('md','report')" data-i18n="export.report_md">Lagebericht (Markdown)</button>
|
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('md','report')">Lagebericht (Markdown)</button>
|
||||||
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('json','report')" data-i18n="export.report_json">Lagebericht (JSON)</button>
|
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('json','report')">Lagebericht (JSON)</button>
|
||||||
<hr class="export-dropdown-divider" role="separator">
|
<hr class="export-dropdown-divider" role="separator">
|
||||||
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('md','full')" data-i18n="export.full_md">Vollexport (Markdown)</button>
|
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('md','full')">Vollexport (Markdown)</button>
|
||||||
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('json','full')" data-i18n="export.full_json">Vollexport (JSON)</button>
|
<button class="export-dropdown-item" role="menuitem" onclick="App.exportIncident('json','full')">Vollexport (JSON)</button>
|
||||||
<hr class="export-dropdown-divider" role="separator">
|
<hr class="export-dropdown-divider" role="separator">
|
||||||
<button class="export-dropdown-item" role="menuitem" onclick="App.printIncident()" data-i18n="export.print">Drucken / PDF</button>
|
<button class="export-dropdown-item" role="menuitem" onclick="App.printIncident()">Drucken / PDF</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary btn-small" id="archive-incident-btn" data-i18n="btn.archive">Archivieren</button>
|
<button class="btn btn-secondary btn-small" id="archive-incident-btn">Archivieren</button>
|
||||||
<button class="btn btn-danger btn-small" id="delete-incident-btn" data-i18n="btn.delete">Löschen</button>
|
<button class="btn btn-danger btn-small" id="delete-incident-btn">Löschen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="incident-header-row2">
|
<div class="incident-header-row2">
|
||||||
@@ -158,37 +154,37 @@
|
|||||||
<div class="progress-steps">
|
<div class="progress-steps">
|
||||||
<div class="progress-step" id="step-researching">
|
<div class="progress-step" id="step-researching">
|
||||||
<div class="progress-step-dot"></div>
|
<div class="progress-step-dot"></div>
|
||||||
<span data-i18n="progress.step_research">Recherche</span>
|
<span>Recherche</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-step" id="step-analyzing">
|
<div class="progress-step" id="step-analyzing">
|
||||||
<div class="progress-step-dot"></div>
|
<div class="progress-step-dot"></div>
|
||||||
<span data-i18n="progress.step_analysis">Analyse</span>
|
<span>Analyse</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-step" id="step-factchecking">
|
<div class="progress-step" id="step-factchecking">
|
||||||
<div class="progress-step-dot"></div>
|
<div class="progress-step-dot"></div>
|
||||||
<span data-i18n="progress.step_factcheck">Faktencheck</span>
|
<span>Faktencheck</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-track">
|
<div class="progress-track">
|
||||||
<div class="progress-fill" id="progress-fill"></div>
|
<div class="progress-fill" id="progress-fill"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-label-container">
|
<div class="progress-label-container">
|
||||||
<span id="progress-label" class="progress-label" data-i18n="progress.wait">Warte auf Start...</span>
|
<span id="progress-label" class="progress-label">Warte auf Start...</span>
|
||||||
<span id="progress-timer" class="progress-timer"></span>
|
<span id="progress-timer" class="progress-timer"></span>
|
||||||
</div>
|
</div>
|
||||||
<button id="progress-cancel-btn" class="progress-cancel-btn" onclick="App.cancelRefresh()" data-i18n="btn.cancel">Abbrechen</button>
|
<button id="progress-cancel-btn" class="progress-cancel-btn" onclick="App.cancelRefresh()">Abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Layout-Toolbar -->
|
<!-- Layout-Toolbar -->
|
||||||
<div class="layout-toolbar" id="layout-toolbar" style="display:none;">
|
<div class="layout-toolbar" id="layout-toolbar" style="display:none;">
|
||||||
<div class="layout-toggles">
|
<div class="layout-toggles">
|
||||||
<button class="layout-toggle-btn active" data-tile="lagebild" onclick="LayoutManager.toggleTile('lagebild')" aria-pressed="true" data-i18n="tile.lagebild">Lagebild</button>
|
<button class="layout-toggle-btn active" data-tile="lagebild" onclick="LayoutManager.toggleTile('lagebild')" aria-pressed="true">Lagebild</button>
|
||||||
<button class="layout-toggle-btn active" data-tile="faktencheck" onclick="LayoutManager.toggleTile('faktencheck')" aria-pressed="true" data-i18n="tile.faktencheck">Faktencheck</button>
|
<button class="layout-toggle-btn active" data-tile="faktencheck" onclick="LayoutManager.toggleTile('faktencheck')" aria-pressed="true">Faktencheck</button>
|
||||||
<button class="layout-toggle-btn active" data-tile="quellen" onclick="LayoutManager.toggleTile('quellen')" aria-pressed="true" data-i18n="tile.quellen">Quellen</button>
|
<button class="layout-toggle-btn active" data-tile="quellen" onclick="LayoutManager.toggleTile('quellen')" aria-pressed="true">Quellen</button>
|
||||||
<button class="layout-toggle-btn active" data-tile="timeline" onclick="LayoutManager.toggleTile('timeline')" aria-pressed="true" data-i18n="tile.timeline">Timeline</button>
|
<button class="layout-toggle-btn active" data-tile="timeline" onclick="LayoutManager.toggleTile('timeline')" aria-pressed="true">Timeline</button>
|
||||||
<button class="layout-toggle-btn active" data-tile="karte" onclick="LayoutManager.toggleTile('karte')" aria-pressed="true" data-i18n="tile.karte">Karte</button>
|
<button class="layout-toggle-btn active" data-tile="karte" onclick="LayoutManager.toggleTile('karte')" aria-pressed="true">Karte</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary btn-small" onclick="LayoutManager.reset()" data-i18n="btn.layout_reset">Layout zurücksetzen</button>
|
<button class="btn btn-secondary btn-small" onclick="LayoutManager.reset()">Layout zurücksetzen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- gridstack Dashboard-Grid -->
|
<!-- gridstack Dashboard-Grid -->
|
||||||
@@ -197,7 +193,7 @@
|
|||||||
<div class="grid-stack-item-content">
|
<div class="grid-stack-item-content">
|
||||||
<div class="card incident-analysis-summary">
|
<div class="card incident-analysis-summary">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal(LangManager.t('card.situation_report'), 'summary-content')" data-i18n="card.situation_report">Lagebild</div>
|
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Lagebild', 'summary-content')">Lagebild</div>
|
||||||
<span class="lagebild-timestamp" id="lagebild-timestamp"></span>
|
<span class="lagebild-timestamp" id="lagebild-timestamp"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="summary-content">
|
<div id="summary-content">
|
||||||
@@ -211,12 +207,12 @@
|
|||||||
<div class="grid-stack-item-content">
|
<div class="grid-stack-item-content">
|
||||||
<div class="card incident-analysis-factcheck" id="factcheck-card">
|
<div class="card incident-analysis-factcheck" id="factcheck-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal(LangManager.t('card.factcheck'), 'factcheck-list')" data-i18n="card.factcheck">Faktencheck</div>
|
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Faktencheck', 'factcheck-list')">Faktencheck</div>
|
||||||
<div class="fc-filter-bar" id="fc-filters"></div>
|
<div class="fc-filter-bar" id="fc-filters"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="factcheck-list" id="factcheck-list">
|
<div class="factcheck-list" id="factcheck-list">
|
||||||
<div class="empty-state" style="padding:20px;">
|
<div class="empty-state" style="padding:20px;">
|
||||||
<div class="empty-state-text" data-i18n="empty.no_factchecks">Noch keine Fakten geprüft</div>
|
<div class="empty-state-text">Noch keine Fakten geprüft</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,8 +224,8 @@
|
|||||||
<div class="card source-overview-card">
|
<div class="card source-overview-card">
|
||||||
<div class="card-header source-overview-header-toggle" onclick="App.toggleSourceOverview()" role="button" tabindex="0" aria-expanded="false">
|
<div class="card-header source-overview-header-toggle" onclick="App.toggleSourceOverview()" role="button" tabindex="0" aria-expanded="false">
|
||||||
<span class="source-overview-chevron" id="source-overview-chevron" title="Aufklappen" aria-hidden="true">▸</span>
|
<span class="source-overview-chevron" id="source-overview-chevron" title="Aufklappen" aria-hidden="true">▸</span>
|
||||||
<div class="card-title clickable" data-i18n="card.sources_overview">Quellenübersicht</div>
|
<div class="card-title clickable">Quellenübersicht</div>
|
||||||
<button class="btn btn-secondary btn-small source-detail-btn" onclick="event.stopPropagation(); openContentModal(LangManager.t('card.sources_overview'), 'source-overview-content')" data-i18n="card.detail_view">Detailansicht</button>
|
<button class="btn btn-secondary btn-small source-detail-btn" onclick="event.stopPropagation(); openContentModal('Quellenübersicht', 'source-overview-content')">Detailansicht</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="source-overview-content" style="display:none;"></div>
|
<div id="source-overview-content" style="display:none;"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -240,25 +236,25 @@
|
|||||||
<div class="grid-stack-item-content">
|
<div class="grid-stack-item-content">
|
||||||
<div class="card timeline-card">
|
<div class="card timeline-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal(LangManager.t('card.timeline'), 'timeline')" data-i18n="card.timeline">Ereignis-Timeline</div>
|
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Ereignis-Timeline', 'timeline')">Ereignis-Timeline</div>
|
||||||
<div class="ht-controls">
|
<div class="ht-controls">
|
||||||
<div class="ht-filter-group">
|
<div class="ht-filter-group">
|
||||||
<button class="ht-filter-btn active" data-filter="all" onclick="App.setTimelineFilter('all')" aria-pressed="true" data-i18n="timeline.all">Alle</button>
|
<button class="ht-filter-btn active" data-filter="all" onclick="App.setTimelineFilter('all')" aria-pressed="true">Alle</button>
|
||||||
<button class="ht-filter-btn" data-filter="articles" onclick="App.setTimelineFilter('articles')" aria-pressed="false" data-i18n="timeline.articles">Meldungen</button>
|
<button class="ht-filter-btn" data-filter="articles" onclick="App.setTimelineFilter('articles')" aria-pressed="false">Meldungen</button>
|
||||||
<button class="ht-filter-btn" data-filter="snapshots" onclick="App.setTimelineFilter('snapshots')" aria-pressed="false" data-i18n="timeline.snapshots">Lageberichte</button>
|
<button class="ht-filter-btn" data-filter="snapshots" onclick="App.setTimelineFilter('snapshots')" aria-pressed="false">Lageberichte</button>
|
||||||
</div>
|
</div>
|
||||||
<span class="ht-count" id="article-count"></span>
|
<span class="ht-count" id="article-count"></span>
|
||||||
<div class="ht-range-group">
|
<div class="ht-range-group">
|
||||||
<button class="ht-range-btn" data-range="24h" onclick="App.setTimelineRange('24h')" aria-pressed="false" data-i18n="timeline.range_24h">24h</button>
|
<button class="ht-range-btn" data-range="24h" onclick="App.setTimelineRange('24h')" aria-pressed="false">24h</button>
|
||||||
<button class="ht-range-btn" data-range="7d" onclick="App.setTimelineRange('7d')" aria-pressed="false" data-i18n="timeline.range_7d">7T</button>
|
<button class="ht-range-btn" data-range="7d" onclick="App.setTimelineRange('7d')" aria-pressed="false">7T</button>
|
||||||
<button class="ht-range-btn active" data-range="all" onclick="App.setTimelineRange('all')" aria-pressed="true" data-i18n="timeline.range_all">Alles</button>
|
<button class="ht-range-btn active" data-range="all" onclick="App.setTimelineRange('all')" aria-pressed="true">Alles</button>
|
||||||
</div>
|
</div>
|
||||||
<label for="timeline-search" class="sr-only">Timeline durchsuchen</label>
|
<label for="timeline-search" class="sr-only">Timeline durchsuchen</label>
|
||||||
<input type="text" id="timeline-search" class="timeline-filter-input" placeholder="Suche..." data-i18n-placeholder="timeline.search_placeholder" oninput="App.debouncedRerenderTimeline()">
|
<input type="text" id="timeline-search" class="timeline-filter-input" placeholder="Suche..." oninput="App.debouncedRerenderTimeline()">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="timeline" class="ht-timeline-container">
|
<div id="timeline" class="ht-timeline-container">
|
||||||
<div class="ht-empty" data-i18n="empty.no_articles">Noch keine Meldungen</div>
|
<div class="ht-empty">Noch keine Meldungen</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -268,12 +264,12 @@
|
|||||||
<div class="grid-stack-item-content">
|
<div class="grid-stack-item-content">
|
||||||
<div class="card map-card">
|
<div class="card map-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title" data-i18n="card.map">Geografische Verteilung</div>
|
<div class="card-title">Geografische Verteilung</div>
|
||||||
<span class="map-stats" id="map-stats"></span>
|
<span class="map-stats" id="map-stats"></span>
|
||||||
<button class="btn btn-secondary btn-small" id="geoparse-btn" onclick="App.triggerGeoparse()" title="Orte aus Artikeln erkennen" data-i18n="btn.detect_locations">Orte erkennen</button>
|
<button class="btn btn-secondary btn-small" id="geoparse-btn" onclick="App.triggerGeoparse()" title="Orte aus Artikeln erkennen">Orte erkennen</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="map-container" id="map-container">
|
<div class="map-container" id="map-container">
|
||||||
<div class="map-empty" id="map-empty" data-i18n="empty.no_locations">Keine Orte erkannt</div>
|
<div class="map-empty" id="map-empty">Keine Orte erkannt</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -290,103 +286,103 @@
|
|||||||
<div class="modal-overlay" id="modal-new" role="dialog" aria-modal="true" aria-labelledby="modal-new-title">
|
<div class="modal-overlay" id="modal-new" role="dialog" aria-modal="true" aria-labelledby="modal-new-title">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="modal-title" id="modal-new-title" data-i18n="modal.new_incident">Neue Lage anlegen</div>
|
<div class="modal-title" id="modal-new-title">Neue Lage anlegen</div>
|
||||||
<button class="modal-close" onclick="closeModal('modal-new')" aria-label="Schließen">×</button>
|
<button class="modal-close" onclick="closeModal('modal-new')" aria-label="Schließen">×</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="new-incident-form">
|
<form id="new-incident-form">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inc-title" data-i18n="form.incident_title">Titel des Vorfalls</label>
|
<label for="inc-title">Titel des Vorfalls</label>
|
||||||
<input type="text" id="inc-title" required aria-required="true" placeholder="z.B. Explosion in Madrid" data-i18n-placeholder="form.incident_title_placeholder">
|
<input type="text" id="inc-title" required aria-required="true" placeholder="z.B. Explosion in Madrid">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inc-description" data-i18n="form.description">Beschreibung / Kontext</label>
|
<label for="inc-description">Beschreibung / Kontext</label>
|
||||||
<textarea id="inc-description" placeholder="Weitere Details zum Vorfall (optional)" data-i18n-placeholder="form.description_placeholder"></textarea>
|
<textarea id="inc-description" placeholder="Weitere Details zum Vorfall (optional)"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inc-type" data-i18n="form.incident_type">Art der Lage</label>
|
<label for="inc-type">Art der Lage</label>
|
||||||
<select id="inc-type" onchange="toggleTypeDefaults()">
|
<select id="inc-type" onchange="toggleTypeDefaults()">
|
||||||
<option value="adhoc" data-i18n="form.type_adhoc">Ad-hoc Lage (Breaking News)</option>
|
<option value="adhoc">Ad-hoc Lage (Breaking News)</option>
|
||||||
<option value="research" data-i18n="form.type_research">Recherche (Hintergrund)</option>
|
<option value="research">Recherche (Hintergrund)</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-hint" id="type-hint" data-i18n="form.type_hint_adhoc">
|
<div class="form-hint" id="type-hint">
|
||||||
RSS-Feeds + WebSearch, automatische Aktualisierung empfohlen
|
RSS-Feeds + WebSearch, automatische Aktualisierung empfohlen
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label data-i18n="form.sources">Quellen</label>
|
<label>Quellen</label>
|
||||||
<div class="toggle-group">
|
<div class="toggle-group">
|
||||||
<label class="toggle-label">
|
<label class="toggle-label">
|
||||||
<input type="checkbox" id="inc-international" checked>
|
<input type="checkbox" id="inc-international" checked>
|
||||||
<span class="toggle-switch"></span>
|
<span class="toggle-switch"></span>
|
||||||
<span class="toggle-text" data-i18n="form.international_sources">Internationale Quellen einbeziehen</span>
|
<span class="toggle-text">Internationale Quellen einbeziehen</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="form-hint" id="sources-hint" data-i18n="form.sources_hint_intl">DE + internationale Feeds (Reuters, BBC, Al Jazeera etc.)</div>
|
<div class="form-hint" id="sources-hint">DE + internationale Feeds (Reuters, BBC, Al Jazeera etc.)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label data-i18n="form.visibility">Sichtbarkeit</label>
|
<label>Sichtbarkeit</label>
|
||||||
<div class="toggle-group">
|
<div class="toggle-group">
|
||||||
<label class="toggle-label">
|
<label class="toggle-label">
|
||||||
<input type="checkbox" id="inc-visibility" checked>
|
<input type="checkbox" id="inc-visibility" checked>
|
||||||
<span class="toggle-switch"></span>
|
<span class="toggle-switch"></span>
|
||||||
<span class="toggle-text" id="visibility-text" data-i18n="form.visibility_public">Öffentlich — für alle Nutzer sichtbar</span>
|
<span class="toggle-text" id="visibility-text">Öffentlich — für alle Nutzer sichtbar</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inc-refresh-mode" data-i18n="form.refresh_mode">Aktualisierung</label>
|
<label for="inc-refresh-mode">Aktualisierung</label>
|
||||||
<select id="inc-refresh-mode" onchange="toggleRefreshInterval()">
|
<select id="inc-refresh-mode" onchange="toggleRefreshInterval()">
|
||||||
<option value="manual" data-i18n="form.refresh_manual">Manuell</option>
|
<option value="manual">Manuell</option>
|
||||||
<option value="auto" data-i18n="form.refresh_auto">Automatisch</option>
|
<option value="auto">Automatisch</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group conditional-field" id="refresh-interval-field">
|
<div class="form-group conditional-field" id="refresh-interval-field">
|
||||||
<label for="inc-refresh-value" data-i18n="form.interval">Intervall</label>
|
<label for="inc-refresh-value">Intervall</label>
|
||||||
<div class="interval-input-group">
|
<div class="interval-input-group">
|
||||||
<input type="number" id="inc-refresh-value" min="10" value="15">
|
<input type="number" id="inc-refresh-value" min="10" value="15">
|
||||||
<select id="inc-refresh-unit" onchange="updateIntervalMin()">
|
<select id="inc-refresh-unit" onchange="updateIntervalMin()">
|
||||||
<option value="1" selected data-i18n="form.unit_minutes">Minuten</option>
|
<option value="1" selected>Minuten</option>
|
||||||
<option value="60" data-i18n="form.unit_hours">Stunden</option>
|
<option value="60">Stunden</option>
|
||||||
<option value="1440" data-i18n="form.unit_days">Tage</option>
|
<option value="1440">Tage</option>
|
||||||
<option value="10080" data-i18n="form.unit_weeks">Wochen</option>
|
<option value="10080">Wochen</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inc-retention" data-i18n="form.retention">Aufbewahrung (Tage)</label>
|
<label for="inc-retention">Aufbewahrung (Tage)</label>
|
||||||
<input type="number" id="inc-retention" min="0" max="999" value="30" placeholder="0 = Unbegrenzt">
|
<input type="number" id="inc-retention" min="0" max="999" value="30" placeholder="0 = Unbegrenzt">
|
||||||
<div class="form-hint" data-i18n="form.retention_hint">0 = Unbegrenzt, max. 999 Tage</div>
|
<div class="form-hint">0 = Unbegrenzt, max. 999 Tage</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-top: 8px;">
|
<div class="form-group" style="margin-top: 8px;">
|
||||||
<label data-i18n="form.email_notifications">E-Mail-Benachrichtigungen</label>
|
<label>E-Mail-Benachrichtigungen</label>
|
||||||
<div class="form-hint" style="margin-bottom: 8px;" data-i18n="form.email_notify_hint">Per E-Mail benachrichtigen bei:</div>
|
<div class="form-hint" style="margin-bottom: 8px;">Per E-Mail benachrichtigen bei:</div>
|
||||||
<div class="toggle-group">
|
<div class="toggle-group">
|
||||||
<label class="toggle-label">
|
<label class="toggle-label">
|
||||||
<input type="checkbox" id="inc-notify-summary">
|
<input type="checkbox" id="inc-notify-summary">
|
||||||
<span class="toggle-switch"></span>
|
<span class="toggle-switch"></span>
|
||||||
<span class="toggle-text" data-i18n="form.notify_summary">Neues Lagebild</span>
|
<span class="toggle-text">Neues Lagebild</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="toggle-group" style="margin-top: 8px;">
|
<div class="toggle-group" style="margin-top: 8px;">
|
||||||
<label class="toggle-label">
|
<label class="toggle-label">
|
||||||
<input type="checkbox" id="inc-notify-new-articles">
|
<input type="checkbox" id="inc-notify-new-articles">
|
||||||
<span class="toggle-switch"></span>
|
<span class="toggle-switch"></span>
|
||||||
<span class="toggle-text" data-i18n="form.notify_articles">Neue Artikel</span>
|
<span class="toggle-text">Neue Artikel</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="toggle-group" style="margin-top: 8px;">
|
<div class="toggle-group" style="margin-top: 8px;">
|
||||||
<label class="toggle-label">
|
<label class="toggle-label">
|
||||||
<input type="checkbox" id="inc-notify-status-change">
|
<input type="checkbox" id="inc-notify-status-change">
|
||||||
<span class="toggle-switch"></span>
|
<span class="toggle-switch"></span>
|
||||||
<span class="toggle-text" data-i18n="form.notify_status">Statusänderung Faktencheck</span>
|
<span class="toggle-text">Statusänderung Faktencheck</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" onclick="closeModal('modal-new')" data-i18n="btn.cancel">Abbrechen</button>
|
<button type="button" class="btn btn-secondary" onclick="closeModal('modal-new')">Abbrechen</button>
|
||||||
<button type="submit" class="btn btn-primary" id="modal-new-submit" data-i18n="modal.create_incident">Lage anlegen</button>
|
<button type="submit" class="btn btn-primary" id="modal-new-submit">Lage anlegen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -396,7 +392,7 @@
|
|||||||
<div class="modal-overlay" id="modal-sources" role="dialog" aria-modal="true" aria-labelledby="modal-sources-title">
|
<div class="modal-overlay" id="modal-sources" role="dialog" aria-modal="true" aria-labelledby="modal-sources-title">
|
||||||
<div class="modal modal-wide">
|
<div class="modal modal-wide">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="modal-title" id="modal-sources-title" data-i18n="modal.source_management">Quellenverwaltung</div>
|
<div class="modal-title" id="modal-sources-title">Quellenverwaltung</div>
|
||||||
<button class="modal-close" onclick="closeModal('modal-sources')" aria-label="Schließen">×</button>
|
<button class="modal-close" onclick="closeModal('modal-sources')" aria-label="Schließen">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body sources-modal-body">
|
<div class="modal-body sources-modal-body">
|
||||||
@@ -532,29 +528,29 @@
|
|||||||
<div class="modal-overlay" id="modal-feedback" role="dialog" aria-modal="true" aria-labelledby="modal-feedback-title">
|
<div class="modal-overlay" id="modal-feedback" role="dialog" aria-modal="true" aria-labelledby="modal-feedback-title">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="modal-title" id="modal-feedback-title" data-i18n="modal.feedback">Feedback senden</div>
|
<div class="modal-title" id="modal-feedback-title">Feedback senden</div>
|
||||||
<button class="modal-close" onclick="closeModal('modal-feedback')" aria-label="Schließen">×</button>
|
<button class="modal-close" onclick="closeModal('modal-feedback')" aria-label="Schließen">×</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="feedback-form">
|
<form id="feedback-form">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="fb-category" data-i18n="form.feedback_category">Kategorie</label>
|
<label for="fb-category">Kategorie</label>
|
||||||
<select id="fb-category">
|
<select id="fb-category">
|
||||||
<option value="bug" data-i18n="form.fb_bug">Fehlerbericht</option>
|
<option value="bug">Fehlerbericht</option>
|
||||||
<option value="feature" data-i18n="form.fb_feature">Feature-Wunsch</option>
|
<option value="feature">Feature-Wunsch</option>
|
||||||
<option value="question" data-i18n="form.fb_question">Frage</option>
|
<option value="question">Frage</option>
|
||||||
<option value="other" data-i18n="form.fb_other">Sonstiges</option>
|
<option value="other">Sonstiges</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="fb-message" data-i18n="form.fb_message">Nachricht</label>
|
<label for="fb-message">Nachricht</label>
|
||||||
<textarea id="fb-message" required aria-required="true" minlength="10" maxlength="5000" rows="6" placeholder="Beschreibe dein Anliegen (mind. 10 Zeichen)..." data-i18n-placeholder="form.fb_placeholder"></textarea>
|
<textarea id="fb-message" required aria-required="true" minlength="10" maxlength="5000" rows="6" placeholder="Beschreibe dein Anliegen (mind. 10 Zeichen)..."></textarea>
|
||||||
<div class="form-hint"><span id="fb-char-count">0</span> / 5.000 Zeichen</div>
|
<div class="form-hint"><span id="fb-char-count">0</span> / 5.000 Zeichen</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" onclick="closeModal('modal-feedback')" data-i18n="btn.cancel">Abbrechen</button>
|
<button type="button" class="btn btn-secondary" onclick="closeModal('modal-feedback')">Abbrechen</button>
|
||||||
<button type="submit" class="btn btn-primary" id="fb-submit-btn" data-i18n="btn.submit_feedback">Absenden</button>
|
<button type="submit" class="btn btn-primary" id="fb-submit-btn">Absenden</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -566,7 +562,6 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/gridstack@12/dist/gridstack-all.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/gridstack@12/dist/gridstack-all.js"></script>
|
||||||
<script src="/static/vendor/leaflet.js"></script>
|
<script src="/static/vendor/leaflet.js"></script>
|
||||||
<script src="/static/vendor/leaflet.markercluster.js"></script>
|
<script src="/static/vendor/leaflet.markercluster.js"></script>
|
||||||
<script src="/static/js/lang.js?v=20260305a"></script>
|
|
||||||
<script src="/static/js/api.js?v=20260304h"></script>
|
<script src="/static/js/api.js?v=20260304h"></script>
|
||||||
<script src="/static/js/ws.js?v=20260304h"></script>
|
<script src="/static/js/ws.js?v=20260304h"></script>
|
||||||
<script src="/static/js/components.js?v=20260304h"></script>
|
<script src="/static/js/components.js?v=20260304h"></script>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<script>(function(){var t=localStorage.getItem('osint_theme');if(t)document.documentElement.setAttribute('data-theme',t);try{var a=JSON.parse(localStorage.getItem('osint_a11y')||'{}');Object.keys(a).forEach(function(k){if(a[k])document.documentElement.setAttribute('data-a11y-'+k,'true');});}catch(e){}var l=localStorage.getItem('osint_lang')||'de';document.documentElement.lang=l;})()</script>
|
<script>(function(){var t=localStorage.getItem('osint_theme');if(t)document.documentElement.setAttribute('data-theme',t);try{var a=JSON.parse(localStorage.getItem('osint_a11y')||'{}');Object.keys(a).forEach(function(k){if(a[k])document.documentElement.setAttribute('data-a11y-'+k,'true');});}catch(e){}})()</script>
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||||
@@ -12,16 +12,15 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/css/style.css?v=20260305a">
|
<link rel="stylesheet" href="/static/css/style.css?v=20260304a">
|
||||||
<script src="/static/js/lang.js?v=20260305a"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<a href="#login-form" class="skip-link" data-i18n="login.skip_link">Zum Anmeldeformular springen</a>
|
<a href="#login-form" class="skip-link">Zum Anmeldeformular springen</a>
|
||||||
<main class="login-container">
|
<main class="login-container">
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<div class="login-logo">
|
<div class="login-logo">
|
||||||
<h1>Aegis<span style="color: var(--accent)">Sight</span></h1>
|
<h1>Aegis<span style="color: var(--accent)">Sight</span></h1>
|
||||||
<div class="subtitle" data-i18n="login.subtitle">Lagemonitor</div>
|
<div class="subtitle">Lagemonitor</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="login-error" class="login-error" role="alert" aria-live="assertive"></div>
|
<div id="login-error" class="login-error" role="alert" aria-live="assertive"></div>
|
||||||
@@ -30,34 +29,29 @@
|
|||||||
<!-- Schritt 1: E-Mail eingeben -->
|
<!-- Schritt 1: E-Mail eingeben -->
|
||||||
<form id="email-form">
|
<form id="email-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email" data-i18n="login.email_label">E-Mail-Adresse</label>
|
<label for="email">E-Mail-Adresse</label>
|
||||||
<input type="email" id="email" name="email" autocomplete="email" required aria-required="true" placeholder="name@organisation.de" data-i18n-placeholder="login.email_placeholder">
|
<input type="email" id="email" name="email" autocomplete="email" required aria-required="true" placeholder="name@organisation.de">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary btn-full" id="email-btn" data-i18n="login.submit">Anmelden</button>
|
<button type="submit" class="btn btn-primary btn-full" id="email-btn">Anmelden</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Schritt 2: Code eingeben -->
|
<!-- Schritt 2: Code eingeben -->
|
||||||
<form id="code-form" style="display:none;">
|
<form id="code-form" style="display:none;">
|
||||||
<p id="code-sent-text" style="color: var(--text-secondary); margin: 0 0 16px 0; font-size: 14px;">
|
<p style="color: var(--text-secondary); margin: 0 0 16px 0; font-size: 14px;">
|
||||||
Ein 6-stelliger Code wurde an <strong id="sent-email"></strong> gesendet.
|
Ein 6-stelliger Code wurde an <strong id="sent-email"></strong> gesendet.
|
||||||
</p>
|
</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="code" data-i18n="login.code_label">Code eingeben</label>
|
<label for="code">Code eingeben</label>
|
||||||
<input type="text" id="code" name="code" autocomplete="one-time-code" required aria-required="true"
|
<input type="text" id="code" name="code" autocomplete="one-time-code" required aria-required="true"
|
||||||
placeholder="000000" maxlength="6" pattern="[0-9]{6}"
|
placeholder="000000" maxlength="6" pattern="[0-9]{6}"
|
||||||
style="text-align:center; font-size:24px; letter-spacing:8px; font-family:monospace;">
|
style="text-align:center; font-size:24px; letter-spacing:8px; font-family:monospace;">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary btn-full" id="code-btn" data-i18n="login.verify">Verifizieren</button>
|
<button type="submit" class="btn btn-primary btn-full" id="code-btn">Verifizieren</button>
|
||||||
<button type="button" class="btn btn-secondary btn-full" id="back-btn" style="margin-top:8px;" data-i18n="login.back">Zurück</button>
|
<button type="button" class="btn btn-secondary btn-full" id="back-btn" style="margin-top:8px;">Zurück</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div style="text-align:center;margin-top:16px;">
|
<div style="text-align:center;margin-top:16px;">
|
||||||
<div class="lang-switcher" id="lang-switcher" style="display:inline-flex;margin-bottom:8px;">
|
<button class="btn btn-secondary btn-small theme-toggle-btn" id="theme-toggle" onclick="ThemeManager.toggle()" title="Theme wechseln" aria-label="Theme wechseln">☼</button>
|
||||||
<button class="lang-btn" data-lang="de" onclick="LangManager.setLang('de')" title="Deutsch">DE</button>
|
|
||||||
<button class="lang-btn" data-lang="en" onclick="LangManager.setLang('en')" title="English">EN</button>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<button class="btn btn-secondary btn-small theme-toggle-btn" id="theme-toggle" onclick="ThemeManager.toggle()" data-i18n-title="header.theme_toggle" title="Theme wechseln" aria-label="Theme wechseln">☼</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -138,7 +132,7 @@
|
|||||||
errorEl.style.display = 'none';
|
errorEl.style.display = 'none';
|
||||||
successEl.style.display = 'none';
|
successEl.style.display = 'none';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = LangManager.t('login.sending');
|
btn.textContent = 'Wird gesendet...';
|
||||||
|
|
||||||
currentEmail = document.getElementById('email').value.trim();
|
currentEmail = document.getElementById('email').value.trim();
|
||||||
|
|
||||||
@@ -165,7 +159,7 @@
|
|||||||
errorEl.style.display = 'block';
|
errorEl.style.display = 'block';
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = LangManager.t('login.submit');
|
btn.textContent = 'Anmelden';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,7 +170,7 @@
|
|||||||
const btn = document.getElementById('code-btn');
|
const btn = document.getElementById('code-btn');
|
||||||
errorEl.style.display = 'none';
|
errorEl.style.display = 'none';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = LangManager.t('login.verifying');
|
btn.textContent = 'Wird geprüft...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/auth/verify-code', {
|
const response = await fetch('/api/auth/verify-code', {
|
||||||
@@ -202,7 +196,7 @@
|
|||||||
errorEl.style.display = 'block';
|
errorEl.style.display = 'block';
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = LangManager.t('login.verify');
|
btn.textContent = 'Verifizieren';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@@ -29,9 +29,9 @@ const UI = {
|
|||||||
<span class="incident-dot ${dotClass}" id="dot-${incident.id}" aria-hidden="true"></span>
|
<span class="incident-dot ${dotClass}" id="dot-${incident.id}" aria-hidden="true"></span>
|
||||||
<div style="flex:1;min-width:0;">
|
<div style="flex:1;min-width:0;">
|
||||||
<div class="incident-name">${this.escape(incident.title)}</div>
|
<div class="incident-name">${this.escape(incident.title)}</div>
|
||||||
<div class="incident-meta">${LangManager.t('sidebar.articles', { n: incident.article_count })} · ${this.escape(creator)}</div>
|
<div class="incident-meta">${incident.article_count} Artikel · ${this.escape(creator)}</div>
|
||||||
</div>
|
</div>
|
||||||
${incident.visibility === 'private' ? `<span class="badge badge-private" style="font-size:9px;" aria-label="Private Lage">${LangManager.t('status.private')}</span>` : ''}
|
${incident.visibility === 'private' ? '<span class="badge badge-private" style="font-size:9px;" aria-label="Private Lage">PRIVAT</span>' : ''}
|
||||||
${incident.refresh_mode === 'auto' ? '<span class="badge badge-auto" role="img" aria-label="Auto-Refresh aktiv">↻</span>' : ''}
|
${incident.refresh_mode === 'auto' ? '<span class="badge badge-auto" role="img" aria-label="Auto-Refresh aktiv">↻</span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -40,15 +40,34 @@ const UI = {
|
|||||||
/**
|
/**
|
||||||
* Faktencheck-Eintrag rendern.
|
* Faktencheck-Eintrag rendern.
|
||||||
*/
|
*/
|
||||||
_fcStatuses: ['confirmed','unconfirmed','contradicted','developing','established','disputed','unverified'],
|
factCheckLabels: {
|
||||||
get factCheckLabels() {
|
confirmed: 'Bestätigt durch mehrere Quellen',
|
||||||
const o = {}; this._fcStatuses.forEach(s => o[s] = LangManager.t('fc.' + s)); return o;
|
unconfirmed: 'Nicht unabhängig bestätigt',
|
||||||
|
contradicted: 'Widerlegt',
|
||||||
|
developing: 'Faktenlage noch im Fluss',
|
||||||
|
established: 'Gesicherter Fakt (3+ Quellen)',
|
||||||
|
disputed: 'Umstrittener Sachverhalt',
|
||||||
|
unverified: 'Nicht unabhängig verifizierbar',
|
||||||
},
|
},
|
||||||
get factCheckTooltips() {
|
|
||||||
const o = {}; this._fcStatuses.forEach(s => o[s] = LangManager.t('fc_tooltip.' + s)); return o;
|
factCheckTooltips: {
|
||||||
|
confirmed: 'Bestätigt: Mindestens zwei unabhängige, seriöse Quellen stützen diese Aussage übereinstimmend.',
|
||||||
|
established: 'Gesichert: Drei oder mehr unabhängige Quellen bestätigen den Sachverhalt. Hohe Verlässlichkeit.',
|
||||||
|
developing: 'Unklar: Die Faktenlage ist noch im Fluss. Neue Informationen können das Bild verändern.',
|
||||||
|
unconfirmed: 'Unbestätigt: Bisher nur aus einer Quelle bekannt. Eine unabhängige Bestätigung steht aus.',
|
||||||
|
unverified: 'Ungeprüft: Die Aussage konnte bisher nicht anhand verfügbarer Quellen überprüft werden.',
|
||||||
|
disputed: 'Umstritten: Quellen widersprechen sich. Es gibt sowohl stützende als auch widersprechende Belege.',
|
||||||
|
contradicted: 'Widerlegt: Zuverlässige Quellen widersprechen dieser Aussage. Wahrscheinlich falsch.',
|
||||||
},
|
},
|
||||||
get factCheckChipLabels() {
|
|
||||||
const o = {}; this._fcStatuses.forEach(s => o[s] = LangManager.t('fc_chip.' + s)); return o;
|
factCheckChipLabels: {
|
||||||
|
confirmed: 'Bestätigt',
|
||||||
|
unconfirmed: 'Unbestätigt',
|
||||||
|
contradicted: 'Widerlegt',
|
||||||
|
developing: 'Unklar',
|
||||||
|
established: 'Gesichert',
|
||||||
|
disputed: 'Umstritten',
|
||||||
|
unverified: 'Ungeprüft',
|
||||||
},
|
},
|
||||||
|
|
||||||
factCheckIcons: {
|
factCheckIcons: {
|
||||||
@@ -87,7 +106,7 @@ const UI = {
|
|||||||
</label>`;
|
</label>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
return `<button class="fc-dropdown-toggle" onclick="App.toggleFcDropdown(event)" aria-haspopup="true" aria-expanded="false">${LangManager.t('evidence.filter')}</button>
|
return `<button class="fc-dropdown-toggle" onclick="App.toggleFcDropdown(event)" aria-haspopup="true" aria-expanded="false">Filter</button>
|
||||||
<div class="fc-dropdown-menu" id="fc-dropdown-menu">${items}</div>`;
|
<div class="fc-dropdown-menu" id="fc-dropdown-menu">${items}</div>`;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -101,7 +120,7 @@ const UI = {
|
|||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<div class="factcheck-claim">${this.escape(fc.claim)}</div>
|
<div class="factcheck-claim">${this.escape(fc.claim)}</div>
|
||||||
<div style="display:flex;align-items:center;gap:6px;margin-top:2px;">
|
<div style="display:flex;align-items:center;gap:6px;margin-top:2px;">
|
||||||
<span class="factcheck-sources">${count !== 1 ? LangManager.t('evidence.sources', { n: count }) : LangManager.t('evidence.source', { n: count })}</span>
|
<span class="factcheck-sources">${count} Quelle${count !== 1 ? 'n' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="evidence-block">${this.renderEvidence(fc.evidence || '')}</div>
|
<div class="evidence-block">${this.renderEvidence(fc.evidence || '')}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +132,7 @@ const UI = {
|
|||||||
* Evidence mit erklärenden Text UND Quellen-Chips rendern.
|
* Evidence mit erklärenden Text UND Quellen-Chips rendern.
|
||||||
*/
|
*/
|
||||||
renderEvidence(text) {
|
renderEvidence(text) {
|
||||||
if (!text) return `<span class="evidence-empty">${LangManager.t('evidence.empty')}</span>`;
|
if (!text) return '<span class="evidence-empty">Keine Belege</span>';
|
||||||
|
|
||||||
const urls = text.match(/https?:\/\/[^\s,)]+/g) || [];
|
const urls = text.match(/https?:\/\/[^\s,)]+/g) || [];
|
||||||
if (urls.length === 0) {
|
if (urls.length === 0) {
|
||||||
@@ -204,12 +223,12 @@ const UI = {
|
|||||||
bar.classList.remove('progress-bar--complete', 'progress-bar--error');
|
bar.classList.remove('progress-bar--complete', 'progress-bar--error');
|
||||||
|
|
||||||
const steps = {
|
const steps = {
|
||||||
queued: { active: 0, label: LangManager.t('progress.queued') },
|
queued: { active: 0, label: 'In Warteschlange...' },
|
||||||
researching: { active: 1, label: LangManager.t('progress.researching') },
|
researching: { active: 1, label: 'Recherchiert Quellen...' },
|
||||||
deep_researching: { active: 1, label: LangManager.t('progress.deep_researching') },
|
deep_researching: { active: 1, label: 'Tiefenrecherche läuft...' },
|
||||||
analyzing: { active: 2, label: LangManager.t('progress.analyzing') },
|
analyzing: { active: 2, label: 'Analysiert Meldungen...' },
|
||||||
factchecking: { active: 3, label: LangManager.t('progress.factchecking') },
|
factchecking: { active: 3, label: 'Faktencheck läuft...' },
|
||||||
cancelling: { active: 0, label: LangManager.t('progress.cancelling') },
|
cancelling: { active: 0, label: 'Wird abgebrochen...' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const step = steps[status] || steps.queued;
|
const step = steps[status] || steps.queued;
|
||||||
@@ -217,7 +236,7 @@ const UI = {
|
|||||||
// Queue-Position anzeigen
|
// Queue-Position anzeigen
|
||||||
let labelText = step.label;
|
let labelText = step.label;
|
||||||
if (status === 'queued' && extra.queue_position > 1) {
|
if (status === 'queued' && extra.queue_position > 1) {
|
||||||
labelText = LangManager.t('progress.queued_position', { position: extra.queue_position });
|
labelText = `In Warteschlange (Position ${extra.queue_position})...`;
|
||||||
} else if (extra.detail) {
|
} else if (extra.detail) {
|
||||||
labelText = extra.detail;
|
labelText = extra.detail;
|
||||||
}
|
}
|
||||||
@@ -306,24 +325,24 @@ const UI = {
|
|||||||
// Label mit Summary
|
// Label mit Summary
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (data.new_articles > 0) {
|
if (data.new_articles > 0) {
|
||||||
parts.push(LangManager.t('progress.new_articles', { n: data.new_articles }));
|
parts.push(`${data.new_articles} neue Artikel`);
|
||||||
}
|
}
|
||||||
if (data.confirmed_count > 0) {
|
if (data.confirmed_count > 0) {
|
||||||
parts.push(LangManager.t('progress.facts_confirmed', { n: data.confirmed_count }));
|
parts.push(`${data.confirmed_count} Fakten bestätigt`);
|
||||||
}
|
}
|
||||||
if (data.contradicted_count > 0) {
|
if (data.contradicted_count > 0) {
|
||||||
parts.push(LangManager.t('progress.contradicted', { n: data.contradicted_count }));
|
parts.push(`${data.contradicted_count} widerlegt`);
|
||||||
}
|
}
|
||||||
const summaryText = parts.length > 0 ? parts.join(', ') : LangManager.t('progress.no_developments');
|
const summaryText = parts.length > 0 ? parts.join(', ') : 'Keine neuen Entwicklungen';
|
||||||
const label = document.getElementById('progress-label');
|
const label = document.getElementById('progress-label');
|
||||||
if (label) label.textContent = LangManager.t('progress.complete_detail', { summary: summaryText });
|
if (label) label.textContent = `Abgeschlossen: ${summaryText}`;
|
||||||
|
|
||||||
// Cancel-Button ausblenden
|
// Cancel-Button ausblenden
|
||||||
const cancelBtn = document.getElementById('progress-cancel-btn');
|
const cancelBtn = document.getElementById('progress-cancel-btn');
|
||||||
if (cancelBtn) cancelBtn.style.display = 'none';
|
if (cancelBtn) cancelBtn.style.display = 'none';
|
||||||
|
|
||||||
bar.setAttribute('aria-valuenow', '100');
|
bar.setAttribute('aria-valuenow', '100');
|
||||||
bar.setAttribute('aria-valuetext', LangManager.t('progress.complete'));
|
bar.setAttribute('aria-valuetext', 'Abgeschlossen');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -344,8 +363,8 @@ const UI = {
|
|||||||
const label = document.getElementById('progress-label');
|
const label = document.getElementById('progress-label');
|
||||||
if (label) {
|
if (label) {
|
||||||
label.textContent = willRetry
|
label.textContent = willRetry
|
||||||
? LangManager.t('progress.failed_retry', { delay })
|
? `Fehlgeschlagen \u2014 erneuter Versuch in ${delay}s...`
|
||||||
: LangManager.t('progress.failed', { error: errorMsg });
|
: `Fehlgeschlagen: ${errorMsg}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel-Button ausblenden
|
// Cancel-Button ausblenden
|
||||||
@@ -387,7 +406,7 @@ const UI = {
|
|||||||
* Zusammenfassung mit Inline-Zitaten und Quellenverzeichnis rendern.
|
* Zusammenfassung mit Inline-Zitaten und Quellenverzeichnis rendern.
|
||||||
*/
|
*/
|
||||||
renderSummary(summary, sourcesJson, incidentType) {
|
renderSummary(summary, sourcesJson, incidentType) {
|
||||||
if (!summary) return `<span style="color:var(--text-tertiary);">${LangManager.t('empty.no_summary')}</span>`;
|
if (!summary) return '<span style="color:var(--text-tertiary);">Noch keine Zusammenfassung.</span>';
|
||||||
|
|
||||||
let sources = [];
|
let sources = [];
|
||||||
try { sources = JSON.parse(sourcesJson || '[]'); } catch(e) {}
|
try { sources = JSON.parse(sourcesJson || '[]'); } catch(e) {}
|
||||||
@@ -432,7 +451,7 @@ const UI = {
|
|||||||
// Nach Quelle aggregieren
|
// Nach Quelle aggregieren
|
||||||
const sourceMap = {};
|
const sourceMap = {};
|
||||||
articles.forEach(a => {
|
articles.forEach(a => {
|
||||||
const name = a.source || LangManager.t('time.unknown');
|
const name = a.source || 'Unbekannt';
|
||||||
if (!sourceMap[name]) {
|
if (!sourceMap[name]) {
|
||||||
sourceMap[name] = { count: 0, languages: new Set(), urls: [] };
|
sourceMap[name] = { count: 0, languages: new Set(), urls: [] };
|
||||||
}
|
}
|
||||||
@@ -457,7 +476,7 @@ const UI = {
|
|||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
let html = `<div class="source-overview-header">`;
|
let html = `<div class="source-overview-header">`;
|
||||||
html += `<span class="source-overview-stat">${LangManager.t('sources.articles_from_sources', { articles: articles.length, sources: sources.length })}</span>`;
|
html += `<span class="source-overview-stat">${articles.length} Artikel aus ${sources.length} Quellen</span>`;
|
||||||
html += `<div class="source-lang-chips">${langChips}</div>`;
|
html += `<div class="source-lang-chips">${langChips}</div>`;
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
|
|
||||||
@@ -478,15 +497,17 @@ const UI = {
|
|||||||
/**
|
/**
|
||||||
* Kategorie-Labels.
|
* Kategorie-Labels.
|
||||||
*/
|
*/
|
||||||
_catKeys: ['nachrichtenagentur','oeffentlich_rechtlich','qualitaetszeitung','behoerde','fachmedien','think_tank','international','regional','boulevard','sonstige'],
|
_categoryLabels: {
|
||||||
_catDomainMap: {'oeffentlich-rechtlich':'oeffentlich_rechtlich','think-tank':'think_tank'},
|
'nachrichtenagentur': 'Agentur',
|
||||||
get _categoryLabels() {
|
'oeffentlich-rechtlich': 'ÖR',
|
||||||
const o = {};
|
'qualitaetszeitung': 'Qualität',
|
||||||
this._catKeys.forEach(k => {
|
'behoerde': 'Behörde',
|
||||||
const domainKey = k.replace(/_/g, '-');
|
'fachmedien': 'Fach',
|
||||||
o[domainKey] = LangManager.t('sources.cat_short_' + k);
|
'think-tank': 'Think Tank',
|
||||||
});
|
'international': 'Intl.',
|
||||||
return o;
|
'regional': 'Regional',
|
||||||
|
'boulevard': 'Boulevard',
|
||||||
|
'sonstige': 'Sonstige',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -496,7 +517,7 @@ const UI = {
|
|||||||
const catLabel = this._categoryLabels[feeds[0]?.category] || feeds[0]?.category || '';
|
const catLabel = this._categoryLabels[feeds[0]?.category] || feeds[0]?.category || '';
|
||||||
const feedCount = feeds.filter(f => f.source_type !== 'excluded').length;
|
const feedCount = feeds.filter(f => f.source_type !== 'excluded').length;
|
||||||
const hasMultiple = feedCount > 1;
|
const hasMultiple = feedCount > 1;
|
||||||
const displayName = domain || feeds[0]?.name || LangManager.t('time.unknown');
|
const displayName = domain || feeds[0]?.name || 'Unbekannt';
|
||||||
const escapedDomain = this.escape(domain);
|
const escapedDomain = this.escape(domain);
|
||||||
|
|
||||||
if (isExcluded) {
|
if (isExcluded) {
|
||||||
@@ -507,10 +528,10 @@ const UI = {
|
|||||||
<div class="source-group-info">
|
<div class="source-group-info">
|
||||||
<span class="source-group-name">${this.escape(displayName)}</span>${notesHtml}
|
<span class="source-group-name">${this.escape(displayName)}</span>${notesHtml}
|
||||||
</div>
|
</div>
|
||||||
<span class="source-excluded-badge">${LangManager.t('sources.excluded')}</span>
|
<span class="source-excluded-badge">Gesperrt</span>
|
||||||
<div class="source-group-actions">
|
<div class="source-group-actions">
|
||||||
<button class="btn btn-small btn-secondary" onclick="App.unblockDomain('${escapedDomain}')">${LangManager.t('btn.unblock')}</button>
|
<button class="btn btn-small btn-secondary" onclick="App.unblockDomain('${escapedDomain}')">Entsperren</button>
|
||||||
<button class="source-delete-btn" onclick="App.deleteDomain('${escapedDomain}')" title="${LangManager.t('btn.delete')}" aria-label="${LangManager.t('btn.delete')}">×</button>
|
<button class="source-delete-btn" onclick="App.deleteDomain('${escapedDomain}')" title="Löschen" aria-label="Löschen">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -527,22 +548,22 @@ const UI = {
|
|||||||
realFeeds.forEach((feed, i) => {
|
realFeeds.forEach((feed, i) => {
|
||||||
const isLast = i === realFeeds.length - 1;
|
const isLast = i === realFeeds.length - 1;
|
||||||
const connector = isLast ? '\u2514\u2500' : '\u251C\u2500';
|
const connector = isLast ? '\u2514\u2500' : '\u251C\u2500';
|
||||||
const typeLabel = feed.source_type === 'rss_feed' ? 'RSS' : 'Web'; // RSS/Web are brand names, no translation needed
|
const typeLabel = feed.source_type === 'rss_feed' ? 'RSS' : 'Web';
|
||||||
const urlDisplay = feed.url ? this._shortenUrl(feed.url) : '';
|
const urlDisplay = feed.url ? this._shortenUrl(feed.url) : '';
|
||||||
feedRows += `<div class="source-feed-row">
|
feedRows += `<div class="source-feed-row">
|
||||||
<span class="source-feed-connector">${connector}</span>
|
<span class="source-feed-connector">${connector}</span>
|
||||||
<span class="source-feed-name">${this.escape(feed.name)}</span>
|
<span class="source-feed-name">${this.escape(feed.name)}</span>
|
||||||
<span class="source-type-badge type-${feed.source_type}">${typeLabel}</span>
|
<span class="source-type-badge type-${feed.source_type}">${typeLabel}</span>
|
||||||
<span class="source-feed-url" title="${this.escape(feed.url || '')}">${this.escape(urlDisplay)}</span>
|
<span class="source-feed-url" title="${this.escape(feed.url || '')}">${this.escape(urlDisplay)}</span>
|
||||||
<button class="source-edit-btn" onclick="App.editSource(${feed.id})" title="${LangManager.t('btn.edit')}" aria-label="${LangManager.t('btn.edit')}">✎</button>
|
<button class="source-edit-btn" onclick="App.editSource(${feed.id})" title="Bearbeiten" aria-label="Bearbeiten">✎</button>
|
||||||
<button class="source-delete-btn" onclick="App.deleteSingleFeed(${feed.id})" title="${LangManager.t('btn.delete')}" aria-label="${LangManager.t('btn.delete')}">×</button>
|
<button class="source-delete-btn" onclick="App.deleteSingleFeed(${feed.id})" title="Löschen" aria-label="Löschen">×</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
feedRows += '</div>';
|
feedRows += '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
const feedCountBadge = feedCount > 0
|
const feedCountBadge = feedCount > 0
|
||||||
? `<span class="source-feed-count">${feedCount !== 1 ? LangManager.t('sources.feeds_count', {n: feedCount}) : LangManager.t('sources.feed_count', {n: feedCount})}</span>`
|
? `<span class="source-feed-count">${feedCount} Feed${feedCount !== 1 ? 's' : ''}</span>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return `<div class="source-group">
|
return `<div class="source-group">
|
||||||
@@ -554,9 +575,9 @@ const UI = {
|
|||||||
<span class="source-category-badge cat-${feeds[0]?.category || 'sonstige'}">${catLabel}</span>
|
<span class="source-category-badge cat-${feeds[0]?.category || 'sonstige'}">${catLabel}</span>
|
||||||
${feedCountBadge}
|
${feedCountBadge}
|
||||||
<div class="source-group-actions" onclick="event.stopPropagation()">
|
<div class="source-group-actions" onclick="event.stopPropagation()">
|
||||||
${!hasMultiple && feeds[0]?.id ? `<button class="source-edit-btn" onclick="App.editSource(${feeds[0].id})" title="${LangManager.t('btn.edit')}" aria-label="${LangManager.t('btn.edit')}">✎</button>` : ''}
|
${!hasMultiple && feeds[0]?.id ? `<button class="source-edit-btn" onclick="App.editSource(${feeds[0].id})" title="Bearbeiten" aria-label="Bearbeiten">✎</button>` : ''}
|
||||||
<button class="btn btn-small btn-secondary" onclick="App.blockDomainDirect('${escapedDomain}')">${LangManager.t('btn.block')}</button>
|
<button class="btn btn-small btn-secondary" onclick="App.blockDomainDirect('${escapedDomain}')">Sperren</button>
|
||||||
<button class="source-delete-btn" onclick="App.deleteDomain('${escapedDomain}')" title="${LangManager.t('btn.delete')}" aria-label="${LangManager.t('btn.delete')}">×</button>
|
<button class="source-delete-btn" onclick="App.deleteDomain('${escapedDomain}')" title="Löschen" aria-label="Löschen">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${feedRows}
|
${feedRows}
|
||||||
@@ -596,7 +617,7 @@ const UI = {
|
|||||||
// Statistik trotzdem anzeigen
|
// Statistik trotzdem anzeigen
|
||||||
if (locations && locations.length > 0) {
|
if (locations && locations.length > 0) {
|
||||||
const totalArticles = locations.reduce((s, l) => s + l.article_count, 0);
|
const totalArticles = locations.reduce((s, l) => s + l.article_count, 0);
|
||||||
if (statsEl) statsEl.textContent = LangManager.t('map.stats', {places: locations.length, articles: totalArticles});
|
if (statsEl) statsEl.textContent = `${locations.length} Orte / ${totalArticles} Artikel`;
|
||||||
if (emptyEl) emptyEl.style.display = 'none';
|
if (emptyEl) emptyEl.style.display = 'none';
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -617,7 +638,7 @@ const UI = {
|
|||||||
|
|
||||||
// Statistik
|
// Statistik
|
||||||
const totalArticles = locations.reduce((s, l) => s + l.article_count, 0);
|
const totalArticles = locations.reduce((s, l) => s + l.article_count, 0);
|
||||||
if (statsEl) statsEl.textContent = LangManager.t('map.stats', {places: locations.length, articles: totalArticles});
|
if (statsEl) statsEl.textContent = `${locations.length} Orte / ${totalArticles} Artikel`;
|
||||||
|
|
||||||
// Container-Hoehe sicherstellen (Leaflet braucht px-Hoehe)
|
// Container-Hoehe sicherstellen (Leaflet braucht px-Hoehe)
|
||||||
if (container.offsetHeight < 50) {
|
if (container.offsetHeight < 50) {
|
||||||
@@ -670,11 +691,11 @@ const UI = {
|
|||||||
popupHtml += `<div class="map-popup-title">${this.escape(loc.location_name)}`;
|
popupHtml += `<div class="map-popup-title">${this.escape(loc.location_name)}`;
|
||||||
if (loc.country_code) popupHtml += ` <span class="map-popup-cc">${this.escape(loc.country_code)}</span>`;
|
if (loc.country_code) popupHtml += ` <span class="map-popup-cc">${this.escape(loc.country_code)}</span>`;
|
||||||
popupHtml += `</div>`;
|
popupHtml += `</div>`;
|
||||||
popupHtml += `<div class="map-popup-count">${LangManager.t('map.articles', {n: loc.article_count})}</div>`;
|
popupHtml += `<div class="map-popup-count">${loc.article_count} Artikel</div>`;
|
||||||
popupHtml += `<div class="map-popup-articles">`;
|
popupHtml += `<div class="map-popup-articles">`;
|
||||||
const maxShow = 5;
|
const maxShow = 5;
|
||||||
loc.articles.slice(0, maxShow).forEach(art => {
|
loc.articles.slice(0, maxShow).forEach(art => {
|
||||||
const headline = this.escape(art.headline || LangManager.t('map.no_title'));
|
const headline = this.escape(art.headline || 'Ohne Titel');
|
||||||
const source = this.escape(art.source || '');
|
const source = this.escape(art.source || '');
|
||||||
if (art.source_url) {
|
if (art.source_url) {
|
||||||
popupHtml += `<a href="${this.escape(art.source_url)}" target="_blank" rel="noopener" class="map-popup-article">${headline} <span class="map-popup-source">${source}</span></a>`;
|
popupHtml += `<a href="${this.escape(art.source_url)}" target="_blank" rel="noopener" class="map-popup-article">${headline} <span class="map-popup-source">${source}</span></a>`;
|
||||||
@@ -683,7 +704,7 @@ const UI = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (loc.articles.length > maxShow) {
|
if (loc.articles.length > maxShow) {
|
||||||
popupHtml += `<div class="map-popup-more">${LangManager.t('map.more', {n: loc.articles.length - maxShow})}</div>`;
|
popupHtml += `<div class="map-popup-more">+${loc.articles.length - maxShow} weitere</div>`;
|
||||||
}
|
}
|
||||||
popupHtml += `</div></div>`;
|
popupHtml += `</div></div>`;
|
||||||
|
|
||||||
@@ -723,8 +744,8 @@ const UI = {
|
|||||||
if (layer instanceof L.TileLayer) this._map.removeLayer(layer);
|
if (layer instanceof L.TileLayer) this._map.removeLayer(layer);
|
||||||
});
|
});
|
||||||
|
|
||||||
// OSM-Kacheln je nach Sprache (DE: deutsche Ortsnamen, EN: internationale)
|
// Deutsche OSM-Kacheln: deutsche Ortsnamen, einheitlich fuer beide Themes
|
||||||
const tileUrl = LangManager.mapTileUrl();
|
const tileUrl = 'https://tile.openstreetmap.de/{z}/{x}/{y}.png';
|
||||||
const attribution = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>';
|
const attribution = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>';
|
||||||
|
|
||||||
L.tileLayer(tileUrl, { attribution, maxZoom: 18 }).addTo(this._map);
|
L.tileLayer(tileUrl, { attribution, maxZoom: 18 }).addTo(this._map);
|
||||||
|
|||||||
@@ -1,573 +0,0 @@
|
|||||||
/* ============================================================
|
|
||||||
* lang.js – i18n for AegisSight Monitor (DE / EN)
|
|
||||||
* LangManager singleton + TRANSLATIONS dictionary
|
|
||||||
* ============================================================ */
|
|
||||||
|
|
||||||
// ---------- Translation dictionary ----------
|
|
||||||
|
|
||||||
const TRANSLATIONS = {
|
|
||||||
|
|
||||||
// ── Login page (index.html) ──────────────────────────────
|
|
||||||
'login.subtitle': { de: 'Lagemonitor', en: 'Situation Monitor' },
|
|
||||||
'login.email_label': { de: 'E-Mail-Adresse', en: 'Email Address' },
|
|
||||||
'login.email_placeholder': { de: 'name@organisation.de', en: 'name@organization.com' },
|
|
||||||
'login.submit': { de: 'Anmelden', en: 'Sign In' },
|
|
||||||
'login.sending': { de: 'Wird gesendet...', en: 'Sending...' },
|
|
||||||
'login.code_sent': { de: 'Ein 6-stelliger Code wurde an {email} gesendet.', en: 'A 6-digit code has been sent to {email}.' },
|
|
||||||
'login.code_label': { de: 'Code eingeben', en: 'Enter Code' },
|
|
||||||
'login.verify': { de: 'Verifizieren', en: 'Verify' },
|
|
||||||
'login.verifying': { de: 'Wird geprüft...', en: 'Verifying...' },
|
|
||||||
'login.back': { de: 'Zurück', en: 'Back' },
|
|
||||||
'login.skip_link': { de: 'Zum Anmeldeformular springen', en: 'Skip to login form' },
|
|
||||||
|
|
||||||
// ── Sidebar navigation ──────────────────────────────────
|
|
||||||
'nav.new_incident': { de: '+ Neue Lage / Recherche', en: '+ New Incident / Research' },
|
|
||||||
'nav.filter_all': { de: 'Alle', en: 'All' },
|
|
||||||
'nav.filter_mine': { de: 'Eigene', en: 'Mine' },
|
|
||||||
'nav.active_incidents': { de: 'Aktive Lagen', en: 'Active Incidents' },
|
|
||||||
'nav.active_research': { de: 'Aktive Recherchen', en: 'Active Research' },
|
|
||||||
'nav.archive': { de: 'Archiv', en: 'Archive' },
|
|
||||||
'nav.manage_sources': { de: 'Quellen verwalten', en: 'Manage Sources' },
|
|
||||||
'nav.send_feedback': { de: 'Feedback senden', en: 'Send Feedback' },
|
|
||||||
'nav.sources_count': { de: '{count} Quellen', en: '{count} Sources' },
|
|
||||||
'nav.articles_count': { de: '{count} Artikel', en: '{count} Articles' },
|
|
||||||
|
|
||||||
// ── Header bar ──────────────────────────────────────────
|
|
||||||
'header.theme_toggle': { de: 'Theme wechseln', en: 'Toggle Theme' },
|
|
||||||
'header.logout': { de: 'Abmelden', en: 'Sign Out' },
|
|
||||||
'header.license_expired': { de: 'Abgelaufen', en: 'Expired' },
|
|
||||||
'header.license_unknown': { de: 'Unbekannt', en: 'Unknown' },
|
|
||||||
'header.license_warning': { de: 'Lizenz abgelaufen – nur Lesezugriff', en: 'License expired – read-only access' },
|
|
||||||
'header.skip_link': { de: 'Zum Hauptinhalt springen', en: 'Skip to main content' },
|
|
||||||
|
|
||||||
// ── Empty states ────────────────────────────────────────
|
|
||||||
'empty.no_incident': { de: 'Kein Vorfall ausgewählt', en: 'No Incident Selected' },
|
|
||||||
'empty.no_incident_text': { de: 'Erstelle eine neue Lage oder wähle einen bestehenden Vorfall aus der Seitenleiste.', en: 'Create a new incident or select an existing one from the sidebar.' },
|
|
||||||
'empty.no_factchecks': { de: 'Noch keine Fakten geprüft', en: 'No facts checked yet' },
|
|
||||||
'empty.no_articles': { de: 'Noch keine Meldungen', en: 'No reports yet' },
|
|
||||||
'empty.no_summary': { de: 'Noch keine Zusammenfassung. Klicke auf "Aktualisieren" um die Recherche zu starten.', en: 'No summary yet. Click "Refresh" to start research.' },
|
|
||||||
'empty.no_locations': { de: 'Keine Orte erkannt', en: 'No locations detected' },
|
|
||||||
|
|
||||||
// ── Buttons ─────────────────────────────────────────────
|
|
||||||
'btn.refresh': { de: 'Aktualisieren', en: 'Refresh' },
|
|
||||||
'btn.refreshing': { de: 'Läuft...', en: 'Running...' },
|
|
||||||
'btn.edit': { de: 'Bearbeiten', en: 'Edit' },
|
|
||||||
'btn.export': { de: 'Exportieren', en: 'Export' },
|
|
||||||
'btn.archive': { de: 'Archivieren', en: 'Archive' },
|
|
||||||
'btn.unarchive': { de: 'Wiederherstellen', en: 'Restore' },
|
|
||||||
'btn.delete': { de: 'Löschen', en: 'Delete' },
|
|
||||||
'btn.cancel': { de: 'Abbrechen', en: 'Cancel' },
|
|
||||||
'btn.save': { de: 'Speichern', en: 'Save' },
|
|
||||||
'btn.confirm': { de: 'Bestätigen', en: 'Confirm' },
|
|
||||||
'btn.close': { de: 'Schließen', en: 'Close' },
|
|
||||||
'btn.detect_locations': { de: 'Orte erkennen', en: 'Detect Locations' },
|
|
||||||
'btn.layout_reset': { de: 'Layout zurücksetzen', en: 'Reset Layout' },
|
|
||||||
'btn.block_domain': { de: 'Domain sperren', en: 'Block Domain' },
|
|
||||||
'btn.add_source': { de: '+ Quelle', en: '+ Source' },
|
|
||||||
'btn.discover': { de: 'Erkennen', en: 'Discover' },
|
|
||||||
'btn.discovering': { de: 'Suche Feeds...', en: 'Finding Feeds...' },
|
|
||||||
'btn.submit_feedback': { de: 'Absenden', en: 'Submit' },
|
|
||||||
'btn.sending_feedback': { de: 'Wird gesendet...', en: 'Sending...' },
|
|
||||||
'btn.block': { de: 'Sperren', en: 'Block' },
|
|
||||||
'btn.unblock': { de: 'Entsperren', en: 'Unblock' },
|
|
||||||
'btn.save_source': { de: 'Quelle speichern', en: 'Save Source' },
|
|
||||||
|
|
||||||
// ── Modal titles and labels ─────────────────────────────
|
|
||||||
'modal.new_incident': { de: 'Neue Lage anlegen', en: 'Create New Incident' },
|
|
||||||
'modal.edit_incident': { de: 'Lage bearbeiten', en: 'Edit Incident' },
|
|
||||||
'modal.create_incident': { de: 'Lage anlegen', en: 'Create Incident' },
|
|
||||||
'modal.source_management': { de: 'Quellenverwaltung', en: 'Source Management' },
|
|
||||||
'modal.feedback': { de: 'Feedback senden', en: 'Send Feedback' },
|
|
||||||
'modal.confirmation': { de: 'Bestätigung', en: 'Confirmation' },
|
|
||||||
|
|
||||||
// ── Form labels and hints ──────────────────────────────
|
|
||||||
'form.incident_title': { de: 'Titel des Vorfalls', en: 'Incident Title' },
|
|
||||||
'form.incident_title_placeholder': { de: 'z.B. Explosion in Madrid', en: 'e.g. Explosion in Madrid' },
|
|
||||||
'form.description': { de: 'Beschreibung / Kontext', en: 'Description / Context' },
|
|
||||||
'form.description_placeholder': { de: 'Weitere Details zum Vorfall (optional)', en: 'Additional details (optional)' },
|
|
||||||
'form.incident_type': { de: 'Art der Lage', en: 'Incident Type' },
|
|
||||||
'form.type_adhoc': { de: 'Ad-hoc Lage (Breaking News)', en: 'Ad-hoc Incident (Breaking News)' },
|
|
||||||
'form.type_research': { de: 'Recherche (Hintergrund)', en: 'Research (Background)' },
|
|
||||||
'form.type_hint_adhoc': { de: 'RSS-Feeds + WebSearch, automatische Aktualisierung empfohlen', en: 'RSS feeds + web search, automatic refresh recommended' },
|
|
||||||
'form.type_hint_research': { de: 'Tiefenrecherche, manuelle Aktualisierung empfohlen', en: 'Deep research, manual refresh recommended' },
|
|
||||||
'form.sources': { de: 'Quellen', en: 'Sources' },
|
|
||||||
'form.international_sources': { de: 'Internationale Quellen einbeziehen', en: 'Include international sources' },
|
|
||||||
'form.sources_hint_intl': { de: 'DE + internationale Feeds (Reuters, BBC, Al Jazeera etc.)', en: 'DE + international feeds (Reuters, BBC, Al Jazeera etc.)' },
|
|
||||||
'form.sources_hint_de': { de: 'Nur deutschsprachige Feeds', en: 'German-language feeds only' },
|
|
||||||
'form.visibility': { de: 'Sichtbarkeit', en: 'Visibility' },
|
|
||||||
'form.visibility_public': { de: 'Öffentlich — für alle Nutzer sichtbar', en: 'Public — visible to all users' },
|
|
||||||
'form.visibility_private': { de: 'Privat — nur für dich sichtbar', en: 'Private — visible only to you' },
|
|
||||||
'form.refresh_mode': { de: 'Aktualisierung', en: 'Refresh Mode' },
|
|
||||||
'form.refresh_manual': { de: 'Manuell', en: 'Manual' },
|
|
||||||
'form.refresh_auto': { de: 'Automatisch', en: 'Automatic' },
|
|
||||||
'form.interval': { de: 'Intervall', en: 'Interval' },
|
|
||||||
'form.unit_minutes': { de: 'Minuten', en: 'Minutes' },
|
|
||||||
'form.unit_hours': { de: 'Stunden', en: 'Hours' },
|
|
||||||
'form.unit_days': { de: 'Tage', en: 'Days' },
|
|
||||||
'form.unit_weeks': { de: 'Wochen', en: 'Weeks' },
|
|
||||||
'form.retention': { de: 'Aufbewahrung (Tage)', en: 'Retention (Days)' },
|
|
||||||
'form.retention_hint': { de: '0 = Unbegrenzt, max. 999 Tage', en: '0 = Unlimited, max. 999 days' },
|
|
||||||
'form.email_notifications': { de: 'E-Mail-Benachrichtigungen', en: 'Email Notifications' },
|
|
||||||
'form.email_notify_hint': { de: 'Per E-Mail benachrichtigen bei:', en: 'Send email notifications for:' },
|
|
||||||
'form.notify_summary': { de: 'Neues Lagebild', en: 'New Situation Report' },
|
|
||||||
'form.notify_articles': { de: 'Neue Artikel', en: 'New Articles' },
|
|
||||||
'form.notify_status': { de: 'Statusänderung Faktencheck', en: 'Fact Check Status Change' },
|
|
||||||
'form.feedback_category': { de: 'Kategorie', en: 'Category' },
|
|
||||||
'form.fb_bug': { de: 'Fehlerbericht', en: 'Bug Report' },
|
|
||||||
'form.fb_feature': { de: 'Feature-Wunsch', en: 'Feature Request' },
|
|
||||||
'form.fb_question': { de: 'Frage', en: 'Question' },
|
|
||||||
'form.fb_other': { de: 'Sonstiges', en: 'Other' },
|
|
||||||
'form.fb_message': { de: 'Nachricht', en: 'Message' },
|
|
||||||
'form.fb_placeholder': { de: 'Beschreibe dein Anliegen (mind. 10 Zeichen)...', en: 'Describe your concern (min. 10 characters)...' },
|
|
||||||
'form.fb_chars': { de: '{count} / 5.000 Zeichen', en: '{count} / 5,000 characters' },
|
|
||||||
'form.validation_title': { de: 'Bitte einen Titel eingeben.', en: 'Please enter a title.' },
|
|
||||||
'form.validation_min_chars': { de: 'Bitte mindestens 10 Zeichen eingeben.', en: 'Please enter at least 10 characters.' },
|
|
||||||
|
|
||||||
// ── Dashboard card / tile titles ────────────────────────
|
|
||||||
'card.situation_report': { de: 'Lagebild', en: 'Situation Report' },
|
|
||||||
'card.factcheck': { de: 'Faktencheck', en: 'Fact Check' },
|
|
||||||
'card.sources_overview': { de: 'Quellenübersicht', en: 'Source Overview' },
|
|
||||||
'card.timeline': { de: 'Ereignis-Timeline', en: 'Event Timeline' },
|
|
||||||
'card.map': { de: 'Geografische Verteilung', en: 'Geographic Distribution' },
|
|
||||||
'card.detail_view': { de: 'Detailansicht', en: 'Detail View' },
|
|
||||||
|
|
||||||
// ── Layout toolbar toggle buttons ──────────────────────
|
|
||||||
'tile.lagebild': { de: 'Lagebild', en: 'Report' },
|
|
||||||
'tile.faktencheck': { de: 'Faktencheck', en: 'Fact Check' },
|
|
||||||
'tile.quellen': { de: 'Quellen', en: 'Sources' },
|
|
||||||
'tile.timeline': { de: 'Timeline', en: 'Timeline' },
|
|
||||||
'tile.karte': { de: 'Karte', en: 'Map' },
|
|
||||||
|
|
||||||
// ── Fact check statuses (full labels) ──────────────────
|
|
||||||
'fc.confirmed': { de: 'Bestätigt durch mehrere Quellen', en: 'Confirmed by multiple sources' },
|
|
||||||
'fc.unconfirmed': { de: 'Nicht unabhängig bestätigt', en: 'Not independently confirmed' },
|
|
||||||
'fc.contradicted': { de: 'Widerlegt', en: 'Contradicted' },
|
|
||||||
'fc.developing': { de: 'Faktenlage noch im Fluss', en: 'Facts still developing' },
|
|
||||||
'fc.established': { de: 'Gesicherter Fakt (3+ Quellen)', en: 'Established fact (3+ sources)' },
|
|
||||||
'fc.disputed': { de: 'Umstrittener Sachverhalt', en: 'Disputed claim' },
|
|
||||||
'fc.unverified': { de: 'Nicht unabhängig verifizierbar', en: 'Cannot be independently verified' },
|
|
||||||
|
|
||||||
// ── Fact check chip labels (short) ─────────────────────
|
|
||||||
'fc_chip.confirmed': { de: 'Bestätigt', en: 'Confirmed' },
|
|
||||||
'fc_chip.unconfirmed': { de: 'Unbestätigt', en: 'Unconfirmed' },
|
|
||||||
'fc_chip.contradicted': { de: 'Widerlegt', en: 'Contradicted' },
|
|
||||||
'fc_chip.developing': { de: 'Unklar', en: 'Developing' },
|
|
||||||
'fc_chip.established': { de: 'Gesichert', en: 'Established' },
|
|
||||||
'fc_chip.disputed': { de: 'Umstritten', en: 'Disputed' },
|
|
||||||
'fc_chip.unverified': { de: 'Ungeprüft', en: 'Unverified' },
|
|
||||||
|
|
||||||
// ── Fact check tooltips (detailed) ─────────────────────
|
|
||||||
'fc_tooltip.confirmed': { de: 'Bestätigt: Mindestens zwei unabhängige, seriöse Quellen stützen diese Aussage übereinstimmend.', en: 'Confirmed: At least two independent, reputable sources support this claim consistently.' },
|
|
||||||
'fc_tooltip.established': { de: 'Gesichert: Drei oder mehr unabhängige Quellen bestätigen den Sachverhalt. Hohe Verlässlichkeit.', en: 'Established: Three or more independent sources confirm the facts. High reliability.' },
|
|
||||||
'fc_tooltip.developing': { de: 'Unklar: Die Faktenlage ist noch im Fluss. Neue Informationen können das Bild verändern.', en: 'Developing: The facts are still emerging. New information may change the picture.' },
|
|
||||||
'fc_tooltip.unconfirmed': { de: 'Unbestätigt: Bisher nur aus einer Quelle bekannt. Eine unabhängige Bestätigung steht aus.', en: 'Unconfirmed: Known from only one source so far. Independent confirmation is pending.' },
|
|
||||||
'fc_tooltip.unverified': { de: 'Ungeprüft: Die Aussage konnte bisher nicht anhand verfügbarer Quellen überprüft werden.', en: 'Unverified: The claim could not yet be verified against available sources.' },
|
|
||||||
'fc_tooltip.disputed': { de: 'Umstritten: Quellen widersprechen sich. Es gibt sowohl stützende als auch widersprechende Belege.', en: 'Disputed: Sources contradict each other. There is both supporting and contradicting evidence.' },
|
|
||||||
'fc_tooltip.contradicted': { de: 'Widerlegt: Zuverlässige Quellen widersprechen dieser Aussage. Wahrscheinlich falsch.', en: 'Contradicted: Reliable sources contradict this claim. Probably false.' },
|
|
||||||
|
|
||||||
// ── Incident statuses ──────────────────────────────────
|
|
||||||
'status.breaking': { de: 'Breaking', en: 'Breaking' },
|
|
||||||
'status.research': { de: 'Recherche', en: 'Research' },
|
|
||||||
'status.international': { de: 'International', en: 'International' },
|
|
||||||
'status.de_only': { de: 'Nur DE', en: 'DE Only' },
|
|
||||||
'status.manual': { de: 'Manuell', en: 'Manual' },
|
|
||||||
'status.auto_interval': { de: 'Auto alle {interval}', en: 'Auto every {interval}' },
|
|
||||||
'status.private': { de: 'PRIVAT', en: 'PRIVATE' },
|
|
||||||
|
|
||||||
// ── Time expressions ───────────────────────────────────
|
|
||||||
'time.just_now': { de: 'gerade eben', en: 'just now' },
|
|
||||||
'time.minutes_ago': { de: 'vor {n}m', en: '{n}m ago' },
|
|
||||||
'time.hours_ago': { de: 'vor {n}h', en: '{n}h ago' },
|
|
||||||
'time.days_ago': { de: 'vor {n}d', en: '{n}d ago' },
|
|
||||||
'time.today': { de: 'Heute', en: 'Today' },
|
|
||||||
'time.yesterday': { de: 'Gestern', en: 'Yesterday' },
|
|
||||||
'time.unknown': { de: 'Unbekannt', en: 'Unknown' },
|
|
||||||
'time.stand': { de: 'Stand: {time}', en: 'Updated: {time}' },
|
|
||||||
'time.clock': { de: 'Uhr', en: '' },
|
|
||||||
'time.week': { de: '1 Woche', en: '1 week' },
|
|
||||||
'time.weeks': { de: '{n} Wochen', en: '{n} weeks' },
|
|
||||||
'time.day': { de: '1 Tag', en: '1 day' },
|
|
||||||
'time.days': { de: '{n} Tage', en: '{n} days' },
|
|
||||||
'time.hour': { de: '1 Stunde', en: '1 hour' },
|
|
||||||
'time.hours': { de: '{n} Stunden', en: '{n} hours' },
|
|
||||||
'time.minutes_short': { de: '{n} Min.', en: '{n} min' },
|
|
||||||
|
|
||||||
// ── Timeline ───────────────────────────────────────────
|
|
||||||
'timeline.all': { de: 'Alle', en: 'All' },
|
|
||||||
'timeline.articles': { de: 'Meldungen', en: 'Reports' },
|
|
||||||
'timeline.snapshots': { de: 'Lageberichte', en: 'Situation Reports' },
|
|
||||||
'timeline.range_24h': { de: '24h', en: '24h' },
|
|
||||||
'timeline.range_7d': { de: '7T', en: '7d' },
|
|
||||||
'timeline.range_all': { de: 'Alles', en: 'All' },
|
|
||||||
'timeline.search_placeholder': { de: 'Suche...', en: 'Search...' },
|
|
||||||
'timeline.no_entries_range': { de: 'Keine Einträge im gewählten Zeitraum.', en: 'No entries in selected time range.' },
|
|
||||||
'timeline.no_entries_start': { de: 'Noch keine Meldungen. Starte eine Recherche mit "Aktualisieren".', en: 'No reports yet. Start research with "Refresh".' },
|
|
||||||
'timeline.no_entries': { de: 'Keine Einträge.', en: 'No entries.' },
|
|
||||||
'timeline.entry': { de: '{n} Eintrag', en: '{n} entry' },
|
|
||||||
'timeline.entries': { de: '{n} Einträge', en: '{n} entries' },
|
|
||||||
'timeline.messages': { de: '{n} Meldungen', en: '{n} reports' },
|
|
||||||
'timeline.message': { de: '{n} Meldung', en: '{n} report' },
|
|
||||||
'timeline.snapshot_badge': { de: 'Lagebericht', en: 'Situation Report' },
|
|
||||||
'timeline.articles_label': { de: '{n} Artikel', en: '{n} articles' },
|
|
||||||
'timeline.facts_label': { de: '{n} Fakten', en: '{n} facts' },
|
|
||||||
'timeline.report_count': { de: '{n} Lagebericht', en: '{n} report' },
|
|
||||||
'timeline.reports_count': { de: '{n} Lageberichte', en: '{n} reports' },
|
|
||||||
'timeline.open_article': { de: 'Artikel öffnen', en: 'Open article' },
|
|
||||||
|
|
||||||
// ── Progress bar ───────────────────────────────────────
|
|
||||||
'progress.queued': { de: 'In Warteschlange...', en: 'Queued...' },
|
|
||||||
'progress.queued_position': { de: 'In Warteschlange (Position {pos})...', en: 'Queued (position {pos})...' },
|
|
||||||
'progress.researching': { de: 'Recherchiert Quellen...', en: 'Researching sources...' },
|
|
||||||
'progress.deep_researching': { de: 'Tiefenrecherche läuft...', en: 'Deep research in progress...' },
|
|
||||||
'progress.analyzing': { de: 'Analysiert Meldungen...', en: 'Analyzing reports...' },
|
|
||||||
'progress.factchecking': { de: 'Faktencheck läuft...', en: 'Fact checking in progress...' },
|
|
||||||
'progress.cancelling': { de: 'Wird abgebrochen...', en: 'Cancelling...' },
|
|
||||||
'progress.step_research': { de: 'Recherche', en: 'Research' },
|
|
||||||
'progress.step_analysis': { de: 'Analyse', en: 'Analysis' },
|
|
||||||
'progress.step_factcheck': { de: 'Faktencheck', en: 'Fact Check' },
|
|
||||||
'progress.wait': { de: 'Warte auf Start...', en: 'Waiting to start...' },
|
|
||||||
'progress.complete': { de: 'Abgeschlossen', en: 'Completed' },
|
|
||||||
'progress.complete_detail': { de: 'Abgeschlossen: {summary}', en: 'Completed: {summary}' },
|
|
||||||
'progress.failed': { de: 'Fehlgeschlagen: {error}', en: 'Failed: {error}' },
|
|
||||||
'progress.failed_retry': { de: 'Fehlgeschlagen — erneuter Versuch in {delay}s...', en: 'Failed — retrying in {delay}s...' },
|
|
||||||
'progress.new_articles': { de: '{n} neue Artikel', en: '{n} new articles' },
|
|
||||||
'progress.facts_confirmed': { de: '{n} Fakten bestätigt', en: '{n} facts confirmed' },
|
|
||||||
'progress.contradicted': { de: '{n} widerlegt', en: '{n} contradicted' },
|
|
||||||
'progress.no_developments': { de: 'Keine neuen Entwicklungen', en: 'No new developments' },
|
|
||||||
|
|
||||||
// ── Export menu items ──────────────────────────────────
|
|
||||||
'export.report_md': { de: 'Lagebericht (Markdown)', en: 'Situation Report (Markdown)' },
|
|
||||||
'export.report_json': { de: 'Lagebericht (JSON)', en: 'Situation Report (JSON)' },
|
|
||||||
'export.full_md': { de: 'Vollexport (Markdown)', en: 'Full Export (Markdown)' },
|
|
||||||
'export.full_json': { de: 'Vollexport (JSON)', en: 'Full Export (JSON)' },
|
|
||||||
'export.print': { de: 'Drucken / PDF', en: 'Print / PDF' },
|
|
||||||
|
|
||||||
// ── Accessibility panel ────────────────────────────────
|
|
||||||
'a11y.title': { de: 'Barrierefreiheit', en: 'Accessibility' },
|
|
||||||
'a11y.contrast': { de: 'Hoher Kontrast', en: 'High Contrast' },
|
|
||||||
'a11y.focus': { de: 'Verstärkte Focus-Anzeige', en: 'Enhanced Focus Indicators' },
|
|
||||||
'a11y.fontsize': { de: 'Größere Schrift', en: 'Larger Text' },
|
|
||||||
'a11y.motion': { de: 'Animationen aus', en: 'Reduce Animations' },
|
|
||||||
|
|
||||||
// ── Notifications ──────────────────────────────────────
|
|
||||||
'notif.title': { de: 'Benachrichtigungen', en: 'Notifications' },
|
|
||||||
'notif.mark_read': { de: 'Alle gelesen', en: 'Mark all read' },
|
|
||||||
'notif.empty': { de: 'Keine Benachrichtigungen', en: 'No notifications' },
|
|
||||||
|
|
||||||
// ── Toast messages ─────────────────────────────────────
|
|
||||||
'toast.incident_created': { de: 'Lage "{title}" angelegt. Recherche gestartet.', en: 'Incident "{title}" created. Research started.' },
|
|
||||||
'toast.incident_updated': { de: 'Lage aktualisiert.', en: 'Incident updated.' },
|
|
||||||
'toast.incident_deleted': { de: 'Lage gelöscht.', en: 'Incident deleted.' },
|
|
||||||
'toast.incident_archived': { de: 'Lage archiviert.', en: 'Incident archived.' },
|
|
||||||
'toast.incident_restored': { de: 'Lage wiederhergestellt.', en: 'Incident restored.' },
|
|
||||||
'toast.refresh_already_running': { de: 'Recherche läuft bereits...', en: 'Research already running...' },
|
|
||||||
'toast.refresh_skipped': { de: 'Recherche läuft bereits oder ist in der Warteschlange.', en: 'Research already running or queued.' },
|
|
||||||
'toast.refresh_complete': { de: 'Recherche abgeschlossen: {summary}', en: 'Research complete: {summary}' },
|
|
||||||
'toast.refresh_cancelled': { de: 'Recherche abgebrochen.', en: 'Research cancelled.' },
|
|
||||||
'toast.refresh_error': { de: 'Recherche-Fehler: {error}', en: 'Research error: {error}' },
|
|
||||||
'toast.data_refreshed': { de: 'Daten aktualisiert.', en: 'Data refreshed.' },
|
|
||||||
'toast.refresh_failed': { de: 'Aktualisierung fehlgeschlagen: {error}', en: 'Refresh failed: {error}' },
|
|
||||||
'toast.export_success': { de: 'Export heruntergeladen', en: 'Export downloaded' },
|
|
||||||
'toast.export_failed': { de: 'Export fehlgeschlagen: {error}', en: 'Export failed: {error}' },
|
|
||||||
'toast.feedback_sent': { de: 'Feedback gesendet. Vielen Dank!', en: 'Feedback sent. Thank you!' },
|
|
||||||
'toast.load_error': { de: 'Fehler beim Laden der Lagen: {error}', en: 'Error loading incidents: {error}' },
|
|
||||||
'toast.detail_error': { de: 'Fehler beim Laden: {error}', en: 'Error loading: {error}' },
|
|
||||||
'toast.generic_error': { de: 'Fehler: {error}', en: 'Error: {error}' },
|
|
||||||
'toast.domain_blocked': { de: '{domain} gesperrt.', en: '{domain} blocked.' },
|
|
||||||
'toast.domain_unblocked': { de: '{domain} entsperrt.', en: '{domain} unblocked.' },
|
|
||||||
'toast.domain_deleted': { de: '{domain} gelöscht.', en: '{domain} deleted.' },
|
|
||||||
'toast.feed_deleted': { de: 'Feed gelöscht.', en: 'Feed deleted.' },
|
|
||||||
'toast.source_added': { de: 'Quelle hinzugefügt.', en: 'Source added.' },
|
|
||||||
'toast.source_updated': { de: 'Quelle aktualisiert.', en: 'Source updated.' },
|
|
||||||
'toast.source_error': { de: 'Fehler beim Laden der Quellen: {error}', en: 'Error loading sources: {error}' },
|
|
||||||
'toast.discover_no_rss': { de: 'Kein RSS-Feed gefunden. Als Web-Quelle speichern?', en: 'No RSS feed found. Save as web source?' },
|
|
||||||
'toast.discover_exists': { de: 'Feed bereits vorhanden.', en: 'Feed already exists.' },
|
|
||||||
'toast.discover_error': { de: 'Erkennung fehlgeschlagen: {error}', en: 'Discovery failed: {error}' },
|
|
||||||
'toast.domain_required': { de: 'Domain ist erforderlich.', en: 'Domain is required.' },
|
|
||||||
'toast.name_required': { de: 'Name ist erforderlich. Bitte erst "Erkennen" klicken.', en: 'Name is required. Please click "Discover" first.' },
|
|
||||||
'toast.url_required': { de: 'Bitte URL oder Domain eingeben.', en: 'Please enter URL or domain.' },
|
|
||||||
'toast.geoparse_failed': { de: 'Geoparsing fehlgeschlagen: {error}', en: 'Geoparsing failed: {error}' },
|
|
||||||
'toast.locations_found': { de: '{locations} Orte aus {articles} Artikeln erkannt', en: '{locations} locations detected from {articles} articles' },
|
|
||||||
'toast.no_locations': { de: 'Keine neuen Orte gefunden', en: 'No new locations found' },
|
|
||||||
'toast.cancel_failed': { de: 'Abbrechen fehlgeschlagen: {error}', en: 'Cancel failed: {error}' },
|
|
||||||
|
|
||||||
// ── Confirmation dialogs ───────────────────────────────
|
|
||||||
'confirm.delete_incident': { de: 'Lage wirklich löschen? Alle gesammelten Daten gehen verloren.', en: 'Really delete incident? All collected data will be lost.' },
|
|
||||||
'confirm.archive_incident': { de: 'Lage wirklich archivieren?', en: 'Really archive incident?' },
|
|
||||||
'confirm.restore_incident': { de: 'Lage wirklich wiederherstellen?', en: 'Really restore incident?' },
|
|
||||||
'confirm.cancel_refresh': { de: 'Laufende Recherche abbrechen?', en: 'Cancel running research?' },
|
|
||||||
'confirm.block_domain': { de: '"{domain}" wirklich sperren? Alle Feeds dieser Domain werden deaktiviert.', en: 'Really block "{domain}"? All feeds for this domain will be deactivated.' },
|
|
||||||
'confirm.delete_domain': { de: 'Alle Quellen von "{domain}" wirklich löschen?', en: 'Really delete all sources from "{domain}"?' },
|
|
||||||
'confirm.unblock_add': { de: '"{domain}" ist gesperrt. Trotzdem hinzufügen? Die Domain wird dabei entsperrt.', en: '"{domain}" is blocked. Add anyway? The domain will be unblocked.' },
|
|
||||||
|
|
||||||
// ── Source management ──────────────────────────────────
|
|
||||||
'sources.all_types': { de: 'Alle Typen', en: 'All Types' },
|
|
||||||
'sources.rss_feed': { de: 'RSS-Feed', en: 'RSS Feed' },
|
|
||||||
'sources.web_source': { de: 'Web-Quelle', en: 'Web Source' },
|
|
||||||
'sources.excluded': { de: 'Gesperrt', en: 'Blocked' },
|
|
||||||
'sources.all_categories': { de: 'Alle Kategorien', en: 'All Categories' },
|
|
||||||
'sources.cat_nachrichtenagentur': { de: 'Nachrichtenagentur', en: 'News Agency' },
|
|
||||||
'sources.cat_oeffentlich_rechtlich': { de: 'Öffentlich-Rechtlich', en: 'Public Broadcasting' },
|
|
||||||
'sources.cat_qualitaetszeitung': { de: 'Qualitätszeitung', en: 'Quality Newspaper' },
|
|
||||||
'sources.cat_behoerde': { de: 'Behörde', en: 'Government Agency' },
|
|
||||||
'sources.cat_fachmedien': { de: 'Fachmedien', en: 'Trade Media' },
|
|
||||||
'sources.cat_think_tank': { de: 'Think Tank', en: 'Think Tank' },
|
|
||||||
'sources.cat_international': { de: 'International', en: 'International' },
|
|
||||||
'sources.cat_regional': { de: 'Regional', en: 'Regional' },
|
|
||||||
'sources.cat_boulevard': { de: 'Boulevard', en: 'Tabloid' },
|
|
||||||
'sources.cat_sonstige': { de: 'Sonstige', en: 'Other' },
|
|
||||||
'sources.search_placeholder': { de: 'Suche...', en: 'Search...' },
|
|
||||||
'sources.no_sources': { de: 'Keine Quellen gefunden', en: 'No sources found' },
|
|
||||||
'sources.loading': { de: 'Lade Quellen...', en: 'Loading sources...' },
|
|
||||||
'sources.rss_feeds_stat': { de: 'RSS-Feeds', en: 'RSS Feeds' },
|
|
||||||
'sources.web_sources_stat': { de: 'Web-Quellen', en: 'Web Sources' },
|
|
||||||
'sources.blocked_stat': { de: 'Gesperrt', en: 'Blocked' },
|
|
||||||
'sources.articles_total': { de: 'Artikel gesamt', en: 'Total Articles' },
|
|
||||||
'sources.domain_label': { de: 'Domain', en: 'Domain' },
|
|
||||||
'sources.notes_label': { de: 'Notizen', en: 'Notes' },
|
|
||||||
'sources.notes_placeholder': { de: 'Optional', en: 'Optional' },
|
|
||||||
'sources.name_label': { de: 'Name', en: 'Name' },
|
|
||||||
'sources.category_label': { de: 'Kategorie', en: 'Category' },
|
|
||||||
'sources.type_label': { de: 'Typ', en: 'Type' },
|
|
||||||
'sources.rss_url_label': { de: 'RSS-Feed URL', en: 'RSS Feed URL' },
|
|
||||||
'sources.url_placeholder': { de: 'z.B. netzpolitik.org', en: 'e.g. netzpolitik.org' },
|
|
||||||
'sources.block_domain_placeholder': { de: 'z.B. bild.de', en: 'e.g. bild.de' },
|
|
||||||
'sources.feeds_count': { de: '{n} Feeds', en: '{n} Feeds' },
|
|
||||||
'sources.feed_count': { de: '{n} Feed', en: '{n} Feed' },
|
|
||||||
'sources.articles_from_sources': { de: '{articles} Artikel aus {sources} Quellen', en: '{articles} articles from {sources} sources' },
|
|
||||||
'sources.cat_short_nachrichtenagentur': { de: 'Agentur', en: 'Agency' },
|
|
||||||
'sources.cat_short_oeffentlich_rechtlich': { de: 'ÖR', en: 'Public' },
|
|
||||||
'sources.cat_short_qualitaetszeitung': { de: 'Qualität', en: 'Quality' },
|
|
||||||
'sources.cat_short_behoerde': { de: 'Behörde', en: 'Gov' },
|
|
||||||
'sources.cat_short_fachmedien': { de: 'Fach', en: 'Trade' },
|
|
||||||
'sources.cat_short_think_tank': { de: 'Think Tank', en: 'Think Tank' },
|
|
||||||
'sources.cat_short_international': { de: 'Intl.', en: 'Intl.' },
|
|
||||||
'sources.cat_short_regional': { de: 'Regional', en: 'Regional' },
|
|
||||||
'sources.cat_short_boulevard': { de: 'Boulevard', en: 'Tabloid' },
|
|
||||||
'sources.cat_short_sonstige': { de: 'Sonstige', en: 'Other' },
|
|
||||||
|
|
||||||
// ── Sidebar labels ─────────────────────────────────────
|
|
||||||
'sidebar.no_adhoc': { de: 'Keine Ad-hoc-Lagen', en: 'No ad-hoc incidents' },
|
|
||||||
'sidebar.no_own_adhoc': { de: 'Keine eigenen Ad-hoc-Lagen', en: 'No own ad-hoc incidents' },
|
|
||||||
'sidebar.no_research': { de: 'Keine Recherchen', en: 'No research' },
|
|
||||||
'sidebar.no_own_research': { de: 'Keine eigenen Recherchen', en: 'No own research' },
|
|
||||||
'sidebar.no_archive': { de: 'Kein Archiv', en: 'No archive' },
|
|
||||||
'sidebar.articles': { de: '{n} Artikel', en: '{n} articles' },
|
|
||||||
'sidebar.incident_selected': { de: 'Lage ausgewählt: {title}', en: 'Incident selected: {title}' },
|
|
||||||
|
|
||||||
// ── Incident detail ────────────────────────────────────
|
|
||||||
'incident.created_by': { de: 'von', en: 'by' },
|
|
||||||
'incident.delete_only_creator': { de: 'Nur {name} kann diese Lage löschen', en: 'Only {name} can delete this incident' },
|
|
||||||
|
|
||||||
// ── Refresh history ────────────────────────────────────
|
|
||||||
'refresh.title': { de: 'Refresh-Verlauf', en: 'Refresh History' },
|
|
||||||
'refresh.loading': { de: 'Lade...', en: 'Loading...' },
|
|
||||||
'refresh.load_error': { de: 'Fehler beim Laden', en: 'Error loading' },
|
|
||||||
'refresh.no_history': { de: 'Noch keine Refreshes durchgeführt', en: 'No refreshes performed yet' },
|
|
||||||
'refresh.articles': { de: '{n} Artikel', en: '{n} articles' },
|
|
||||||
'refresh.running': { de: 'Läuft...', en: 'Running...' },
|
|
||||||
'refresh.attempt': { de: 'Versuch {n}', en: 'Attempt {n}' },
|
|
||||||
'refresh.trigger_auto': { de: 'Auto', en: 'Auto' },
|
|
||||||
'refresh.trigger_manual': { de: 'Manuell', en: 'Manual' },
|
|
||||||
'refresh.collapse': { de: 'Einklappen', en: 'Collapse' },
|
|
||||||
'refresh.expand': { de: 'Aufklappen', en: 'Expand' },
|
|
||||||
|
|
||||||
// ── Evidence rendering ─────────────────────────────────
|
|
||||||
'evidence.empty': { de: 'Keine Belege', en: 'No evidence' },
|
|
||||||
'evidence.sources': { de: '{n} Quellen', en: '{n} sources' },
|
|
||||||
'evidence.source': { de: '{n} Quelle', en: '{n} source' },
|
|
||||||
'evidence.filter': { de: 'Filter', en: 'Filter' },
|
|
||||||
|
|
||||||
// ── Map ────────────────────────────────────────────────
|
|
||||||
'map.places': { de: '{n} Orte', en: '{n} locations' },
|
|
||||||
'map.articles': { de: '{n} Artikel', en: '{n} articles' },
|
|
||||||
'map.stats': { de: '{places} Orte / {articles} Artikel', en: '{places} locations / {articles} articles' },
|
|
||||||
'map.more': { de: '+{n} weitere', en: '+{n} more' },
|
|
||||||
'map.no_title': { de: 'Ohne Titel', en: 'No Title' },
|
|
||||||
'map.starting': { de: 'Wird gestartet...', en: 'Starting...' },
|
|
||||||
|
|
||||||
// ── API error translations ─────────────────────────────
|
|
||||||
'err.unauthorized': { de: 'Nicht autorisiert', en: 'Unauthorized' },
|
|
||||||
'err.not_found': { de: 'Lage nicht gefunden', en: 'Incident not found' },
|
|
||||||
'err.verification_failed': { de: 'Verifikation fehlgeschlagen', en: 'Verification failed' },
|
|
||||||
'err.request_failed': { de: 'Anfrage fehlgeschlagen', en: 'Request failed' },
|
|
||||||
'err.verification_check_failed': { de: 'Verifizierung fehlgeschlagen', en: 'Verification failed' },
|
|
||||||
'err.generic': { de: 'Fehler', en: 'Error' },
|
|
||||||
|
|
||||||
// ── Fact check status (for notifications) ──────────────
|
|
||||||
'fc_status.confirmed': { de: 'Bestätigt', en: 'Confirmed' },
|
|
||||||
'fc_status.established': { de: 'Gesichert', en: 'Established' },
|
|
||||||
'fc_status.unconfirmed': { de: 'Unbestätigt', en: 'Unconfirmed' },
|
|
||||||
'fc_status.contradicted': { de: 'Widersprochen', en: 'Contradicted' },
|
|
||||||
'fc_status.disputed': { de: 'Umstritten', en: 'Disputed' },
|
|
||||||
'fc_status.developing': { de: 'In Entwicklung', en: 'Developing' },
|
|
||||||
'fc_status.unverified': { de: 'Ungeprüft', en: 'Unverified' },
|
|
||||||
|
|
||||||
// ── Miscellaneous ──────────────────────────────────────
|
|
||||||
'misc.search_sources': { de: 'Quellen durchsuchen...', en: 'Search sources...' },
|
|
||||||
'misc.incident': { de: 'Lage', en: 'Incident' },
|
|
||||||
'misc.articles_progress': { de: '{done}/{total} Artikel...', en: '{done}/{total} articles...' },
|
|
||||||
|
|
||||||
// ── Sidebar stats ─────────────────────────────────────
|
|
||||||
'sidebar.sources_count': { de: '{n} Quellen', en: '{n} sources' },
|
|
||||||
'sidebar.articles_count': { de: '{n} Artikel', en: '{n} articles' },
|
|
||||||
|
|
||||||
// ── Source discovery toasts ───────────────────────────
|
|
||||||
'toast.discover_added': { de: '{domain}: {count} Feeds hinzugefügt', en: '{domain}: {count} feeds added' },
|
|
||||||
'toast.discover_added_skipped': { de: '{domain}: {count} Feeds hinzugefügt ({skipped} bereits vorhanden)', en: '{domain}: {count} feeds added ({skipped} already exist)' },
|
|
||||||
'toast.discover_all_exist': { de: '{domain}: Alle {count} Feeds bereits vorhanden.', en: '{domain}: All {count} feeds already exist.' },
|
|
||||||
'toast.discover_none': { de: '{domain}: Keine relevanten Feeds gefunden.', en: '{domain}: No relevant feeds found.' },
|
|
||||||
|
|
||||||
// ── Source form hints ─────────────────────────────────
|
|
||||||
'form.sources_hint_intl': { de: 'DE + internationale Feeds (Reuters, BBC, Al Jazeera etc.)', en: 'DE + international feeds (Reuters, BBC, Al Jazeera etc.)' },
|
|
||||||
'form.sources_hint_de': { de: 'Nur deutschsprachige Quellen (DE, AT, CH)', en: 'German-language sources only (DE, AT, CH)' },
|
|
||||||
|
|
||||||
// ── Session warning ───────────────────────────────────
|
|
||||||
'toast.session_expiring': { de: 'Session läuft in {mins} Minute(n) ab. Bitte erneut anmelden.', en: 'Session expires in {mins} minute(s). Please log in again.' },
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// ---------- Known API error messages (DE -> EN) ----------
|
|
||||||
|
|
||||||
const _API_ERROR_MAP = {
|
|
||||||
'Nicht autorisiert': 'Unauthorized',
|
|
||||||
'Lage nicht gefunden': 'Incident not found',
|
|
||||||
'Verifikation fehlgeschlagen': 'Verification failed',
|
|
||||||
'Anfrage fehlgeschlagen': 'Request failed',
|
|
||||||
'Verifizierung fehlgeschlagen': 'Verification failed',
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// ---------- LangManager singleton ----------
|
|
||||||
|
|
||||||
const LangManager = (() => {
|
|
||||||
const STORAGE_KEY = 'osint_lang';
|
|
||||||
const SUPPORTED = ['de', 'en'];
|
|
||||||
const DEFAULT = 'de';
|
|
||||||
|
|
||||||
let _lang = DEFAULT;
|
|
||||||
|
|
||||||
/* ---- public API ---- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialise language from localStorage (anti-flicker).
|
|
||||||
* Call as early as possible, ideally before DOM paint.
|
|
||||||
*/
|
|
||||||
function init() {
|
|
||||||
const stored = localStorage.getItem(STORAGE_KEY);
|
|
||||||
_lang = (stored && SUPPORTED.includes(stored)) ? stored : DEFAULT;
|
|
||||||
document.documentElement.lang = _lang;
|
|
||||||
|
|
||||||
// Hydrate once DOM is ready
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', _hydrate);
|
|
||||||
} else {
|
|
||||||
_hydrate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switch language, persist, hydrate DOM, dispatch event.
|
|
||||||
* @param {string} lang – 'de' or 'en'
|
|
||||||
*/
|
|
||||||
function setLang(lang) {
|
|
||||||
if (!SUPPORTED.includes(lang)) return;
|
|
||||||
_lang = lang;
|
|
||||||
localStorage.setItem(STORAGE_KEY, lang);
|
|
||||||
document.documentElement.lang = lang;
|
|
||||||
_hydrate();
|
|
||||||
document.dispatchEvent(new CustomEvent('langchange', { detail: { lang } }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return translated string for the given key.
|
|
||||||
* Supports {var} placeholder replacement via the vars object.
|
|
||||||
* Falls back to the key itself when no translation is found.
|
|
||||||
*
|
|
||||||
* @param {string} key – e.g. 'login.submit'
|
|
||||||
* @param {Object} [vars] – e.g. { email: 'foo@bar.de' }
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function t(key, vars) {
|
|
||||||
const entry = TRANSLATIONS[key];
|
|
||||||
let text = (entry && entry[_lang] !== undefined) ? entry[_lang] : key;
|
|
||||||
|
|
||||||
if (vars && typeof vars === 'object') {
|
|
||||||
for (const [k, v] of Object.entries(vars)) {
|
|
||||||
text = text.replace(new RegExp('\\{' + k + '\\}', 'g'), v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Walk the DOM and update every element that carries an i18n data-attribute.
|
|
||||||
* data-i18n="key" -> textContent
|
|
||||||
* data-i18n-placeholder="key" -> placeholder attribute
|
|
||||||
* data-i18n-title="key" -> title attribute
|
|
||||||
*/
|
|
||||||
function _hydrate() {
|
|
||||||
// textContent
|
|
||||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
|
||||||
const key = el.getAttribute('data-i18n');
|
|
||||||
if (key) el.textContent = t(key);
|
|
||||||
});
|
|
||||||
|
|
||||||
// placeholder
|
|
||||||
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
|
||||||
const key = el.getAttribute('data-i18n-placeholder');
|
|
||||||
if (key) el.setAttribute('placeholder', t(key));
|
|
||||||
});
|
|
||||||
|
|
||||||
// title
|
|
||||||
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
|
||||||
const key = el.getAttribute('data-i18n-title');
|
|
||||||
if (key) el.setAttribute('title', t(key));
|
|
||||||
});
|
|
||||||
|
|
||||||
// lang-switcher active state
|
|
||||||
document.querySelectorAll('.lang-btn').forEach(btn => {
|
|
||||||
btn.classList.toggle('active', btn.getAttribute('data-lang') === _lang);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translate known German backend error messages to English.
|
|
||||||
* If current language is 'de' or the message is unknown, returns the original.
|
|
||||||
*
|
|
||||||
* @param {string} msg – error message from the API
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function translateApiError(msg) {
|
|
||||||
if (_lang === 'de' || !msg) return msg;
|
|
||||||
return _API_ERROR_MAP[msg] || msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the appropriate OpenStreetMap tile URL for the current language.
|
|
||||||
* DE -> openstreetmap.de EN -> openstreetmap.org
|
|
||||||
*
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function mapTileUrl() {
|
|
||||||
// Immer deutsche OSM-Kacheln verwenden - tile.openstreetmap.org hat strikte Rate-Limits
|
|
||||||
// und zeigt bei Ueberschreitung graue Kacheln. Die DE-Tiles zeigen Ortsnamen
|
|
||||||
// die in beiden Sprachen lesbar sind.
|
|
||||||
return 'https://tile.openstreetmap.de/{z}/{x}/{y}.png';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- expose ---- */
|
|
||||||
return {
|
|
||||||
init,
|
|
||||||
setLang,
|
|
||||||
t,
|
|
||||||
_hydrate,
|
|
||||||
translateApiError,
|
|
||||||
mapTileUrl,
|
|
||||||
/** Getter for the current language code */
|
|
||||||
get lang() { return _lang; },
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Auto-init as early as possible to prevent flash of untranslated content
|
|
||||||
LangManager.init();
|
|
||||||
In neuem Issue referenzieren
Einen Benutzer sperren