Skip to content

Commit c9a2bb5

Browse files
authored
feat: add the Italian (it) locale across all four translated surfaces (#2062)
* feat: add the Italian (it) locale across all four translated surfaces Italian ships on every surface a language has to cover, so the parity check in tests/src/unit/test_locale_parity.py stays green: - settings UI: 419 messages, 29 tool_groups, 87 tool title/description overrides - custom component: all 93 strings.json keys, placeholders unchanged - add-on and add-on-dev: name + description for all 16 resp. 26 schema keys of the respective flavor The settings UI messages were built by mapping every English key to its translation programmatically, so the key set, the key order and the {placeholders} are identical to the English source by construction. tool_groups and tools have no en.json counterpart — English there comes from the tool definitions — so they are checked against the same static parse that generates the docs (scripts/extract_tools.py), and tool_groups carries exactly the 29 primary tags the settings UI can render. The component catalog includes the 16 common.* strings the options form assembles at runtime. Three of them quote something the reader has to match against another screen, so those stay verbatim: the "Local network" dropdown option in connect_local_lan, and the "HA-MCP File & YAML Tools" entry title in tools_module_not_installed and tools_module_not_loaded. The prose around them is translated, and oauth_select_legacy_mode names this catalog's own authentication-mode label and legacy option. One settings UI message is deliberately identical to English: common.minutes_short, "{count} min", which abbreviates the same way in Italian. Four tool_groups labels equal their English key as well — Assist, Dashboard, HACS and Matter are the words Italian uses too, where es has four such labels and de has eight. Apart from those five, no translated string in any of the four catalogs repeats its English source; meta.dir is "ltr" here as in every locale, English included, because it is a direction flag rather than text. Wording is consistent across surfaces. "skill" stays untranslated as a product term — it names skill_content and ha_get_skill_guide, and de treats it the same way. Where a string's English is byte-identical on two surfaces, its Italian is too: 49 such pairs span the settings UI and the two add-on catalogs, and all 49 match, so a reader who meets the same toggle in the web UI and in the add-on options reads the same sentence both times. AGENTS.md § Translations names the shipped locale codes and is pinned by test_agents_md_lists_every_shipped_locale, so the documented list moves in this commit rather than a separate one. * fix: apply the Italian review round and resolve the decision enum through the catalog The review on #2062 raised 15 items; all are addressed here. Italian wording — 25 values across the four surfaces: - tool_groups rendered Dashboard and Dashboards both as "Dashboard", so the tools tab showed two adjacent sections under one heading: the screenshot tool, then the six dashboard CRUD tools. It was the only duplicate heading in any of the six locales — dashboard is invariable in Italian, so the distinction has to be re-created rather than translated, and the screenshot group now names itself. - visibility.enforce.only_enforced parsed as partial negation, which understated enforce mode on the checkbox label itself. - accessibility.theme.preset said "predefinito" (= default) for "preset", one line below a genuine "default" use of the same word. - visibility.allowlist title and help left a feminine plural adjective agreeing with no noun, against the masculine "ID entità consentiti" in the same section. - "Retain per entity" was a dangling participle in both the field label and the state readout; it names snapshots in both. - backup.bulk.deleted hard-coded the plural where the English carries "(s)". - four noun-noun juxtapositions dropped the preposition the same file uses elsewhere. - accessibility.colors.accent said "risalto" (= highlight). - ha_manage_updates rendered un-skip as "annulla l'esclusione", which presupposes a verb the sentence never uses for skip. - the component's update entity carried an imperative in a slot Home Assistant renders as an entity name. Two of those had to move on more than one surface at once: - "Extra YAML write keys" had two Italian renderings, one per surface. That was the single divergent group; Italian cross-surface divergence is now 0 of 53. - three strings glossed add-on as "app (componente aggiuntivo)" where their English carries no gloss. tools.read_only.help keeps its gloss — its English does say "Apps (add-ons)" — and each of the other three spans the settings UI and an add-on flavor, so the gloss had to go from both sides together or the group would have split. settings.js interpolated body.current_decision straight into policies.pending.already_decided, so Italian read "Questa approvazione è già stata approved"; every other locale renders the same way, since the value is a Decision literal rather than display text. It resolves through the catalog now, falling back to the raw value. All seven catalogs gain policies.pending.decision.approved and .denied — 421 settings UI messages now, where the first commit on this branch describes 419 — and the source baseline is refreshed. test_every_decided_outcome_has_a_catalog_word pins this. It derives the outcome set from the Decision literal itself, so extending the enum without adding a word goes red. Verified by injection: dropping a catalog key, breaking only the catalog lookup while leaving the call in place, and adding an outcome to Decision each turned it red, with the control green before and after and every file restored byte-identical. Two more the review surfaced, folded in rather than deferred: six locale counts in test prose were off by one once Italian made six languages (test_locale_parity, test_config_flow, test_settings_ui_i18n), and the locales README still called tool_groups and tools optional, which test_locale_parity has required exactly for some time. * fix: read the guard's settings.js anchor from the exported constant CodeQL's py/import-and-import-from flagged the new test: it added `import ha_mcp.settings_ui` to build a path while the module was already pulled in with `from ha_mcp.settings_ui import ...` two lines above. _SETTINGS_JS is exported for exactly this and is what test_settings_ui.py already uses, so the guard reads it instead of the file on disk. It now asserts against the JS the server actually serves, after the two injection points are substituted, rather than the template behind it. * fix: apply the second Italian review round Six items from the round-two review: - it.json and the dev add-on YAML: `basata` -> `basato`, the participle agrees with `il componente aggiuntivo`, not with the feminine noun it no longer follows. The two other `basata` sites agree with `scoperta` and stay. - it.json `ha_manage_updates`: `annulla gli aggiornamenti ignorati` read as cancelling the updates. The English source at tools_updates.py:956 is `un-skip`, so `ripristina`. - zh-Hans.json `policies.pending.already_decided`: dropped the ASCII space before `{decision}`, which now interpolates a Chinese word. The nine other placeholder sites in that catalog interpolate Latin or numeric values, where the space is correct, and are unchanged. - locales/README.md: the `tool_groups`/`tools` completeness rule holds for non-English catalogs only. en.json ships both empty on purpose and is the file a new catalog gets copied from, so the exception is worth stating. - test_locale_parity.py: the identical-share comment counted 419 messages, en.json holds 421. The 2.1% share is unchanged. - test_every_decided_outcome_has_a_catalog_word: the key check ran against en.json alone, so a key added there and nowhere else passed while every other language rendered the English enum. It now runs over every catalog. The settings.js anchor was a literal substring that broke on renaming `body` or wrapping the line; it is two regexes now, and the second one reads the value side of the `already_decided` interpolation object so a lookup left bound to an unused local no longer passes. Verified by injection: dropping the key from it.json, adding an outcome to `Decision`, reverting the interpolation to the raw enum, and removing the catalog lookup each turn the test red; renaming `body` and splitting the concatenation keep it green. * test: assert the already-decided copy on the rendered alert The previous guard pinned the decision lookup by matching settings.js source text. Red-teaming that guard found six edits that reintroduce the bug it describes while it stays green: a catalog whose value is still the English enum, a key suffix the concatenation builds but no catalog holds, a single-line commented-out copy of the old code standing in for live code, an interpolation no message consumes, a value outside `Decision`, and a second call site. Both anchors were `re.search` first-matches over the whole file and were never required to be the same call site. Two plausible refactors — lowercasing the catalog word, adding a second interpolated value whose expression contains an object literal — turned it red with a message asserting the resolution was gone while it sat untouched two lines above. Source-text matching cannot carry that claim, so the check moves to the behaviour: - `TestAlreadyDecidedCopy` in test_settings_ui_js_behavior.py drives `policyDecide` against a stubbed 409 for every (non-English catalog, `Decision` outcome) pair and asserts the alert holds the catalog word and not the enum. Cases come from `get_args(Decision)`, so a new outcome arrives as new cases. - `test_every_decided_outcome_has_a_catalog_word` keeps the type-derived half — every outcome has a key in every catalog — and now also rejects a non-English value spelled the way the backend spells it. It runs without node, which the JSDOM tests need. Verified by injection against both: the English-value catalog reds both, the suffix drift, the commented-out code, the unconsumed interpolation and the removed lookup red the behaviour test, dropping a key or extending `Decision` reds the catalog test. The two refactors that produced false reds are green. A second call site rendering the same sentence stays uncovered — the behaviour test drives the handler that exists. * fix: apply the third Italian review round The CJK spacing class is six strings, not four. `{operation}` is catalog-derived exactly as `{title}` is: all four `policyPut` call sites pass a `t()` result as `opLabel`, and `common.operation_failed` and `policies.errors.conflict` have no other consumer. The stray ASCII space is gone from those two and from the four `tools.aria.*` values. Eighteen zh-Hans values still put a space between a placeholder and a CJK character, and all of them interpolate Latin or numeric values — `{operator}` is the raw op literal, not the translated `policies.operators.*` word — where the space is correct typography. A 503 on the decide path no longer splices the server's English paragraph into a translated clause. `policyDecide` now answers it the way `policyLoadPending` already does: `policies.pending.disabled` when the flag is known off, and otherwise the server diagnostic on its own, since it names which of the remaining causes applied. `TestDecideUnavailableCopy` pins both branches in an Italian UI. The 503 body moves to `POLICY_UNAVAILABLE_MESSAGE` so the guard pins the string the server sends rather than a hand-copied one; the first draft of that fixture had already dropped the middle sentence, which is the one naming the three causes. locales/README.md: the claim that CI rejects a catalog missing a key was false for `messages` and contradicted AGENTS.md. English is the per-key fallback, and `_untranslated_keys` counts an omission against the share ceiling rather than failing on it. The `tools` bullet now names the two value rules that gate it beyond key exactness, the closing paragraph states the ceiling for both sections rather than for `messages` alone, and the worked example names a code no catalog ships. Guards: - The already-decided alert is compared as a whole sentence. Containment could not fail on catalog content, because the host it searches is that same catalog value with the word already substituted. - The catalog-word check is stripped and casefolded and rejects blank values, so `Approved`, ` approved ` and `` no longer read as translated. - It runs over `PredicateOp` as well, which `displayPredicate` resolves the same way with the raw enum as its fallback. - `_already_decided_cases` asserts both discovered sets are non-empty. - Failure messages name the on-disk catalog file rather than a lowercased `zh-hans.json`. `harness.mjs` prefixes a rejected invoke like a synchronous throw so `_assert_clean_init` can see it. Unprefixed, a test whose invoke rejects recorded nothing and passed its own negative assertions. Verified by injection, each against a control where one exists: the harness prefix reds a negative-only test whose invoke was renamed while the unprefixed push leaves it green; the whole-sentence comparison reds a drifted host key where containment stays green; the casefolded comparison reds a sentence-cased `Approved` that the exact comparison passed. The blank check, the dropped `PredicateOp` key, the two emptied discovery sets and a reverted 503 branch each red their own guard. * fix: apply the fourth Italian review round Four findings from the review of the third round: - policyDecide synthesised `{error: 'HTTP <status>'}` when the body did not parse, which is truthy, so the 503 branch alerted that bare token and the translated `policies.pending.unavailable` line could not render from it. The body is now read into an empty object and only a parsed string `error` counts as a server message. Dropping the stand-in also moves the generic branch, which read the same value: `detail` is now `serverError || resp.statusText || 'HTTP ' + resp.status`, so an unparsable non-503 shows the reason phrase where it previously showed `HTTP <status>`, and reproduces the old string only where `statusText` is empty. Covered by a new test that drives a non-JSON 503 and asserts the Italian line. - `_assert_catalog_words` described a failure mode that does not happen: `build_payload` merges English under the selected locale, so a key one catalog omits renders English's value, not the raw enum. Docstring and assertion message now say that, and the operator test says it too. - The 503 fixture in `test_pending_list_shows_server_message_when_feature_on_but_503` was still hand-written and had drifted from the server string; its own assertion passed because of the drift. Both fixtures now read `POLICY_UNAVAILABLE_MESSAGE` through one module-level helper. - The comment on `POLICY_UNAVAILABLE_MESSAGE` named three routes; the constant covers five, and the stub handlers that send it are not sidecar-only — a main server without an approval queue serves them too.
1 parent 1786ad9 commit c9a2bb5

19 files changed

Lines changed: 1915 additions & 53 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,7 @@ fully validate a component change before merge.
713713

714714
A language ships on all four surfaces or not at all —
715715
`tests/src/unit/test_locale_parity.py` enforces it. One Home Assistant language
716-
code (`de`, `es`, `fr`, `ru`, `zh-Hans`) names every file:
716+
code (`de`, `es`, `fr`, `it`, `ru`, `zh-Hans`) names every file:
717717
`src/ha_mcp/settings_ui/locales/<code>.json`,
718718
`custom_components/ha_mcp_tools/translations/<code>.json`, and
719719
`homeassistant-addon{,-dev}/translations/<code>.yaml`.

custom_components/ha_mcp_tools/translations/it.json

Lines changed: 161 additions & 0 deletions
Large diffs are not rendered by default.

homeassistant-addon-dev/translations/it.yaml

Lines changed: 312 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
---
2+
configuration:
3+
backup_hint:
4+
name: Livello del suggerimento di snapshot completo di HA (rivolto all'LLM)
5+
description: |-
6+
Regola il tono del suggerimento che l'LLM vede nella descrizione dello
7+
strumento `ha_manage_backup(scope='snapshot')` su quando proporre un
8+
backup completo di Home Assistant prima di operazioni rischiose (per
9+
esempio eliminazioni massive di dispositivi). Riguarda SOLTANTO quella
10+
frase rivolta all'LLM.
11+
12+
Per gli snapshot automatici a ogni modifica di automazioni / script /
13+
helper / ecc., usa l'opzione separata **Attiva il backup automatico
14+
delle modifiche** qui sotto.
15+
secret_path:
16+
name: Percorso segreto personalizzato
17+
description: |
18+
Percorso HTTP personalizzato facoltativo per il server MCP. Lascialo vuoto per usare il percorso sicuro generato automaticamente.
19+
enable_tool_search:
20+
name: Attiva la ricerca degli strumenti
21+
description: >-
22+
Sostituisce il catalogo completo degli strumenti con una scoperta basata
23+
sulla ricerca. Riduce il contesto a riposo da circa 46K a 5K token. ⚠️ NON
24+
attivarla se usi Claude in modalità Sonnet o Opus: quei modelli hanno una
25+
propria ricerca di strumenti / strumenti differiti integrata, che va in
26+
conflitto con la nostra. Per usare la ricerca di ha-mcp con Claude,
27+
disattiva prima quella integrata di Claude; altrimenti lascia questa
28+
opzione disattivata. Usala solo con LLM privi di strumenti differiti
29+
nativi (per esempio Claude Haiku, modelli locali compatibili con OpenAI) o
30+
con finestre di contesto più piccole. Gli strumenti si trovano tramite
31+
ha_search_tools e vengono eseguiti tramite proxy suddivisi per categoria
32+
(lettura/scrittura/eliminazione). Richiede il riavvio del componente
33+
aggiuntivo per avere effetto; poi riconnetti o aggiorna il server MCP nel
34+
tuo client di IA in modo che ricarichi l'elenco degli strumenti. Riavviare
35+
solo il componente aggiuntivo NON aggiorna l'elenco degli strumenti
36+
memorizzato nella cache del client; vale per qualsiasi impostazione
37+
modificata qui.
38+
enable_tool_security_policies:
39+
name: Attiva i criteri di sicurezza degli strumenti (avanzato)
40+
description: >-
41+
Subordina all'approvazione dell'utente le chiamate agli strumenti ad alto
42+
rischio (controllo di serrature e allarmi, scrittura di automazioni,
43+
ecc.). Quando viene chiamato uno strumento controllato, l'agente chiede
44+
all'utente di aprire la scheda Criteri di sicurezza degli strumenti
45+
nell'interfaccia web e di fare clic su Approva prima che la chiamata
46+
prosegua. Le regole per singolo strumento, con condizioni facoltative
47+
sugli argomenti, si configurano nella scheda Criteri di sicurezza degli
48+
strumenti. Disattivato per impostazione predefinita. Richiede il riavvio
49+
per avere effetto.
50+
read_only_mode:
51+
name: Modalità di sola lettura
52+
description: >-
53+
Disattiva tutti gli strumenti di scrittura e impedisce a qualunque
54+
strumento di eseguire chiamate di scrittura o distruttive. Gli strumenti
55+
misti di lettura/scrittura (backup, componenti aggiuntivi,
56+
preferenze energetiche, pipeline vocali e la modalità codice quando è
57+
attiva) restano disponibili, ma con le operazioni di scrittura bloccate.
58+
È la stessa opzione della scheda Strumenti nell'interfaccia web.
59+
Disattivata per impostazione predefinita. Richiede il riavvio per avere
60+
effetto.
61+
enable_mandatory_bps:
62+
name: Allega le skill di buone pratiche alle scritture
63+
description: >-
64+
Interruttore principale della funzione che consegna il contenuto delle
65+
skill agli strumenti di scrittura (issue #1182). Quando è attivo
66+
(impostazione predefinita), i sei strumenti di scrittura della
67+
configurazione (automazioni, script, scene, helper, dashboard, YAML
68+
grezzo) allegano i file di riferimento canonici sulle buone pratiche di
69+
Home Assistant sotto `skill_content` a ogni scrittura riuscita e
70+
incorporano automaticamente le sezioni di riferimento citate dagli avvisi
71+
sulle buone pratiche. Ogni strumento espone inoltre un parametro
72+
`MandatoryBPS` per singola chiamata, che l'agente può impostare su false
73+
nelle chiamate successive una volta ottenuti i contenuti. Quando questo
74+
interruttore principale è disattivo NON viene inviato alcun
75+
skill_content, a prescindere dal parametro per singola chiamata o dagli
76+
avvisi sulle buone pratiche. Si consiglia di lasciarlo attivo come prima
77+
scelta; disattivalo solo per modelli con finestre di contesto molto
78+
piccole. Disattivarlo può peggiorare l'accuratezza delle scritture.
79+
Richiede il riavvio per avere effetto.
80+
enable_strict_mandatory_bps:
81+
name: Modalità rigorosa per le buone pratiche
82+
description: >-
83+
Modalità rigorosa: impedisce al client di usare lo strumento finché non è
84+
in grado di dimostrare di aver letto le buone pratiche. Mentre è attiva,
85+
i sei strumenti di scrittura soggetti alle buone pratiche (automazioni,
86+
script, scene, helper, dashboard, YAML grezzo) sono bloccati e
87+
restituiscono un errore che invita il client a leggere la skill sulle
88+
buone pratiche tramite ha_get_skill_guide e a restituire la chiave di
89+
conferma che ottiene lì. Mentre è attiva, lo strumento ha_get_skill_guide
90+
resta forzatamente abilitato: è l'unico a pubblicare la chiave di
91+
conferma. È figlia dell'opzione «Allega le skill di buone pratiche alle
92+
scritture» qui sopra e resta inerte finché quella è disattivata. Richiede
93+
il riavvio per avere effetto.
94+
enable_auto_backup:
95+
name: Attiva il backup automatico delle modifiche
96+
description: >-
97+
Cattura uno snapshot per entità prima di ogni chiamata coperta a uno
98+
strumento MCP di scrittura o distruttivo (automazione, script, scena,
99+
helper, dashboard, etichetta, categoria, gruppo, zona, area, calendario,
100+
cose da fare, entità, integrazione e i relativi strumenti di rimozione).
101+
Gli snapshot vengono salvati come file YAML in /data/ha_mcp_backups/
102+
(modificabile con HAMCP_BACKUP_DIR) e si elencano, ripristinano o
103+
eliminano dalla scheda Backup dell'interfaccia web delle impostazioni
104+
oppure con ha_manage_backup(scope='edits', ...). Funziona secondo il
105+
criterio del massimo impegno: gli errori registrano un WARNING ma non
106+
bloccano mai la scrittura sottostante. Attivo per impostazione
107+
predefinita; togli la spunta per rinunciarvi. Richiede il riavvio per
108+
avere effetto.
109+
auto_backup_throttle_minutes:
110+
name: Limitazione del backup automatico (minuti)
111+
description: >-
112+
Finestra di limitazione per entità. 0 (impostazione predefinita) cattura
113+
uno snapshot a ogni scrittura coperta. N>0 cattura al massimo uno snapshot
114+
ogni N minuti per entità. Intervallo 0–1440.
115+
auto_backup_retain_per_entity:
116+
name: Conservazione dei backup automatici (per entità)
117+
description: >-
118+
Numero massimo di snapshot conservati per entità. Gli snapshot più vecchi
119+
oltre questo limite vengono ruotati a ogni cattura riuscita. Valore
120+
predefinito 100, intervallo 1–10000.
121+
enable_snapshot_delete:
122+
name: Consenti l'eliminazione degli snapshot
123+
description: >-
124+
Consente a ha_manage_backup di eliminare gli archivi tarball degli
125+
snapshot completi di HA (scope='snapshot', action='delete'). Disattivato
126+
per impostazione predefinita: uno snapshot può essere l'ultimo punto di
127+
ripristino dopo una modifica sbagliata, quindi deve essere una persona ad
128+
autorizzarlo qui. Anche quando è attivo, i backup pianificati, lo snapshot
129+
più recente rimasto e tutto ciò che è più nuovo della soglia di anzianità
130+
qui sotto restano protetti.
131+
snapshot_delete_min_age_days:
132+
name: Età minima dello snapshot per l'eliminazione (giorni)
133+
description: >-
134+
Uno snapshot deve avere almeno questa anzianità per poter essere
135+
eliminato. Intervallo 0–365; 0 disattiva la soglia (restano comunque
136+
valide le protezioni dello snapshot più recente e dei backup pianificati).
137+
Valore predefinito 7.
138+
verify_ssl:
139+
name: Verifica il certificato TLS
140+
description: >-
141+
Verifica il certificato TLS del server Home Assistant. Il componente
142+
aggiuntivo si collega tramite il proxy del Supervisor, quindi di norma
143+
questo non ha alcun effetto e conviene lasciarlo attivo. Disattivalo solo
144+
se hai riconfigurato il componente aggiuntivo per parlare con HA tramite
145+
un nome host HTTPS pubblico con certificato autofirmato o con nome host
146+
non corrispondente. Disattivarlo indebolisce la sicurezza del trasporto:
147+
lascialo attivo a meno che tu non sappia di averne bisogno. Richiede il
148+
riavvio per avere effetto.
149+
tool_search_max_results:
150+
name: Numero massimo di risultati della ricerca degli strumenti
151+
description: >-
152+
Numero massimo di strumenti restituiti da ha_search_tools quando la
153+
ricerca degli strumenti è attiva. Valori bassi (2-3) risparmiano token di
154+
contesto ma possono escludere strumenti pertinenti. Intervallo: 2-10.
155+
Richiede il riavvio.
156+
disabled_tools:
157+
name: Strumenti disattivati (separati da virgole)
158+
description: >-
159+
Gli strumenti elencati qui vengono forzatamente disattivati e bloccati
160+
nell'interfaccia web delle impostazioni (scheda Strumenti): non possono
161+
essere riattivati lì senza prima togliere questa opzione. Un piccolo
162+
insieme di strumenti obbligatori (ha_search, ha_get_overview,
163+
ha_get_state, ha_report_issue, ha_manage_backup) non può essere
164+
disattivato e continua a funzionare anche se elencato qui.
165+
ha_get_skill_guide può essere disattivato solo mentre la modalità rigorosa
166+
per le buone pratiche (enable_strict_mandatory_bps) è disattivata. Nomi
167+
degli strumenti separati da virgole (per esempio
168+
ha_call_event,ha_eval_template).
169+
pinned_tools:
170+
name: Strumenti fissati (separati da virgole)
171+
description: >-
172+
Gli strumenti elencati qui restano fissati in modo bloccato: compaiono in
173+
cima alla scheda Strumenti e non possono essere sganciati
174+
dall'interfaccia web delle impostazioni senza prima togliere questa
175+
opzione. Utile in combinazione con la ricerca degli strumenti. Nomi degli
176+
strumenti separati da virgole.

src/ha_mcp/settings_ui/__init__.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,23 @@
188188
).replace("__HA_MCP_JS__", _SETTINGS_JS)
189189

190190

191+
# Body of the 503 the five live-approval routes — pending, approve, deny,
192+
# tool-schema and value-source — answer with whenever these stub handlers are
193+
# in use: the stdio sidecar, but equally a main server with no approval queue
194+
# (feature off, or the policy package failed to import). Config GET/PUT are
195+
# served for real even then. Module-level so the guard on the settings-UI copy
196+
# pins the string the server actually sends, rather than a hand-copied
197+
# paraphrase that drifts away from it.
198+
POLICY_UNAVAILABLE_MESSAGE = (
199+
"Tool security policies live approvals are not active. "
200+
"Either the feature is turned off in App (add-on) config, the "
201+
"settings UI is running in stdio-sidecar mode, or the "
202+
"policy package failed to import at startup. Check the "
203+
"App (add-on) log for ImportError / RuntimeError details if you "
204+
"expected gating to be on."
205+
)
206+
207+
191208
def _build_stub_policy_handlers(*, data_dir: Path) -> dict[str, Any]:
192209
"""Sidecar variant of the tool security policies handlers.
193210
@@ -243,19 +260,7 @@ async def unavailable(_: Request) -> JSONResponse:
243260
# Either way, point users at the addon log for the real reason
244261
# (a startup ImportError on the policy package surfaces here as
245262
# the same 503 with a "ModuleNotFoundError" in the log).
246-
return JSONResponse(
247-
{
248-
"error": (
249-
"Tool security policies live approvals are not active. "
250-
"Either the feature is turned off in App (add-on) config, the "
251-
"settings UI is running in stdio-sidecar mode, or the "
252-
"policy package failed to import at startup. Check the "
253-
"App (add-on) log for ImportError / RuntimeError details if you "
254-
"expected gating to be on."
255-
)
256-
},
257-
status_code=503,
258-
)
263+
return JSONResponse({"error": POLICY_UNAVAILABLE_MESSAGE}, status_code=503)
259264

260265
return {
261266
"policy_get_config": get_config,

src/ha_mcp/settings_ui/locales/de.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,8 @@
353353
"policies.pending.unavailable": "Live-Genehmigungen nicht verfügbar. Überprüfe das Add-on-Protokoll.",
354354
"policies.pending.expires": "Läuft ab: {time}",
355355
"policies.pending.already_decided": "Diese Genehmigung war bereits {decision}, möglicherweise von einem anderen Tab oder einer anderen Sitzung.",
356+
"policies.pending.decision.approved": "genehmigt",
357+
"policies.pending.decision.denied": "abgelehnt",
356358
"policies.pending.invalid_token": "Dieses Genehmigungs-Token ist nicht mehr gültig (bereits verbraucht oder abgelaufen).",
357359
"policies.pending.action_failed": "Genehmigungs-Aktion fehlgeschlagen: {detail}",
358360
"visibility.errors.reach_server": "Server konnte nicht erreicht werden: {message}",

src/ha_mcp/settings_ui/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,8 @@
353353
"policies.pending.unavailable": "Live approvals unavailable. Check the App (add-on) log.",
354354
"policies.pending.expires": "Expires: {time}",
355355
"policies.pending.already_decided": "This approval was already {decision}, possibly by another tab or session.",
356+
"policies.pending.decision.approved": "approved",
357+
"policies.pending.decision.denied": "denied",
356358
"policies.pending.invalid_token": "This approval token is no longer valid (already consumed or expired).",
357359
"policies.pending.action_failed": "Approval action failed: {detail}",
358360
"visibility.errors.reach_server": "Could not reach the server: {message}",

src/ha_mcp/settings_ui/locales/es.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,8 @@
353353
"policies.pending.unavailable": "Las aprobaciones en vivo no están disponibles. Revisa el registro de la app (complemento).",
354354
"policies.pending.expires": "Caduca: {time}",
355355
"policies.pending.already_decided": "Esta aprobación ya estaba {decision}, posiblemente desde otra pestaña o sesión.",
356+
"policies.pending.decision.approved": "aprobada",
357+
"policies.pending.decision.denied": "denegada",
356358
"policies.pending.invalid_token": "Este token de aprobación ya no es válido (ya se ha consumido o ha caducado).",
357359
"policies.pending.action_failed": "Ha fallado la acción de aprobación: {detail}",
358360
"visibility.errors.reach_server": "No se ha podido contactar con el servidor: {message}",

src/ha_mcp/settings_ui/locales/fr.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,8 @@
353353
"policies.pending.unavailable": "Approbations en direct indisponibles. Vérifie le journal de l'App (add-on).",
354354
"policies.pending.expires": "Expire : {time}",
355355
"policies.pending.already_decided": "Cette approbation était déjà {decision}, peut-être par un autre onglet ou une autre session.",
356+
"policies.pending.decision.approved": "approuvée",
357+
"policies.pending.decision.denied": "refusée",
356358
"policies.pending.invalid_token": "Ce jeton d'approbation n'est plus valide (déjà consommé ou expiré).",
357359
"policies.pending.action_failed": "L'action d'approbation a échoué : {detail}",
358360
"visibility.errors.reach_server": "Impossible d'atteindre le serveur : {message}",

0 commit comments

Comments
 (0)