diff --git a/src/static/js/lang.js b/src/static/js/lang.js new file mode 100644 index 0000000..66de728 --- /dev/null +++ b/src/static/js/lang.js @@ -0,0 +1,538 @@ +/* ============================================================ + * 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' }, + + // ── 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' }, + + // ── 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...' }, +}; + + +// ---------- 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)); + }); + } + + /** + * 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() { + if (_lang === 'de') { + return 'https://tile.openstreetmap.de/{z}/{x}/{y}.png'; + } + return 'https://tile.openstreetmap.org/{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();