--- name: acp-translate description: Sync DE/FR translations with en.json, add a new language, drop a language, or check translation status for the Adaptive Cover Pro integration. Triggers on phrases like "sync translations", "update translations", "add [language]", "drop [language]", "translate", "translation status", "retranslate". --- # ACP Translate Maintains **four parallel translation bundles**, each with `en.json` as the single source of truth and every other language matching its structure exactly: 1. **`custom_components/adaptive_cover_pro/translations/`** — the standard HA translation files (`title`, `config`, `options`, `entity`, `selector`, `services`). Validated by hassfest against HA's strict schema. 2. **`custom_components/adaptive_cover_pro/summary_i18n/`** — the config-summary label bundle (`en.json` / `de.json` / `fr.json`), a nested tree of dotted-key → template strings consumed by `_build_config_summary` in `config_flow.py`. This lives **outside** `translations/` on purpose: it is a custom `config_summary` category that hassfest's schema forbids as a top-level key in `translations/en.json`, so it is loaded directly by `_load_summary_labels` instead of via `async_get_translations`. 3. **`custom_components/adaptive_cover_pro/reason_i18n/`** — the pipeline-reason label bundle (`en.json` / `de.json` / `fr.json`), a nested tree of dotted reason-code → Python `str.format` template strings consumed by the pure `reason_i18n.py` resolver to localize pipeline decision-trace reasons, position explanations, and control-state reasons on `sensor.*` attributes and for the companion Lovelace card. This also lives **outside** `translations/` on purpose: it is not an HA translation category, so it is loaded directly by `reason_i18n.py`'s resolver instead of via `async_get_translations`. 4. **`custom_components/adaptive_cover_pro/troubleshoot_i18n/`** — the diagnostics-triage finding bundle (`en.json` / `de.json` / `fr.json`), a nested tree of dotted `TriageCode` → `str.format` template strings consumed by `troubleshoot_i18n.py` to render `TRIAGE_RULES` findings in the config-flow Troubleshoot step, the config summary, and `scripts/triage_json.py`. Same rationale as the two above: not an HA translation category, loaded directly by its own resolver. **Every new triage rule adds a leaf here**, so this bundle changes whenever `diagnostics/triage.py` gains a row. ⚠️ **Every operation below applies to ALL FOUR directories.** When syncing, adding, or dropping a language, process `translations/.json`, `summary_i18n/.json`, `reason_i18n/.json`, _and_ `troubleshoot_i18n/.json`. The `summary_i18n/en.json` source of truth must stay byte-identical (flattened) to the code-owned `_SUMMARY_LABELS_EN` (config_flow.py) + `COVER_TYPE_LABELS_EN` / `GEOMETRY_LABELS_EN` (cover_types/\_summary_labels.py) dicts — a drift guard in `tests/test_config_flow_summary_i18n.py` / `tests/test_policy_summary_i18n.py` enforces this. Likewise, the `reason_i18n/en.json` source of truth must stay byte-identical (flattened) to the code-owned `_REASON_TEMPLATES_EN` dict (`reason_i18n.py`) — a drift guard in `tests/test_reason_i18n.py` enforces this, and `troubleshoot_i18n/en.json` has the identical relationship to `_TRIAGE_TEMPLATES_EN` (`troubleshoot_i18n.py`), guarded by `tests/test_troubleshoot_i18n.py`. If you change those code dicts, regenerate the matching `en.json` first, then sync de/fr. Officially shipped languages: **en, de, fr**. Any new language is added only on explicit maintainer request via this skill. ## Picking the Operation Match what the user asks for: | User says… | Operation | | ----------------------------------------------------------------------------- | ----------------- | | "sync translations", "update translations", "propagate the en.json changes" | **Sync** | | "add [language]", "rebuild [language] from scratch", "retranslate [language]" | **Add language** | | "drop [language]", "remove [language]", "stop shipping [language]" | **Drop language** | | "translation status", "how are translations doing", "check translations" | **Status** | If ambiguous, ask the user one clarifying question before proceeding. --- ## Model Strategy | Operation | Model | Scope | | ------------------------------- | -------------------------- | --------------------------------------------------------------------- | | **Sync** (incremental) | Haiku only | All changed/added keys — no Sonnet review | | **Add language** (full rebuild) | Haiku bulk → Sonnet review | Haiku: all 766 keys; Sonnet: only `data_description` keys (see below) | **Why Haiku-only for Sync:** Incremental changes are small and build on an existing high-quality baseline. Placeholder preservation is verified by tests. Sonnet review is not cost-justified for small diffs. **Why Sonnet for Add language data_descriptions:** Full rebuilds produce ~286 long help-text strings with domain concepts (azimuth, elevation, glare zones, climate modes). These benefit from a register/accuracy pass. Step descriptions, labels, and error/abort messages are simple enough for Haiku alone. **Sonnet review-pass key pattern** (Add language only): - Any dotpath containing `.data_description.` — these are the long help text strings Everything else stays with Haiku output. **Cost budget:** ~$0.03 per 2-language sync; ~$0.25 per 2-language full rebuild. If a run seems headed above $0.50 for one language, stop and ask the user. --- ## Operation 1 — Sync Use when `translations/en.json` has changed and DE/FR need to catch up. ### Steps > **All four bundles.** Run the diff + dispatch for `translations/`, `summary_i18n/`, `reason_i18n/`, **and** `troubleshoot_i18n/`. A given language's four files can have independent deltas — compute and propagate each separately. The `summary_i18n` files are small nested trees (flatten the same way); their dotpaths look like `rules.force`, `cover_types.blind`, `geometry.slat.depth`. The `reason_i18n` files are likewise small nested trees; their dotpaths look like `solar.tracking`, `manual.holding_label`, `fragment.season_summer`, `engine.direct_sun`. The `troubleshoot_i18n` files are the smallest; their dotpaths are all under one `triage` root — `triage.custom_safety_bypass`, `triage.skip_age`, `triage.sun_tracking_gate_closed`. 1. **Diff.** Load all translation files (all four directories) via Bash+Python (see **Reading Translation Files** — do NOT use the Read tool). Flatten each to dot-path keys. For each non-en file compute: - `added`: keys in en but not in target - `removed`: keys in target but not in en - `changed`: keys where the en value text changed since the target was last generated. Detect by heuristic: if `target[k]` looks like an obvious placeholder (equals `en[k]` verbatim, or is a short English phrase when the rest of the file is clearly in the target language), treat it as changed. When unsure, retranslate — cost of a re-translation is negligible. 2. **If nothing to do**, report "DE/FR already in sync with en.json" and exit. 3. **Dispatch one subagent per language in parallel** (single message, two `Agent` tool calls). Each subagent handles all FOUR of that language's files (`translations/.json`, `summary_i18n/.json`, `reason_i18n/.json`, and `troubleshoot_i18n/.json`) and receives: - Absolute paths to `translations/en.json` + `summary_i18n/en.json` + `reason_i18n/en.json` + `troubleshoot_i18n/en.json` and to its own four target files - The list of `added` + `changed` dotpaths to translate, per file - The list of `removed` dotpaths to strip from each target - The language name and ISO code The subagent's job (Haiku only — no Sonnet review for Sync): - Extract the values at the requested dotpaths from en.json via Bash+Python (see **Reading Translation Files** — do NOT use the Read tool directly on these files). - Run the Haiku translation prompt (see **Subagent Prompt Templates**) on all of them. - Load the target file, merge in the new values, delete `removed` keys, and write it back via Bash+Python. - Return a one-paragraph summary: counts added/changed/removed, any placeholder-preservation warnings, cost estimate. 4. **Verify.** Run `./scripts/validate_translations.py --ci` (covers `translations/` only — it does not validate `summary_i18n/`, `reason_i18n/`, or `troubleshoot_i18n/`) and `venv/bin/python -m pytest tests/test_translations.py tests/test_config_flow_summary_i18n.py tests/test_policy_summary_i18n.py tests/test_reason_i18n.py tests/test_troubleshoot_i18n.py -q`. If either fails, report the failure verbatim and stop. Do not attempt a second auto-sync round. 5. **Report.** Use the Output Format below. --- ## Operation 2 — Add Language Use to rebuild DE/FR from scratch **or** to add a brand-new language. ### Steps 1. **Validate.** The language code must be a valid HA locale (BCP-47 form: `de`, `fr`, `es`, `pt-BR`, `zh-Hans`, etc.). If `en`, refuse — we don't retranslate English. If the file already exists and the user did not say "rebuild" or "retranslate", confirm they want to overwrite. 2. **Delete the existing file** if rebuilding, so the subagent produces a clean file. 3. **Dispatch one subagent per requested language in parallel.** If the user says "add DE and FR", send a single message with two `Agent` tool calls. Each subagent builds all FOUR files for its language and receives: - Absolute paths to `translations/en.json`, `summary_i18n/en.json`, `reason_i18n/en.json`, AND `troubleshoot_i18n/en.json` - Absolute paths to the four target files it must write (`translations/.json`, `summary_i18n/.json`, `reason_i18n/.json`, `troubleshoot_i18n/.json`) - Language name + ISO code - The domain-term glossary (see below) — customized per language The subagent's job: - Load the full `translations/en.json`, `summary_i18n/en.json`, `reason_i18n/en.json`, and `troubleshoot_i18n/en.json` trees via Bash+Python (see **Reading Translation Files**). - **Pass 1 — Haiku bulk:** Run the Haiku translation prompt on all keys from all four files. Capture output as a JSON object. - **Pass 2 — Sonnet review (data_descriptions only):** Filter the Haiku output to include ONLY keys containing `.data_description.` (these exist only in `translations/`, not the other three bundles) — do not send any other keys to Sonnet. `reason_i18n` and `troubleshoot_i18n` have no `data_description` keys at all, so they never enter the Sonnet pass — they stay Haiku-only, same as summary_i18n's non-data_description keys. Run the Sonnet review prompt on this filtered subset. Merge corrected values back into the Haiku output. - Write `translations/.json`, `summary_i18n/.json`, `reason_i18n/.json`, AND `troubleshoot_i18n/.json` via Bash+Python: each with the same nested structure as its en.json source, 2-space indent, `ensure_ascii=False`, trailing newline. - Return a summary: key counts written per file, how many data_description keys were reviewed by Sonnet, placeholder warnings, cost estimate. 4. **Update tooling.** After subagents return: - Add the language code to the `LANGUAGES` constant in `scripts/validate_translations.py` (unless already present). - Update any per-language lists in `tests/test_translations.py`. 5. **Verify.** Run `./scripts/validate_translations.py --ci` (covers `translations/` only — it does not validate `summary_i18n/`, `reason_i18n/`, or `troubleshoot_i18n/`) and `venv/bin/python -m pytest tests/test_translations.py tests/test_config_flow_summary_i18n.py tests/test_policy_summary_i18n.py tests/test_reason_i18n.py tests/test_troubleshoot_i18n.py -q`. Do not proceed to commit if either fails. 6. **Report.** If the language is new (not in the previously-shipped set), remind the user to update README's supported-languages list and add a release-notes line. --- ## Operation 3 — Drop Language 1. Confirm the language is not `en`, `de`, or `fr`. If the user asks to drop one of the core three, explicitly confirm with them before proceeding (this changes the officially supported set). 2. Delete ALL FOUR of `translations/.json`, `summary_i18n/.json`, `reason_i18n/.json`, and `troubleshoot_i18n/.json`. 3. Remove the code from `scripts/validate_translations.py` `LANGUAGES` list. 4. Remove any language-specific expectations from `tests/test_translations.py`. 5. Run `venv/bin/python -m pytest tests/test_translations.py tests/test_config_flow_summary_i18n.py tests/test_policy_summary_i18n.py tests/test_reason_i18n.py tests/test_troubleshoot_i18n.py -q` to confirm. 6. Report what was removed. --- ## Operation 4 — Status 1. Run `./scripts/validate_translations.py` (no flags) and show its dashboard output. This covers `translations/` only — it does not validate `summary_i18n/`, `reason_i18n/`, or `troubleshoot_i18n/`. 2. Run `venv/bin/python -m pytest tests/test_translations.py tests/test_config_flow_summary_i18n.py tests/test_policy_summary_i18n.py tests/test_reason_i18n.py tests/test_troubleshoot_i18n.py -q` and show pass/fail counts (covers all four bundles' parity + the en-source drift guards). 3. No subagents, no writes. --- ## Reading Translation Files ⚠️ **The `translations/` JSON files exceed the Read tool's 25,000-token limit. Never use the Read tool directly on `translations/en.json`, `de.json`, or `fr.json`.** Use Bash+Python instead. The `summary_i18n/`, `reason_i18n/` and `troubleshoot_i18n/` files are smaller (~10 KB) and Read-safe, but use the same Bash+Python flatten/merge flow for consistency and to keep the write format identical (2-space indent, `ensure_ascii=False`, trailing newline). **Extract specific dotpath values from en.json (Sync):** ```bash python3 << 'EOF' import json, functools def get_path(d, dotpath): return functools.reduce(lambda x, k: x[k], dotpath.split('.'), d) with open('/path/to/en.json') as f: en = json.load(f) dotpaths = ['config.step.blind_spot.data.blind_spot_left', ...] print(json.dumps({p: get_path(en, p) for p in dotpaths}, ensure_ascii=False, indent=2)) EOF ``` **Load full en.json tree (Add language):** ```bash python3 -c " import json with open('/path/to/en.json') as f: d = json.load(f) print(json.dumps(d, ensure_ascii=False)) " ``` **Load, update, and write back a target file:** ```bash python3 << 'EOF' import json with open('/path/to/de.json') as f: target = json.load(f) # Apply changes (set dotpath values, remove keys, etc.) # target['config']['step']['blind_spot']['data']['blind_spot_left'] = 'Neuer Wert' with open('/path/to/de.json', 'w') as f: json.dump(target, f, ensure_ascii=False, indent=2) f.write('\n') EOF ``` --- ## Subagent Prompt Templates Use these verbatim when dispatching. Substitute `<...>` placeholders before sending. ### Haiku translation prompt (used for ALL operations) ``` You are translating Home Assistant integration UI strings from English to (). Source file: ⚠️ Do NOT use the Read tool on this file — it exceeds the token limit and will error. Use the Bash tool with Python to extract the values you need. Translate ONLY these dotpath keys (flattened form): To extract source values, use the Bash tool: python3 -c " import json, functools def get(d, p): return functools.reduce(lambda x,k: x[k], p.split('.'), d) en = json.load(open('')) paths = [] print(json.dumps({p: get(en, p) for p in paths}, ensure_ascii=False, indent=2)) " To update the target file, use the Bash tool: python3 -c " import json, functools def set_path(d, p, v): keys = p.split('.'); functools.reduce(lambda x,k: x[k], keys[:-1], d)[keys[-1]] = v with open('') as f: t = json.load(f) # set_path(t, 'config.step.blind_spot.data.blind_spot_left', 'translated value') with open('', 'w') as f: json.dump(t, f, ensure_ascii=False, indent=2); f.write('\n') " Translation rules — non-negotiable: 1. Preserve every placeholder exactly as-is: {summary}, {entity}, {position}, {hours}, {minutes}, {name}, {version}, etc. 2. Preserve markdown and formatting: **bold**, newlines (\n), bullet markers (-), numbered lists, backticks. 3. Preserve HTML/XML-style tags if present (
, , etc.). 4. Preserve unit symbols (%, °, m, cm, K) and numeric values verbatim. 5. Use these domain terms consistently — do NOT invent alternatives: 6. Keep the register close to Home Assistant's UI voice: clear, concise, second-person imperative for instructions ("Select…", "Enter…", "Configure…"). 7. Do NOT translate proper names, entity IDs, integration names, or mdi: icon references. Self-review — before outputting, check each translation for: - ✅ All {placeholders} present and unchanged - ✅ Domain terms match the glossary above - ✅ No English words left in output (except proper names and technical terms from the glossary) - ✅ Register is natural for a technical UI (not overly formal or casual) Output format: a single JSON object mapping each input dotpath to its translation. Nothing else. No commentary, no markdown fences. Example output: {"config.step.geometry.title": "Géométrie du cache", "config.step.geometry.description": "Configurez les dimensions..."} ``` ### Sonnet review prompt (Add language only — data_description keys only) ``` Review these translations of Home Assistant config-flow help text (data_description fields). These are long strings explaining configuration options to end users — they require accurate domain terminology and natural register. Fix translations that: - Sound stiff, machine-translated, or overly literal - Misrepresent a technical concept (azimuth, elevation, tilt, glare zone, cover pipeline, FOV) - Use inconsistent register with HA's UI voice - Drop or alter a placeholder — if a placeholder is missing, flag it as ERROR: do not silently fix Domain terms that must be used consistently: Do NOT change translations that are already correct and natural. Return the SAME JSON shape with the same keys, corrected values only where needed. Output JSON only, no commentary. ⚠️ Input contains ONLY data_description keys. Do not add, remove, or rename any keys. Input: ``` ### Domain glossary (append to both Haiku and Sonnet prompts per language) **German (de):** - azimuth → Azimut - elevation → Höhe - tilt → Neigung - slat → Lamelle - glare zone → Blendungszone - cover → Beschattung - awning → Markise - blind → Jalousie - venetian blind → Jalousie (mit Lamellen) - climate mode → Klimamodus - override → Übersteuerung / Überschreibung - manual override → Manuelle Übersteuerung - force override → Zwangsübersteuerung - motion sensor → Bewegungssensor - presence → Anwesenheit - field of view / FOV → Sichtfeld / FOV - occupancy → Anwesenheit / Belegung - acceptance angle → Akzeptanzwinkel - sunset → Sonnenuntergang **French (fr):** - azimuth → azimut - elevation → élévation - tilt → inclinaison - slat → lamelle - glare zone → zone d'éblouissement - cover → protection / store - awning → store banne - blind → store - venetian blind → store vénitien - climate mode → mode climatique - override → dérogation - manual override → dérogation manuelle - force override → dérogation forcée - motion sensor → détecteur de mouvement - presence → présence - field of view / FOV → champ de vision / FOV - occupancy → présence / occupation - acceptance angle → angle d'acceptation - sunset → coucher du soleil For other languages, tell the subagent to "use the HA community standard translation of these terms for ; when in doubt prefer the shortest unambiguous term." --- ## File Format Non-EN files (in ALL FOUR of `translations/`, `summary_i18n/`, `reason_i18n/`, and `troubleshoot_i18n/`) must: - Be valid JSON, 2-space indent. - Use `ensure_ascii=False` (keep accented characters as-is, not `\uXXXX`). - End with exactly one trailing newline. - Preserve the matching en.json's nested structure exactly — for `translations/` every top-level section including `services`; for `summary_i18n/` the full nested label tree (`rules`, `weather`, `cover_types`, `geometry`, …); for `reason_i18n/` the full nested reason-code tree (`solar`, `manual`, `fragment`, `engine`, …). - Contain no `mdi:` icon references, no zero-width characters, no empty string values. ⚠️ **Placeholder parity is critical for `summary_i18n/` and `reason_i18n/`.** Each label is a Python `str.format` template; the translated value must carry the IDENTICAL set of `{field}` placeholders — including any format specs, e.g. `{distance:.2f}` — and escaped literal `{{`/`}}` as the English source, or the consuming code raises at render time (`_build_config_summary` for `summary_i18n/`; the `reason_i18n.py` resolver for `reason_i18n/`). `tests/test_config_flow_summary_i18n.py` enforces this per key for `summary_i18n/`; `tests/test_reason_i18n.py` (`test_reason_placeholder_parity_de_fr`) enforces it for `reason_i18n/`. --- ## Safety Rules - **Never delete `en.json`** (in any of `translations/`, `summary_i18n/`, or `reason_i18n/`). - **Never write a translation file without running the validator and tests afterwards.** - **Never silently drop keys from a target file.** If a key was removed from en.json, the Sync operation must list it under "removed" in the report. - **If Haiku output fails JSON parse**, retry once; if still failing, dispatch Sonnet for that batch as a fallback. Do not return partial results. - **Respect the cost budget.** If a projected run exceeds $0.50 for one language, stop and ask the user. --- ## Output Format After any Sync or Add run, report: ``` Translation complete (branch: ) translations/ en: keys (unchanged) de.json: + added, ~ changed, - removed → keys fr.json: + added, ~ changed, - removed → keys summary_i18n/ en: keys (unchanged) de.json: + added, ~ changed, - removed → keys fr.json: + added, ~ changed, - removed → keys reason_i18n/ en: keys (unchanged) de.json: + added, ~ changed, - removed → keys fr.json: + added, ~ changed, - removed → keys Validator: ✅ Tests: ✅ ( passed) Cost estimate: ~$ (Haiku: keys; Sonnet review: data_description keys or "none — Sync") Warnings: - ``` Drop and Status operations use a shorter free-form report.