diff --git a/src/static/js/app.js b/src/static/js/app.js
index 4ec06a6..f27ca26 100644
--- a/src/static/js/app.js
+++ b/src/static/js/app.js
@@ -164,6 +164,31 @@ const A11yManager = {
localStorage.setItem(this._key, JSON.stringify(this._settings));
},
+ _updateLabels() {
+ const btn = document.getElementById('a11y-btn');
+ if (btn) {
+ btn.title = LangManager.t('a11y.title');
+ btn.setAttribute('aria-label', LangManager.t('a11y.title'));
+ }
+ const panel = document.getElementById('a11y-panel');
+ if (panel) {
+ panel.setAttribute('aria-label', LangManager.t('a11y.title'));
+ const title = panel.querySelector('.a11y-panel-title');
+ if (title) title.textContent = LangManager.t('a11y.title');
+ }
+ const keys = ['contrast', 'focus', 'fontsize', 'motion'];
+ keys.forEach(k => {
+ const cb = document.getElementById('a11y-' + k);
+ if (cb) {
+ const label = cb.closest('.a11y-option');
+ if (label) {
+ const span = label.querySelector('span:last-child');
+ if (span) span.textContent = LangManager.t('a11y.' + k);
+ }
+ }
+ });
+ },
+
_openPanel() {
this._isOpen = true;
document.getElementById('a11y-panel').style.display = '';
@@ -317,7 +342,7 @@ const NotificationCenter = {
list.innerHTML = this._notifications.map(n => {
const time = new Date(n.timestamp);
- const timeStr = time.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
+ const timeStr = time.toLocaleTimeString(LangManager.lang === 'de' ? 'de-DE' : 'en-GB', { hour: '2-digit', minute: '2-digit' });
const unreadClass = n.read ? '' : ' unread';
const icon = n.icon || 'info';
return `
@@ -374,6 +399,19 @@ const NotificationCenter = {
}
},
+ _updateLabels() {
+ const bell = document.getElementById('notification-bell');
+ if (bell) {
+ bell.title = LangManager.t('notif.title');
+ bell.setAttribute('aria-label', LangManager.t('notif.title'));
+ }
+ const panelTitle = document.querySelector('.notification-panel-title');
+ if (panelTitle) panelTitle.textContent = LangManager.t('notif.title');
+ const markRead = document.getElementById('notification-mark-read');
+ if (markRead) markRead.textContent = LangManager.t('notif.mark_read');
+ this._renderList();
+ },
+
async _syncFromDB() {
try {
const items = await API.listNotifications(50);
@@ -489,7 +527,7 @@ const App = {
// Feedback
document.getElementById('feedback-form').addEventListener('submit', (e) => this.submitFeedback(e));
document.getElementById('fb-message').addEventListener('input', (e) => {
- document.getElementById('fb-char-count').textContent = e.target.value.length.toLocaleString('de-DE');
+ document.getElementById('fb-char-count').textContent = e.target.value.length.toLocaleString(LangManager.lang === 'de' ? 'de-DE' : 'en-GB');
});
// Sidebar-Chevrons initial auf offen setzen (Archiv geschlossen)
@@ -677,7 +715,7 @@ const App = {
const deleteBtn = document.getElementById('delete-incident-btn');
const isCreator = incident.created_by_username === this._currentUsername;
deleteBtn.disabled = !isCreator;
- deleteBtn.title = isCreator ? '' : `Nur ${incident.created_by_username} kann diese Lage löschen`;
+ deleteBtn.title = isCreator ? '' : LangManager.t('incident.delete_only_creator', { name: incident.created_by_username });
// Zusammenfassung mit Quellenverzeichnis
const summaryText = document.getElementById('summary-text');
@@ -695,7 +733,8 @@ const App = {
const updated = incident.updated_at ? new Date(incident.updated_at) : null;
const metaUpdated = document.getElementById('meta-updated');
if (updated) {
- const fullDate = `${updated.toLocaleDateString('de-DE')} ${updated.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}`;
+ const _loc = LangManager.lang === 'de' ? 'de-DE' : 'en-GB';
+ const fullDate = `${updated.toLocaleDateString(_loc)} ${updated.toLocaleTimeString(_loc, { hour: '2-digit', minute: '2-digit' })}`;
metaUpdated.textContent = LangManager.t('time.stand', { time: App._timeAgo(updated) });
metaUpdated.title = fullDate;
} else {
@@ -951,13 +990,14 @@ const App = {
entries.forEach(e => {
const d = new Date(e.timestamp || 0);
let key, label, ts;
+ const _dtLoc = LangManager.lang === 'de' ? 'de-DE' : 'en-GB';
if (granularity === 'hour') {
key = `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}-${d.getHours()}`;
- label = d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short' }) + ', ' + d.getHours().toString().padStart(2, '0') + ':00';
+ label = d.toLocaleDateString(_dtLoc, { day: '2-digit', month: 'short' }) + ', ' + d.getHours().toString().padStart(2, '0') + ':00';
ts = new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()).getTime();
} else {
key = `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
- label = d.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: 'short' });
+ label = d.toLocaleDateString(_dtLoc, { weekday: 'short', day: '2-digit', month: 'short' });
ts = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 12).getTime();
}
if (!bucketMap[key]) {
@@ -1069,8 +1109,8 @@ const App = {
const yesterday = new Date(today.getTime() - 86400000);
const bucketDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
let label;
- const dateStr = d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short' });
const locale = LangManager.lang === 'de' ? 'de-DE' : 'en-GB';
+ const dateStr = d.toLocaleDateString(locale, { day: '2-digit', month: 'short' });
if (bucketDay.getTime() === today.getTime()) {
label = LangManager.t('time.today') + ', ' + dateStr;
} else if (bucketDay.getTime() === yesterday.getTime()) {
@@ -1287,7 +1327,7 @@ const App = {
const dateField = (type === 'research' && article.published_at)
? article.published_at : article.collected_at;
const time = dateField
- ? new Date(dateField).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
+ ? new Date(dateField).toLocaleTimeString(LangManager.lang === 'de' ? 'de-DE' : 'en-GB', { hour: '2-digit', minute: '2-digit' })
: '--:--';
const headline = article.headline_de || article.headline;
@@ -1329,7 +1369,7 @@ const App = {
*/
_renderSnapshotEntry(snapshot) {
const time = snapshot.created_at
- ? new Date(snapshot.created_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
+ ? new Date(snapshot.created_at).toLocaleTimeString(LangManager.lang === 'de' ? 'de-DE' : 'en-GB', { hour: '2-digit', minute: '2-digit' })
: '--:--';
const stats = [];
@@ -1546,7 +1586,7 @@ const App = {
try {
const st = await API.getGeoparseStatus(incidentId);
if (st.status === 'running') {
- if (btn) btn.textContent = `${st.processed}/${st.total} Artikel...`;
+ if (btn) btn.textContent = LangManager.t('misc.articles_progress', { done: st.processed, total: st.total });
} else {
clearInterval(this._geoparsePolling);
this._geoparsePolling = null;
@@ -1667,8 +1707,9 @@ const App = {
list.innerHTML = logs.map(log => {
const started = new Date(log.started_at);
- const timeStr = started.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }) + ' ' +
- started.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
+ const _rlLoc = LangManager.lang === 'de' ? 'de-DE' : 'en-GB';
+ const timeStr = started.toLocaleDateString(_rlLoc, { day: '2-digit', month: '2-digit' }) + ' ' +
+ started.toLocaleTimeString(_rlLoc, { hour: '2-digit', minute: '2-digit' });
let detail = '';
if (log.status === 'completed') {
@@ -1874,7 +1915,7 @@ const App = {
handleRefreshSummary(msg) {
const d = msg.data;
- const title = d.incident_title || 'Lage';
+ const title = d.incident_title || LangManager.t('misc.incident');
// Abschluss-Animation auslösen wenn pending
if (this._pendingComplete === msg.incident_id) {
@@ -2065,16 +2106,16 @@ const App = {
const stats = await API.getSourceStats();
const srcCount = document.getElementById('stat-sources-count');
const artCount = document.getElementById('stat-articles-count');
- if (srcCount) srcCount.textContent = `${stats.total_sources} Quellen`;
- if (artCount) artCount.textContent = `${stats.total_articles} Artikel`;
+ if (srcCount) srcCount.textContent = LangManager.t('sidebar.sources_count', { n: stats.total_sources });
+ if (artCount) artCount.textContent = LangManager.t('sidebar.articles_count', { n: stats.total_articles });
} catch {
// Fallback: aus Lagen berechnen
const totalArticles = this.incidents.reduce((sum, i) => sum + i.article_count, 0);
const totalSources = this.incidents.reduce((sum, i) => sum + i.source_count, 0);
const srcCount = document.getElementById('stat-sources-count');
const artCount = document.getElementById('stat-articles-count');
- if (srcCount) srcCount.textContent = `${totalSources} Quellen`;
- if (artCount) artCount.textContent = `${totalArticles} Artikel`;
+ if (srcCount) srcCount.textContent = LangManager.t('sidebar.sources_count', { n: totalSources });
+ if (artCount) artCount.textContent = LangManager.t('sidebar.articles_count', { n: totalArticles });
}
},
@@ -2165,6 +2206,8 @@ const App = {
this._sourcesOnly = sources.filter(s => s.source_type !== 'excluded');
this._blacklistOnly = sources.filter(s => s.source_type === 'excluded');
+ this._lastSourceStats = stats;
+ this._sourcesLoaded = true;
this.renderSourceStats(stats);
this.renderSourceList();
} catch (err) {
@@ -2575,7 +2618,7 @@ const App = {
document.getElementById('src-domain').value = this._discoveredData.domain || '';
document.getElementById('src-notes').value = '';
- const typeLabel = this._discoveredData.source_type === 'rss_feed' ? 'RSS-Feed' : 'Web-Quelle';
+ const typeLabel = this._discoveredData.source_type === 'rss_feed' ? LangManager.t('sources.rss_feed') : LangManager.t('sources.web_source');
document.getElementById('src-type-display').value = typeLabel;
const rssGroup = document.getElementById('src-rss-url-group');
@@ -2603,13 +2646,12 @@ const App = {
document.getElementById('src-discovery-result').style.display = 'none';
if (result.added_count > 0) {
- UI.showToast(`${result.domain}: ${result.added_count} Feeds hinzugefügt` +
- (result.skipped_count > 0 ? ` (${result.skipped_count} bereits vorhanden)` : ''),
- 'success');
+ const key = result.skipped_count > 0 ? 'toast.discover_added_skipped' : 'toast.discover_added';
+ UI.showToast(LangManager.t(key, { domain: result.domain, count: result.added_count, skipped: result.skipped_count }), 'success');
} else if (result.skipped_count > 0) {
- UI.showToast(`${result.domain}: Alle ${result.skipped_count} Feeds bereits vorhanden.`, 'info');
+ UI.showToast(LangManager.t('toast.discover_all_exist', { domain: result.domain, count: result.skipped_count }), 'info');
} else {
- UI.showToast(`${result.domain}: Keine relevanten Feeds gefunden.`, 'info');
+ UI.showToast(LangManager.t('toast.discover_none', { domain: result.domain }), 'info');
}
this.toggleSourceForm(false);
@@ -2653,7 +2695,7 @@ const App = {
document.getElementById('src-notes').value = source.notes || '';
document.getElementById('src-domain').value = source.domain || '';
- const typeLabel = source.source_type === 'rss_feed' ? 'RSS-Feed' : 'Web-Quelle';
+ const typeLabel = source.source_type === 'rss_feed' ? LangManager.t('sources.rss_feed') : LangManager.t('sources.web_source');
document.getElementById('src-type-display').value = typeLabel;
const rssGroup = document.getElementById('src-rss-url-group');
@@ -2745,6 +2787,15 @@ const App = {
this.updateSidebarStats();
// Update map tiles
UI._applyMapTiles();
+ // Update accessibility panel labels
+ A11yManager._updateLabels();
+ // Update notification center labels
+ NotificationCenter._updateLabels();
+ // Update source management if visible
+ if (this._sourcesLoaded) {
+ this.renderSourceStats(this._lastSourceStats || { by_type: {}, total_articles: 0 });
+ this.renderSourceList();
+ }
},
};
@@ -2953,7 +3004,7 @@ function buildDetailedSourceOverview() {
// Nach Quelle gruppieren
const sourceMap = {};
articles.forEach(a => {
- const name = a.source || 'Unbekannt';
+ const name = a.source || LangManager.t('time.unknown');
if (!sourceMap[name]) sourceMap[name] = { articles: [], languages: new Set() };
sourceMap[name].articles.push(a);
sourceMap[name].languages.add((a.language || 'de').toUpperCase());
@@ -2973,7 +3024,7 @@ function buildDetailedSourceOverview() {
.join('');
let html = ``;
@@ -2991,7 +3042,7 @@ function buildDetailedSourceOverview() {
data.articles.forEach(a => {
const headline = UI.escape(a.headline_de || a.headline || LangManager.t('map.no_title'));
const time = a.collected_at
- ? new Date(a.collected_at).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
+ ? new Date(a.collected_at).toLocaleString(LangManager.lang === 'de' ? 'de-DE' : 'en-GB', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
: '';
const langBadge = a.language && a.language !== 'de'
? `${a.language.toUpperCase()}` : '';
@@ -3045,8 +3096,8 @@ function updateSourcesHint() {
const hint = document.getElementById('sources-hint');
if (hint) {
hint.textContent = intl
- ? 'DE + internationale Feeds (Reuters, BBC, Al Jazeera etc.)'
- : 'Nur deutschsprachige Quellen (DE, AT, CH)';
+ ? LangManager.t('form.sources_hint_intl')
+ : LangManager.t('form.sources_hint_de');
}
}
@@ -3056,11 +3107,11 @@ function toggleTypeDefaults() {
const refreshMode = document.getElementById('inc-refresh-mode');
if (type === 'research') {
- hint.textContent = 'Nur WebSearch (Deep Research), manuelle Aktualisierung empfohlen';
+ hint.textContent = LangManager.t('form.type_hint_research');
refreshMode.value = 'manual';
toggleRefreshInterval();
} else {
- hint.textContent = 'RSS-Feeds + WebSearch, automatische Aktualisierung empfohlen';
+ hint.textContent = LangManager.t('form.type_hint_adhoc');
}
}
@@ -3129,7 +3180,7 @@ setInterval(() => {
} else if (remaining <= fiveMinutes && !App._sessionWarningShown) {
App._sessionWarningShown = true;
const mins = Math.ceil(remaining / 60000);
- UI.showToast(`Session läuft in ${mins} Minute${mins !== 1 ? 'n' : ''} ab. Bitte erneut anmelden.`, 'warning', 15000);
+ UI.showToast(LangManager.t('toast.session_expiring', { mins }), 'warning', 15000);
}
} catch (e) { /* Token nicht parsbar */ }
}, 60000);
diff --git a/src/static/js/lang.js b/src/static/js/lang.js
index 680b6c9..092169b 100644
--- a/src/static/js/lang.js
+++ b/src/static/js/lang.js
@@ -405,6 +405,25 @@ const TRANSLATIONS = {
// ── 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.' },
};
@@ -531,10 +550,10 @@ const LangManager = (() => {
* @returns {string}
*/
function mapTileUrl() {
- if (_lang === 'de') {
- return 'https://tile.openstreetmap.de/{z}/{x}/{y}.png';
- }
- return 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
+ // 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 ---- */