diff --git a/CHANGELOG.md b/CHANGELOG.md index ca56b8f..f9a619d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.7.5 - 2026-07-26 +- Dashboard-Sprachumschaltung übersetzt jetzt auch dynamisch gerenderte + Status-, Discovery-, Detail-, Listen-, Button- und Aufklapptexte. +- Aufklapp-Hinweise (`expand`/`collapse`) kommen nicht mehr fest aus CSS auf + Deutsch, sondern werden pro Sprache gesetzt. +- Detail-Cache wird beim Sprachwechsel geleert, damit keine alten deutschen + HTML-Fragmente in der englischen Oberfläche sichtbar bleiben. + ## 1.7.4 - 2026-07-26 - Dashboard-Sprachumschaltung aktualisiert statische Texte, Labels, Platzhalter und wichtige Laufzeittexte direkt beim Wechsel. diff --git a/addon/config.yaml b/addon/config.yaml index 3638d72..f29fcd4 100644 --- a/addon/config.yaml +++ b/addon/config.yaml @@ -1,5 +1,5 @@ name: SillyHome Next -version: "1.7.4" +version: "1.7.5" slug: sillyhome_next description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren url: http://192.168.6.31:3000/pino/sillyhome-next diff --git a/app/static/index.html b/app/static/index.html index b7b65db..ea4907f 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -65,10 +65,10 @@ .group-panel > summary::-webkit-details-marker { display:none; } details.collapsible > summary::after, .manual-context > summary::after, - .group-panel > summary::after { content:"aufklappen"; color:#9fb0be; font-weight:600; font-size:.86rem; } + .group-panel > summary::after { content:attr(data-closed-label); color:#9fb0be; font-weight:600; font-size:.86rem; } details[open].collapsible > summary::after, .manual-context[open] > summary::after, - .group-panel[open] > summary::after { content:"zuklappen"; } + .group-panel[open] > summary::after { content:attr(data-open-label); } .steps { display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:10px; margin-top:12px; } .step { background:#121922; border:1px solid var(--border); border-radius:8px; padding:10px; } .step-number { display:inline-grid; place-items:center; width:28px; height:28px; border-radius:8px; background:var(--complement); color:#03151d; font-weight:900; margin-bottom:8px; } @@ -345,6 +345,8 @@ const I18N = { unavailable_start: "Startdaten sind gerade nicht verfügbar.", system_loading: "Systemübersicht lädt ...", system_delayed: "Systemübersicht verzögert", + expand: "aufklappen", + collapse: "zuklappen", context_detected: "Kontext erkannt", active_approved: "aktiv freigegeben", shadow_prediction: "Prüfmodus mit Vorhersage", @@ -478,6 +480,8 @@ const I18N = { unavailable_start: "Start data is currently unavailable.", system_loading: "Loading system overview ...", system_delayed: "System overview delayed", + expand: "expand", + collapse: "collapse", context_detected: "Context detected", active_approved: "actively approved", shadow_prediction: "Review mode with prediction", @@ -554,6 +558,182 @@ const I18N = { }, }, }; +const DE_TO_EN_TEXT = new Map(Object.entries({ + "Aktiv / Prüfmodus": "Active / review mode", + "Aktive Gewichtung": "Active weight", + "Aktoren": "Actuators", + "Aktualisieren": "Refresh", + "Aktualisiert.": "Refreshed.", + "Aktuelle Blocker:": "Current blockers:", + "Aktuelle Situation auswerten": "Evaluate current situation", + "Aktuell ist kein gelerntes Handlungsmuster fällig.": "No learned action pattern is due right now.", + "Alle relevanten Vorschläge": "All relevant suggestions", + "Als Gruppe speichern": "Save as group", + "Anderes Gerät": "Another device", + "Annahmen": "Assumptions", + "Anomalien": "Anomalies", + "Anomalien offen": "anomalies open", + "Aktor-Simulation": "Actuator simulation", + "Aufgabenliste": "Task list", + "Automationen neu suchen": "Find automations again", + "Automatische Gewichtsanpassungen": "Automatic weight adjustments", + "Automatische Relevanz": "Automatic relevance", + "Bedienoberfläche.": "user interface.", + "Bereit.": "Ready.", + "Bestes Szenario": "Best scenario", + "Bestes Szenario berechnen": "Calculate best scenario", + "Betriebsart:": "Mode:", + "Beitragsfaktoren": "Contributing factors", + "Cache aktuell mit": "Cache current with", + "Cache wird nach Discovery aufgebaut": "Cache will be built after discovery", + "Cache-Zeitpunkt": "Cache time", + "Cooldown Sekunden": "Cooldown seconds", + "Dashboard bereit in": "Dashboard ready in", + "Dashboard bleibt bedienbar.": "The dashboard remains usable.", + "Dauer:": "Duration:", + "Davon eindeutig geregelt:": "Clearly regulated:", + "Details öffnen": "Open details", + "Diese Kontext-Auswahl speichern": "Save this context selection", + "Discovery-Gruppen": "Discovery groups", + "Entfernen": "Remove", + "Entscheidungsakte": "Decision record", + "Entity-IDs der Gruppe": "Group entity IDs", + "Entity-IDs manuell ergänzen": "Add entity IDs manually", + "Ergebnis:": "Result:", + "Erkannt:": "Detected:", + "Erkannte HA-Automationen:": "Detected HA automations:", + "Erneut versuchen: Aktion im Dashboard noch einmal starten; der nächste Lauf schreibt einen neuen Eintrag.": "Try again: start the dashboard action again; the next run will create a new entry.", + "Freigabe": "Approval", + "Freigabebereit": "Ready for approval", + "Freigabestatus:": "Approval status:", + "Freigabestufe": "Approval stage", + "Für die Simulation müssen zuerst Kontextsensoren ausgewählt sein.": "Select context sensors before running a simulation.", + "Geladene Startdaten": "Loaded start data", + "Gelernte Handlungen": "Learned actions", + "Gelernt / Wartet": "Learned / waiting", + "Gerät auswählen": "Select device", + "Geräteliste konnte nicht geladen werden:": "Device list could not be loaded:", + "Geräteliste lädt im Hintergrund ...": "Device list loading in the background ...", + "Geräteliste wird geladen ...": "Device list is loading ...", + "Gewichtung": "Weight", + "Gewichtung korrigieren": "Adjust weight", + "Gewichtung übernehmen": "Apply weight", + "Gewichtung speichern": "Save weight", + "Gewichtungen speichern": "Save weights", + "Gruppen-Gewicht in %": "Group weight in %", + "Gruppen-Gewichtung": "Group weighting", + "Gruppenname": "Group name", + "HA-Automationen pausieren": "pause HA automations", + "HA-Automationen pausiert lassen": "leave HA automations paused", + "HA-Automationen fortsetzen": "resume HA automations", + "Handlungen": "Actions", + "Job p95": "Job p95", + "Jobs aktiv": "Active jobs", + "Kategorie": "Category", + "Kategorien": "categories", + "Keine Annahmen gespeichert.": "No assumptions stored.", + "Keine aktiven Automation-Konflikte erkannt.": "No active automation conflicts detected.", + "Keine eindeutig passende HA-Automation gefunden.": "No clearly matching HA automation found.", + "Keine gesicherten Punkte gespeichert.": "No confirmed points stored.", + "Keine lokalen Sicherheitsblocker für die aktuelle Vorhersage.": "No local safety blockers for the current prediction.", + "Keine offenen Anomalien fuer diesen Aktor.": "No open anomalies for this actuator.", + "Keine passenden Geräte gefunden": "No matching devices found", + "Keine passenden Vorschläge": "No matching suggestions", + "Keine Simulationsergebnisse.": "No simulation results.", + "Keine Unsicherheit gespeichert.": "No uncertainty stored.", + "Kein Kommentar": "No comment", + "Kein numerischen Hauptsensor verwenden": "Do not use a numeric main sensor", + "Keinen numerischen Hauptsensor verwenden": "Do not use a numeric main sensor", + "Kontext": "Context", + "Kontext selbst festlegen": "Set context manually", + "Kontext wird automatisch analysiert ...": "Context is being analyzed automatically ...", + "Kontext-Entity entfernt.": "Context entity removed.", + "Kontext-Entity ausgewählt.": "context entity selected.", + "Kontextzuordnung:": "Context assignment:", + "Kritisch": "Critical", + "Langsame Jobs": "Slow jobs", + "Langsam:": "Slow:", + "Lädt ...": "Loading ...", + "Lernbereit": "Ready to learn", + "Lernbereite Geräte": "Devices ready to learn", + "Lernen": "Learning", + "Lernfortschritt": "Learning progress", + "Lernsystem": "Learning system", + "Letzte automatische Prüfung:": "Last automatic check:", + "Letztes Training:": "Last training:", + "Manuelle Kontext-Auswahl gespeichert.": "Manual context selection saved.", + "Manuelle Sicherheitssperre aktiv": "Manual safety block active", + "Mindest-Sicherheit in %": "Minimum confidence in %", + "Noch keine Aktoren ausgewählt.": "No actuators selected yet.", + "Noch keine aktuelle Entscheidungsfaktoren berechnet.": "No current decision factors calculated yet.", + "Noch keine automatische Gewichtsanpassung.": "No automatic weight adjustment yet.", + "Noch keine Daten": "No data yet", + "Noch keine Gruppe gespeichert.": "No group saved yet.", + "Noch keine Kontext-Entity ausgewählt.": "No context entity selected yet.", + "Noch keine verwendeten Sensoren oder Zustände für eine Gewichtung ausgewählt.": "No sensors or states selected for weighting yet.", + "Noch kein geeigneter Kontext erkannt. SillyHome prüft bei neuen HA-Daten erneut.": "No suitable context detected yet. SillyHome will check again when new HA data arrives.", + "Noch kein Modell-Snapshot gespeichert.": "No model snapshot saved yet.", + "Numerische Helper": "Numeric helpers", + "Optionaler Haupt-Messsensor": "Optional main measurement sensor", + "Passende Home-Assistant-Automationen": "Matching Home Assistant automations", + "Passende Home-Assistant-Automationen neu geprüft.": "Matching Home Assistant automations checked again.", + "Prüfen": "Review", + "Prüfung läuft ...": "Checking ...", + "Rollback möglich": "Rollback available", + "Sicherheit und manuelles Gegensteuern": "Safety and manual override", + "Sicherheitsprofil speichern": "Save safety profile", + "Sicherheitsprofil gespeichert.": "Safety profile saved.", + "SillyHome parallel aktivieren": "Activate SillyHome in parallel", + "SillyHome stoppen; HA-Automationen pausiert lassen": "Stop SillyHome; leave HA automations paused", + "SillyHome stoppen und pausierte HA-Automationen fortsetzen": "Stop SillyHome and resume paused HA automations", + "SillyHome übernehmen lassen": "Let SillyHome take over", + "SillyHome übernehmen lassen und passende HA-Automationen pausieren": "Let SillyHome take over and pause matching HA automations", + "Simulation läuft ...": "Simulation running ...", + "Simulation übernommen.": "Simulation applied.", + "Simulation übernommen und Dry-run gestartet.": "Simulation applied and dry-run started.", + "Simulationsergebnis ist nicht mehr verfügbar. Bitte neu simulieren.": "Simulation result is no longer available. Please simulate again.", + "Simulierte Gewichtung in %": "Simulated weight in %", + "Simulierter Zustand": "Simulated state", + "Sensor-Gewichtung": "Sensor weighting", + "Sensor-Gewichtung gespeichert.": "Sensor weighting saved.", + "Start:": "Start:", + "Status teilweise verfügbar. Das Dashboard bleibt bedienbar.": "Status partially available. The dashboard remains usable.", + "Status wird geprüft ...": "Checking status ...", + "Statusgrund:": "Status reason:", + "System bereit": "System ready", + "Systemübersicht bereit in": "System overview ready in", + "Systemübersicht langsam:": "System overview slow:", + "Systemübersicht verzögert:": "System overview delayed:", + "Übernehmen + Dry-run starten": "Apply + start dry-run", + "Unsicherheit": "Uncertainty", + "Verwendete Sensoren/Zustände ändern": "Change used sensors/states", + "Vorhersage": "Prediction", + "Vorhersage korrekt": "Prediction correct", + "Vorhersage falsch": "Prediction wrong", + "Was SillyHome aktuell vorhersagt": "What SillyHome currently predicts", + "Wissen": "Knowledge", + "Wähle ein Gerät aus der geladenen Liste oder trage eine Entity-ID ein.": "Select a device from the loaded list or enter an entity ID.", + "Würde nach Sicherheitsprüfung schalten.": "Would switch after safety check.", + "Zeitprofile": "Time profiles", + "Zuordnung": "Assignment", + "Zuordnungssicherheit:": "Assignment confidence:", + "Zurück zur Übersicht": "Back to overview", + "Zustände": "States", + "Zustände vergleichen": "Compare states", + "aktiv": "active", + "aktiv freigegeben": "actively approved", + "automatisch erledigt": "automatic", + "bereit": "ready", + "keine": "none", + "keine Vorhersage": "no prediction", + "läuft/offen": "running/open", + "noch offen": "pending", + "offen": "open", + "pausiert": "paused", + "relevant": "relevant", + "statistisch relevanter Kandidat": "statistically relevant candidate", + "unbekannt": "unknown", +})); let uiLang = localStorage.getItem("sillyhome.ui.language") || "de"; function jumpToSection(target) { @@ -595,6 +775,7 @@ function setLanguage(language) { uiLang = I18N[language] ? language : "de"; localStorage.setItem("sillyhome.ui.language", uiLang); document.documentElement.lang = uiLang; + cachedDetailHtml.clear(); applyStaticTranslations(); syncSettingsView(); renderActuatorSelect(); @@ -630,7 +811,72 @@ function setPlaceholder(selector, key) { if (element) element.placeholder = ui(key); } +function translateText(value) { + if (uiLang !== "en") return value; + const text = String(value ?? ""); + const direct = DE_TO_EN_TEXT.get(text.trim()); + if (direct) { + return text.replace(text.trim(), direct); + } + let translated = text; + const replacements = [...DE_TO_EN_TEXT.entries()] + .filter(([source]) => source.length > 3) + .sort(([left], [right]) => right.length - left.length); + for (const [source, target] of replacements) { + translated = translated.replaceAll(source, target); + } + translated = translated + .replace(/Bereit in (\d+) ms/g, "Ready in $1 ms") + .replace(/Weitere (\d+) Geräte anzeigen/g, "Show $1 more devices") + .replace(/(\d+) von (\d+); Suche oder Typ weiter eingrenzen/g, "$1 of $2; narrow search or type") + .replace(/Szenario (\d+)/g, "Scenario $1") + .replace(/Ø (\d+) %/g, "avg. $1 %") + .replace(/(\d+) Beispiele/g, "$1 examples") + .replace(/(\d+) eindeutig/g, "$1 clear") + .replace(/(\d+) % Beitrag/g, "$1 % contribution") + .replace(/(\d+) % Profilklarheit/g, "$1 % profile clarity") + .replace(/mit (\d+) % Sicherheit/g, "with $1 % confidence") + .replace(/Prüfung ([^:]+):/g, "Check $1:") + .replace(/Keine Zusammenfassung/g, "No summary") + .replace(/Dauer: läuft\/offen/g, "Duration: running/open"); + return translated; +} + +function localizeFragment(root = document) { + for (const summary of root.querySelectorAll?.("details.collapsible > summary, .manual-context > summary, .group-panel > summary") || []) { + summary.dataset.closedLabel = ui("expand"); + summary.dataset.openLabel = ui("collapse"); + } + if (uiLang !== "en") return; + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { + acceptNode(node) { + const parent = node.parentElement; + if (!parent || ["SCRIPT", "STYLE", "TEXTAREA", "INPUT"].includes(parent.tagName)) { + return NodeFilter.FILTER_REJECT; + } + return node.nodeValue.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; + }, + }); + const textNodes = []; + while (walker.nextNode()) textNodes.push(walker.currentNode); + for (const node of textNodes) { + node.nodeValue = translateText(node.nodeValue); + } + for (const input of root.querySelectorAll?.("input[placeholder], textarea[placeholder]") || []) { + input.placeholder = translateText(input.placeholder); + } + for (const optgroup of root.querySelectorAll?.("optgroup[label]") || []) { + optgroup.label = translateText(optgroup.label); + } +} + function applyStaticTranslations() { + document.body.dataset.closedLabel = ui("expand"); + document.body.dataset.openLabel = ui("collapse"); + for (const summary of document.querySelectorAll("details.collapsible > summary, .manual-context > summary, .group-panel > summary")) { + summary.dataset.closedLabel = ui("expand"); + summary.dataset.openLabel = ui("collapse"); + } setText("header .brand p", "tagline"); setText("label[for='section-jump']", "menu"); const navOptions = document.querySelectorAll("#section-jump option"); @@ -693,6 +939,7 @@ function applyStaticTranslations() { setText("#settings h2", "settings"); setText("#settings .muted", "settings_hint"); setText("label[for='language-select']", "language"); + localizeFragment(document); } function formatDateTime(value) { @@ -879,8 +1126,9 @@ async function doLoadOverview() { if (budget) { const loadMs = dashboard._load_elapsed_ms; budget.textContent = loadMs <= DASHBOARD_TIMEOUT_MS - ? `Bereit in ${loadMs} ms` - : `Langsam: ${loadMs} ms`; + ? `Bereit in ${loadMs} ms` + : `Langsam: ${loadMs} ms`; + budget.textContent = translateText(budget.textContent); } } catch (error) { document.getElementById("configured-actuators").innerHTML = `
${escapeHtml(error.message)}
`; @@ -918,10 +1166,11 @@ async function doLoadSystemOverview() { budget.textContent = loadMs <= DASHBOARD_TIMEOUT_MS ? `Systemübersicht bereit in ${loadMs} ms` : `Systemübersicht langsam: ${loadMs} ms`; + budget.textContent = translateText(budget.textContent); } scheduleDashboardExtras(); } catch (error) { - document.getElementById("status").innerHTML = `Systemübersicht verzögert: ${escapeHtml(error.message)}
`; + document.getElementById("status").innerHTML = `${escapeHtml(translateText(`Systemübersicht verzögert: ${error.message}`))}
`; if (budget) budget.textContent = ui("system_delayed"); } } @@ -965,7 +1214,7 @@ async function loadDashboardExtras() { if (reconciliation.status === "fulfilled") { const text = document.getElementById("reconciliation-status"); if (text) { - text.textContent = `Letzte automatische Prüfung: ${formatDateTime(reconciliation.value.last_completed_at)}`; + text.textContent = translateText(`Letzte automatische Prüfung: ${formatDateTime(reconciliation.value.last_completed_at)}`); } } } catch (_) { @@ -1002,6 +1251,8 @@ async function loadStatus() { status.innerHTML = `${escapeHtml(error.message)}
`; chips.innerHTML = ""; } + localizeFragment(status); + localizeFragment(chips); } function renderDashboardStatus(dashboard) { @@ -1066,6 +1317,9 @@ function renderDashboardStatus(dashboard) { `${escapeHtml(error.message)}
`; } + localizeFragment(box); } async function applySimulationWeights(actuatorId, scenarioId, startDryRun) { diff --git a/pyproject.toml b/pyproject.toml index 62650fe..4aef309 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sillyhome-next" -version = "1.7.4" +version = "1.7.5" description = "Lokales Smart-Home-Intelligenzsystem für Home Assistant" requires-python = ">=3.11" dependencies = [