UX-01: translations and task ordering for the add-on Configuration tab #50

Merged
admin merged 1 commits from UX-01 into release/1.0 2026-09-05 13:11:52 +02:00
Owner

UX-01 — translations and ordering for the add-on Configuration tab

"the layout, labels, ux and values in the addon config feel too technical" — the owner, 2026-09-04

Scope: the rescoped card. Translations + ordering only. The hiding/defaults half was dropped in the
rescope comment (UX-02 supersedes the surface; the Supervisor Configuration tab cannot be removed, so this
card makes it legible rather than hostile). Nothing here hides an option and nothing here moves a default.

The problem, measured

There was no translations/ directory at all, so Home Assistant fell back to rendering the raw option
keys. Every key is present in options: and the frontend keeps a field when name in options || required,
so all 70 options render, always, in schema: order, with no description. The operator read
unit_sign_disagree_w, saturation_cycles, integrator_max_w — and had no way to tell a constant derived
from 1890 closed-loop test runs from their own inverter's rating.

(The card says 63 options / 55 required; SAFETY-30, SAFETY-31 and SAFETY-32 have landed since it was
written. The measured figures today are 70 options, 59 of them required, plus 7 per-entry keys.)

What changed

file
translations/en.yaml name + description for all 70 options and all 7 per-entry keys of inverters:
translations/nl.yaml the same, written for a Flemish homeowner — afname / injectie, kwartierpiek, capaciteitstarief, laadniveau, uitschakelgrens — not transliterated
config.yaml reordered into task order in both halves, comments carried with their keys. No value, no range, no behaviour
tools/supervisor_shape_probe.py new section F: measures the rendered result against the real Supervisor image
tools/translations_lint.py the strict parser + coverage audit, shared by the check and the probe
test_translations.py the check that stops this rotting, mutation-tested both ways
DOCS.md, CHANGELOG.md what the tab can and cannot do, stated without over-claiming

⭐ The highest-value half: the description says whether to touch it

option what the operator now reads
unit_sign_disagree_s "DERIVED FROM 1890 CLOSED-LOOP TEST RUNS (SAFETY-30): 85 s is 8.5 times the worst legitimate opposition those runs produce (10.0 s)… Do not change it without re-deriving it."
circulation_w "MEASURED ON 924 CLOSED-LOOP TEST RUNS… the worst legitimate opposition collapses from 505 s at 100 W to 5 s at 400 W, and 400 W is the sharp knee this add-on will not let you go below."
meter_freeze_s "MEASURED… over 2399 s of this house at its settled operating point, 1194 polls, the longest run without a distinct reading is 15.4 s; 300 s is 19.5 times that."
bms_soc_floor "MEASURED, not chosen: register 0x0560 reads 10 on the reference GW5000S-BP. Read it off the pack you actually have rather than copying this figure."
max_w, peak_cap_w "THIS IS YOUR NUMBER, not a derived one…"
cycle_cost_threshold_eur "⚠️ THIS ONE IS NOT DERIVED FROM A MEASUREMENT. 0.10 is a product decision and it is still an open question with the owner (OPT-08)."

The last row is the one I would point a reviewer at: it would have been easy to give it the same authoritative
voice as the others, and that would have been a lie the config surface then repeats forever.

