mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
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
@ -43,7 +43,13 @@ from homeassistant.const import (
|
|||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
EntityCategory,
|
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 (
|
from homeassistant.exceptions import (
|
||||||
HomeAssistantError,
|
HomeAssistantError,
|
||||||
InvalidStateError,
|
InvalidStateError,
|
||||||
@ -245,6 +251,7 @@ class EntityDescription(metaclass=FrozenOrThawed, frozen_or_thawed=True):
|
|||||||
has_entity_name: bool = False
|
has_entity_name: bool = False
|
||||||
name: str | UndefinedType | None = UNDEFINED
|
name: str | UndefinedType | None = UNDEFINED
|
||||||
translation_key: str | None = None
|
translation_key: str | None = None
|
||||||
|
translation_placeholders: Mapping[str, str] | None = None
|
||||||
unit_of_measurement: str | None = None
|
unit_of_measurement: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@ -429,6 +436,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
|
|||||||
"state",
|
"state",
|
||||||
"supported_features",
|
"supported_features",
|
||||||
"translation_key",
|
"translation_key",
|
||||||
|
"translation_placeholders",
|
||||||
"unique_id",
|
"unique_id",
|
||||||
"unit_of_measurement",
|
"unit_of_measurement",
|
||||||
}
|
}
|
||||||
@ -473,6 +481,9 @@ class Entity(
|
|||||||
# If we reported this entity was added without its platform set
|
# If we reported this entity was added without its platform set
|
||||||
_no_platform_reported = False
|
_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
|
# Protect for multiple updates
|
||||||
_update_staged = False
|
_update_staged = False
|
||||||
|
|
||||||
@ -537,6 +548,7 @@ class Entity(
|
|||||||
_attr_state: StateType = STATE_UNKNOWN
|
_attr_state: StateType = STATE_UNKNOWN
|
||||||
_attr_supported_features: int | None = None
|
_attr_supported_features: int | None = None
|
||||||
_attr_translation_key: str | None
|
_attr_translation_key: str | None
|
||||||
|
_attr_translation_placeholders: Mapping[str, str]
|
||||||
_attr_unique_id: str | None = None
|
_attr_unique_id: str | None = None
|
||||||
_attr_unit_of_measurement: str | None
|
_attr_unit_of_measurement: str | None
|
||||||
|
|
||||||
@ -628,6 +640,29 @@ class Entity(
|
|||||||
f".{self.translation_key}.name"
|
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(
|
def _name_internal(
|
||||||
self,
|
self,
|
||||||
device_class_name: str | None,
|
device_class_name: str | None,
|
||||||
@ -643,7 +678,7 @@ class Entity(
|
|||||||
):
|
):
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
assert isinstance(name, str)
|
assert isinstance(name, str)
|
||||||
return name
|
return self._substitute_name_placeholders(name)
|
||||||
if hasattr(self, "entity_description"):
|
if hasattr(self, "entity_description"):
|
||||||
description_name = self.entity_description.name
|
description_name = self.entity_description.name
|
||||||
if description_name is UNDEFINED and self._default_to_device_class_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 self.entity_description.translation_key
|
||||||
return None
|
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
|
# DO NOT OVERWRITE
|
||||||
# These properties and methods are either managed by Home Assistant or they
|
# These properties and methods are either managed by Home Assistant or they
|
||||||
# are used to perform a very specific function. Overwriting these may
|
# are used to perform a very specific function. Overwriting these may
|
||||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Iterable, Mapping
|
from collections.abc import Iterable, Mapping
|
||||||
import logging
|
import logging
|
||||||
|
import string
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@ -242,6 +243,42 @@ class _TranslationCache:
|
|||||||
|
|
||||||
self.loaded[language].update(components)
|
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
|
@callback
|
||||||
def _build_category_cache(
|
def _build_category_cache(
|
||||||
self,
|
self,
|
||||||
@ -274,12 +311,14 @@ class _TranslationCache:
|
|||||||
).setdefault(category, {})
|
).setdefault(category, {})
|
||||||
|
|
||||||
if isinstance(resource, dict):
|
if isinstance(resource, dict):
|
||||||
category_cache.update(
|
resources_flatten = recursive_flatten(
|
||||||
recursive_flatten(
|
|
||||||
f"component.{component}.{category}.",
|
f"component.{component}.{category}.",
|
||||||
resource,
|
resource,
|
||||||
)
|
)
|
||||||
|
resources_flatten = self._validate_placeholders(
|
||||||
|
language, resources_flatten, category_cache
|
||||||
)
|
)
|
||||||
|
category_cache.update(resources_flatten)
|
||||||
else:
|
else:
|
||||||
category_cache[f"component.{component}.{category}"] = resource
|
category_cache[f"component.{component}.{category}"] = resource
|
||||||
|
|
||||||
|
@ -11,11 +11,12 @@
|
|||||||
'key': 'blah',
|
'key': 'blah',
|
||||||
'name': <UndefinedType._singleton: 0>,
|
'name': <UndefinedType._singleton: 0>,
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_entity_description_as_dataclass.1
|
# 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
|
# name: test_extending_entity_description
|
||||||
dict({
|
dict({
|
||||||
@ -30,11 +31,12 @@
|
|||||||
'key': 'blah',
|
'key': 'blah',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.1
|
# 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
|
# name: test_extending_entity_description.10
|
||||||
dict({
|
dict({
|
||||||
@ -50,11 +52,12 @@
|
|||||||
'mixin': 'mixin',
|
'mixin': 'mixin',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.11
|
# 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
|
# name: test_extending_entity_description.12
|
||||||
dict({
|
dict({
|
||||||
@ -70,11 +73,12 @@
|
|||||||
'mixin': 'mixin',
|
'mixin': 'mixin',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.13
|
# 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
|
# name: test_extending_entity_description.14
|
||||||
dict({
|
dict({
|
||||||
@ -90,11 +94,12 @@
|
|||||||
'mixin': 'mixin',
|
'mixin': 'mixin',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.15
|
# 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
|
# name: test_extending_entity_description.16
|
||||||
dict({
|
dict({
|
||||||
@ -110,11 +115,12 @@
|
|||||||
'mixin': 'mixin',
|
'mixin': 'mixin',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.17
|
# 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
|
# name: test_extending_entity_description.18
|
||||||
dict({
|
dict({
|
||||||
@ -130,11 +136,12 @@
|
|||||||
'mixin': 'mixin',
|
'mixin': 'mixin',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.19
|
# 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
|
# name: test_extending_entity_description.2
|
||||||
dict({
|
dict({
|
||||||
@ -149,6 +156,7 @@
|
|||||||
'key': 'blah',
|
'key': 'blah',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
@ -166,11 +174,12 @@
|
|||||||
'mixin': 'mixin',
|
'mixin': 'mixin',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.21
|
# 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
|
# name: test_extending_entity_description.22
|
||||||
dict({
|
dict({
|
||||||
@ -186,11 +195,12 @@
|
|||||||
'mixin': 'mixin',
|
'mixin': 'mixin',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.23
|
# 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
|
# name: test_extending_entity_description.24
|
||||||
dict({
|
dict({
|
||||||
@ -206,11 +216,12 @@
|
|||||||
'mixin': 'mixin',
|
'mixin': 'mixin',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.25
|
# 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
|
# name: test_extending_entity_description.26
|
||||||
dict({
|
dict({
|
||||||
@ -226,11 +237,12 @@
|
|||||||
'mixin': 'mixin',
|
'mixin': 'mixin',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.27
|
# 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
|
# name: test_extending_entity_description.28
|
||||||
dict({
|
dict({
|
||||||
@ -246,14 +258,15 @@
|
|||||||
'mixin': 'mixin',
|
'mixin': 'mixin',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.29
|
# 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
|
# 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
|
# name: test_extending_entity_description.30
|
||||||
dict({
|
dict({
|
||||||
@ -267,11 +280,12 @@
|
|||||||
'key': 'blah',
|
'key': 'blah',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.31
|
# 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
|
# name: test_extending_entity_description.4
|
||||||
dict({
|
dict({
|
||||||
@ -287,11 +301,12 @@
|
|||||||
'key': 'blah',
|
'key': 'blah',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.5
|
# 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
|
# name: test_extending_entity_description.6
|
||||||
dict({
|
dict({
|
||||||
@ -307,11 +322,12 @@
|
|||||||
'mixin': 'mixin',
|
'mixin': 'mixin',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.7
|
# 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
|
# name: test_extending_entity_description.8
|
||||||
dict({
|
dict({
|
||||||
@ -327,9 +343,10 @@
|
|||||||
'mixin': 'mixin',
|
'mixin': 'mixin',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'translation_key': None,
|
'translation_key': None,
|
||||||
|
'translation_placeholders': None,
|
||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_extending_entity_description.9
|
# 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(
|
@pytest.mark.parametrize(
|
||||||
("has_entity_name", "entity_name", "expected_friendly_name"),
|
("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(
|
async def test_get_translations(
|
||||||
hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None
|
hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None
|
||||||
) -> 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…
x
Reference in New Issue
Block a user