Skip to content

Commit 083e851

Browse files
Patch76claude
andauthored
feat: add the Dutch (nl) locale across all four translated surfaces (#2145)
* feat: add the Dutch (nl) locale across all four translated surfaces Dutch is the largest installation base Home Assistant reports that ha-mcp does not ship a catalog for. Home Assistant publishes no language statistic — the analytics integration collects a country code, not a locale — but the Netherlands is the fourth-largest country by active installations, and every language ahead of Dutch under a country-to-language reading already ships here. The two authored catalogs carry what a catalog owes before it merges: a word for each decided Decision outcome and each PredicateOp operator, which render inside otherwise translated sentences. The remaining 723 strings are the post-merge sync's to fill. Each authored surface also carries one key whose English addresses the reader. translate_locales.py samples those per surface to tell the engine which register to imitate, so they decide the register of everything the sync writes — and a surface without one falls back to the engine's own default. Home Assistant's own Dutch frontend is informal throughout (in the shipped 20260729.5 catalogs, 608 of 7839 strings use je/jij/jouw and none use u/uw), so both sampled strings are written that way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(i18n): carry the 409 host sentence and the long exists label in nl TestAlreadyDecidedCopy drives the real 409 handler under every non-English catalog and compares the whole rendered alert, so a catalog that carries the Decision word but not the sentence it is interpolated into fails with a KeyError on policies.pending.already_decided. Whole-sentence rather than containment is deliberate — the host falling back to English still reads as translated copy around an English clause. policies.operators.exists_long is the second gap: settings.js renders it for the exists option of the condition-editor dropdown, but it is UI-only rather than a PredicateOp member, so the enum-derived checks never ask for it and the Dutch dropdown would show the English label until the sync fills it. Both were invisible to a local run: the JS behaviour tests skip unless tests/js/ has its npm dependencies installed. With them installed the file runs 187 passed, 0 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(component): open pending component version 1.3.3 The Dutch catalog adds a file under custom_components/ha_mcp_tools, and master is level with the released stable again: the HACS mirror published v1.3.2 as its latest stable release, so the 1.3.2 this branch used to open is now shipped. Without a bump the PR-time Component Version Gate reads manifest == released stable and fails, and merged that way the change would ride a number whose tag already exists. Patch level: this adds no service and no argument the server depends on, so MIN_COMPONENT_VERSION stays at 1.2.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(i18n): pin the component surface's register anchor test_every_shipped_catalog_gets_reader_addressing_samples only ever ran over the settings catalogs: _TRANSLATED_LOCALES globs the settings locales directory, and every COMPONENT_DIR reference in that file is monkeypatched to a fixture. The component surface samples its own catalog (_surface_catalogs, deliberately, because the two authored surfaces may use different registers), so nothing shipped was checking the one that ships. The margin differs sharply between the two. A settings catalog draws its register sample from hundreds of keys, so one English string losing its second person costs it a candidate. A component catalog starts empty and is filled a key at a time, so early on the whole surface can rest on a single shared key -- nl today is exactly that, common.oauth_not_serving. Drop the second person from that key's English and _style_sample_keys returns nothing, the engine is told nothing about how Dutch addresses its reader and falls back to its own register for every later string, and the only trace is a line on stderr inside an unattended workflow run. Mirrored parametrization over the shipped component catalogs, plus the same empty-glob guard the settings check carries. Two deliberate choices: the list is derived from the production COMPONENT_DIR rather than a rebuilt path, since a catalog the list misses is a catalog the check silently never runs on; and the test goes through _surface_catalogs rather than loading the JSON itself, because that it reads the component catalog rather than the settings one is the property at issue. Verified by injection rather than by reading: removing "you" from the English common.oauth_not_serving fails nl alone and leaves the six catalogs carrying 93 keys green, so the check discriminates the single-key case instead of being globally sensitive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: name exists_long and the per-surface cost of an empty component catalog Two things this PR had to discover that neither document said. policies.operators.exists_long is the condition editor's own dropdown label for the exists option, rendered from settings.js with an English fallback. It is UI-only rather than a PredicateOp member, so the enum-derived check iterates get_args(PredicateOp) and never asks for it, and the documented rule -- "every PredicateOp operator needs a translated word" -- does not lead an author there. A catalog without it reads English in that dropdown until the sync fills it. Nothing goes red. "The component catalog may start empty" is true and cheap to read as free. Each surface samples its own catalog for the address register the engine imitates, so a component catalog left at a key or two rests entirely on whichever of them addresses the reader, and losing that costs the register for every later string of that language. The README states the second one as an instruction rather than leaving it implied, because the boundary is sharper than it reads. Measured while proving the new test: a synthetic component catalog holding a single key whose English does not address the reader fails it, while a truly empty catalog is excluded by design and never parametrized. So the check starts biting at the first authored key, and most component strings do not address the reader -- an author who fills that catalog from the top of the file lands on an anchorless key and gets a red check for a catalog that is otherwise fine. One sentence naming the choice costs less than rediscovering it from the failure. One addition each to AGENTS.md § Translations and the settings-locales README's "Adding a language", the two places an author looks when adding a language. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: skip blank translations when sampling a catalog's address register `_style_sample_keys` decided a key was sampleable from its presence in the translated catalog and the second person in its English source, never looking at the translated value. A key committed as `""` therefore reached `_style_samples`, which renders `translated[key]` verbatim, and spent one of the three sample slots on a pair whose target side is empty. Nothing else rejects that value. An engine answer of this shape is refused by `_validate`, but a hand-committed one is only type-checked (`_validate_string_map` in `src/ha_mcp/settings_ui/_i18n.py`), and the parity ceilings count a key untranslated only when it equals the English or is absent (`_untranslated_keys` in `tests/src/unit/test_locale_parity.py`), so `""` reads there as translated. The cost is largest exactly where the margin is smallest: a component catalog whose register rests on one key would hand the engine an empty pair as the register for every later string of that language, and say so on stderr only. Requiring a non-blank value in the sampler protects the unattended sync run itself and makes the shipped-catalog checks fail on a blank-anchored catalog rather than pass it. Verified by injection: blanking `nl`'s single component key fails `test_every_shipped_component_catalog_gets_reader_addressing_samples[nl]` with this guard and passes without it, and the new parametrized regression case goes red when the guard is removed. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 20199b6 commit 083e851

11 files changed

Lines changed: 504 additions & 5 deletions

File tree

AGENTS.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,7 @@ identity holds by construction.
755755

756756
A language ships on all four surfaces or not at all —
757757
`tests/src/unit/test_locale_parity.py` enforces it. One Home Assistant language
758-
code (`de`, `es`, `fr`, `it`, `ru`, `zh-Hans`) names every file:
758+
code (`de`, `es`, `fr`, `it`, `nl`, `ru`, `zh-Hans`) names every file:
759759
`src/ha_mcp/settings_ui/locales/<code>.json`,
760760
`custom_components/ha_mcp_tools/translations/<code>.json`, and
761761
`homeassistant-addon{,-dev}/translations/<code>.yaml`.
@@ -773,6 +773,15 @@ spells the backend literal counts as untranslated), so does
773773
into, and at least one translated key must have English that addresses the
774774
reader in the second person, which is where `scripts/translate_locales.py`
775775
reads the catalog's address register.
776+
`policies.operators.exists_long` is the trap in that list: the condition editor
777+
renders it as its own dropdown label, but it is UI-only rather than a
778+
`PredicateOp` member, so no enum-derived check asks for it and a catalog
779+
without it reads English there until the sync fills it. Each surface reads that
780+
register from its own catalog, so a component catalog left at a key or two
781+
rests on whichever of them addresses the reader — losing it costs the engine
782+
the register for every later string of that language and says so only on
783+
stderr, which is why
784+
`test_every_shipped_component_catalog_gets_reader_addressing_samples` pins it.
776785
`src/ha_mcp/settings_ui/locales/README.md` names the tests — including the one
777786
that skips locally until `tests/js/` has its npm dependencies.
778787

custom_components/ha_mcp_tools/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
# in CI. The
2626
# capability negotiation — not this version — gates each WS command (see
2727
# ``websocket_api.CAPABILITIES``).
28-
COMPONENT_VERSION = "1.3.2"
28+
COMPONENT_VERSION = "1.3.3"
2929

3030
# Config-entry discriminator (``entry.data[CONF_ENTRY_TYPE]``). A missing value
3131
# means "tools" so the pre-existing services entry keeps working across the

custom_components/ha_mcp_tools/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@
2222
"requirements": [
2323
"ruamel.yaml>=0.18.0"
2424
],
25-
"version": "1.3.2"
25+
"version": "1.3.3"
2626
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"common": {
3+
"oauth_not_serving": "Legacy OAuth levert deze nog niet — start Home Assistant opnieuw op wanneer je daarom wordt gevraagd, om ze te activeren."
4+
}
5+
}
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
---
2+
# GENERATED FILE — do not edit. Translations live in
3+
# src/ha_mcp/settings_ui/locales/nl.json (keys addon.*, features.*,
4+
# addon_dev.*); regenerate with: python scripts/generate_locales.py
5+
configuration:
6+
backup_hint:
7+
name: Full-HA snapshot suggestion level (LLM-facing)
8+
description: 'Tunes the wording of the hint the LLM sees in the
9+
10+
`ha_manage_backup(scope=''snapshot'')` tool description for when to
11+
12+
suggest a full Home Assistant tarball backup before risky operations
13+
14+
(e.g. mass device deletes). Affects ONLY this one LLM-facing prompt
15+
16+
sentence.
17+
18+
19+
For per-edit automatic snapshots of automations / scripts / helpers /
20+
21+
etc., use the separate **Enable auto-backup of edits** toggle below.'
22+
secret_path:
23+
name: Secret path override
24+
description: 'Optional custom HTTP path for the MCP server. Leave empty to use
25+
the auto-generated secure path.
26+
27+
'
28+
enable_tool_search:
29+
name: Enable tool search
30+
description: 'Replace the full tool catalog with search-based discovery. Reduces
31+
idle context by roughly 90%, to about 5K tokens. ⚠️ Do NOT enable this in clients
32+
with their own built-in tool search / deferred tools (claude.ai, Claude Desktop,
33+
Claude Code) — the two search layers conflict, and the client''s built-in tool
34+
search is the better choice there; leave this off. Whether tools are deferred
35+
depends on your client and model combination: use this when your setup loads
36+
the full catalog up front — models without native deferred tools (e.g. Gemini,
37+
OpenAI-compatible local models, Claude Haiku), clients that inline all tool
38+
schemas regardless of model (e.g. GitHub Copilot CLI), or smaller context windows.
39+
Some Codex models and ChatGPT include deferred tools too — check your client/model
40+
directly to confirm its features so you don''t leave this enabled unnecessarily.
41+
Tools are found via ha_search_tools and executed via categorized proxies (read/write/delete).
42+
Requires restart to take effect.'
43+
enable_tool_security_policies:
44+
name: Enable Tool Security Policies (advanced)
45+
description: Gate high-stakes tool calls (lock/alarm control, automation writes,
46+
etc.) behind user approval. When a guarded tool is called, the agent tells the
47+
user to open the Tool Security Policies tab in the web UI and click Approve
48+
before the call proceeds. Per-tool rules with optional argument conditions are
49+
configured in the Tool Security Policies tab. Off by default. Requires restart
50+
to take effect.
51+
read_only_mode:
52+
name: Read Only Mode
53+
description: Toggles all write tools off, and removes ability for tools to make
54+
any write or destructive calls. Mixed read/write tools (backups, add-ons, energy
55+
preferences, voice pipelines, and code mode when enabled) stay available with
56+
their write operations blocked. Same toggle as the web UI Tools tab. Off by
57+
default. Requires restart to take effect.
58+
enable_beta_features:
59+
name: Enable beta features (master)
60+
description: ⚠ DANGER — these tools can PERMANENTLY DAMAGE your Home Assistant
61+
installation. They write to your YAML config, your filesystem, install custom
62+
components, run arbitrary sandboxed Python, and edit tool docstrings the AI
63+
sees. There is no warranty and no support guarantee — you enable them at your
64+
OWN RISK. Take a Home Assistant backup before turning this on, and never enable
65+
in production without one. Master gate for the 5 experimental sub-flags below;
66+
sub-flags are ignored at runtime while this master is off, even when explicitly
67+
set to true. The same toggle is also surfaced in the web settings UI under "Beta
68+
features (dangerous)" — either surface reflects the other on restart.
69+
enable_yaml_config_editing:
70+
name: Enable YAML config editing (beta)
71+
description: Beta feature. Allows AI assistants to add, replace, or remove top-level
72+
keys in configuration.yaml and packages/*.yaml. Only whitelisted keys are allowed
73+
(e.g., template, sensor, command_line, mqtt, knx); core keys like homeassistant,
74+
http, and recorder are blocked. Each edit validates YAML syntax, runs a config
75+
check, and creates an automatic backup. Changes to most keys require a full
76+
HA restart to take effect. See docs/beta.md for known limitations. Dedicated
77+
tools (automations, scripts, scenes, helpers, template sensors) should be preferred
78+
when available. REQUIRES the master "Enable beta features" toggle above (and
79+
in the web UI) to be on — otherwise this sub-flag is ignored at runtime regardless
80+
of its value here.
81+
enable_yaml_packages_automation:
82+
name: Allow automation in packages/*.yaml
83+
description: Sub-toggle of YAML config editing. When on, ha_config_set_yaml accepts
84+
yaml_path='automation' inside packages/*.yaml. When off, the call is rejected
85+
both client-side and server-side. The storage-mode tool (ha_config_set_automation)
86+
is unaffected. Default OFF; only takes effect when "Enable YAML config editing"
87+
above is also on.
88+
enable_yaml_packages_script:
89+
name: Allow script in packages/*.yaml
90+
description: Sub-toggle of YAML config editing. When on, ha_config_set_yaml accepts
91+
yaml_path='script' inside packages/*.yaml. When off, the call is rejected both
92+
client-side and server-side. The storage-mode tool (ha_config_set_script) is
93+
unaffected. Default OFF; only takes effect when "Enable YAML config editing"
94+
above is also on.
95+
enable_yaml_packages_scene:
96+
name: Allow scene in packages/*.yaml
97+
description: Sub-toggle of YAML config editing. When on, ha_config_set_yaml accepts
98+
yaml_path='scene' inside packages/*.yaml. When off, the call is rejected both
99+
client-side and server-side. The storage-mode tool (ha_config_set_scene) is
100+
unaffected. Default OFF; only takes effect when "Enable YAML config editing"
101+
above is also on.
102+
enable_yaml_edit_confirm:
103+
name: Require confirmation for YAML edits (diff preview)
104+
description: 'Sub-toggle of YAML config editing, ON by default (recommended).
105+
The first ha_config_set_yaml call returns a unified diff of exactly what would
106+
change on disk plus a confirm token and writes nothing; the edit only lands
107+
when the call is repeated with that token. Exists so unintended changes outside
108+
the requested edit are caught before they reach disk (issue #1720). Turn off
109+
only to save one round-trip per edit.'
110+
enable_code_mode:
111+
name: Enable custom tool sandbox (beta)
112+
description: Beta feature. Enables the ha_manage_custom_tool tool, which lets
113+
AI assistants create, run, save, and delete custom Python code in a secure sandbox
114+
when no built-in tool can handle the request. Code runs in an isolated interpreter
115+
with no filesystem or arbitrary network access. Sandbox code can hit the HA
116+
REST API (api_get/api_post), send WebSocket commands (ws_send), call existing
117+
MCP tools (call_tool), or remove a saved tool (delete_saved_tool). Saved tools
118+
persist to /data/saved_tools.json by default so they survive add-on restarts,
119+
and are visible to any client that can connect. See docs/beta.md for known limitations.
120+
Requires restart to take effect. REQUIRES the master "Enable beta features"
121+
toggle above (and in the web UI) to be on — otherwise this sub-flag is ignored
122+
at runtime regardless of its value here.
123+
enable_lite_docstrings:
124+
name: Enable lite tool docstrings (beta)
125+
description: 'Beta feature. Replaces the docstrings on a handful of heavy ha-mcp
126+
tools (automations, scripts, scenes, helpers, dashboards, ha_call_service, ha_config_set_yaml)
127+
with shorter variants that defer schema and example detail to the ha_get_skill_guide
128+
tool (or its skill:// resource). WARNING: this reduces idle token usage, but
129+
may degrade LLM performance — the trimmed descriptions rely on the LLM actually
130+
calling the skill tool or reading the skill resource for detail, which is not
131+
guaranteed (some models will skip the extra tool call and end up with less guidance
132+
than they had before). Best paired with a client that supports MCP resources
133+
or with enable_tool_search. Requires restart to take effect. REQUIRES the master
134+
"Enable beta features" toggle above (and in the web UI) to be on — otherwise
135+
this sub-flag is ignored at runtime regardless of its value here.'
136+
enable_mandatory_bps:
137+
name: Attach best-practice skills on writes
138+
description: 'Master switch for the write-tool skill content delivery feature
139+
(issue #1182). When enabled (default), the six config write tools (automations,
140+
scripts, scenes, helpers, dashboards, raw YAML) attach the canonical Home Assistant
141+
best-practice reference files under `skill_content` on every successful write,
142+
plus auto-embed any reference sections cited by best-practice warnings. Each
143+
tool also exposes a per-call `MandatoryBPS` parameter the agent can set to false
144+
on subsequent calls once it has the content. When this master switch is off,
145+
NO skill_content goes out regardless of the per-call parameter or BP warnings.
146+
Recommended ON as the first choice; disable only for models with very small
147+
context windows. Turning this off may degrade write accuracy. Requires restart
148+
to take effect.'
149+
enable_strict_mandatory_bps:
150+
name: Strict best-practices mode
151+
description: 'Strict mode: prevents the client from using the tool until it can
152+
prove that it read the best practices. While on, the six best-practice write
153+
tools (automations, scripts, scenes, helpers, dashboards, raw YAML) are blocked
154+
and return an error directing the client to read the best-practices skill via
155+
ha_get_skill_guide and pass back the acknowledgment key it obtains there. While
156+
on, the ha_get_skill_guide tool is locked enabled — it is the only publisher
157+
of the acknowledgment key. Child of the "Attach best-practice skills on writes"
158+
option above and inert while that parent is off. Requires restart to take effect.'
159+
enable_filesystem_tools:
160+
name: Enable filesystem tools (beta)
161+
description: 'Sets HAMCP_ENABLE_FILESYSTEM_TOOLS=true. Enables direct file read/write
162+
access to your Home Assistant filesystem. WARNING: This gives the MCP server
163+
sensitive direct file access to your system. Only enable if you trust the AI
164+
assistant with file operations. Requires restart to take effect. REQUIRES the
165+
master "Enable beta features" toggle above (and in the web UI) to be on — otherwise
166+
this sub-flag is ignored at runtime regardless of its value here.'
167+
enable_dashboard_screenshot:
168+
name: Enable dashboard screenshot mode (beta)
169+
description: Beta feature — disabled by default. Adds the ha_get_dashboard_screenshot
170+
tool plus include_screenshot / return_screenshot options on the dashboard get/set
171+
tools, so AI assistants can inspect one or more responsive Lovelace images (e.g.
172+
to verify a dashboard they just created). Supports stable named views, mobile/tablet/desktop
173+
batches, and PNG/JPEG/WebP/BMP output. Rendering runs in a separate, opt-in
174+
engine — balloob's "Puppet" add-on (headless Chromium) — which you install once
175+
(add balloob's add-on repository, then install "Puppet") and give a long-lived
176+
access token; on Docker/Container deployments you run that engine as a sidecar
177+
and set HAMCP_DASHBOARD_SCREENSHOT_ENGINE_URL. Nothing heavy is installed unless
178+
you both enable this and install the engine. Requires restart to take effect.
179+
REQUIRES the master "Enable beta features" toggle above (and in the web UI)
180+
to be on — otherwise this sub-flag is ignored at runtime regardless of its value
181+
here.
182+
enable_auto_backup:
183+
name: Enable auto-backup of edits
184+
description: Captures a per-entity snapshot before every wrapped write/destructive
185+
MCP tool call (automation, script, scene, helper, dashboard, label, category,
186+
group, zone, area, calendar, todo, entity, integration, and sibling remove/delete
187+
tools). Snapshots are saved as YAML files under /data/ha_mcp_backups/ (override
188+
via HAMCP_BACKUP_DIR) and listed, restored, or deleted via the Backups tab in
189+
the web settings UI or via ha_manage_backup(scope='edits', ...). Best-effort
190+
— failures log a WARNING but never block the underlying write. On by default;
191+
uncheck to opt out. Requires restart to take effect.
192+
auto_backup_throttle_minutes:
193+
name: Auto-backup throttle (minutes)
194+
description: Per-entity throttle window. 0 (default) captures a snapshot on every
195+
wrapped write. N>0 captures at most one snapshot per N minutes per entity. Range
196+
0–1440.
197+
auto_backup_retain_per_entity:
198+
name: Auto-backup retention (per entity)
199+
description: Maximum number of snapshots kept per entity. Older snapshots beyond
200+
this cap are rotated out on each successful capture. Default 100, range 1–10000.
201+
enable_snapshot_delete:
202+
name: Allow snapshot deletion
203+
description: Lets ha_manage_backup delete full HA snapshot tarballs (scope='snapshot',
204+
action='delete'). Off by default — a snapshot may be the last recovery point
205+
after a mistaken change, so a human must opt in here. Even when on, scheduled
206+
backups, the newest remaining snapshot, and anything younger than the age floor
207+
below stay protected.
208+
snapshot_delete_min_age_days:
209+
name: Minimum snapshot age to delete (days)
210+
description: A snapshot must be at least this old before it can be deleted. Range
211+
0–365; 0 disables the floor (the newest-snapshot and scheduled-backup protections
212+
still apply). Default 7.
213+
tool_search_max_results:
214+
name: Tool search max results
215+
description: 'Maximum number of tools returned by ha_search_tools when tool search
216+
is enabled. Lower values (2-3) save context tokens but may miss relevant tools.
217+
Range: 2-10. Requires restart.'
218+
disabled_tools:
219+
name: Disabled tools (comma-separated)
220+
description: Tools listed here are forced off and locked in the web Settings UI
221+
(Tools tab) — they cannot be re-enabled there without unsetting this option
222+
first. A small set of mandatory tools (ha_search, ha_get_overview, ha_get_state,
223+
ha_report_issue, ha_manage_backup) cannot be disabled and keep running even
224+
if listed here. ha_get_skill_guide can be disabled only while strict best-practices
225+
mode (enable_strict_mandatory_bps) is off. Comma-separated tool names (e.g.
226+
ha_call_event,ha_eval_template).
227+
pinned_tools:
228+
name: Pinned tools (comma-separated)
229+
description: Tools listed here are locked pinned — they appear at the top of the
230+
Tools tab and cannot be unpinned from the web Settings UI without unsetting
231+
this option first. Useful in combination with Tool Search. Comma-separated tool
232+
names.
233+
verify_ssl:
234+
name: Verify TLS certificate
235+
description: Verify the Home Assistant server's TLS certificate. The add-on connects
236+
via the Supervisor proxy, so this normally has no effect and should stay enabled.
237+
Disable only if you've reconfigured the add-on to talk to HA over a public HTTPS
238+
hostname with a self-signed certificate or hostname mismatch. Disabling weakens
239+
transport security — leave on unless you know you need it. Requires restart
240+
to take effect.

0 commit comments

Comments
 (0)