Skip to content

Commit 8ef9d1e

Browse files
Patch76claude
andauthored
docs: correct the meta-only stub claim in the add-a-language procedure (#2144)
Both the locales README and AGENTS.md said a new language's settings catalog may start as a meta-only stub. Following that literally leaves a PR red: the Decision and PredicateOp word checks in test_settings_ui_i18n.py apply to every catalog and are not behind LOCALE_COMPLETENESS_CHECKS, and once a catalog does carry messages, the address-register check in test_translate_locales.py wants one whose English addresses the reader. Both documents now name what a catalog owes before it merges, and the README's list of ungated PR-CI checks gains the two checks that live outside test_locale_parity.py. Verified by running the corrected procedure: a stub with meta plus those keys, an empty component catalog, regenerated projections and the code added to the AGENTS.md locale list leaves the three locale test files green. Three comments carried the same claim and now describe what they actually guard: the _TRANSLATED_LOCALES exclusion in test_translate_locales.py, and the completeness marker plus the tools-tab docstring in test_settings_ui_i18n.py. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6213fc8 commit 8ef9d1e

5 files changed

Lines changed: 57 additions & 25 deletions

File tree

AGENTS.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -762,9 +762,16 @@ code (`de`, `es`, `fr`, `it`, `ru`, `zh-Hans`) names every file:
762762
That list of codes is itself pinned by
763763
`test_agents_md_lists_every_shipped_locale`: adding a language means adding its
764764
code here, in the same PR, or the suite goes red. To add a language, add the
765-
two authored catalogs (settings UI + component; the settings one can start as
766-
a `meta`-only stub), regenerate, and let the translation pipeline below fill
767-
the strings.
765+
two authored catalogs (settings UI + component), regenerate, and let the
766+
translation pipeline below fill the strings. The component catalog may start
767+
empty; the settings one may not start `meta`-only, because three ungated
768+
checks read the shipped catalogs themselves: every decided `Decision` outcome
769+
and every `PredicateOp` operator needs a translated word
770+
(`policies.pending.decision.*`, `policies.operators.*` — a value that still
771+
spells the backend literal counts as untranslated), and at least one
772+
translated key must have English that addresses the reader in the second
773+
person, which is where `scripts/translate_locales.py` reads the catalog's
774+
address register. `src/ha_mcp/settings_ui/locales/README.md` names the tests.
768775

769776
Settings UI catalogs are auto-discovered (no registration). Their `messages` may
770777
omit keys — English is the per-key fallback — but may not carry one `en.json`

src/ha_mcp/settings_ui/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Any reachable HA instance works for `HOMEASSISTANT_URL` / `HOMEASSISTANT_TOKEN`.
3737
- `settings.html` — the page markup. `__HA_MCP_CSS__` and `__HA_MCP_JS__` are filled once at import. `__HA_MCP_THEME_PREFS__`, `__HA_MCP_I18N__`, `__HA_MCP_LANG__`, and `__HA_MCP_DIR__` are filled per request by `_render_settings_html()`.
3838
- `settings.js` — the client script, injected into `<script>__HA_MCP_JS__</script>`. Not served as a separate asset.
3939
- `settings.css` — the stylesheet, injected into `<style>__HA_MCP_CSS__</style>`.
40-
- `_i18n.py` and `locales/*.json` — auto-discovered translation catalogs. English is the per-key fallback for `messages`. Adding a language is not a single-file change: it ships on all four translated surfaces or not at all. Its `tool_groups` and `tools` sections must end up exact — the post-merge locale-sync workflow fills and verifies them, so a `meta`-only stub may merge — and `en.json` is the wrong starting point because it carries both of those empty by design. See `locales/README.md` for the procedure and the repository-root `AGENTS.md` § Translations for the rules CI enforces. The wheel, sdist and binary declarations match the locale directory by pattern, so a new catalog needs no packaging edit — but keep those patterns intact when touching packaging.
40+
- `_i18n.py` and `locales/*.json` — auto-discovered translation catalogs. English is the per-key fallback for `messages`. Adding a language is not a single-file change: it ships on all four translated surfaces or not at all. Its `tool_groups` and `tools` sections must end up exact — the post-merge locale-sync workflow fills and verifies them, so a near-empty catalog may merge, though not a `meta`-only one: the `Decision` and `PredicateOp` words plus one reader-addressing key are checked ungated against the shipped catalogs — and `en.json` is the wrong starting point because it carries both of those empty by design. See `locales/README.md` for the procedure and the repository-root `AGENTS.md` § Translations for the rules CI enforces. The wheel, sdist and binary declarations match the locale directory by pattern, so a new catalog needs no packaging edit — but keep those patterns intact when touching packaging.
4141

4242
## Gotchas (read before editing)
4343

src/ha_mcp/settings_ui/locales/README.md

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ Home Assistant language code names every file:
2424
- `homeassistant-addon/translations/<code>.yaml` (generated)
2525
- `homeassistant-addon-dev/translations/<code>.yaml` (generated)
2626

27-
Add the two authored catalogs — this one may start as a `meta`-only stub
28-
(`native_name`, `dir`) — then run `python scripts/generate_locales.py` and
29-
merge: the post-merge `locale-sync.yml` workflow machine-fills every string
30-
over its next daily runs. To fill them in your own PR instead, run
27+
Add the two authored catalogs, then run `python scripts/generate_locales.py`
28+
and merge: the post-merge `locale-sync.yml` workflow machine-fills every string
29+
over its next daily runs. The component catalog may start as an empty object;
30+
this one needs `meta` (`native_name`, `dir`) plus the handful of `messages`
31+
keys the ungated checks below demand — a `meta`-only catalog is red in PR CI.
32+
To fill them in your own PR instead, run
3133
`scripts/translate_locales.py` yourself and review its output like any
3234
other diff. Also add the new code to the locale list in the repository-root
3335
`AGENTS.md`
@@ -71,9 +73,26 @@ import, so the failure names the file but arrives as a broken test module:
7173

7274
## What CI checks
7375

74-
In PR CI (`tests/src/unit/test_locale_parity.py`, ungated):
76+
In PR CI (ungated — `tests/src/unit/test_locale_parity.py` unless another file
77+
is named):
7578

7679
- Every surface carries the same set of language codes.
80+
- Every decided `Decision` outcome (all but `pending`) and every
81+
`PredicateOp` operator has a word in every catalog, non-blank — and in every
82+
catalog but `en.json` not still spelled the way the backend does
83+
(`test_every_decided_outcome_has_a_catalog_word`,
84+
`test_every_predicate_operator_has_a_catalog_word` in
85+
`tests/src/unit/test_settings_ui_i18n.py`). These words render inside
86+
otherwise translated sentences, and the page payload merges English
87+
underneath, so a missing key shows English's own word rather than reading as
88+
a gap — and the bare enum literal where English lacks the key too. That is
89+
why they are owed at once rather than left to the sync.
90+
- At least one translated key whose English addresses the reader in the
91+
second person, so `scripts/translate_locales.py` can show the engine
92+
how this catalog addresses its reader
93+
(`test_every_shipped_catalog_gets_reader_addressing_samples` in
94+
`tests/src/unit/test_translate_locales.py`). Without one the pipeline
95+
translates the rest of the catalog with no register to imitate.
7796
- The generated files (both add-on YAMLs, `FEATURE_META`) are byte-exact
7897
generator output (`test_derived_catalogs_match_the_canonical_store`); run
7998
`python scripts/generate_locales.py` after touching any `addon.*`,
@@ -82,9 +101,9 @@ In PR CI (`tests/src/unit/test_locale_parity.py`, ungated):
82101
matches the baseline — a hand edit that drops a placeholder fails the PR
83102
that makes it; a translation awaiting a machine rewrite is excluded.
84103

85-
In the post-merge `locale-sync.yml` workflow only (the same test file, gated
86-
behind `LOCALE_COMPLETENESS_CHECKS=1` — a PR that changes English merges
87-
without these, and the daily sync owes them afterwards):
104+
In the post-merge `locale-sync.yml` workflow only (the same files, gated behind
105+
`LOCALE_COMPLETENESS_CHECKS=1` — a PR that changes English merges without
106+
these, and the daily sync owes them afterwards):
88107

89108
- `tool_groups` and `tools` name exactly the renderable groups and tools.
90109
- At most 5% of this catalog's `messages`, and 5% of its `tools` texts, may be

tests/src/unit/test_settings_ui_i18n.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -309,9 +309,10 @@ def test_shipped_catalog_loads_and_is_registered(locale: str) -> None:
309309

310310
# Same gate as ``test_locale_parity.completeness`` (see the marker comment
311311
# there): filled tool sections are the post-merge locale-sync workflow's to
312-
# owe, not the PR's — a new language legitimately merges as a ``meta``-only
313-
# stub the daily sync then fills. ``test_locale_sync_gate_shape`` pins the
314-
# env-var wiring on both files.
312+
# owe, not the PR's — a new language legitimately merges as a near-empty
313+
# catalog the daily sync then fills, carrying only what the ungated checks in
314+
# this file ask of it. ``test_locale_sync_gate_shape`` pins the env-var wiring
315+
# on both files.
315316
_completeness = pytest.mark.skipif(
316317
not os.environ.get("LOCALE_COMPLETENESS_CHECKS"),
317318
reason=(
@@ -326,9 +327,9 @@ def test_shipped_catalog_loads_and_is_registered(locale: str) -> None:
326327
def test_shipped_catalog_translates_the_tools_tab(locale: str) -> None:
327328
"""Both tool sections must be filled once the sync has run.
328329
329-
Split from the structural check above so a ``meta``-only stub catalog
330-
can merge and be filled post-merge; an empty section here after a clean
331-
sync run means the fill never happened.
330+
Split from the structural check above so a near-empty catalog can merge
331+
and be filled post-merge; an empty section here after a clean sync run
332+
means the fill never happened.
332333
"""
333334
catalog = CATALOGS[locale]
334335

tests/src/unit/test_translate_locales.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@
3636
)
3737

3838
_SETTINGS_LOCALES = _REPO_ROOT / "src" / "ha_mcp" / "settings_ui" / "locales"
39-
# Catalogs that carry translated text. A new language may ship as a meta-only
40-
# stub for the pipeline to fill (AGENTS.md § Translations), and a stub has no
41-
# wording to sample yet — requiring samples of it would forbid the documented
42-
# way to add a language.
39+
# Catalogs that carry translated text. A catalog with no `messages` yet has no
40+
# wording to sample, so sampling it would fail on emptiness rather than on the
41+
# property this checks. Such a catalog cannot ship anyway — the `Decision` and
42+
# `PredicateOp` word checks in test_settings_ui_i18n.py apply to every catalog
43+
# (AGENTS.md § Translations) — so the exclusion narrows this check rather than
44+
# opening a way past it.
4345
_TRANSLATED_LOCALES = sorted(
4446
path.stem
4547
for path in _SETTINGS_LOCALES.glob("*.json")
@@ -790,9 +792,12 @@ class TestMetaOnlyStub:
790792
def test_documented_stub_language_flow_does_not_crash(
791793
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
792794
) -> None:
793-
"""AGENTS.md says a new language may start as a meta-only stub catalog
794-
and the pipeline fills every string — so a catalog with no messages/
795-
tools/tool_groups sections must plan cleanly and be writable."""
795+
"""A new language starts as a near-empty catalog the pipeline fills, and
796+
the sections it does not carry yet are absent rather than empty — so a
797+
catalog with no messages/tools/tool_groups sections must plan cleanly
798+
and be writable. What a shipped catalog owes beyond that is checked
799+
against the real files (see the address-register check above), not
800+
here."""
796801
locales = tmp_path / "locales"
797802
locales.mkdir()
798803
(locales / "en.json").write_text(

0 commit comments

Comments
 (0)