Skip to content

Commit 229b95d

Browse files
Patch76claude
andauthored
docs: make the add-a-language procedure match what CI enforces (#2074)
* docs: make the add-a-language procedure match what CI enforces Both places that tell a contributor how to add a language described a procedure that fails. `locales/README.md` and `settings_ui/AGENTS.md` said to copy `en.json` and translate the values; doing exactly that produces five red tests, because `en.json` ships `tools` and `tool_groups` empty while every catalog is required to carry both, and because a language has to land on all four translated surfaces rather than this one. The README also called `tool_groups` and `tools` optional, which contradicts AGENTS.md § Translations and the check that enforces it. Neither file mentioned that the locale list in AGENTS.md is itself pinned by a test, so that step was documented nowhere. In the other direction, `settings_ui/AGENTS.md` asked for locale JSON to stay listed in the wheel, sdist and binary packaging declarations. All three match the locale directory by pattern, so a new catalog needs no packaging edit at all; the line now states the invariant that does have to hold. CONTRIBUTING.md said nothing about translations and now points at the procedure. The README additionally records what CI checks — surface parity, the exact `tool_groups`/`tools` sets, the 5% and 15% identical-or-missing ceilings, and that adding a language moves no English source and therefore needs no baseline regeneration. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: name the two checks the add-a-language procedure still left out Both were raised in review on #2074, and both are the defect this PR is about: a procedure that does not say what CI enforces. Both add-on flavors are held to the same 5% untranslated ceiling as the settings UI catalog — `test_addon_catalog_is_not_a_copy_of_english` is parametrized over both directories and passes `_MAX_ENGLISH_IDENTICAL_SHARE` — while the README named 5% for this catalog and 15% for the component ones and left the add-on flavors unstated. And an English string shipped from more than one catalog has to read the same in every language: `test_one_english_string_reads_the_same_on_every_surface` compares the surfaces against each other rather than against English, so a contributor can translate all four catalogs faithfully and still go red. That step belongs in the procedure, not only in the check list, because it changes how a switch is worded rather than what CI reports afterwards. AGENTS.md § Translations states both already; the README, which is the file a translator opens first, did not. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: match the per-tool field rule and the AGENTS.md pointer to the code `_validate_tools` skips a missing `title` or `description` per tool, and `test_settings_catalog_keys_name_real_groups_and_tools` compares only the tool-name key set, so describing both fields as "not optional" overstated what CI enforces. The exactness is the key set; a field left out counts as untranslated against the 5% `tools` ceiling, which is how the root AGENTS.md already words it. The scoped `src/ha_mcp/settings_ui/AGENTS.md` has no Translations section, so its bare "`AGENTS.md` § Translations" resolved to itself. Name the repository-root file, both there and in the same reference in `locales/README.md`. * docs: close the four remaining gaps between the procedure and the checks Same class as the rest of this PR: rules a contributor hits but the README does not state. - `meta.native_name` has to be non-empty, differ from English's own name and be unique across catalogs (`test_native_names_name_their_own_language`). Copying a catalog and leaving the name is exactly what the procedure above invites, and the bullet was purely descriptive. - `messages` values are also constrained at load time: the inline-markup allowlist and the panel-link rules raise inside `load_catalogs()`, so breaking one surfaces as an import error rather than a named locale failure. - The four-file list carried no per-surface content rules, so following only this README produces four files and three red surfaces. Summarised, with a pointer to the root `AGENTS.md` § Translations. - The one-wording rule is conditional in the assertion — the settings UI is the tiebreak only where a group has a settings UI member. Groups carried by the two add-on flavors alone have none, and that axis was missing from the framing above as well. * docs: say in AGENTS.md that the locale list is pinned The list of language codes in § Translations is asserted by `test_agents_md_lists_every_shipped_locale`, and the section never said so — the one rule this file owns that a contributor adding a language has to act on. The procedure in `locales/README.md` already pointed here for it. Kept off the pinned line itself: the test asserts exactly one line carrying `names every file:` and parses every backticked token on that line as a locale code. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 0717e84 commit 229b95d

4 files changed

Lines changed: 101 additions & 17 deletions

File tree

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,9 @@ code (`de`, `es`, `fr`, `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`.
720+
That list of codes is itself pinned by
721+
`test_agents_md_lists_every_shipped_locale`: adding a language means adding its
722+
code here, in the same PR, or the suite goes red.
720723

721724
Settings UI catalogs are auto-discovered (no registration). Their `messages` may
722725
omit keys — English is the per-key fallback — but `tool_groups` and `tools` may

CONTRIBUTING.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ uv run lefthook install --reset-hooks-path
4949
- **Code**: Follow existing patterns, add type hints, test new features
5050
- **Docs**: Update README.md for user-facing changes
5151
- **PRs**: Use the template, ensure tests pass
52+
- **Translations**: A language ships on all four translated surfaces or not at
53+
all. Read `src/ha_mcp/settings_ui/locales/README.md` first — a catalog copied
54+
from `en.json` fails CI
5255

5356
## 💤 Abandoned PRs
5457

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 fallback; a new language is added by copying `locales/en.json`, translating values, and preserving keys/placeholders. Locale JSON must remain listed in wheel, sdist, and binary packaging declarations.
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 are mandatory and exact, and `en.json` is the wrong starting point because it carries both of those empty. 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

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,96 @@
11
# Settings UI translations
22

3-
The settings page discovers every `*.json` catalog in this directory. To add a
4-
language, copy `en.json`, rename it to the language code (for example
5-
`it.json`), translate the values, and keep the keys and `{placeholders}`
6-
unchanged. No Python or JavaScript registration is required.
7-
8-
Catalog sections:
9-
10-
- `meta.native_name`: language name shown in the selector.
11-
- `meta.dir`: `ltr` or `rtl`.
12-
- `messages`: interface labels, help text, notices, and runtime messages.
13-
- `tool_groups`: optional translations keyed by the English MCP tool tag.
14-
- `tools`: optional per-tool `title` and `description` overrides keyed by the
15-
stable MCP tool name. Missing values fall back to the server-provided English.
16-
17-
English is always the fallback, so an incomplete catalog remains usable while
18-
it is being expanded.
3+
The settings page discovers every `*.json` catalog in this directory. No Python
4+
or JavaScript registration is required, and no packaging file needs editing —
5+
the wheel, sdist and binary declarations all match this directory by pattern.
6+
7+
## Adding a language
8+
9+
A language ships on **all four** translated surfaces or not at all, and one
10+
Home Assistant language code names every file:
11+
12+
- `src/ha_mcp/settings_ui/locales/<code>.json` (this directory)
13+
- `custom_components/ha_mcp_tools/translations/<code>.json`
14+
- `homeassistant-addon/translations/<code>.yaml`
15+
- `homeassistant-addon-dev/translations/<code>.yaml`
16+
17+
Adding only this catalog fails `test_every_locale_ships_on_every_surface`. Also
18+
add the new code to the locale list in the repository-root `AGENTS.md`
19+
§ Translations — that list is pinned by
20+
`test_agents_md_lists_every_shipped_locale`.
21+
22+
Only the first of those four files is described below, and the other three
23+
carry content rules of their own: the component catalog needs every `en.json`
24+
key with identical `{placeholders}` and no extra ones, and each add-on YAML
25+
needs a `name` and a `description` for every `schema:` key of *that* flavor's
26+
`config.yaml`, with nothing left behind for a key the schema no longer
27+
declares. The two flavors declare different schemas, so neither YAML is a copy
28+
of the other. The repository-root `AGENTS.md` § Translations states all of
29+
this; a contributor who writes only this catalog goes red on the other three.
30+
31+
**Start from a translated catalog, not from `en.json`.** English for the tool
32+
titles and descriptions comes from the tool definitions at runtime, so `en.json`
33+
ships `tools` and `tool_groups` empty; a copy of it is missing both sections
34+
that this catalog is required to carry.
35+
36+
**Read the other surfaces before you word a switch.** Wherever the same English
37+
text is shipped from more than one catalog, your wording has to be byte-identical
38+
in all of them. That is not only the add-on-options-versus-settings-UI axis: the
39+
two add-on flavors describe most of the same options, so a good part of the
40+
pinned parity is stable-against-dev, with no settings-UI text involved at all.
41+
Translating one surface at a time is exactly how one option ends up with two
42+
different sentences.
43+
44+
## Catalog sections
45+
46+
- `meta.native_name`: language name shown in the selector. It must be
47+
non-empty, must not repeat English's own name, and must differ from every
48+
other catalog's — a copied catalog that keeps the name it was copied from
49+
fails `test_native_names_name_their_own_language`, because the picker would
50+
then offer one label twice.
51+
- `meta.dir`: `ltr` or `rtl`. Omitting it means `ltr`; any other value is
52+
rejected when the catalog loads.
53+
- `messages`: interface labels, help text, notices, and runtime messages. Keys
54+
may be omitted — English is the per-key fallback at runtime — but see the
55+
share limit below before leaving a catalog half-finished.
56+
- `tool_groups`: one entry per renderable MCP tool tag, keyed by the English
57+
tag. Not optional, and exact: no key more and none fewer.
58+
- `tools`: `title` and `description` per tool, keyed by the stable MCP tool
59+
name. The key set is not optional and exact in the same way; either field on
60+
its own may be left out, but a missing one counts as untranslated against the
61+
share limit below.
62+
63+
Keep the keys and `{placeholders}` unchanged in every section.
64+
65+
`messages` values carry two further rules, both enforced when the catalog
66+
loads rather than by a named test — breaking one raises a `ValueError` at
67+
import, so the failure names the file but arrives as a broken test module:
68+
69+
- The only inline markup the page can restore is `<code>`, `<strong>`, `</a>`
70+
and `<a href="#" data-panel-link="...">`, spelled exactly that way. Any other
71+
tag — `<b>`, `<CODE>`, `<code >` — is rejected.
72+
- A `data-panel-link` target must be a tab the settings page declares, and a
73+
translated message must link to the same tabs as its English source, with
74+
the same multiplicity. The order may differ, so a translation is free to
75+
reorder two links to suit its grammar.
76+
77+
## What CI checks
78+
79+
- Every surface carries the same set of language codes.
80+
- `tool_groups` and `tools` name exactly the renderable groups and tools — a
81+
tool added to the codebase turns every locale red in the PR that adds it.
82+
- At most 5% of this catalog's `messages`, and 5% of its `tools` texts, may be
83+
byte-identical to English or missing outright. Both add-on flavors are held to
84+
the same 5%; the component catalogs allow 15%, because they carry product
85+
names as keys of their own. A single tool whose `title` *and* `description` are
86+
both still English fails by name however small the share.
87+
- One wording per English string across surfaces, wherever the same English text
88+
is shipped from more than one catalog. The failure names every group that
89+
disagrees. Where a group has a settings UI member, that is the wording the
90+
other surfaces follow today; a group carried only by the two add-on flavors
91+
has no such member, so there pick one wording and use it in both.
92+
- The English each translation was written against is hashed in
93+
`tests/src/unit/locale_source_baseline.json`, so a later edit to an English
94+
string turns the locales red rather than leaving them silently stale. Adding a
95+
language does not change any English source, so no baseline regeneration is
96+
needed for it.

0 commit comments

Comments
 (0)