Compare commits

..

1 Commits

Author SHA1 Message Date
954c4511a7 Fix complete dashboard i18n refresh
Some checks failed
quality / test (3.11) (push) Has been cancelled
quality / test (3.13) (push) Has been cancelled
2026-07-26 22:25:13 +02:00
4 changed files with 284 additions and 9 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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 = `<p class="bad">${escapeHtml(error.message)}</p>`;
@@ -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 = `<p class="warn">Systemübersicht verzögert: ${escapeHtml(error.message)}</p>`;
document.getElementById("status").innerHTML = `<p class="warn">${escapeHtml(translateText(`Systemübersicht verzögert: ${error.message}`))}</p>`;
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 = `<p class="bad">${escapeHtml(error.message)}</p>`;
chips.innerHTML = "";
}
localizeFragment(status);
localizeFragment(chips);
}
function renderDashboardStatus(dashboard) {
@@ -1066,6 +1317,9 @@ function renderDashboardStatus(dashboard) {
`<div class="metric"><strong>Cache-Zeitpunkt</strong>${escapeHtml(cache.updated_at || "noch offen")}</div>`,
].join("");
renderJobQueue(jobs);
localizeFragment(status);
localizeFragment(chips);
localizeFragment(stats);
}
function renderJobQueue(jobs) {
@@ -1086,6 +1340,7 @@ function renderJobQueue(jobs) {
</div>
`).join("")}
` : "";
localizeFragment(jobsBox);
}
async function loadSummaryData() {
@@ -1159,6 +1414,7 @@ function renderActuatorDiscovery() {
options.innerHTML = "";
select.innerHTML = `<option value="">Geräteliste konnte nicht geladen werden: ${escapeHtml(error.message)}</option>`;
}
localizeFragment(document.getElementById("choose"));
}
async function loadActuatorSuggestions() {
@@ -1185,6 +1441,7 @@ async function loadActuatorSuggestions() {
} catch (_) {
box.innerHTML = "";
}
localizeFragment(box);
}
function actuatorGroupLabel(domain) {
@@ -1232,6 +1489,7 @@ function renderActuatorSelect() {
</optgroup>
`),
].join("");
localizeFragment(select);
}
async function loadContextOptions(actuatorId) {
@@ -1277,6 +1535,7 @@ function renderManualContextSelect() {
select.innerHTML = filtered.length
? optionGroups(filtered, selectedNow)
: `<option value="">Keine passenden Vorschläge</option>`;
localizeFragment(select);
}
function parseEntityIds(value) {
@@ -1374,6 +1633,7 @@ function renderConfiguredActuators() {
} catch (error) {
box.textContent = error.message;
}
localizeFragment(box);
}
async function showActuator(actuatorId, evaluationMessage = "") {
@@ -1386,6 +1646,7 @@ async function showActuator(actuatorId, evaluationMessage = "") {
const box = document.getElementById("actuator-detail");
if (!evaluationMessage && cachedDetailHtml.has(actuatorId)) {
box.innerHTML = cachedDetailHtml.get(actuatorId);
localizeFragment(box);
document.getElementById("detail").scrollIntoView({behavior: "smooth", block: "start"});
renderConfiguredActuators();
return;
@@ -1762,12 +2023,14 @@ async function showActuator(actuatorId, evaluationMessage = "") {
${manualAssignment}
`;
box.innerHTML = detailHtml;
cachedDetailHtml.set(actuatorId, detailHtml);
localizeFragment(box);
cachedDetailHtml.set(actuatorId, box.innerHTML);
document.getElementById("detail").scrollIntoView({behavior: "smooth", block: "start"});
renderConfiguredActuators();
} catch (error) {
box.textContent = error.message;
}
localizeFragment(box);
}
function renderActuatorDetailShell(actuatorId) {
@@ -1784,6 +2047,7 @@ function renderActuatorDetailShell(actuatorId) {
<div class="metric"><strong>Kontext</strong>...</div>
</div>
`;
localizeFragment(document.getElementById("actuator-detail"));
}
async function hydrateContextOptions(record) {
@@ -1811,6 +2075,8 @@ async function hydrateContextOptions(record) {
${contextCategories.map(category => `<option value="${escapeHtml(category)}">${escapeHtml(category)}</option>`).join("")}
`;
renderManualContextSelect();
localizeFragment(numericSelect);
localizeFragment(categorySelect);
}
async function hydrateCurrentContextOptions(actuatorId) {
@@ -1912,6 +2178,7 @@ async function simulateActuator(actuatorId) {
} catch (error) {
box.innerHTML = `<p class="bad">${escapeHtml(error.message)}</p>`;
}
localizeFragment(box);
}
async function applySimulationWeights(actuatorId, scenarioId, startDryRun) {

View File

@@ -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 = [