Base entity ids on English for languages not using Latin script (#91357)
This commit is contained in:
parent
fe28067481
commit
071d3a474f
5 changed files with 350 additions and 26 deletions
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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]",
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue