Files
localthings/custom_components/localthings/catalog.py
T
Marc Billow 6281b40549 refactor(i18n): make the shipped catalog the single source of truth
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
2026-07-24 19:32:09 +00:00

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()