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:
Michael 2024-01-03 17:34:47 +01:00 committed by GitHub
parent d071299233
commit eb01998395
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 425 additions and 24 deletions

View file

@ -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

View file

@ -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

View file

@ -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')"
# ---

View file

@ -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"),
(

View file

@ -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:

View file

@ -0,0 +1,10 @@
{
"entity": {
"switch": {
"other1": { "name": "Anderes 1" },
"other2": { "name": "Anderes 2 {placeholder}" },
"other3": { "name": "" },
"outlet": { "name": "Steckdose {something}" }
}
}
}

View file

@ -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}" }
}
}
}

View file

@ -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}" }
}
}
}