PR #68 restated its own translation data in Python: a 60-line TRANSLATED_SELECT_STATES table of frozensets duplicating every entity.select.*.state key, a second _TRANSLATED_COURSE_TABLES table naming which course tables have translations, and a strings.json that was a 835-line byte-for-byte copy of translations/en.json save 43 [%key:...%] references. Each needed hand-syncing, and one was already drifting. Home Assistant loads exactly one file per language for a custom integration -- translations/<lang>.json. It never reads strings.json and never resolves [%key:...%]; both belong to Core's build tooling, which custom integrations don't run through (hassfest skips a missing strings.json and validates translations/en.json instead). So en.json is the source, and the new catalog.py reads the keys and states back out of it for the two decisions Python genuinely has to make: - select._display() normalizes a raw Samsung option to a lowercase state key only when the catalog knows it, else leaves the vendor's casing alone. Derived sets are identical to the removed literals. - laundry.cycle_select() keys off a device-reported course table only when that table has an entry, else falls back to the name-only 'cycle' key. Translating Table_00 is now a translations-only change. Also fixes six names that had already drifted between the Python descriptors and the catalog, restoring HA's sentence case for two generic ones (Auto release dry, Bubble soak) and taking the catalog's wording for the rest, and adds a test so the vestigial descriptor names can't silently disagree with the UI again. Claude-Session: https://claude.ai/code/session_01GiibJZZLWVvyxq7mc7EDNp
50 lines
2.3 KiB
Python
50 lines
2.3 KiB
Python
"""The integration's own shipped translation catalog, as data.
|
|
|
|
Home Assistant loads exactly one file per language for a custom integration:
|
|
``translations/<lang>.json``. It does not read ``strings.json`` and it does
|
|
not resolve ``[%key:...%]`` references -- both of those belong to Core's
|
|
build tooling (``script/translations``), which custom integrations never run
|
|
through. So ``translations/en.json`` is not a generated artifact here, it is
|
|
the source of truth, and English is the one catalog required to be complete
|
|
(hassfest validates it for custom integrations).
|
|
|
|
That makes the English catalog the authority on *which* translation keys and
|
|
states exist, which is something the Python side genuinely needs to know:
|
|
|
|
* ``select`` has to decide whether to normalize a raw Samsung option into
|
|
a lowercase state key (so Home Assistant can look it up) or leave the
|
|
vendor's own casing alone as a readable fallback.
|
|
* ``laundry.cycle_select`` has to decide whether a device-reported course
|
|
table has translations at all before keying off it.
|
|
|
|
Reading it from the catalog instead of restating it in Python means adding a
|
|
state or a course table is a one-file change, and the two can't drift.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
# Read at import, not lazily: custom integrations are imported in an executor
|
|
# thread, so this stays off the event loop no matter who asks first.
|
|
_ENTITY_CATALOG: dict[str, dict[str, dict]] = json.loads(
|
|
(Path(__file__).parent / 'translations' / 'en.json').read_text(encoding='utf-8')
|
|
).get('entity', {})
|
|
|
|
|
|
def has_entity_translation(platform: str, translation_key: str) -> bool:
|
|
"""Whether `platform`.`translation_key` has an entry in the catalog."""
|
|
return translation_key in _ENTITY_CATALOG.get(platform, {})
|
|
|
|
|
|
def translated_states(platform: str, translation_key: str) -> frozenset[str]:
|
|
"""The state keys translated for `platform`.`translation_key`.
|
|
|
|
Empty when the entity has no translation at all, and also when it has a
|
|
translated *name* but deliberately no state table (a course list whose
|
|
codes we can't map, for instance) -- callers treat both the same way:
|
|
leave the device's value untouched.
|
|
"""
|
|
entry = _ENTITY_CATALOG.get(platform, {}).get(translation_key)
|
|
return frozenset(entry.get('state', ())) if entry else frozenset()
|