fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
250
backend/frontend/static/js/modules/i18n.js
Normal file
250
backend/frontend/static/js/modules/i18n.js
Normal file
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* BreakPilot Studio - i18n Module
|
||||
*
|
||||
* Internationalisierungs-Funktionen:
|
||||
* - t(key): Übersetzungsfunktion
|
||||
* - setLanguage(lang): Sprache wechseln
|
||||
* - applyLanguage(): UI-Texte aktualisieren
|
||||
* - getCurrentLang(): Aktuelle Sprache abrufen
|
||||
*
|
||||
* Refactored: 2026-01-19
|
||||
*/
|
||||
|
||||
import { translations, rtlLanguages, defaultLanguage, availableLanguages } from './translations.js';
|
||||
|
||||
// Aktuelle Sprache (aus localStorage oder Standard)
|
||||
let currentLang = localStorage.getItem('bp_language') || defaultLanguage;
|
||||
|
||||
/**
|
||||
* Übersetzungsfunktion
|
||||
* @param {string} key - Übersetzungsschlüssel
|
||||
* @returns {string} - Übersetzter Text oder Fallback
|
||||
*/
|
||||
export function t(key) {
|
||||
const lang = translations[currentLang] || translations[defaultLanguage];
|
||||
return lang[key] || translations[defaultLanguage][key] || key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktuelle Sprache abrufen
|
||||
* @returns {string} - Sprachcode (de, en, tr, etc.)
|
||||
*/
|
||||
export function getCurrentLang() {
|
||||
return currentLang;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob aktuelle Sprache RTL ist
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isRTL() {
|
||||
return rtlLanguages.includes(currentLang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprache wechseln
|
||||
* @param {string} lang - Neuer Sprachcode
|
||||
*/
|
||||
export function setLanguage(lang) {
|
||||
if (translations[lang]) {
|
||||
currentLang = lang;
|
||||
localStorage.setItem('bp_language', lang);
|
||||
applyLanguage();
|
||||
return true;
|
||||
}
|
||||
console.warn(`Language '${lang}' not available`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet die aktuelle Sprache auf alle UI-Elemente an
|
||||
*/
|
||||
export function applyLanguage() {
|
||||
// RTL-Unterstützung
|
||||
if (isRTL()) {
|
||||
document.body.classList.add('rtl');
|
||||
document.documentElement.setAttribute('dir', 'rtl');
|
||||
} else {
|
||||
document.body.classList.remove('rtl');
|
||||
document.documentElement.setAttribute('dir', 'ltr');
|
||||
}
|
||||
|
||||
// Alle Elemente mit data-i18n-Attribut aktualisieren
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n');
|
||||
const translated = t(key);
|
||||
|
||||
// Verschiedene Element-Typen behandeln
|
||||
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
|
||||
el.placeholder = translated;
|
||||
} else {
|
||||
el.textContent = translated;
|
||||
}
|
||||
});
|
||||
|
||||
// Elemente mit data-i18n-title für Tooltips
|
||||
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n-title');
|
||||
el.title = t(key);
|
||||
});
|
||||
|
||||
// Elemente mit data-i18n-value für value-Attribute
|
||||
document.querySelectorAll('[data-i18n-value]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n-value');
|
||||
el.value = t(key);
|
||||
});
|
||||
|
||||
// Custom Event für andere Module
|
||||
window.dispatchEvent(new CustomEvent('languageChanged', {
|
||||
detail: { language: currentLang }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert spezifische UI-Texte (Legacy-Kompatibilität)
|
||||
* Diese Funktion wird von älterem Code verwendet der direkt UI-IDs referenziert
|
||||
*/
|
||||
export function updateUITexts() {
|
||||
// Sidebar
|
||||
const sidebarAreas = document.querySelector('.sidebar h4');
|
||||
if (sidebarAreas) sidebarAreas.textContent = t('sidebar_areas');
|
||||
|
||||
// Breadcrumb / Brand
|
||||
const brandSub = document.querySelector('.brand-sub');
|
||||
if (brandSub) brandSub.textContent = t('brand_sub');
|
||||
|
||||
// Tab Labels
|
||||
const navCompare = document.getElementById('nav-compare');
|
||||
if (navCompare) navCompare.textContent = t('nav_compare');
|
||||
|
||||
const navTiles = document.getElementById('nav-tiles');
|
||||
if (navTiles) navTiles.textContent = t('nav_tiles');
|
||||
|
||||
// Buttons
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
if (uploadBtn) {
|
||||
const textSpan = uploadBtn.querySelector('.btn-text');
|
||||
if (textSpan) textSpan.textContent = t('btn_upload');
|
||||
}
|
||||
|
||||
const deleteBtn = document.getElementById('deleteBtn');
|
||||
if (deleteBtn) {
|
||||
const textSpan = deleteBtn.querySelector('.btn-text');
|
||||
if (textSpan) textSpan.textContent = t('btn_delete');
|
||||
}
|
||||
|
||||
// Card Headers
|
||||
document.querySelectorAll('.card-header').forEach(header => {
|
||||
const icon = header.querySelector('i');
|
||||
const iconHTML = icon ? icon.outerHTML : '';
|
||||
|
||||
// Original / Cleaned Sections
|
||||
if (header.closest('.scan-section')?.classList.contains('original-scan')) {
|
||||
header.innerHTML = iconHTML + ' ' + t('original_scan');
|
||||
} else if (header.closest('.scan-section')?.classList.contains('cleaned-scan')) {
|
||||
header.innerHTML = iconHTML + ' ' + t('cleaned_version');
|
||||
}
|
||||
});
|
||||
|
||||
// Tiles - MC
|
||||
const mcTile = document.querySelector('.mc-tile');
|
||||
if (mcTile) {
|
||||
const title = mcTile.querySelector('.tile-content h3');
|
||||
if (title) title.textContent = t('mc_title');
|
||||
const desc = mcTile.querySelector('.tile-content p');
|
||||
if (desc) desc.textContent = t('mc_desc');
|
||||
}
|
||||
|
||||
// Tiles - Cloze
|
||||
const clozeTile = document.querySelector('.cloze-tile');
|
||||
if (clozeTile) {
|
||||
const title = clozeTile.querySelector('.tile-content h3');
|
||||
if (title) title.textContent = t('cloze_title');
|
||||
const desc = clozeTile.querySelector('.tile-content p');
|
||||
if (desc) desc.textContent = t('cloze_desc');
|
||||
}
|
||||
|
||||
// Tiles - Q&A
|
||||
const qaTile = document.querySelector('.qa-tile');
|
||||
if (qaTile) {
|
||||
const title = qaTile.querySelector('.tile-content h3');
|
||||
if (title) title.textContent = t('qa_title');
|
||||
const desc = qaTile.querySelector('.tile-content p');
|
||||
if (desc) desc.textContent = t('qa_desc');
|
||||
}
|
||||
|
||||
// Tiles - Mindmap
|
||||
const mindmapTile = document.querySelector('.mindmap-tile');
|
||||
if (mindmapTile) {
|
||||
const title = mindmapTile.querySelector('.tile-content h3');
|
||||
if (title) title.textContent = t('mindmap_title');
|
||||
const desc = mindmapTile.querySelector('.tile-content p');
|
||||
if (desc) desc.textContent = t('mindmap_desc');
|
||||
}
|
||||
|
||||
// Footer
|
||||
const imprintLink = document.querySelector('footer a[href*="imprint"]');
|
||||
if (imprintLink) imprintLink.textContent = t('imprint');
|
||||
|
||||
const privacyLink = document.querySelector('footer a[href*="privacy"]');
|
||||
if (privacyLink) privacyLink.textContent = t('privacy');
|
||||
|
||||
const contactLink = document.querySelector('footer a[href*="contact"]');
|
||||
if (contactLink) contactLink.textContent = t('contact');
|
||||
|
||||
// Process Button
|
||||
const fullProcessBtn = document.getElementById('fullProcessBtn');
|
||||
if (fullProcessBtn) {
|
||||
const textSpan = fullProcessBtn.querySelector('.btn-text');
|
||||
if (textSpan) textSpan.textContent = t('btn_full_process');
|
||||
}
|
||||
|
||||
// Status Bar
|
||||
const statusText = document.getElementById('statusText');
|
||||
if (statusText && statusText.textContent === 'Bereit' || statusText?.textContent === 'Ready') {
|
||||
statusText.textContent = t('status_ready');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert das Sprachwahl-UI
|
||||
* @param {string} containerId - ID des Containers für Sprachauswahl
|
||||
*/
|
||||
export function initLanguageSelector(containerId = 'language-selector') {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
// Dropdown erstellen
|
||||
container.innerHTML = `
|
||||
<select id="language-select" class="language-select">
|
||||
${Object.entries(availableLanguages).map(([code, name]) =>
|
||||
`<option value="${code}" ${code === currentLang ? 'selected' : ''}>${name}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
`;
|
||||
|
||||
// Event Handler
|
||||
const select = document.getElementById('language-select');
|
||||
if (select) {
|
||||
select.addEventListener('change', (e) => {
|
||||
setLanguage(e.target.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert einen Status-Text mit Platzhaltern
|
||||
* @param {string} key - Übersetzungsschlüssel
|
||||
* @param {Object} vars - Variablen zum Einsetzen
|
||||
* @returns {string} - Formatierter Text
|
||||
*/
|
||||
export function tFormat(key, vars = {}) {
|
||||
let text = t(key);
|
||||
Object.entries(vars).forEach(([k, v]) => {
|
||||
text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), v);
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
// Re-export für Convenience
|
||||
export { translations, rtlLanguages, defaultLanguage, availableLanguages };
|
||||
Reference in New Issue
Block a user