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 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -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 `<div class="notification-item${unreadClass}" onclick="NotificationCenter._handleClick(${n.incident_id})" data-id="${n.incident_id}" role="button" tabindex="0">
|
||||
@@ -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 = `<div class="source-overview-header">
|
||||
<span class="source-overview-stat">${articles.length} Artikel aus ${sources.length} Quellen</span>
|
||||
<span class="source-overview-stat">${LangManager.t('sources.articles_from_sources', { articles: articles.length, sources: sources.length })}</span>
|
||||
<div class="source-lang-chips">${langChips}</div>
|
||||
</div>`;
|
||||
|
||||
@@ -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'
|
||||
? `<span class="lang-badge">${a.language.toUpperCase()}</span>` : '';
|
||||
@@ -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);
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren