refactor(i18n): drop the vestigial descriptor name field

Since entity.py started routing named descriptors through the catalog
under desc.key, SamsungEntityDescription.name has been read for its
value nowhere -- only twice as a flag, to decide whether an entity was
translated at all. That left 148 English names duplicated between Python
and translations/en.json with nothing keeping them honest: six had
already drifted, invisibly, because editing the Python side changes
nothing a user sees.

So the field is gone, and translation_key defaults to desc.key. A
descriptor now sets translation_key only to share one catalog entry
across descriptors or to point at a differently-named one, and the
catalog is the only place an entity name exists.

Every descriptor resolves to exactly the translation key, icon, entity
category, enabled-default and gating it did before -- with one
deliberate exception: the hood fan, previously the sole descriptor with
no key at all, now resolves to 'fan'. That is inert, because fan.py sets
_attr_name = None so the entity presents as the device itself.

The three helpers that forwarded a name into a descriptor
(laundry.bool_option_switch, washer._bool_option_switch, air_purifier's
sensor table) lose that parameter. test_translations.py now requires a
catalog entry for every descriptor rather than only translated ones.

Claude-Session: https://claude.ai/code/session_01GiibJZZLWVvyxq7mc7EDNp
This commit is contained in:
Marc Billow committed 2026-07-24 19:45:37 +00:00
1 parent 6281b40549
commit 10c850f2b9
20 files changed
+241 -267

No files matched your search

+7 -3
View File
@@ -20,9 +20,13 @@ def _make_entity(desc, href='/x/vs/0', key_override=None, instance='', instance_
return LocalThingsEntity(_FakeCoordinator(), bound)
def test_explicit_fallback_name_uses_translation_instead_of_attr_name():
"""An _attr_name would take precedence over HA's translation catalog."""
desc = BinarySensorDesc(key='enabled', name='Explicit name')
def test_descriptor_key_is_the_default_translation_key():
"""A descriptor names itself through the catalog, keyed by its own key.
Nothing sets _attr_name: that would take precedence over HA's
translation catalog and make the entity untranslatable.
"""
desc = BinarySensorDesc(key='enabled')
entity = _make_entity(desc, instance_name='Cubed Ice')
assert entity.translation_key == 'enabled'
assert not hasattr(entity, '_attr_name')
+1 -1
View File
@@ -89,7 +89,7 @@ async def test_unknown_vendor_option_round_trips_to_exact_raw_value():
self.writes.append(value)
desc = SelectDesc(
key='mode', name='Mode', translation_key='door_alert',
key='mode', translation_key='door_alert',
options=('Known', 'FutureVendorMode'), write_fn=lambda *args: None,
)
capability = Capability(href='/x/vs/0', entities=(desc,))
+16 -26
View File
@@ -125,21 +125,32 @@ def test_no_catalog_carries_unresolved_core_references():
assert unresolved == [], language
def test_every_translatable_descriptor_has_an_entity_catalog_entry():
# The hood fan is its device's primary feature: fan.py sets _attr_name = None
# so it presents as the device itself, and never reads a catalog name.
UNNAMED_DESCRIPTORS = {("fan", "fan")}
def test_every_descriptor_has_an_entity_catalog_entry():
"""A descriptor's name comes from the catalog or nowhere.
``translation_key`` defaults to ``desc.key`` (entity.py), and there is no
Python-side name to fall back on, so a descriptor with no catalog entry
is an entity with no name.
"""
entity_strings = _load("en")["entity"]
missing = []
for desc in _all_descriptions():
platform = PLATFORM_OF[type(desc)]
if (platform, desc.key) in UNNAMED_DESCRIPTORS:
continue
translation_key = desc.translation_key
if callable(translation_key):
# Runtime table resolvers pick their key out of the catalog
# itself (see laundry.cycle_select), so there's nothing static
# to check here; the generic 'cycle' fallback is asserted below.
continue
if translation_key is None and desc.name is not None:
translation_key = desc.key
if translation_key is None:
continue # Main fan entity: device name + HA fan translations.
platform = PLATFORM_OF[type(desc)]
translation_key = desc.key
if translation_key not in entity_strings.get(platform, {}):
missing.append((platform, desc.key, translation_key))
assert missing == []
@@ -151,27 +162,6 @@ def test_every_translatable_descriptor_has_an_entity_catalog_entry():
assert key in select_strings
def test_descriptor_names_match_the_english_catalog():
"""``SamsungEntityDescription.name`` never reaches the UI.
A descriptor carrying a ``name`` is translated under ``desc.key``
(entity.py), so the catalog wins and the Python string survives purely
as documentation next to the field it describes -- which is only worth
keeping if it still says what the UI says. Editing one side alone is
otherwise invisible.
"""
entity_strings = _load("en")["entity"]
drifted = []
for desc in _all_descriptions():
if desc.name is None or desc.translation_key is not None:
continue
platform = PLATFORM_OF[type(desc)]
catalog_name = entity_strings.get(platform, {}).get(desc.key, {}).get("name")
if catalog_name != desc.name:
drifted.append((platform, desc.key, desc.name, catalog_name))
assert drifted == []
def test_all_entity_state_translation_keys_are_lowercase():
entity_strings = _load("en")["entity"]
for platform in entity_strings.values():