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:
@@ -452,6 +452,14 @@ const App = {
|
||||
const user = await API.getMe();
|
||||
this.user = user;
|
||||
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;
|
||||
|
||||
// Dropdown-Daten befuellen
|
||||
@@ -543,6 +551,15 @@ const App = {
|
||||
if (user.is_global_admin) {
|
||||
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 {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
|
||||
@@ -76,7 +76,10 @@ const UI = {
|
||||
/**
|
||||
* 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',
|
||||
unconfirmed: 'Nicht unabhängig bestätigt',
|
||||
contradicted: 'Widerlegt',
|
||||
@@ -85,8 +88,7 @@ const UI = {
|
||||
disputed: 'Umstrittener Sachverhalt',
|
||||
unverified: 'Nicht unabhängig verifizierbar',
|
||||
},
|
||||
|
||||
factCheckTooltips: {
|
||||
_fcTooltipDefaultsDE: {
|
||||
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.',
|
||||
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.',
|
||||
contradicted: 'Widerlegt: Zuverlässige Quellen widersprechen dieser Aussage. Wahrscheinlich falsch.',
|
||||
},
|
||||
|
||||
factCheckChipLabels: {
|
||||
_fcChipDefaultsDE: {
|
||||
confirmed: 'Bestätigt',
|
||||
unconfirmed: 'Unbestätigt',
|
||||
contradicted: 'Widerlegt',
|
||||
@@ -106,6 +107,34 @@ const UI = {
|
||||
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: {
|
||||
confirmed: '✓',
|
||||
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