Base entity ids on English for languages not using Latin script (#91357)

This commit is contained in:
Erik Montnemery 2023-06-27 14:37:50 +02:00 committed by GitHub
parent fe28067481
commit 071d3a474f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 350 additions and 26 deletions

View file

@ -3,6 +3,8 @@
To update, run python3 -m script.languages [frontend_tag]
"""
DEFAULT_LANGUAGE = "en"
LANGUAGES = {
"af",
"ar",
@ -66,3 +68,46 @@ LANGUAGES = {
"zh-Hans",
"zh-Hant",
}
NATIVE_ENTITY_IDS = {
"af",
"bs",
"ca",
"cs",
"cy",
"da",
"de",
"en",
"en-GB",
"eo",
"es",
"es-419",
"et",
"eu",
"fi",
"fr",
"fy",
"gl",
"gsw",
"hr",
"hu",
"id",
"is",
"it",
"ka",
"lb",
"lt",
"lv",
"nb",
"nl",
"nn",
"pl",
"pt",
"pt-BR",
"ro",
"sk",
"sl",
"sr-Latn",
"sv",
"tr",
}

View file

@ -381,17 +381,31 @@ class Entity(ABC):
return self.entity_description.has_entity_name
return False
@cached_property
def _device_class_name(self) -> str | None:
def _device_class_name_helper(
self,
component_translations: dict[str, Any],
) -> str | None:
"""Return a translated name of the entity based on its device class."""
if not self.has_entity_name:
return None
device_class_key = self.device_class or "_"
platform = self.platform
name_translation_key = (
f"component.{platform.domain}.entity_component." f"{device_class_key}.name"
f"component.{platform.domain}.entity_component.{device_class_key}.name"
)
return platform.component_translations.get(name_translation_key)
return component_translations.get(name_translation_key)
@cached_property
def _object_id_device_class_name(self) -> str | None:
"""Return a translated name of the entity based on its device class."""
return self._device_class_name_helper(
self.platform.object_id_component_translations
)
@cached_property
def _device_class_name(self) -> str | None:
"""Return a translated name of the entity based on its device class."""
return self._device_class_name_helper(self.platform.component_translations)
def _default_to_device_class_name(self) -> bool:
"""Return True if an unnamed entity should be named by its device class."""
@ -408,15 +422,18 @@ class Entity(ABC):
f".{self.translation_key}.name"
)
@property
def name(self) -> str | UndefinedType | None:
def _name_internal(
self,
device_class_name: str | None,
platform_translations: dict[str, Any],
) -> str | UndefinedType | None:
"""Return the name of the entity."""
if hasattr(self, "_attr_name"):
return self._attr_name
if (
self.has_entity_name
and (name_translation_key := self._name_translation_key)
and (name := self.platform.platform_translations.get(name_translation_key))
and (name := platform_translations.get(name_translation_key))
):
if TYPE_CHECKING:
assert isinstance(name, str)
@ -424,15 +441,42 @@ class Entity(ABC):
if hasattr(self, "entity_description"):
description_name = self.entity_description.name
if description_name is UNDEFINED and self._default_to_device_class_name():
return self._device_class_name
return device_class_name
return description_name
# The entity has no name set by _attr_name, translation_key or entity_description
# Check if the entity should be named by its device class
if self._default_to_device_class_name():
return self._device_class_name
return device_class_name
return UNDEFINED
@property
def suggested_object_id(self) -> str | None:
"""Return input for object id."""
# The check for self.platform guards against integrations not using an
# EntityComponent and can be removed in HA Core 2024.1
# mypy doesn't know about fget: https://github.com/python/mypy/issues/6185
if self.__class__.name.fget is Entity.name.fget and self.platform: # type: ignore[attr-defined]
name = self._name_internal(
self._object_id_device_class_name,
self.platform.object_id_platform_translations,
)
else:
name = self.name
return None if name is UNDEFINED else name
@property
def name(self) -> str | UndefinedType | None:
"""Return the name of the entity."""
# The check for self.platform guards against integrations not using an
# EntityComponent and can be removed in HA Core 2024.1
if not self.platform:
return self._name_internal(None, {})
return self._name_internal(
self._device_class_name,
self.platform.platform_translations,
)
@property
def state(self) -> StateType:
"""Return the state of the entity."""

View file

@ -31,6 +31,7 @@ from homeassistant.exceptions import (
PlatformNotReady,
RequiredParameterMissing,
)
from homeassistant.generated import languages
from homeassistant.setup import async_start_setup
from homeassistant.util.async_ import run_callback_threadsafe
@ -128,6 +129,8 @@ class EntityPlatform:
self.entities: dict[str, Entity] = {}
self.component_translations: dict[str, Any] = {}
self.platform_translations: dict[str, Any] = {}
self.object_id_component_translations: dict[str, Any] = {}
self.object_id_platform_translations: dict[str, Any] = {}
self._tasks: list[asyncio.Task[None]] = []
# Stop tracking tasks after setup is completed
self._setup_complete = False
@ -294,22 +297,43 @@ class EntityPlatform:
logger = self.logger
hass = self.hass
full_name = f"{self.domain}.{self.platform_name}"
object_id_language = (
hass.config.language
if hass.config.language in languages.NATIVE_ENTITY_IDS
else languages.DEFAULT_LANGUAGE
)
try:
self.component_translations = await translation.async_get_translations(
hass, hass.config.language, "entity_component", {self.domain}
async def get_translations(
language: str, category: str, integration: str
) -> dict[str, Any]:
"""Get entity translations."""
try:
return await translation.async_get_translations(
hass, language, category, {integration}
)
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.debug(
"Could not load translations for %s",
integration,
exc_info=err,
)
return {}
self.component_translations = await get_translations(
hass.config.language, "entity_component", self.domain
)
self.platform_translations = await get_translations(
hass.config.language, "entity", self.platform_name
)
if object_id_language == hass.config.language:
self.object_id_component_translations = self.component_translations
self.object_id_platform_translations = self.platform_translations
else:
self.object_id_component_translations = await get_translations(
object_id_language, "entity_component", self.domain
)
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.debug(
"Could not load translations for %s", self.domain, exc_info=err
)
try:
self.platform_translations = await translation.async_get_translations(
hass, hass.config.language, "entity", {self.platform_name}
)
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.debug(
"Could not load translations for %s", self.platform_name, exc_info=err
self.object_id_platform_translations = await get_translations(
object_id_language, "entity", self.platform_name
)
logger.info("Setting up %s", full_name)
@ -652,9 +676,11 @@ class EntityPlatform:
if entity.use_device_name:
suggested_object_id = device_name
else:
suggested_object_id = f"{device_name} {entity_name}"
suggested_object_id = (
f"{device_name} {entity.suggested_object_id}"
)
if not suggested_object_id:
suggested_object_id = entity_name
suggested_object_id = entity.suggested_object_id
if self.entity_namespace is not None:
suggested_object_id = f"{self.entity_namespace} {suggested_object_id}"
@ -709,7 +735,7 @@ class EntityPlatform:
# Generate entity ID
if entity.entity_id is None or generate_new_entity_id:
suggested_object_id = (
suggested_object_id or entity_name or DEVICE_DEFAULT_NAME
suggested_object_id or entity.suggested_object_id or DEVICE_DEFAULT_NAME
)
if self.entity_namespace is not None:

View file

@ -15,10 +15,61 @@ req = requests.get(
data = json.loads(req.content)
languages = set(data.keys())
# Languages which can be used for entity IDs.
# Languages in the set are those which use a writing system based on the Latin
# script. Languages not in this set will instead base the entity ID on English.
# Note: Although vietnamese writing is based on the Latin script, it's too ambiguous
# after accents and diacritics have been removed by slugify
NATIVE_ENTITY_IDS = {
"af", # Afrikaans
"bs", # Bosanski
"ca", # Català
"cs", # Čeština
"cy", # Cymraeg
"da", # Dansk
"de", # Deutsch
"en", # English
"en-GB", # English (GB)
"eo", # Esperanto
"es", # Español
"es-419", # Español (Latin America)
"et", # Eesti
"eu", # Euskara
"fi", # Suomi
"fr", # Français
"fy", # Frysk
"gl", # Galego
"gsw", # Schwiizerdütsch
"hr", # Hrvatski
"hu", # Magyar
"id", # Indonesia
"is", # Íslenska
"it", # Italiano
"ka", # Kartuli
"lb", # Lëtzebuergesch
"lt", # Lietuvių
"lv", # Latviešu
"nb", # Nederlands
"nl", # Norsk Bokmål
"nn", # Norsk Nynorsk"
"pl", # Polski
"pt", # Português
"pt-BR", # Português (BR)
"ro", # Română
"sk", # Slovenčina
"sl", # Slovenščina
"sr-Latn", # Srpski
"sv", # Svenska
"tr", # Türkçe
}
Path("homeassistant/generated/languages.py").write_text(
format_python_namespace(
{
"DEFAULT_LANGUAGE": "en",
"LANGUAGES": languages,
"NATIVE_ENTITY_IDS": NATIVE_ENTITY_IDS,
},
generator="script.languages [frontend_tag]",
)

View file

@ -1,5 +1,6 @@
"""Tests for the EntityPlatform helper."""
import asyncio
from collections.abc import Iterable
from datetime import timedelta
import logging
from typing import Any
@ -18,6 +19,7 @@ from homeassistant.helpers import (
)
from homeassistant.helpers.entity import (
DeviceInfo,
Entity,
EntityCategory,
async_generate_entity_id,
)
@ -1669,3 +1671,159 @@ async def test_entity_name_influences_entity_id(
assert len(hass.states.async_entity_ids()) == 1
assert registry.async_get(expected_entity_id) is not None
@pytest.mark.parametrize(
("language", "has_entity_name", "expected_entity_id"),
(
("en", False, "test_domain.test_qwer"), # Set to <platform>_<unique_id>
("en", True, "test_domain.device_bla_english_name"),
("sv", True, "test_domain.device_bla_swedish_name"),
# Chinese uses english for entity_id
("cn", True, "test_domain.device_bla_english_name"),
),
)
async def test_translated_entity_name_influences_entity_id(
hass: HomeAssistant,
language: str,
has_entity_name: bool,
expected_entity_id: str,
) -> None:
"""Test entity_id is influenced by translated entity name."""
class TranslatedEntity(Entity):
_attr_unique_id = "qwer"
_attr_device_info = {
"identifiers": {("hue", "1234")},
"connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")},
"name": "Device Bla",
}
_attr_translation_key = "test"
def __init__(self, has_entity_name: bool) -> None:
"""Initialize."""
self._attr_has_entity_name = has_entity_name
registry = er.async_get(hass)
translations = {
"en": {"component.test.entity.test_domain.test.name": "English name"},
"sv": {"component.test.entity.test_domain.test.name": "Swedish name"},
"cn": {"component.test.entity.test_domain.test.name": "Chinese name"},
}
hass.config.language = language
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([TranslatedEntity(has_entity_name)])
return True
platform = MockPlatform(async_setup_entry=async_setup_entry)
config_entry = MockConfigEntry(entry_id="super-mock-id")
entity_platform = MockEntityPlatform(
hass, platform_name=config_entry.domain, platform=platform
)
with patch(
"homeassistant.helpers.entity_platform.translation.async_get_translations",
side_effect=async_get_translations,
):
assert await entity_platform.async_setup_entry(config_entry)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) == 1
assert registry.async_get(expected_entity_id) is not None
@pytest.mark.parametrize(
("language", "has_entity_name", "device_class", "expected_entity_id"),
(
("en", False, None, "test_domain.test_qwer"), # Set to <platform>_<unique_id>
(
"en",
False,
"test_class",
"test_domain.test_qwer",
), # Set to <platform>_<unique_id>
("en", True, "test_class", "test_domain.device_bla_english_cls"),
("sv", True, "test_class", "test_domain.device_bla_swedish_cls"),
# Chinese uses english for entity_id
("cn", True, "test_class", "test_domain.device_bla_english_cls"),
),
)
async def test_translated_device_class_name_influences_entity_id(
hass: HomeAssistant,
language: str,
has_entity_name: bool,
device_class: str | None,
expected_entity_id: str,
) -> None:
"""Test entity_id is influenced by translated entity name."""
class TranslatedDeviceClassEntity(Entity):
_attr_unique_id = "qwer"
_attr_device_info = {
"identifiers": {("hue", "1234")},
"connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")},
"name": "Device Bla",
}
def __init__(self, device_class: str | None, has_entity_name: bool) -> None:
"""Initialize."""
self._attr_device_class = device_class
self._attr_has_entity_name = has_entity_name
def _default_to_device_class_name(self) -> bool:
"""Return True if an unnamed entity should be named by its device class."""
return self.device_class is not None
registry = er.async_get(hass)
translations = {
"en": {"component.test_domain.entity_component.test_class.name": "English cls"},
"sv": {"component.test_domain.entity_component.test_class.name": "Swedish cls"},
"cn": {"component.test_domain.entity_component.test_class.name": "Chinese cls"},
}
hass.config.language = language
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([TranslatedDeviceClassEntity(device_class, has_entity_name)])
return True
platform = MockPlatform(async_setup_entry=async_setup_entry)
config_entry = MockConfigEntry(entry_id="super-mock-id")
entity_platform = MockEntityPlatform(
hass, platform_name=config_entry.domain, platform=platform
)
with patch(
"homeassistant.helpers.entity_platform.translation.async_get_translations",
side_effect=async_get_translations,
):
assert await entity_platform.async_setup_entry(config_entry)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) == 1
assert registry.async_get(expected_entity_id) is not None