|
1 | 1 | # Settings UI translations |
2 | 2 |
|
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