From b604a808421028ec81fcf2d42eb8dcfeb3b55c96 Mon Sep 17 00:00:00 2001 From: claude-dev Date: Thu, 5 Mar 2026 16:36:28 +0100 Subject: [PATCH] i18n: Fix gray map tiles, translate A11y/Notification panels, remaining strings - Fix gray map on EN: always use tile.openstreetmap.de (org has rate limits) - Add A11yManager._updateLabels() for live language switch of accessibility panel - Add NotificationCenter._updateLabels() for notification panel translation - Replace all remaining hardcoded de-DE locales with dynamic locale switch - Translate sidebar stats, source discovery toasts, session expiry warning - Translate source form hints, type labels, article progress counter - Add 15+ new translation keys for missing strings Co-Authored-By: Claude Opus 4.6 --- src/static/js/app.js | 115 ++++++++++++++++++++++++++++++------------ src/static/js/lang.js | 27 ++++++++-- 2 files changed, 106 insertions(+), 36 deletions(-) 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 = `
- ${articles.length} Artikel aus ${sources.length} Quellen + ${LangManager.t('sources.articles_from_sources', { articles: articles.length, sources: sources.length })}
${langChips}
`; @@ -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 ---- */