⚠️ The ordering constraints — preserved, and said so explicitly

  • test_inverter.py:1788 — the nested per-entry max_w: int( (6-space) still appears BEFORE the flat
    max_w: int( (2-space) in the schema: half.
    CFG-04 baited that on purpose so a first-match
    stripped-line reader is caught, and the reorder keeps it baited: inverters: is in the hardware group,
    flat max_w in ratings and limits. Run, not assumed: moving the nested block below the flat one is a
    1-byte edit and it goes red in test_inverter.py part 12 and in the new test_translations.py.
  • test_inverter.py:1436/1508 — \n inverters: []\n is still in the options: half.
  • test_maintenance.py:134, test_peakguard.py's _cfg_halves/_top_level, test_validate.py,
    test_watchdog.py, test_freeze.py, test_price.py, test_tariff.py, test_arbitrage.py,
    test_price_forecast.py
    — every one of these anchors on two-space indentation and on the
    \nschema: split, so all are order-independent within a half. Checked before reordering and green after.
  • The options: and schema: halves already disagreed on order before this card (they diverged at index
    10). They now agree, and test_translations.py part 4 pins that.

⚠️ What Home Assistant actually renders — measured, not assumed

tools/supervisor_shape_probe.py section F runs against
ghcr.io/home-assistant/amd64-hassio-supervisor:2026.08.0 — the version the owner runs — and uses
Supervisor's own code rather than a re-implementation:

  • supervisor.store.data._read_app_translations() loads both files and validates them with
    SCHEMA_APP_TRANSLATIONS. ⚠️ A file that fails that schema is dropped with a log warning and nothing
    else
    , so "it is on disk" was never the question. The probe asserts both loaded.
  • App.schema_ui is UiOptions(coresys)(data["schema"]) — so the schema: half decides the render
    order
    , not options:. That is why the ordering work is on the schema half.
  • The label resolution is transcribed from the shipped frontend bundle in that image
    (api/panel/frontend_latest/5680.*.js, frontend 20250925.1):
    computeLabel = translations[lang]?…name || translations["en"]?…name || schema.name, and the same for
    description. The fallback is per key, which is why a missing nl key degrades and a wrong one does not.
  • The probe then prints every rendered row in both languages — the transcript, not a claim.

all cases passed, exit 0, sections A–E (CFG-04's originals) still green.

⚠️ One measured negative result, recorded rather than glossed

The per-entry keys inside the inverters: repeatable section still render RAW. UiOptions gives
inverters type: "schema" with multiple: true; _convertSchemaElement sends exactly that case to a
selector, not to a nested ha-form:

if("schema"===t.type&&!t.multiple) return {…type:"expandable"…}          // not us
if("schema"===t.type){…return {object:{multiple:t.multiple,fields:o}}}   // us

and ha-selector-object labels those fields fields[name].label || name — the converter sets neither
label nor translation_key. So on frontend 20250925.1 the operator still sees setpoint_entity, max_w,
bms_soc_floor inside each card. The fields: block is still shipped and validated — it is the shape
Supervisor's own SCHEMA_TRANSLATION_CONFIGURATION defines for exactly this, and a frontend that wires
label through would consume it — but this PR does not claim it reaches the screen today. The
inverters label and description themselves do. The probe asserts the current state so the day it
changes, the transcript says so.

The check, and its mutation ledger

test_translations.py fails if a translation key names a non-existent option, or an option has no
translation
— both directions, both languages, top level and per entry, name and description.

Mutation-tested both ways. Eleven mutations are applied in-process and each is required to NAME the key
it broke
, not merely to go red — plus a negative control that the unmutated input is clean. Six more were
run on disk, through the purge-and-re-read protocol (bytecode purged, count printed before/after, the
edited file re-read from disk, byte delta printed):

# mutation delta result
M1 delete one option's translation from en.yaml −65 B RED — names saturation_cycles
M2 rename an option in config.yaml's schema half +4 B RED — both languages
M3 add an orphan translation key to nl.yaml +47 B RED — names max_watt
M4b empty one Dutch description (file still valid) −711 B RED — names meter_freeze_s
M5 move the nested inverters: block below flat max_w 1 B RED in test_inverter.py and test_translations.py
M6 break one label's group prefix (Advanced → Site) −4 B RED — contiguity + nl/en grouping
M7 delete a provenance citation (circulation_w loses "924") −19 B RED — provenance check

(A seventh, description: \"\" written malformed, went red as a TranslationSyntaxError — the parser
refuses rather than skipping, which is the point of part 5.)

One gap found by asking what the check would not see — and closed

The configuration: coverage above cannot see a mutation that costs the whole file.
_read_app_translations() validates each file through SCHEMA_APP_TRANSLATIONS and, on refusal, logs a
warning and drops all of it
— every label, not just the offending key — so the tab silently returns to raw
keys with one line in a log nobody is reading. Two such shapes are reachable and were previously invisible to
CI: an unrecognised top-level block (netwrok:), and a network: value that is a mapping rather than a
string (network: is {str: str}). Both are now refused by audit() and are two of the eleven
in-process mutations, so the check that catches them is itself graded.

⚠️ Honest survivors, with their size

Enumerated because a disclosed gap is worth more than a claim of completeness. Each was built and run:

  1. A factual figure inside a description can be falsified without going red, unless it happens to be one
    of the cited tokens. Run: unit_sign_disagree_s's "8.5 times" → "3 times", −2 B, green. The check
    grades that the citation is present and names the right record; it cannot grade that the sentence is
    true. Closing this would mean deriving the prose from the decision records, which is a much larger card.
  2. Swapping two options' descriptions survives. Run: heartbeat_s ↔ step_w, 0 B, green. Both keys
    still have a non-empty description, and nothing ties a description to its key's meaning.
  3. Out of this card's scope, and pre-existing: changing a shipped default that no suite pins survives.
    Run: step_w: 10 → 20, 0 B, and test_control.py, test_clamp.py, test_validate.py and
    test_peakguard.py all stay green. This is the same family as CFG-04's DEFECT 2 (the two unpinned
    in-code max_w defaults at validate.py:377 / inverter.py:382), which the UX-01 rescope explicitly
    moved to its own card. It should get one.
  4. The Dutch is graded for presence, not for quality. Part 2 catches an nl string left identical to the
    English one, and part 4 catches a wrong group prefix — nothing checks that the Dutch reads well. A native
    reader is the instrument for that, and this PR is asking for one.

No behaviour and no value moved

  • The 175 non-comment lines of config.yaml are byte-identical to release/1.0's, reordered.
    Verified by sorting and diffing both files' non-comment lines: identical, 175 = 175.
  • The only comment lines that changed are the old # --- group --- banners (replaced) and five sentences
    whose above/below the reorder made false
    — e.g. bms_soc_floor's "whatever is set above" now names
    peak_soc_floor, and the roster note's "THE FIVE FLAT KEYS ABOVE" now names the six keys
    build_roster() actually reads (it had been stale since SAFETY-32 added bms_soc_floor).
  • translations/ and tools/ are not copied into the add-on image (Dockerfile copies app/ and
    run.sh only) — correct, because Supervisor reads translations from the repository folder, not the
    image. Nothing about the running add-on changes.

Suite

20/20 green — the 19 existing files plus test_translations.py. test_peakguard.py (CI-06's flaky one)
passed in five consecutive full runs here; no red was seen at any point, so nothing needed re-running in
isolation.

Not done here, deliberately

  • Not deployed. The add-on is live in production on 0.6.0; deploying is a supervised human step.
  • Not merged.
  • Overlaps PKG-03 (i18n NL/FR/EN): this card does the add-on config half in EN+NL. PKG-03 keeps FR
    and the rest of the user-facing strings. Worth saying so in that card too.
  • Inverter B does not physically exist. Nothing here presents any figure about it as measured; the roster
    example's 4600 W remains labelled as its datasheet figure (SPIKE-04a).

🤖 Generated with Claude Code

https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU

# UX-01 — translations and ordering for the add-on Configuration tab > *"the layout, labels, ux and values in the addon config feel too technical"* — the owner, 2026-09-04 **Scope: the rescoped card.** Translations + ordering only. The hiding/defaults half was dropped in the rescope comment (UX-02 supersedes the surface; the Supervisor Configuration tab cannot be removed, so this card makes it legible rather than hostile). **Nothing here hides an option and nothing here moves a default.** ## The problem, measured There was no `translations/` directory at all, so Home Assistant fell back to rendering the raw option **keys**. Every key is present in `options:` and the frontend keeps a field when `name in options || required`, so **all 70 options render, always, in `schema:` order, with no description**. The operator read `unit_sign_disagree_w`, `saturation_cycles`, `integrator_max_w` — and had no way to tell a constant derived from 1890 closed-loop test runs from their own inverter's rating. *(The card says 63 options / 55 required; SAFETY-30, SAFETY-31 and SAFETY-32 have landed since it was written. The measured figures today are **70 options, 59 of them `required`, plus 7 per-entry keys**.)* ## What changed | file | | |---|---| | `translations/en.yaml` | `name` + `description` for **all 70 options** and **all 7 per-entry keys** of `inverters:` | | `translations/nl.yaml` | the same, written for a Flemish homeowner — *afname* / *injectie*, *kwartierpiek*, *capaciteitstarief*, *laadniveau*, *uitschakelgrens* — not transliterated | | `config.yaml` | reordered into task order in **both** halves, comments carried with their keys. **No value, no range, no behaviour** | | `tools/supervisor_shape_probe.py` | new **section F**: measures the rendered result against the real Supervisor image | | `tools/translations_lint.py` | the strict parser + coverage audit, shared by the check and the probe | | `test_translations.py` | the check that stops this rotting, mutation-tested both ways | | `DOCS.md`, `CHANGELOG.md` | what the tab can and cannot do, stated without over-claiming | ### ⭐ The highest-value half: the description says **whether to touch it** | option | what the operator now reads | |---|---| | `unit_sign_disagree_s` | *"DERIVED FROM 1890 CLOSED-LOOP TEST RUNS (SAFETY-30): 85 s is 8.5 times the worst legitimate opposition those runs produce (10.0 s)… Do not change it without re-deriving it."* | | `circulation_w` | *"MEASURED ON 924 CLOSED-LOOP TEST RUNS… the worst legitimate opposition collapses from 505 s at 100 W to 5 s at 400 W, and 400 W is the sharp knee this add-on will not let you go below."* | | `meter_freeze_s` | *"MEASURED… over 2399 s of this house at its settled operating point, 1194 polls, the longest run without a distinct reading is 15.4 s; 300 s is 19.5 times that."* | | `bms_soc_floor` | *"MEASURED, not chosen: register 0x0560 reads 10 on the reference GW5000S-BP. Read it off the pack you actually have rather than copying this figure."* | | **`max_w`, `peak_cap_w`** | *"**THIS IS YOUR NUMBER**, not a derived one…"* | | **`cycle_cost_threshold_eur`** | *"⚠️ **THIS ONE IS NOT DERIVED FROM A MEASUREMENT.** 0.10 is a product decision and it is still an open question with the owner (OPT-08)."* | The last row is the one I would point a reviewer at: it would have been easy to give it the same authoritative voice as the others, and that would have been a lie the config surface then repeats forever. ## ⚠️ The ordering constraints — preserved, and said so explicitly * **`test_inverter.py:1788` — the nested per-entry `max_w: int(` (6-space) still appears BEFORE the flat `max_w: int(` (2-space) in the `schema:` half.** CFG-04 baited that on purpose so a first-match stripped-line reader is caught, and the reorder keeps it baited: `inverters:` is in the *hardware* group, flat `max_w` in *ratings and limits*. **Run, not assumed:** moving the nested block below the flat one is a **1-byte** edit and it goes **red** in `test_inverter.py` part 12 *and* in the new `test_translations.py`. * **`test_inverter.py:1436/1508` — `\n inverters: []\n` is still in the `options:` half.** * **`test_maintenance.py:134`, `test_peakguard.py`'s `_cfg_halves`/`_top_level`, `test_validate.py`, `test_watchdog.py`, `test_freeze.py`, `test_price.py`, `test_tariff.py`, `test_arbitrage.py`, `test_price_forecast.py`** — every one of these anchors on **two-space indentation** and on the `\nschema:` split, so all are order-independent *within* a half. Checked before reordering and green after. * **The `options:` and `schema:` halves already disagreed on order before this card** (they diverged at index 10). They now agree, and `test_translations.py` part 4 pins that. ## ⚠️ What Home Assistant actually renders — measured, not assumed `tools/supervisor_shape_probe.py` section F runs against `ghcr.io/home-assistant/amd64-hassio-supervisor:2026.08.0` — the version the owner runs — and uses Supervisor's **own** code rather than a re-implementation: * `supervisor.store.data._read_app_translations()` loads both files and validates them with `SCHEMA_APP_TRANSLATIONS`. ⚠️ **A file that fails that schema is dropped with a log warning and nothing else**, so "it is on disk" was never the question. The probe asserts both loaded. * `App.schema_ui` is `UiOptions(coresys)(data["schema"])` — so the **`schema:` half decides the render order**, not `options:`. That is why the ordering work is on the schema half. * The label resolution is **transcribed from the shipped frontend bundle in that image** (`api/panel/frontend_latest/5680.*.js`, frontend 20250925.1): `computeLabel = translations[lang]?…name || translations["en"]?…name || schema.name`, and the same for `description`. The fallback is **per key**, which is why a missing `nl` key degrades and a wrong one does not. * The probe then **prints every rendered row in both languages** — the transcript, not a claim. `all cases passed`, exit 0, sections A–E (CFG-04's originals) still green. ### ⚠️ One measured **negative** result, recorded rather than glossed **The per-entry keys inside the `inverters:` repeatable section still render RAW.** `UiOptions` gives `inverters` `type: "schema"` **with** `multiple: true`; `_convertSchemaElement` sends exactly that case to a selector, not to a nested `ha-form`: ```js if("schema"===t.type&&!t.multiple) return {…type:"expandable"…} // not us if("schema"===t.type){…return {object:{multiple:t.multiple,fields:o}}} // us ``` and `ha-selector-object` labels those fields `fields[name].label || name` — the converter sets neither `label` nor `translation_key`. So on frontend 20250925.1 the operator still sees `setpoint_entity`, `max_w`, `bms_soc_floor` inside each card. **The `fields:` block is still shipped and validated** — it is the shape Supervisor's own `SCHEMA_TRANSLATION_CONFIGURATION` defines for exactly this, and a frontend that wires `label` through would consume it — but this PR does **not** claim it reaches the screen today. The `inverters` label and description themselves **do**. The probe asserts the current state so the day it changes, the transcript says so. ## The check, and its mutation ledger `test_translations.py` fails if **a translation key names a non-existent option**, or **an option has no translation** — both directions, both languages, top level and per entry, name *and* description. **Mutation-tested both ways.** Eleven mutations are applied in-process and each is required to **NAME the key it broke**, not merely to go red — plus a negative control that the unmutated input is clean. Six more were run **on disk**, through the purge-and-re-read protocol (bytecode purged, count printed before/after, the edited file re-read from disk, byte delta printed): | # | mutation | delta | result | |---|---|---|---| | M1 | delete one option's translation from `en.yaml` | −65 B | **RED** — names `saturation_cycles` | | M2 | rename an option in `config.yaml`'s schema half | +4 B | **RED** — both languages | | M3 | add an orphan translation key to `nl.yaml` | +47 B | **RED** — names `max_watt` | | M4b | empty one Dutch description (file still valid) | −711 B | **RED** — names `meter_freeze_s` | | M5 | move the nested `inverters:` block **below** flat `max_w` | **1 B** | **RED** in `test_inverter.py` *and* `test_translations.py` | | M6 | break one label's group prefix (`Advanced` → `Site`) | −4 B | **RED** — contiguity + nl/en grouping | | M7 | delete a provenance citation (`circulation_w` loses "924") | −19 B | **RED** — provenance check | *(A seventh, `description: \"\"` written malformed, went red as a `TranslationSyntaxError` — the parser refuses rather than skipping, which is the point of part 5.)* ### One gap found by asking what the check would **not** see — and closed The `configuration:` coverage above cannot see a mutation that costs **the whole file**. `_read_app_translations()` validates each file through `SCHEMA_APP_TRANSLATIONS` and, on refusal, **logs a warning and drops all of it** — every label, not just the offending key — so the tab silently returns to raw keys with one line in a log nobody is reading. Two such shapes are reachable and were previously invisible to CI: an unrecognised top-level block (`netwrok:`), and a `network:` value that is a mapping rather than a string (`network:` is `{str: str}`). Both are now refused by `audit()` **and** are two of the eleven in-process mutations, so the check that catches them is itself graded. ## ⚠️ Honest survivors, with their size Enumerated because a disclosed gap is worth more than a claim of completeness. Each was **built and run**: 1. **A factual figure inside a description can be falsified without going red**, unless it happens to be one of the cited tokens. Run: `unit_sign_disagree_s`'s *"8.5 times"* → *"3 times"*, **−2 B, green**. The check grades that the *citation is present and names the right record*; it cannot grade that the sentence is true. Closing this would mean deriving the prose from the decision records, which is a much larger card. 2. **Swapping two options' descriptions survives.** Run: `heartbeat_s` ↔ `step_w`, **0 B, green**. Both keys still have a non-empty description, and nothing ties a description to its key's meaning. 3. **Out of this card's scope, and pre-existing:** changing a shipped default that no suite pins survives. Run: `step_w: 10` → `20`, **0 B**, and `test_control.py`, `test_clamp.py`, `test_validate.py` and `test_peakguard.py` all stay **green**. This is the same family as **CFG-04's DEFECT 2** (the two unpinned in-code `max_w` defaults at `validate.py:377` / `inverter.py:382`), which the UX-01 rescope explicitly moved to its own card. **It should get one.** 4. **The Dutch is graded for presence, not for quality.** Part 2 catches an `nl` string left identical to the English one, and part 4 catches a wrong group prefix — nothing checks that the Dutch reads well. A native reader is the instrument for that, and this PR is asking for one. ## No behaviour and no value moved * The **175 non-comment lines** of `config.yaml` are **byte-identical** to `release/1.0`'s, reordered. Verified by sorting and diffing both files' non-comment lines: identical, 175 = 175. * The only comment lines that changed are the old `# --- group ---` banners (replaced) and **five sentences whose `above`/`below` the reorder made false** — e.g. `bms_soc_floor`'s *"whatever is set above"* now names `peak_soc_floor`, and the roster note's *"THE FIVE FLAT KEYS ABOVE"* now names the six keys `build_roster()` actually reads (it had been stale since SAFETY-32 added `bms_soc_floor`). * `translations/` and `tools/` are **not** copied into the add-on image (`Dockerfile` copies `app/` and `run.sh` only) — correct, because Supervisor reads translations from the **repository** folder, not the image. Nothing about the running add-on changes. ## Suite **20/20 green** — the 19 existing files plus `test_translations.py`. `test_peakguard.py` (CI-06's flaky one) passed in **five consecutive full runs** here; no red was seen at any point, so nothing needed re-running in isolation. ## Not done here, deliberately * **Not deployed.** The add-on is live in production on 0.6.0; deploying is a supervised human step. * **Not merged.** * Overlaps **PKG-03** (i18n NL/FR/EN): this card does the **add-on config** half in EN+NL. PKG-03 keeps FR and the rest of the user-facing strings. Worth saying so in that card too. * Inverter B does not physically exist. Nothing here presents any figure about it as measured; the roster example's 4600 W remains labelled as its **datasheet** figure (SPIKE-04a). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
admin added 1 commit 2026-09-05 12:07:47 +02:00
UX-01: translate the add-on config, and order it by task
checks / unit (pull_request) Successful in 5m1s
fef6ef20a4
The add-on had NO translations/ directory, so Home Assistant fell back to
rendering the raw option KEYS. An operator opening Configuration read
`unit_sign_disagree_w`, `saturation_cycles`, `integrator_max_w` - seventy of
them, in file order, with no description and no way to tell a constant derived
from 1890 closed-loop test runs from their own inverter's rating.

⚠️ PRESENTATION ONLY. Not one default, not one schema range and not one line of
behaviour moved. The 175 non-comment lines of config.yaml are byte-identical to
release/1.0's, reordered; the only comment lines that changed are the group
banners and five sentences whose "above"/"below" the reorder made false.

* translations/en.yaml and translations/nl.yaml: a name and a description for
  all 70 options and all 7 per-entry keys of the `inverters:` repeatable
  section. The Dutch is written for a Flemish homeowner - afname/injectie,
  kwartierpiek, capaciteitstarief - not transliterated from the English.
* ⭐ Every derived constant says WHERE IT CAME FROM and whether to touch it.
  `unit_sign_disagree_s` cites its 1890 closed-loop runs (SAFETY-30);
  `circulation_w` its 924 runs and the measured 400 W knee (SAFETY-31);
  `meter_freeze_s` its 2399 s capture (SAFETY-18); `bms_soc_floor` register
  0x0560. The two numbers that are the operator's - max_w and peak_cap_w - say
  "THIS IS YOUR NUMBER". `cycle_cost_threshold_eur`, which is neither, says it
  is an open product question (OPT-08).
* config.yaml reordered into task order in BOTH halves, comments carried with
  their keys: hardware -> meter -> ratings and limits -> tariff -> maintenance
  -> safety -> advanced.
  ⚠️ test_inverter.py:1788's trap is PRESERVED: the nested per-entry `max_w:`
  still precedes the flat one under `schema:`, and `inverters: []` still ships
  in the `options:` half. Moving the nested block below the flat one is a 1-byte
  edit; it was run and goes red in test_inverter.py and test_translations.py.
* tools/supervisor_shape_probe.py section F MEASURES the result against
  ghcr.io/home-assistant/amd64-hassio-supervisor:2026.08.0 - Supervisor's own
  _read_app_translations(), its own UiOptions, and the frontend's label
  resolution transcribed from the shipped bundle - and prints every rendered row
  in both languages. `all cases passed`, exit 0.
* ⚠️ ONE MEASURED NEGATIVE RESULT, NOT GLOSSED: the per-entry keys inside the
  `inverters:` section still render RAW on frontend 20250925.1, because a
  list-of-objects goes to ha-selector-object and the converter sets no `label`
  and no `translation_key`. The `fields:` translations are shipped and validated
  - that is Supervisor's own shape for this - but this card does not claim they
  reach the screen. The `inverters` label and description do.
* test_translations.py: a translation naming a non-existent option, or an option
  with no translation, fails CI - both directions, both languages, top level and
  per entry. Mutation-tested BOTH WAYS: eleven in-process mutations each
  required to NAME the key they broke, plus a negative control; six more run on
  disk through the purge-and-re-read protocol. Two of the eleven are the ones
  that cost the WHOLE FILE rather than one label - an unrecognised top-level
  block, and a `network:` value that is not a string. Supervisor drops a file
  that fails SCHEMA_APP_TRANSLATIONS with only a log warning, so those silently
  return the entire tab to raw keys; neither is reachable through the
  `configuration:` coverage, which is why they were hunted for and closed.
* tools/translations_lint.py is the hand parser CI needs (no PyYAML in the
  add-on image) and the probe grades it, key by key and string by string,
  against Supervisor's own loader. It is strict and RAISES rather than skipping;
  nine malformed shapes prove it.

Full 20-file suite green (19 existing + test_translations.py).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
admin merged commit f72c48dbfd into release/1.0 2026-09-05 13:11:52 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/goodwe-addon#50