feat(frontend): Light-i18n + Org-Sprache durch /auth/me
Backend: - UserMeResponse um output_language (de | en) erweitert. - /auth/me liefert die Org-Sprache aus organization_settings. Frontend: - Neu: static/js/i18n.js mit T(key)-Helper, I18N.load(lang) und applyDom() ueber data-i18n + data-i18n-attr. - Neu: static/i18n/de.json + en.json (sichtbare Bereiche: Sidebar, Header, Modal-Titel, Faktencheck-Status, Refresh-Hinweise). - dashboard.html: i18n.js Script-Tag vor api.js, data-i18n auf den prominenten Strings (Abmelden, + Neuer Fall, Alle/Eigene, Sidebar- Sektionen, Bericht exportieren, Faktencheck-Tab, Lage anlegen). Tutorial.init() entfernt aus DOMContentLoaded. - components.js: factCheckLabels/Tooltips/ChipLabels als Getter ueber T() mit DE-Fallbacks. - app.js: vor Setup wird I18N.load(user.output_language) aufgerufen und applyDom() ausgefuehrt. Tutorial.init() laeuft nur bei lang === de. Phase 6 von 8 (eng_demo / Org-Sprache).
Dieser Commit ist enthalten in:
@@ -43,6 +43,7 @@ class UserMeResponse(BaseModel):
|
|||||||
credits_remaining: Optional[int] = None
|
credits_remaining: Optional[int] = None
|
||||||
credits_percent_used: Optional[float] = None
|
credits_percent_used: Optional[float] = None
|
||||||
is_global_admin: bool = False
|
is_global_admin: bool = False
|
||||||
|
output_language: str = "de"
|
||||||
|
|
||||||
|
|
||||||
# Incidents (Lagen)
|
# Incidents (Lagen)
|
||||||
|
|||||||
@@ -216,6 +216,12 @@ async def get_me(
|
|||||||
if _staging_mode():
|
if _staging_mode():
|
||||||
is_global_admin_response = False
|
is_global_admin_response = False
|
||||||
|
|
||||||
|
# Org-Sprache fuer Frontend-i18n
|
||||||
|
output_language_iso = "de"
|
||||||
|
if current_user.get("tenant_id"):
|
||||||
|
from services.org_settings import get_org_language
|
||||||
|
output_language_iso = await get_org_language(db, current_user["tenant_id"])
|
||||||
|
|
||||||
return UserMeResponse(
|
return UserMeResponse(
|
||||||
id=current_user["id"],
|
id=current_user["id"],
|
||||||
username=current_user["username"],
|
username=current_user["username"],
|
||||||
@@ -233,6 +239,7 @@ async def get_me(
|
|||||||
read_only_reason=license_info.get("read_only_reason"),
|
read_only_reason=license_info.get("read_only_reason"),
|
||||||
unlimited_budget=unlimited_budget,
|
unlimited_budget=unlimited_budget,
|
||||||
is_global_admin=is_global_admin_response,
|
is_global_admin=is_global_admin_response,
|
||||||
|
output_language=output_language_iso,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,25 +80,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">Abmelden</button>
|
<button class="btn btn-secondary btn-small" id="logout-btn" data-i18n="header.logout">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" style="margin-bottom:6px;">+ Neuer Fall</button>
|
<button class="btn btn-primary btn-full btn-small" id="new-incident-btn" style="margin-bottom:6px;" data-i18n="header.new_incident">+ Neuer Fall</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">Alle</button>
|
<button class="sidebar-filter-btn active" data-filter="all" onclick="App.setSidebarFilter('all')" aria-pressed="true" data-i18n="filter.all">Alle</button>
|
||||||
<button class="sidebar-filter-btn" data-filter="mine" onclick="App.setSidebarFilter('mine')" aria-pressed="false">Eigene</button>
|
<button class="sidebar-filter-btn" data-filter="mine" onclick="App.setSidebarFilter('mine')" aria-pressed="false" data-i18n="filter.own">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>
|
||||||
Live-Monitoring
|
<span data-i18n="sidebar.live_monitoring">Live-Monitoring</span>
|
||||||
<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>
|
||||||
@@ -107,7 +107,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>
|
||||||
Recherchen
|
<span data-i18n="sidebar.research">Recherchen</span>
|
||||||
<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>
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
<div class="incident-header-actions">
|
<div class="incident-header-actions">
|
||||||
<button class="btn btn-primary btn-small" id="refresh-btn">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">Bearbeiten</button>
|
<button class="btn btn-secondary btn-small" id="edit-incident-btn">Bearbeiten</button>
|
||||||
<button class="btn btn-secondary btn-small" onclick="App.openExportModal()">Bericht exportieren</button>
|
<button class="btn btn-secondary btn-small" onclick="App.openExportModal()" data-i18n="modal.export.title">Bericht exportieren</button>
|
||||||
<button class="btn btn-secondary btn-small" id="archive-incident-btn">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">Löschen</button>
|
<button class="btn btn-danger btn-small" id="delete-incident-btn">Löschen</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
<button class="tab-btn" data-tab="lagebild">Lagebild</button>
|
<button class="tab-btn" data-tab="lagebild">Lagebild</button>
|
||||||
<button class="tab-btn" data-tab="timeline">Ereignis-Timeline</button>
|
<button class="tab-btn" data-tab="timeline">Ereignis-Timeline</button>
|
||||||
<button class="tab-btn" data-tab="karte">Geografische Verteilung</button>
|
<button class="tab-btn" data-tab="karte">Geografische Verteilung</button>
|
||||||
<button class="tab-btn" data-tab="faktencheck">Faktencheck</button>
|
<button class="tab-btn" data-tab="faktencheck" data-i18n="tile.factcheck">Faktencheck</button>
|
||||||
<button class="tab-btn" data-tab="pipeline">Analysepipeline</button>
|
<button class="tab-btn" data-tab="pipeline">Analysepipeline</button>
|
||||||
<button class="tab-btn" data-tab="quellen">Quellenübersicht</button>
|
<button class="tab-btn" data-tab="quellen">Quellenübersicht</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -334,7 +334,7 @@
|
|||||||
<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">Titel des Vorfalls</label>
|
<label for="inc-title" data-i18n="modal.new_incident.title_field">Titel des Vorfalls</label>
|
||||||
<input type="text" id="inc-title" required aria-required="true" placeholder="z.B. Explosion in Madrid">
|
<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">
|
||||||
@@ -439,7 +439,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" onclick="closeModal('modal-new')">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">Lage anlegen</button>
|
<button type="submit" class="btn btn-primary" id="modal-new-submit" data-i18n="modal.new_incident.submit">Lage anlegen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -723,16 +723,17 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.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/i18n.js?v=20260513a"></script>
|
||||||
<script src="/static/js/api.js?v=20260423a"></script>
|
<script src="/static/js/api.js?v=20260423a"></script>
|
||||||
<script src="/static/js/ws.js?v=20260316b"></script>
|
<script src="/static/js/ws.js?v=20260316b"></script>
|
||||||
<script src="/static/js/components.js?v=20260427a"></script>
|
<script src="/static/js/components.js?v=20260513a"></script>
|
||||||
<script src="/static/js/layout.js?v=20260316b"></script>
|
<script src="/static/js/layout.js?v=20260316b"></script>
|
||||||
<script src="/static/js/pipeline.js?v=20260501i"></script>
|
<script src="/static/js/pipeline.js?v=20260501i"></script>
|
||||||
<script src="/static/js/app.js?v=20260512a"></script>
|
<script src="/static/js/app.js?v=20260512a"></script>
|
||||||
<script src="/static/js/cluster-data.js?v=20260322f"></script>
|
<script src="/static/js/cluster-data.js?v=20260322f"></script>
|
||||||
<script src="/static/js/tutorial.js?v=20260316z"></script>
|
<script src="/static/js/tutorial.js?v=20260316z"></script>
|
||||||
<script src="/static/js/chat.js?v=20260422a"></script>
|
<script src="/static/js/chat.js?v=20260422a"></script>
|
||||||
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init();Tutorial.init()});</script>
|
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init();/* Tutorial.init() wird in App.init() nach Sprachwahl aufgerufen, damit es bei englischen Orgs unterdrueckt werden kann */});</script>
|
||||||
|
|
||||||
<!-- Map Fullscreen Overlay -->
|
<!-- Map Fullscreen Overlay -->
|
||||||
<div class="map-fullscreen-overlay" id="map-fullscreen-overlay">
|
<div class="map-fullscreen-overlay" id="map-fullscreen-overlay">
|
||||||
|
|||||||
64
src/static/i18n/de.json
Normale Datei
64
src/static/i18n/de.json
Normale Datei
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"sidebar.live_monitoring": "Live-Monitoring",
|
||||||
|
"sidebar.research": "Recherchen",
|
||||||
|
"sidebar.empty": "Keine Lagen vorhanden",
|
||||||
|
"header.logout": "Abmelden",
|
||||||
|
"header.new_incident": "+ Neuer Fall",
|
||||||
|
"header.theme_toggle": "Theme wechseln",
|
||||||
|
"header.notifications": "Benachrichtigungen",
|
||||||
|
"filter.all": "Alle",
|
||||||
|
"filter.own": "Eigene",
|
||||||
|
"filter.everything": "Alles",
|
||||||
|
"common.close": "Schließen",
|
||||||
|
"common.cancel": "Abbrechen",
|
||||||
|
"common.save": "Speichern",
|
||||||
|
"common.delete": "Löschen",
|
||||||
|
"common.edit": "Bearbeiten",
|
||||||
|
"common.loading": "Lädt...",
|
||||||
|
"common.confirm": "Bestätigen",
|
||||||
|
"common.error": "Fehler",
|
||||||
|
"modal.new_incident.title": "Neue Lage anlegen",
|
||||||
|
"modal.new_incident.title_field": "Titel des Vorfalls",
|
||||||
|
"modal.new_incident.description": "Beschreibung / Kontext",
|
||||||
|
"modal.new_incident.enhance": "Beschreibung generieren",
|
||||||
|
"modal.new_incident.visibility": "Sichtbarkeit",
|
||||||
|
"modal.new_incident.visibility_public": "Öffentlich",
|
||||||
|
"modal.new_incident.visibility_private": "Privat",
|
||||||
|
"modal.new_incident.submit": "Lage anlegen",
|
||||||
|
"modal.sources.title": "Quellenverwaltung",
|
||||||
|
"modal.sources.approve_all_high": "Alle ≥ 0.85 genehmigen",
|
||||||
|
"modal.export.title": "Bericht exportieren",
|
||||||
|
"modal.fc_status.title": "Statusänderung Faktencheck",
|
||||||
|
"tile.factcheck": "Faktencheck",
|
||||||
|
"tile.research_evaluated": "Recherche-Lagen werden mehrfach evaluiert...",
|
||||||
|
"tile.summary": "Lagebild",
|
||||||
|
"tile.summary_research": "Recherchebericht",
|
||||||
|
"tile.timeline": "Zeitachse",
|
||||||
|
"tile.map": "Karte",
|
||||||
|
"tile.sources": "Quellen",
|
||||||
|
"fc.label.confirmed": "Bestätigt durch mehrere Quellen",
|
||||||
|
"fc.label.unconfirmed": "Nicht unabhängig bestätigt",
|
||||||
|
"fc.label.contradicted": "Widerlegt",
|
||||||
|
"fc.label.developing": "Faktenlage noch im Fluss",
|
||||||
|
"fc.label.established": "Gesicherter Fakt (3+ Quellen)",
|
||||||
|
"fc.label.disputed": "Umstrittener Sachverhalt",
|
||||||
|
"fc.label.unverified": "Nicht unabhängig verifizierbar",
|
||||||
|
"fc.tooltip.confirmed": "Bestätigt: Mindestens zwei unabhängige, seriöse Quellen stützen diese Aussage übereinstimmend.",
|
||||||
|
"fc.tooltip.established": "Gesichert: Drei oder mehr unabhängige Quellen bestätigen den Sachverhalt. Hohe Verlässlichkeit.",
|
||||||
|
"fc.tooltip.developing": "Unklar: Die Faktenlage ist noch im Fluss. Neue Informationen können das Bild verändern.",
|
||||||
|
"fc.tooltip.unconfirmed": "Unbestätigt: Bisher nur aus einer Quelle bekannt. Eine unabhängige Bestätigung steht aus.",
|
||||||
|
"fc.tooltip.unverified": "Ungeprüft: Die Aussage konnte bisher nicht anhand verfügbarer Quellen überprüft werden.",
|
||||||
|
"fc.tooltip.disputed": "Umstritten: Quellen widersprechen sich. Es gibt sowohl stützende als auch widersprechende Belege.",
|
||||||
|
"fc.tooltip.contradicted": "Widerlegt: Zuverlässige Quellen widersprechen dieser Aussage. Wahrscheinlich falsch.",
|
||||||
|
"fc.chip.confirmed": "Bestätigt",
|
||||||
|
"fc.chip.unconfirmed": "Unbestätigt",
|
||||||
|
"fc.chip.contradicted": "Widerlegt",
|
||||||
|
"fc.chip.developing": "Unklar",
|
||||||
|
"fc.chip.established": "Gesichert",
|
||||||
|
"fc.chip.disputed": "Umstritten",
|
||||||
|
"fc.chip.unverified": "Ungeprüft",
|
||||||
|
"refresh.no_developments": "Keine neuen Entwicklungen",
|
||||||
|
"refresh.new_articles_suffix": "neue Artikel",
|
||||||
|
"refresh.confirmed_suffix": "Fakten bestätigt",
|
||||||
|
"refresh.contradicted_suffix": "widerlegt"
|
||||||
|
}
|
||||||
64
src/static/i18n/en.json
Normale Datei
64
src/static/i18n/en.json
Normale Datei
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"sidebar.live_monitoring": "Live monitoring",
|
||||||
|
"sidebar.research": "Research",
|
||||||
|
"sidebar.empty": "No situations yet",
|
||||||
|
"header.logout": "Sign out",
|
||||||
|
"header.new_incident": "+ New situation",
|
||||||
|
"header.theme_toggle": "Toggle theme",
|
||||||
|
"header.notifications": "Notifications",
|
||||||
|
"filter.all": "All",
|
||||||
|
"filter.own": "Own",
|
||||||
|
"filter.everything": "Everything",
|
||||||
|
"common.close": "Close",
|
||||||
|
"common.cancel": "Cancel",
|
||||||
|
"common.save": "Save",
|
||||||
|
"common.delete": "Delete",
|
||||||
|
"common.edit": "Edit",
|
||||||
|
"common.loading": "Loading...",
|
||||||
|
"common.confirm": "Confirm",
|
||||||
|
"common.error": "Error",
|
||||||
|
"modal.new_incident.title": "Create new situation",
|
||||||
|
"modal.new_incident.title_field": "Incident title",
|
||||||
|
"modal.new_incident.description": "Description / context",
|
||||||
|
"modal.new_incident.enhance": "Generate description",
|
||||||
|
"modal.new_incident.visibility": "Visibility",
|
||||||
|
"modal.new_incident.visibility_public": "Public",
|
||||||
|
"modal.new_incident.visibility_private": "Private",
|
||||||
|
"modal.new_incident.submit": "Create situation",
|
||||||
|
"modal.sources.title": "Source management",
|
||||||
|
"modal.sources.approve_all_high": "Approve all ≥ 0.85",
|
||||||
|
"modal.export.title": "Export report",
|
||||||
|
"modal.fc_status.title": "Fact-check status change",
|
||||||
|
"tile.factcheck": "Fact check",
|
||||||
|
"tile.research_evaluated": "Research situations are evaluated multiple times...",
|
||||||
|
"tile.summary": "Briefing",
|
||||||
|
"tile.summary_research": "Research report",
|
||||||
|
"tile.timeline": "Timeline",
|
||||||
|
"tile.map": "Map",
|
||||||
|
"tile.sources": "Sources",
|
||||||
|
"fc.label.confirmed": "Confirmed by multiple sources",
|
||||||
|
"fc.label.unconfirmed": "Not independently confirmed",
|
||||||
|
"fc.label.contradicted": "Contradicted",
|
||||||
|
"fc.label.developing": "Facts still developing",
|
||||||
|
"fc.label.established": "Established fact (3+ sources)",
|
||||||
|
"fc.label.disputed": "Disputed matter",
|
||||||
|
"fc.label.unverified": "Not independently verifiable",
|
||||||
|
"fc.tooltip.confirmed": "Confirmed: at least two independent, reputable sources support this claim consistently.",
|
||||||
|
"fc.tooltip.established": "Established: three or more independent sources confirm the matter. High reliability.",
|
||||||
|
"fc.tooltip.developing": "Developing: the facts are still in flux. New information may change the picture.",
|
||||||
|
"fc.tooltip.unconfirmed": "Unconfirmed: known from only one source so far. Independent confirmation is pending.",
|
||||||
|
"fc.tooltip.unverified": "Unverified: the claim could not yet be checked against available sources.",
|
||||||
|
"fc.tooltip.disputed": "Disputed: sources disagree. There is both supporting and contradicting evidence.",
|
||||||
|
"fc.tooltip.contradicted": "Contradicted: reliable sources contradict this claim. Likely false.",
|
||||||
|
"fc.chip.confirmed": "Confirmed",
|
||||||
|
"fc.chip.unconfirmed": "Unconfirmed",
|
||||||
|
"fc.chip.contradicted": "Contradicted",
|
||||||
|
"fc.chip.developing": "Developing",
|
||||||
|
"fc.chip.established": "Established",
|
||||||
|
"fc.chip.disputed": "Disputed",
|
||||||
|
"fc.chip.unverified": "Unverified",
|
||||||
|
"refresh.no_developments": "No new developments",
|
||||||
|
"refresh.new_articles_suffix": "new articles",
|
||||||
|
"refresh.confirmed_suffix": "facts confirmed",
|
||||||
|
"refresh.contradicted_suffix": "contradicted"
|
||||||
|
}
|
||||||
@@ -452,6 +452,14 @@ const App = {
|
|||||||
const user = await API.getMe();
|
const user = await API.getMe();
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this._currentUsername = user.email;
|
this._currentUsername = user.email;
|
||||||
|
|
||||||
|
// i18n: Sprache anhand der Org laden (default 'de') und DOM uebersetzen
|
||||||
|
if (window.I18N) {
|
||||||
|
const targetLang = user.output_language || 'de';
|
||||||
|
await window.I18N.load(targetLang);
|
||||||
|
window.I18N.applyDom();
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('header-user').textContent = user.email;
|
document.getElementById('header-user').textContent = user.email;
|
||||||
|
|
||||||
// Dropdown-Daten befuellen
|
// Dropdown-Daten befuellen
|
||||||
@@ -543,6 +551,15 @@ const App = {
|
|||||||
if (user.is_global_admin) {
|
if (user.is_global_admin) {
|
||||||
this._initOrgSwitcher(user.tenant_id);
|
this._initOrgSwitcher(user.tenant_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tutorial nur bei deutscher Org starten -- englische Demo-Mandanten
|
||||||
|
// sollen direkt im Dashboard landen.
|
||||||
|
try {
|
||||||
|
const lang = (window.I18N && window.I18N.lang) || 'de';
|
||||||
|
if (lang === 'de' && typeof Tutorial !== 'undefined' && Tutorial.init) {
|
||||||
|
Tutorial.init();
|
||||||
|
}
|
||||||
|
} catch (e) { /* Tutorial optional */ }
|
||||||
} catch {
|
} catch {
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -76,7 +76,10 @@ const UI = {
|
|||||||
/**
|
/**
|
||||||
* Faktencheck-Eintrag rendern.
|
* Faktencheck-Eintrag rendern.
|
||||||
*/
|
*/
|
||||||
factCheckLabels: {
|
// Faktencheck-Status-Labels (org-sprach-relativ via T()).
|
||||||
|
// Die DE-Fallbacks sind die historische Quelle der Wahrheit; bei
|
||||||
|
// englischer Org liefert T() den EN-Text aus i18n/en.json.
|
||||||
|
_fcLabelDefaultsDE: {
|
||||||
confirmed: 'Bestätigt durch mehrere Quellen',
|
confirmed: 'Bestätigt durch mehrere Quellen',
|
||||||
unconfirmed: 'Nicht unabhängig bestätigt',
|
unconfirmed: 'Nicht unabhängig bestätigt',
|
||||||
contradicted: 'Widerlegt',
|
contradicted: 'Widerlegt',
|
||||||
@@ -85,8 +88,7 @@ const UI = {
|
|||||||
disputed: 'Umstrittener Sachverhalt',
|
disputed: 'Umstrittener Sachverhalt',
|
||||||
unverified: 'Nicht unabhängig verifizierbar',
|
unverified: 'Nicht unabhängig verifizierbar',
|
||||||
},
|
},
|
||||||
|
_fcTooltipDefaultsDE: {
|
||||||
factCheckTooltips: {
|
|
||||||
confirmed: 'Bestätigt: Mindestens zwei unabhängige, seriöse Quellen stützen diese Aussage übereinstimmend.',
|
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.',
|
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.',
|
developing: 'Unklar: Die Faktenlage ist noch im Fluss. Neue Informationen können das Bild verändern.',
|
||||||
@@ -95,8 +97,7 @@ const UI = {
|
|||||||
disputed: 'Umstritten: Quellen widersprechen sich. Es gibt sowohl stützende als auch widersprechende Belege.',
|
disputed: 'Umstritten: Quellen widersprechen sich. Es gibt sowohl stützende als auch widersprechende Belege.',
|
||||||
contradicted: 'Widerlegt: Zuverlässige Quellen widersprechen dieser Aussage. Wahrscheinlich falsch.',
|
contradicted: 'Widerlegt: Zuverlässige Quellen widersprechen dieser Aussage. Wahrscheinlich falsch.',
|
||||||
},
|
},
|
||||||
|
_fcChipDefaultsDE: {
|
||||||
factCheckChipLabels: {
|
|
||||||
confirmed: 'Bestätigt',
|
confirmed: 'Bestätigt',
|
||||||
unconfirmed: 'Unbestätigt',
|
unconfirmed: 'Unbestätigt',
|
||||||
contradicted: 'Widerlegt',
|
contradicted: 'Widerlegt',
|
||||||
@@ -106,6 +107,34 @@ const UI = {
|
|||||||
unverified: 'Ungeprüft',
|
unverified: 'Ungeprüft',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get factCheckLabels() {
|
||||||
|
const out = {};
|
||||||
|
for (const k of Object.keys(this._fcLabelDefaultsDE)) {
|
||||||
|
out[k] = (typeof T === 'function')
|
||||||
|
? T('fc.label.' + k, this._fcLabelDefaultsDE[k])
|
||||||
|
: this._fcLabelDefaultsDE[k];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
get factCheckTooltips() {
|
||||||
|
const out = {};
|
||||||
|
for (const k of Object.keys(this._fcTooltipDefaultsDE)) {
|
||||||
|
out[k] = (typeof T === 'function')
|
||||||
|
? T('fc.tooltip.' + k, this._fcTooltipDefaultsDE[k])
|
||||||
|
: this._fcTooltipDefaultsDE[k];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
get factCheckChipLabels() {
|
||||||
|
const out = {};
|
||||||
|
for (const k of Object.keys(this._fcChipDefaultsDE)) {
|
||||||
|
out[k] = (typeof T === 'function')
|
||||||
|
? T('fc.chip.' + k, this._fcChipDefaultsDE[k])
|
||||||
|
: this._fcChipDefaultsDE[k];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
factCheckIcons: {
|
factCheckIcons: {
|
||||||
confirmed: '✓',
|
confirmed: '✓',
|
||||||
unconfirmed: '?',
|
unconfirmed: '?',
|
||||||
|
|||||||
71
src/static/js/i18n.js
Normale Datei
71
src/static/js/i18n.js
Normale Datei
@@ -0,0 +1,71 @@
|
|||||||
|
// Light-i18n fuer AegisSight Monitor.
|
||||||
|
// Wird vor app.js geladen. T(key) ist global verfuegbar.
|
||||||
|
//
|
||||||
|
// Aufrufer:
|
||||||
|
// await I18N.load(lang); // 'de' oder 'en'
|
||||||
|
// const txt = T('sidebar.live_monitoring');
|
||||||
|
// I18N.applyDom(); // ersetzt alle <... data-i18n="key">...</...>
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const STORAGE_KEY = 'aegis_lang';
|
||||||
|
|
||||||
|
const I18N = {
|
||||||
|
lang: 'de',
|
||||||
|
dict: {},
|
||||||
|
|
||||||
|
async load(lang) {
|
||||||
|
if (!lang) lang = 'de';
|
||||||
|
if (lang !== 'de' && lang !== 'en') lang = 'de';
|
||||||
|
this.lang = lang;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/static/i18n/${lang}.json?v=20260513`);
|
||||||
|
if (res.ok) {
|
||||||
|
this.dict = await res.json();
|
||||||
|
} else {
|
||||||
|
console.warn(`i18n: Konnte ${lang}.json nicht laden (${res.status})`);
|
||||||
|
this.dict = {};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('i18n-Load fehlgeschlagen:', e);
|
||||||
|
this.dict = {};
|
||||||
|
}
|
||||||
|
try { localStorage.setItem(STORAGE_KEY, lang); } catch (_) {}
|
||||||
|
document.documentElement.setAttribute('lang', lang);
|
||||||
|
return this.dict;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Synchroner Initial-Lookup aus localStorage (fuer FOUC-freies Bootstrap).
|
||||||
|
bootLang() {
|
||||||
|
try { return localStorage.getItem(STORAGE_KEY) || 'de'; } catch (_) { return 'de'; }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ersetzt alle data-i18n Attribute im DOM.
|
||||||
|
applyDom(root) {
|
||||||
|
root = root || document;
|
||||||
|
root.querySelectorAll('[data-i18n]').forEach(el => {
|
||||||
|
const key = el.getAttribute('data-i18n');
|
||||||
|
if (!key) return;
|
||||||
|
const txt = this.dict[key];
|
||||||
|
if (txt != null) el.textContent = txt;
|
||||||
|
});
|
||||||
|
// Attribute (z.B. placeholder, title): data-i18n-attr="placeholder:key,title:key2"
|
||||||
|
root.querySelectorAll('[data-i18n-attr]').forEach(el => {
|
||||||
|
const spec = el.getAttribute('data-i18n-attr') || '';
|
||||||
|
spec.split(',').forEach(pair => {
|
||||||
|
const [attr, key] = pair.split(':').map(s => s && s.trim());
|
||||||
|
if (!attr || !key) return;
|
||||||
|
const txt = this.dict[key];
|
||||||
|
if (txt != null) el.setAttribute(attr, txt);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function T(key, fallback) {
|
||||||
|
if (I18N.dict && I18N.dict[key] != null) return I18N.dict[key];
|
||||||
|
return fallback != null ? fallback : key;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.I18N = I18N;
|
||||||
|
window.T = T;
|
||||||
|
})();
|
||||||
In neuem Issue referenzieren
Einen Benutzer sperren