Add support for placeholders in entity name translations (#104453)
* add placeholder support to entity name translation * add negativ tests * make property also available via description * fix doc string in translation_placeholders() * fix detection of placeholder * validate placeholders for localized strings * add test * Cache translation_placeholders property * Make translation_placeholders uncondotionally return dict * Fall back to unsubstituted name in case of mismatch * Only replace failing translations with English * Update snapshots * Blow up on non stable releases * Fix test * Update entity.py --------- Co-authored-by: Erik <erik@montnemery.com> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
d071299233
commit
eb01998395
8 changed files with 425 additions and 24 deletions
|
@ -43,7 +43,13 @@ from homeassistant.const import (
|
|||
STATE_UNKNOWN,
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Context,
|
||||
HomeAssistant,
|
||||
callback,
|
||||
get_release_channel,
|
||||
)
|
||||
from homeassistant.exceptions import (
|
||||
HomeAssistantError,
|
||||
InvalidStateError,
|
||||
|
@ -245,6 +251,7 @@ class EntityDescription(metaclass=FrozenOrThawed, frozen_or_thawed=True):
|
|||
has_entity_name: bool = False
|
||||
name: str | UndefinedType | None = UNDEFINED
|
||||
translation_key: str | None = None
|
||||
translation_placeholders: Mapping[str, str] | None = None
|
||||
unit_of_measurement: str | None = None
|
||||
|
||||
|
||||
|
@ -429,6 +436,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
|
|||
"state",
|
||||
"supported_features",
|
||||
"translation_key",
|
||||
"translation_placeholders",
|
||||
"unique_id",
|
||||
"unit_of_measurement",
|
||||
}
|
||||
|
@ -473,6 +481,9 @@ class Entity(
|
|||
# If we reported this entity was added without its platform set
|
||||
_no_platform_reported = False
|
||||
|
||||
# If we reported the name translation placeholders do not match the name
|
||||
_name_translation_placeholders_reported = False
|
||||
|
||||
# Protect for multiple updates
|
||||
_update_staged = False
|
||||
|
||||
|
@ -537,6 +548,7 @@ class Entity(
|
|||
_attr_state: StateType = STATE_UNKNOWN
|
||||
_attr_supported_features: int | None = None
|
||||
_attr_translation_key: str | None
|
||||
_attr_translation_placeholders: Mapping[str, str]
|
||||
_attr_unique_id: str | None = None
|
||||
_attr_unit_of_measurement: str | None
|
||||
|
||||
|
@ -628,6 +640,29 @@ class Entity(
|
|||
f".{self.translation_key}.name"
|
||||
)
|
||||
|
||||
def _substitute_name_placeholders(self, name: str) -> str:
|
||||
"""Substitute placeholders in entity name."""
|
||||
try:
|
||||
return name.format(**self.translation_placeholders)
|
||||
except KeyError as err:
|
||||
if not self._name_translation_placeholders_reported:
|
||||
if get_release_channel() != "stable":
|
||||
raise HomeAssistantError("Missing placeholder %s" % err) from err
|
||||
report_issue = self._suggest_report_issue()
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Entity %s (%s) has translation placeholders '%s' which do not "
|
||||
"match the name '%s', please %s"
|
||||
),
|
||||
self.entity_id,
|
||||
type(self),
|
||||
self.translation_placeholders,
|
||||
name,
|
||||
report_issue,
|
||||
)
|
||||
self._name_translation_placeholders_reported = True
|
||||
return name
|
||||
|
||||
def _name_internal(
|
||||
self,
|
||||
device_class_name: str | None,
|
||||
|
@ -643,7 +678,7 @@ class Entity(
|
|||
):
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(name, str)
|
||||
return name
|
||||
return self._substitute_name_placeholders(name)
|
||||
if hasattr(self, "entity_description"):
|
||||
description_name = self.entity_description.name
|
||||
if description_name is UNDEFINED and self._default_to_device_class_name():
|
||||
|
@ -853,6 +888,16 @@ class Entity(
|
|||
return self.entity_description.translation_key
|
||||
return None
|
||||
|
||||
@final
|
||||
@cached_property
|
||||
def translation_placeholders(self) -> Mapping[str, str]:
|
||||
"""Return the translation placeholders for translated entity's name."""
|
||||
if hasattr(self, "_attr_translation_placeholders"):
|
||||
return self._attr_translation_placeholders
|
||||
if hasattr(self, "entity_description"):
|
||||
return self.entity_description.translation_placeholders or {}
|
||||
return {}
|
||||
|
||||
# DO NOT OVERWRITE
|
||||
# These properties and methods are either managed by Home Assistant or they
|
||||
# are used to perform a very specific function. Overwriting these may
|
||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
from collections.abc import Iterable, Mapping
|
||||
import logging
|
||||
import string
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
@ -242,6 +243,42 @@ class _TranslationCache:
|
|||
|
||||
self.loaded[language].update(components)
|
||||
|
||||
def _validate_placeholders(
|
||||
self,
|
||||
language: str,
|
||||
updated_resources: dict[str, Any],
|
||||
cached_resources: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Validate if updated resources have same placeholders as cached resources."""
|
||||
if cached_resources is None:
|
||||
return updated_resources
|
||||
|
||||
mismatches: set[str] = set()
|
||||
|
||||
for key, value in updated_resources.items():
|
||||
if key not in cached_resources:
|
||||
continue
|
||||
tuples = list(string.Formatter().parse(value))
|
||||
updated_placeholders = {tup[1] for tup in tuples if tup[1] is not None}
|
||||
|
||||
tuples = list(string.Formatter().parse(cached_resources[key]))
|
||||
cached_placeholders = {tup[1] for tup in tuples if tup[1] is not None}
|
||||
if updated_placeholders != cached_placeholders:
|
||||
_LOGGER.error(
|
||||
(
|
||||
"Validation of translation placeholders for localized (%s) string "
|
||||
"%s failed"
|
||||
),
|
||||
language,
|
||||
key,
|
||||
)
|
||||
mismatches.add(key)
|
||||
|
||||
for mismatch in mismatches:
|
||||
del updated_resources[mismatch]
|
||||
|
||||
return updated_resources
|
||||
|
||||
@callback
|
||||
def _build_category_cache(
|
||||
self,
|
||||
|
@ -274,12 +311,14 @@ class _TranslationCache:
|
|||
).setdefault(category, {})
|
||||
|
||||
if isinstance(resource, dict):
|
||||
category_cache.update(
|
||||
recursive_flatten(
|
||||
f"component.{component}.{category}.",
|
||||
resource,
|
||||
)
|
||||
resources_flatten = recursive_flatten(
|
||||
f"component.{component}.{category}.",
|
||||
resource,
|
||||
)
|
||||
resources_flatten = self._validate_placeholders(
|
||||
language, resources_flatten, category_cache
|
||||
)
|
||||
category_cache.update(resources_flatten)
|
||||
else:
|
||||
category_cache[f"component.{component}.{category}"] = resource
|
||||
|
||||
|
|
|
@ -11,11 +11,12 @@
|
|||
'key': 'blah',
|
||||
'name': <UndefinedType._singleton: 0>,
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_description_as_dataclass.1
|
||||
"EntityDescription(key='blah', device_class='test', entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name=<UndefinedType._singleton: 0>, translation_key=None, unit_of_measurement=None)"
|
||||
"EntityDescription(key='blah', device_class='test', entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name=<UndefinedType._singleton: 0>, translation_key=None, translation_placeholders=None, unit_of_measurement=None)"
|
||||
# ---
|
||||
# name: test_extending_entity_description
|
||||
dict({
|
||||
|
@ -30,11 +31,12 @@
|
|||
'key': 'blah',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.1
|
||||
"test_extending_entity_description.<locals>.FrozenEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
|
||||
"test_extending_entity_description.<locals>.FrozenEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extra='foo')"
|
||||
# ---
|
||||
# name: test_extending_entity_description.10
|
||||
dict({
|
||||
|
@ -50,11 +52,12 @@
|
|||
'mixin': 'mixin',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.11
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription1C(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription1C(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extra='foo')"
|
||||
# ---
|
||||
# name: test_extending_entity_description.12
|
||||
dict({
|
||||
|
@ -70,11 +73,12 @@
|
|||
'mixin': 'mixin',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.13
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription1D(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription1D(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extra='foo')"
|
||||
# ---
|
||||
# name: test_extending_entity_description.14
|
||||
dict({
|
||||
|
@ -90,11 +94,12 @@
|
|||
'mixin': 'mixin',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.15
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription2A(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription2A(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extra='foo')"
|
||||
# ---
|
||||
# name: test_extending_entity_description.16
|
||||
dict({
|
||||
|
@ -110,11 +115,12 @@
|
|||
'mixin': 'mixin',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.17
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription2B(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription2B(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extra='foo')"
|
||||
# ---
|
||||
# name: test_extending_entity_description.18
|
||||
dict({
|
||||
|
@ -130,11 +136,12 @@
|
|||
'mixin': 'mixin',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.19
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription2C(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription2C(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extra='foo')"
|
||||
# ---
|
||||
# name: test_extending_entity_description.2
|
||||
dict({
|
||||
|
@ -149,6 +156,7 @@
|
|||
'key': 'blah',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
|
@ -166,11 +174,12 @@
|
|||
'mixin': 'mixin',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.21
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription2D(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription2D(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extra='foo')"
|
||||
# ---
|
||||
# name: test_extending_entity_description.22
|
||||
dict({
|
||||
|
@ -186,11 +195,12 @@
|
|||
'mixin': 'mixin',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.23
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription3A(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription3A(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extra='foo')"
|
||||
# ---
|
||||
# name: test_extending_entity_description.24
|
||||
dict({
|
||||
|
@ -206,11 +216,12 @@
|
|||
'mixin': 'mixin',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.25
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription3B(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription3B(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extra='foo')"
|
||||
# ---
|
||||
# name: test_extending_entity_description.26
|
||||
dict({
|
||||
|
@ -226,11 +237,12 @@
|
|||
'mixin': 'mixin',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.27
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription4A(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription4A(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extra='foo')"
|
||||
# ---
|
||||
# name: test_extending_entity_description.28
|
||||
dict({
|
||||
|
@ -246,14 +258,15 @@
|
|||
'mixin': 'mixin',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.29
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription4B(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription4B(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extra='foo')"
|
||||
# ---
|
||||
# name: test_extending_entity_description.3
|
||||
"test_extending_entity_description.<locals>.ThawedEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
|
||||
"test_extending_entity_description.<locals>.ThawedEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extra='foo')"
|
||||
# ---
|
||||
# name: test_extending_entity_description.30
|
||||
dict({
|
||||
|
@ -267,11 +280,12 @@
|
|||
'key': 'blah',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.31
|
||||
"test_extending_entity_description.<locals>.CustomInitEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None)"
|
||||
"test_extending_entity_description.<locals>.CustomInitEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None)"
|
||||
# ---
|
||||
# name: test_extending_entity_description.4
|
||||
dict({
|
||||
|
@ -287,11 +301,12 @@
|
|||
'key': 'blah',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.5
|
||||
"test_extending_entity_description.<locals>.MyExtendedEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extension='ext', extra='foo')"
|
||||
"test_extending_entity_description.<locals>.MyExtendedEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extension='ext', extra='foo')"
|
||||
# ---
|
||||
# name: test_extending_entity_description.6
|
||||
dict({
|
||||
|
@ -307,11 +322,12 @@
|
|||
'mixin': 'mixin',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.7
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription1A(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription1A(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extra='foo')"
|
||||
# ---
|
||||
# name: test_extending_entity_description.8
|
||||
dict({
|
||||
|
@ -327,9 +343,10 @@
|
|||
'mixin': 'mixin',
|
||||
'name': 'name',
|
||||
'translation_key': None,
|
||||
'translation_placeholders': None,
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_extending_entity_description.9
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription1B(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
|
||||
"test_extending_entity_description.<locals>.ComplexEntityDescription1B(mixin='mixin', key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, translation_placeholders=None, unit_of_measurement=None, extra='foo')"
|
||||
# ---
|
||||
|
|
|
@ -1137,6 +1137,203 @@ async def test_friendly_name_description_device_class_name(
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"has_entity_name",
|
||||
"translation_key",
|
||||
"translations",
|
||||
"placeholders",
|
||||
"expected_friendly_name",
|
||||
),
|
||||
(
|
||||
(False, None, None, None, "Entity Blu"),
|
||||
(True, None, None, None, "Device Bla Entity Blu"),
|
||||
(
|
||||
True,
|
||||
"test_entity",
|
||||
{
|
||||
"en": {
|
||||
"component.test.entity.test_domain.test_entity.name": "English ent"
|
||||
},
|
||||
},
|
||||
None,
|
||||
"Device Bla English ent",
|
||||
),
|
||||
(
|
||||
True,
|
||||
"test_entity",
|
||||
{
|
||||
"en": {
|
||||
"component.test.entity.test_domain.test_entity.name": "{placeholder} English ent"
|
||||
},
|
||||
},
|
||||
{"placeholder": "special"},
|
||||
"Device Bla special English ent",
|
||||
),
|
||||
(
|
||||
True,
|
||||
"test_entity",
|
||||
{
|
||||
"en": {
|
||||
"component.test.entity.test_domain.test_entity.name": "English ent {placeholder}"
|
||||
},
|
||||
},
|
||||
{"placeholder": "special"},
|
||||
"Device Bla English ent special",
|
||||
),
|
||||
),
|
||||
)
|
||||
async def test_entity_name_translation_placeholders(
|
||||
hass: HomeAssistant,
|
||||
has_entity_name: bool,
|
||||
translation_key: str | None,
|
||||
translations: dict[str, str] | None,
|
||||
placeholders: dict[str, str] | None,
|
||||
expected_friendly_name: str | None,
|
||||
) -> None:
|
||||
"""Test friendly name when the entity name translation has placeholders."""
|
||||
|
||||
async def async_get_translations(
|
||||
hass: HomeAssistant,
|
||||
language: str,
|
||||
category: str,
|
||||
integrations: Iterable[str] | None = None,
|
||||
config_flow: bool | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return all backend translations."""
|
||||
return translations[language]
|
||||
|
||||
ent = MockEntity(
|
||||
unique_id="qwer",
|
||||
device_info={
|
||||
"identifiers": {("hue", "1234")},
|
||||
"connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")},
|
||||
"name": "Device Bla",
|
||||
},
|
||||
)
|
||||
ent.entity_description = entity.EntityDescription(
|
||||
"test",
|
||||
has_entity_name=has_entity_name,
|
||||
translation_key=translation_key,
|
||||
name="Entity Blu",
|
||||
)
|
||||
if placeholders is not None:
|
||||
ent._attr_translation_placeholders = placeholders
|
||||
with patch(
|
||||
"homeassistant.helpers.entity_platform.translation.async_get_translations",
|
||||
side_effect=async_get_translations,
|
||||
):
|
||||
await _test_friendly_name(hass, ent, expected_friendly_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"translation_key",
|
||||
"translations",
|
||||
"placeholders",
|
||||
"release_channel",
|
||||
"expected_error",
|
||||
),
|
||||
(
|
||||
(
|
||||
"test_entity",
|
||||
{
|
||||
"en": {
|
||||
"component.test.entity.test_domain.test_entity.name": "{placeholder} English ent {2ndplaceholder}"
|
||||
},
|
||||
},
|
||||
{"placeholder": "special"},
|
||||
"stable",
|
||||
(
|
||||
"has translation placeholders '{'placeholder': 'special'}' which do "
|
||||
"not match the name '{placeholder} English ent {2ndplaceholder}'"
|
||||
),
|
||||
),
|
||||
(
|
||||
"test_entity",
|
||||
{
|
||||
"en": {
|
||||
"component.test.entity.test_domain.test_entity.name": "{placeholder} English ent {2ndplaceholder}"
|
||||
},
|
||||
},
|
||||
{"placeholder": "special"},
|
||||
"beta",
|
||||
"HomeAssistantError: Missing placeholder '2ndplaceholder'",
|
||||
),
|
||||
(
|
||||
"test_entity",
|
||||
{
|
||||
"en": {
|
||||
"component.test.entity.test_domain.test_entity.name": "{placeholder} English ent"
|
||||
},
|
||||
},
|
||||
None,
|
||||
"stable",
|
||||
(
|
||||
"has translation placeholders '{}' which do "
|
||||
"not match the name '{placeholder} English ent'"
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
async def test_entity_name_translation_placeholder_errors(
|
||||
hass: HomeAssistant,
|
||||
translation_key: str | None,
|
||||
translations: dict[str, str] | None,
|
||||
placeholders: dict[str, str] | None,
|
||||
release_channel: str,
|
||||
expected_error: str,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test entity name translation has placeholder issues."""
|
||||
|
||||
async def async_get_translations(
|
||||
hass: HomeAssistant,
|
||||
language: str,
|
||||
category: str,
|
||||
integrations: Iterable[str] | None = None,
|
||||
config_flow: bool | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return all backend translations."""
|
||||
return translations[language]
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Mock setup entry method."""
|
||||
async_add_entities([ent])
|
||||
return True
|
||||
|
||||
ent = MockEntity(
|
||||
unique_id="qwer",
|
||||
)
|
||||
ent.entity_description = entity.EntityDescription(
|
||||
"test",
|
||||
has_entity_name=True,
|
||||
translation_key=translation_key,
|
||||
name="Entity Blu",
|
||||
)
|
||||
if placeholders is not None:
|
||||
ent._attr_translation_placeholders = placeholders
|
||||
|
||||
platform = MockPlatform(async_setup_entry=async_setup_entry)
|
||||
config_entry = MockConfigEntry(entry_id="super-mock-id")
|
||||
config_entry.add_to_hass(hass)
|
||||
entity_platform = MockEntityPlatform(
|
||||
hass, platform_name=config_entry.domain, platform=platform
|
||||
)
|
||||
|
||||
caplog.clear()
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.entity_platform.translation.async_get_translations",
|
||||
side_effect=async_get_translations,
|
||||
), patch(
|
||||
"homeassistant.helpers.entity.get_release_channel", return_value=release_channel
|
||||
):
|
||||
await entity_platform.async_setup_entry(config_entry)
|
||||
|
||||
assert expected_error in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("has_entity_name", "entity_name", "expected_friendly_name"),
|
||||
(
|
||||
|
|
|
@ -98,6 +98,77 @@ def test_load_translations_files(hass: HomeAssistant) -> None:
|
|||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("language", "expected_translation", "expected_errors"),
|
||||
(
|
||||
(
|
||||
"en",
|
||||
{
|
||||
"component.test.entity.switch.other1.name": "Other 1",
|
||||
"component.test.entity.switch.other2.name": "Other 2",
|
||||
"component.test.entity.switch.other3.name": "Other 3",
|
||||
"component.test.entity.switch.other4.name": "Other 4",
|
||||
"component.test.entity.switch.outlet.name": "Outlet {placeholder}",
|
||||
},
|
||||
[],
|
||||
),
|
||||
(
|
||||
"es",
|
||||
{
|
||||
"component.test.entity.switch.other1.name": "Otra 1",
|
||||
"component.test.entity.switch.other2.name": "Otra 2",
|
||||
"component.test.entity.switch.other3.name": "Otra 3",
|
||||
"component.test.entity.switch.other4.name": "Otra 4",
|
||||
"component.test.entity.switch.outlet.name": "Enchufe {placeholder}",
|
||||
},
|
||||
[],
|
||||
),
|
||||
(
|
||||
"de",
|
||||
{
|
||||
# Correct
|
||||
"component.test.entity.switch.other1.name": "Anderes 1",
|
||||
# Translation has placeholder missing in English
|
||||
"component.test.entity.switch.other2.name": "Other 2",
|
||||
# Correct (empty translation)
|
||||
"component.test.entity.switch.other3.name": "",
|
||||
# Translation missing
|
||||
"component.test.entity.switch.other4.name": "Other 4",
|
||||
# Mismatch in placeholders
|
||||
"component.test.entity.switch.outlet.name": "Outlet {placeholder}",
|
||||
},
|
||||
[
|
||||
"component.test.entity.switch.other2.name",
|
||||
"component.test.entity.switch.outlet.name",
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
async def test_load_translations_files_invalid_localized_placeholders(
|
||||
hass: HomeAssistant,
|
||||
enable_custom_integrations: None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
language: str,
|
||||
expected_translation: dict,
|
||||
expected_errors: bool,
|
||||
) -> None:
|
||||
"""Test the load translation files with invalid localized placeholders."""
|
||||
caplog.clear()
|
||||
translations = await translation.async_get_translations(
|
||||
hass, language, "entity", ["test"]
|
||||
)
|
||||
assert translations == expected_translation
|
||||
|
||||
assert ("Validation of translation placeholders" in caplog.text) == (
|
||||
len(expected_errors) > 0
|
||||
)
|
||||
for expected_error in expected_errors:
|
||||
assert (
|
||||
f"Validation of translation placeholders for localized ({language}) string {expected_error} failed"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_get_translations(
|
||||
hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None
|
||||
) -> None:
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"entity": {
|
||||
"switch": {
|
||||
"other1": { "name": "Anderes 1" },
|
||||
"other2": { "name": "Anderes 2 {placeholder}" },
|
||||
"other3": { "name": "" },
|
||||
"outlet": { "name": "Steckdose {something}" }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"entity": {
|
||||
"switch": {
|
||||
"other1": { "name": "Other 1" },
|
||||
"other2": { "name": "Other 2" },
|
||||
"other3": { "name": "Other 3" },
|
||||
"other4": { "name": "Other 4" },
|
||||
"outlet": { "name": "Outlet {placeholder}" }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"entity": {
|
||||
"switch": {
|
||||
"other1": { "name": "Otra 1" },
|
||||
"other2": { "name": "Otra 2" },
|
||||
"other3": { "name": "Otra 3" },
|
||||
"other4": { "name": "Otra 4" },
|
||||
"outlet": { "name": "Enchufe {placeholder}" }
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue