mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 16:27:08 +00:00
Remove unused legacy state translations (#112023)
* Remove unused state translations There have been replaced with entity translations https://github.com/home-assistant/developers.home-assistant/pull/1557 https://github.com/home-assistant/core/pull/82701 * nothing does merging anymore * useless dispatch * remove * remove platform code from hassfest * preen * Update homeassistant/helpers/translation.py * ruff * fix merge * check is impossible now since we already know if translations exist or not * keep the function for now * remove unreachable code since we filter out `.` before now * reduce * reduce * fix merge conflict (again)
This commit is contained in:
parent
0200d1aa66
commit
33412dd9f6
@ -6,6 +6,7 @@ import asyncio
|
|||||||
from collections.abc import Iterable, Mapping
|
from collections.abc import Iterable, Mapping
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import logging
|
import logging
|
||||||
|
import pathlib
|
||||||
import string
|
import string
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -41,40 +42,18 @@ def recursive_flatten(prefix: Any, data: dict[str, Any]) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def component_translation_path(
|
def component_translation_path(language: str, integration: Integration) -> pathlib.Path:
|
||||||
component: str, language: str, integration: Integration
|
|
||||||
) -> str | None:
|
|
||||||
"""Return the translation json file location for a component.
|
"""Return the translation json file location for a component.
|
||||||
|
|
||||||
For component:
|
For component:
|
||||||
- components/hue/translations/nl.json
|
- components/hue/translations/nl.json
|
||||||
|
|
||||||
For platform:
|
|
||||||
- components/hue/translations/light.nl.json
|
|
||||||
|
|
||||||
If component is just a single file, will return None.
|
|
||||||
"""
|
"""
|
||||||
parts = component.split(".")
|
return integration.file_path / "translations" / f"{language}.json"
|
||||||
domain = parts[0]
|
|
||||||
is_platform = len(parts) == 2
|
|
||||||
|
|
||||||
# If it's a component that is just one file, we don't support translations
|
|
||||||
# Example custom_components/my_component.py
|
|
||||||
if integration.file_path.name != domain:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if is_platform:
|
|
||||||
filename = f"{parts[1]}.{language}.json"
|
|
||||||
else:
|
|
||||||
filename = f"{language}.json"
|
|
||||||
|
|
||||||
translation_path = integration.file_path / "translations"
|
|
||||||
|
|
||||||
return str(translation_path / filename)
|
|
||||||
|
|
||||||
|
|
||||||
def _load_translations_files_by_language(
|
def _load_translations_files_by_language(
|
||||||
translation_files: dict[str, dict[str, str]],
|
translation_files: dict[str, dict[str, pathlib.Path]],
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""Load and parse translation.json files."""
|
"""Load and parse translation.json files."""
|
||||||
loaded: dict[str, dict[str, Any]] = {}
|
loaded: dict[str, dict[str, Any]] = {}
|
||||||
@ -98,47 +77,6 @@ def _load_translations_files_by_language(
|
|||||||
return loaded
|
return loaded
|
||||||
|
|
||||||
|
|
||||||
def _merge_resources(
|
|
||||||
translation_strings: dict[str, dict[str, Any]],
|
|
||||||
components: set[str],
|
|
||||||
category: str,
|
|
||||||
) -> dict[str, dict[str, Any]]:
|
|
||||||
"""Build and merge the resources response for the given components and platforms."""
|
|
||||||
# Build response
|
|
||||||
resources: dict[str, dict[str, Any]] = {}
|
|
||||||
for component in components:
|
|
||||||
domain = component.rpartition(".")[-1]
|
|
||||||
|
|
||||||
domain_resources = resources.setdefault(domain, {})
|
|
||||||
|
|
||||||
# Integrations are able to provide translations for their entities under other
|
|
||||||
# integrations if they don't have an existing device class. This is done by
|
|
||||||
# using a custom device class prefixed with their domain and two underscores.
|
|
||||||
# These files are in platform specific files in the integration folder with
|
|
||||||
# names like `strings.sensor.json`.
|
|
||||||
# We are going to merge the translations for the custom device classes into
|
|
||||||
# the translations of sensor.
|
|
||||||
|
|
||||||
new_value = translation_strings.get(component, {}).get(category)
|
|
||||||
|
|
||||||
if new_value is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if isinstance(new_value, dict):
|
|
||||||
domain_resources.update(new_value)
|
|
||||||
else:
|
|
||||||
_LOGGER.error(
|
|
||||||
(
|
|
||||||
"An integration providing translations for %s provided invalid"
|
|
||||||
" data: %s"
|
|
||||||
),
|
|
||||||
domain,
|
|
||||||
new_value,
|
|
||||||
)
|
|
||||||
|
|
||||||
return resources
|
|
||||||
|
|
||||||
|
|
||||||
def build_resources(
|
def build_resources(
|
||||||
translation_strings: dict[str, dict[str, dict[str, Any] | str]],
|
translation_strings: dict[str, dict[str, dict[str, Any] | str]],
|
||||||
components: set[str],
|
components: set[str],
|
||||||
@ -163,32 +101,20 @@ async def _async_get_component_strings(
|
|||||||
"""Load translations."""
|
"""Load translations."""
|
||||||
translations_by_language: dict[str, dict[str, Any]] = {}
|
translations_by_language: dict[str, dict[str, Any]] = {}
|
||||||
# Determine paths of missing components/platforms
|
# Determine paths of missing components/platforms
|
||||||
files_to_load_by_language: dict[str, dict[str, str]] = {}
|
files_to_load_by_language: dict[str, dict[str, pathlib.Path]] = {}
|
||||||
loaded_translations_by_language: dict[str, dict[str, Any]] = {}
|
loaded_translations_by_language: dict[str, dict[str, Any]] = {}
|
||||||
has_files_to_load = False
|
has_files_to_load = False
|
||||||
for language in languages:
|
for language in languages:
|
||||||
files_to_load: dict[str, str] = {}
|
files_to_load: dict[str, pathlib.Path] = {
|
||||||
files_to_load_by_language[language] = files_to_load
|
domain: component_translation_path(language, integration)
|
||||||
translations_by_language[language] = {}
|
for domain in components
|
||||||
|
|
||||||
for comp in components:
|
|
||||||
domain, _, platform = comp.partition(".")
|
|
||||||
if (
|
if (
|
||||||
not (integration := integrations.get(domain))
|
(integration := integrations.get(domain))
|
||||||
or not integration.has_translations
|
and integration.has_translations
|
||||||
):
|
)
|
||||||
continue
|
}
|
||||||
|
files_to_load_by_language[language] = files_to_load
|
||||||
if platform and integration.is_built_in:
|
has_files_to_load |= bool(files_to_load)
|
||||||
# Legacy state translations are no longer used for built-in integrations
|
|
||||||
# and we avoid trying to load them. This is a temporary measure to allow
|
|
||||||
# them to keep working for custom integrations until we can fully remove
|
|
||||||
# them.
|
|
||||||
continue
|
|
||||||
|
|
||||||
if path := component_translation_path(comp, language, integration):
|
|
||||||
files_to_load[comp] = path
|
|
||||||
has_files_to_load = True
|
|
||||||
|
|
||||||
if has_files_to_load:
|
if has_files_to_load:
|
||||||
loaded_translations_by_language = await hass.async_add_executor_job(
|
loaded_translations_by_language = await hass.async_add_executor_job(
|
||||||
@ -197,18 +123,15 @@ async def _async_get_component_strings(
|
|||||||
|
|
||||||
for language in languages:
|
for language in languages:
|
||||||
loaded_translations = loaded_translations_by_language.setdefault(language, {})
|
loaded_translations = loaded_translations_by_language.setdefault(language, {})
|
||||||
for comp in components:
|
for domain in components:
|
||||||
if "." in comp:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Translations that miss "title" will get integration put in.
|
# Translations that miss "title" will get integration put in.
|
||||||
component_translations = loaded_translations.setdefault(comp, {})
|
component_translations = loaded_translations.setdefault(domain, {})
|
||||||
if "title" not in component_translations and (
|
if "title" not in component_translations and (
|
||||||
integration := integrations.get(comp)
|
integration := integrations.get(domain)
|
||||||
):
|
):
|
||||||
component_translations["title"] = integration.name
|
component_translations["title"] = integration.name
|
||||||
|
|
||||||
translations_by_language[language].update(loaded_translations)
|
translations_by_language.setdefault(language, {}).update(loaded_translations)
|
||||||
|
|
||||||
return translations_by_language
|
return translations_by_language
|
||||||
|
|
||||||
@ -355,10 +278,12 @@ class _TranslationCache:
|
|||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
(
|
(
|
||||||
"Validation of translation placeholders for localized (%s) string "
|
"Validation of translation placeholders for localized (%s) string "
|
||||||
"%s failed"
|
"%s failed: (%s != %s)"
|
||||||
),
|
),
|
||||||
language,
|
language,
|
||||||
key,
|
key,
|
||||||
|
updated_placeholders,
|
||||||
|
cached_placeholders,
|
||||||
)
|
)
|
||||||
mismatches.add(key)
|
mismatches.add(key)
|
||||||
|
|
||||||
@ -382,17 +307,7 @@ class _TranslationCache:
|
|||||||
categories.update(resource)
|
categories.update(resource)
|
||||||
|
|
||||||
for category in categories:
|
for category in categories:
|
||||||
new_resources: Mapping[str, dict[str, Any] | str]
|
new_resources = build_resources(translation_strings, components, category)
|
||||||
|
|
||||||
if category in ("state", "entity_component"):
|
|
||||||
new_resources = _merge_resources(
|
|
||||||
translation_strings, components, category
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
new_resources = build_resources(
|
|
||||||
translation_strings, components, category
|
|
||||||
)
|
|
||||||
|
|
||||||
category_cache = cached.setdefault(category, {})
|
category_cache = cached.setdefault(category, {})
|
||||||
|
|
||||||
for component, resource in new_resources.items():
|
for component, resource in new_resources.items():
|
||||||
@ -430,7 +345,7 @@ async def async_get_translations(
|
|||||||
elif integrations is not None:
|
elif integrations is not None:
|
||||||
components = set(integrations)
|
components = set(integrations)
|
||||||
else:
|
else:
|
||||||
components = _async_get_components(hass, category)
|
components = {comp for comp in hass.config.components if "." not in comp}
|
||||||
|
|
||||||
return await _async_get_translations_cache(hass).async_fetch(
|
return await _async_get_translations_cache(hass).async_fetch(
|
||||||
language, category, components
|
language, category, components
|
||||||
@ -452,7 +367,7 @@ def async_get_cached_translations(
|
|||||||
if integration is not None:
|
if integration is not None:
|
||||||
components = {integration}
|
components = {integration}
|
||||||
else:
|
else:
|
||||||
components = _async_get_components(hass, category)
|
components = {comp for comp in hass.config.components if "." not in comp}
|
||||||
|
|
||||||
return _async_get_translations_cache(hass).get_cached(
|
return _async_get_translations_cache(hass).get_cached(
|
||||||
language, category, components
|
language, category, components
|
||||||
@ -466,21 +381,6 @@ def _async_get_translations_cache(hass: HomeAssistant) -> _TranslationCache:
|
|||||||
return cache
|
return cache
|
||||||
|
|
||||||
|
|
||||||
_DIRECT_MAPPED_CATEGORIES = {"state", "entity_component", "services"}
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_get_components(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
category: str,
|
|
||||||
) -> set[str]:
|
|
||||||
"""Return a set of components for which translations should be loaded."""
|
|
||||||
if category in _DIRECT_MAPPED_CATEGORIES:
|
|
||||||
return hass.config.components
|
|
||||||
# Only 'state' supports merging, so remove platforms from selection
|
|
||||||
return {component for component in hass.config.components if "." not in component}
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_setup(hass: HomeAssistant) -> None:
|
def async_setup(hass: HomeAssistant) -> None:
|
||||||
"""Create translation cache and register listeners for translation loaders.
|
"""Create translation cache and register listeners for translation loaders.
|
||||||
@ -590,13 +490,4 @@ def async_translate_state(
|
|||||||
if localize_key in translations:
|
if localize_key in translations:
|
||||||
return translations[localize_key]
|
return translations[localize_key]
|
||||||
|
|
||||||
translations = async_get_cached_translations(hass, language, "state", domain)
|
|
||||||
if device_class is not None:
|
|
||||||
localize_key = f"component.{domain}.state.{device_class}.{state}"
|
|
||||||
if localize_key in translations:
|
|
||||||
return translations[localize_key]
|
|
||||||
localize_key = f"component.{domain}.state._.{state}"
|
|
||||||
if localize_key in translations:
|
|
||||||
return translations[localize_key]
|
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from itertools import chain
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -12,7 +11,6 @@ import voluptuous as vol
|
|||||||
from voluptuous.humanize import humanize_error
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util import slugify
|
|
||||||
from script.translations import upload
|
from script.translations import upload
|
||||||
|
|
||||||
from .model import Config, Integration
|
from .model import Config, Integration
|
||||||
@ -414,49 +412,6 @@ def gen_ha_hardware_schema(config: Config, integration: Integration):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def gen_platform_strings_schema(config: Config, integration: Integration) -> vol.Schema:
|
|
||||||
"""Generate platform strings schema like strings.sensor.json.
|
|
||||||
|
|
||||||
Example of valid data:
|
|
||||||
{
|
|
||||||
"state": {
|
|
||||||
"moon__phase": {
|
|
||||||
"full": "Full"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def device_class_validator(value: str) -> str:
|
|
||||||
"""Key validator for platform states.
|
|
||||||
|
|
||||||
Platform states are only allowed to provide states for device classes they prefix.
|
|
||||||
"""
|
|
||||||
if not value.startswith(f"{integration.domain}__"):
|
|
||||||
raise vol.Invalid(
|
|
||||||
f"Device class need to start with '{integration.domain}__'. Key {value} is invalid. See https://developers.home-assistant.io/docs/internationalization/core#stringssensorjson"
|
|
||||||
)
|
|
||||||
|
|
||||||
slug_friendly = value.replace("__", "_", 1)
|
|
||||||
slugged = slugify(slug_friendly)
|
|
||||||
|
|
||||||
if slug_friendly != slugged:
|
|
||||||
raise vol.Invalid(
|
|
||||||
f"invalid device class {value}. After domain__, needs to be all lowercase, no spaces."
|
|
||||||
)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
return vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Optional("state"): cv.schema_with_slug_keys(
|
|
||||||
cv.schema_with_slug_keys(str, slug_validator=translation_key_validator),
|
|
||||||
slug_validator=device_class_validator,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
ONBOARDING_SCHEMA = vol.Schema(
|
ONBOARDING_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required("area"): {str: translation_value_validator},
|
vol.Required("area"): {str: translation_value_validator},
|
||||||
@ -525,32 +480,6 @@ def validate_translation_file( # noqa: C901
|
|||||||
"name or add exception to ALLOW_NAME_TRANSLATION",
|
"name or add exception to ALLOW_NAME_TRANSLATION",
|
||||||
)
|
)
|
||||||
|
|
||||||
platform_string_schema = gen_platform_strings_schema(config, integration)
|
|
||||||
platform_strings = [integration.path.glob("strings.*.json")]
|
|
||||||
|
|
||||||
if config.specific_integrations:
|
|
||||||
platform_strings.append(integration.path.glob("translations/*.en.json"))
|
|
||||||
|
|
||||||
for path in chain(*platform_strings):
|
|
||||||
name = str(path.relative_to(integration.path))
|
|
||||||
|
|
||||||
try:
|
|
||||||
strings = json.loads(path.read_text())
|
|
||||||
except ValueError as err:
|
|
||||||
integration.add_error("translations", f"Invalid JSON in {name}: {err}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
platform_string_schema(strings)
|
|
||||||
except vol.Invalid as err:
|
|
||||||
msg = f"Invalid {path.name}: {humanize_error(strings, err)}"
|
|
||||||
if config.specific_integrations:
|
|
||||||
integration.add_warning("translations", msg)
|
|
||||||
else:
|
|
||||||
integration.add_error("translations", msg)
|
|
||||||
else:
|
|
||||||
find_references(strings, path.name, references)
|
|
||||||
|
|
||||||
if config.specific_integrations:
|
if config.specific_integrations:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -2050,12 +2050,7 @@ async def test_state_translated(
|
|||||||
):
|
):
|
||||||
if category == "entity":
|
if category == "entity":
|
||||||
return {
|
return {
|
||||||
"component.hue.entity.light.translation_key.state.on": "state_is_on"
|
"component.hue.entity.light.translation_key.state.on": "state_is_on",
|
||||||
}
|
|
||||||
if category == "state":
|
|
||||||
return {
|
|
||||||
"component.some_domain.state.some_device_class.off": "state_is_off",
|
|
||||||
"component.some_domain.state._.foo": "state_is_foo",
|
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@ -2066,16 +2061,6 @@ async def test_state_translated(
|
|||||||
tpl8 = template.Template('{{ state_translated("light.hue_5678") }}', hass)
|
tpl8 = template.Template('{{ state_translated("light.hue_5678") }}', hass)
|
||||||
assert tpl8.async_render() == "state_is_on"
|
assert tpl8.async_render() == "state_is_on"
|
||||||
|
|
||||||
tpl9 = template.Template(
|
|
||||||
'{{ state_translated("some_domain.with_device_class_1") }}', hass
|
|
||||||
)
|
|
||||||
assert tpl9.async_render() == "state_is_off"
|
|
||||||
|
|
||||||
tpl10 = template.Template(
|
|
||||||
'{{ state_translated("some_domain.with_device_class_2") }}', hass
|
|
||||||
)
|
|
||||||
assert tpl10.async_render() == "state_is_foo"
|
|
||||||
|
|
||||||
tpl11 = template.Template('{{ state_translated("domain.is_unavailable") }}', hass)
|
tpl11 = template.Template('{{ state_translated("domain.is_unavailable") }}', hass)
|
||||||
assert tpl11.async_render() == "unavailable"
|
assert tpl11.async_render() == "unavailable"
|
||||||
|
|
||||||
|
@ -47,35 +47,10 @@ async def test_component_translation_path(
|
|||||||
{"switch": [{"platform": "test"}, {"platform": "test_embedded"}]},
|
{"switch": [{"platform": "test"}, {"platform": "test_embedded"}]},
|
||||||
)
|
)
|
||||||
assert await async_setup_component(hass, "test_package", {"test_package": None})
|
assert await async_setup_component(hass, "test_package", {"test_package": None})
|
||||||
|
int_test_package = await async_get_integration(hass, "test_package")
|
||||||
(
|
|
||||||
int_test,
|
|
||||||
int_test_embedded,
|
|
||||||
int_test_package,
|
|
||||||
) = await asyncio.gather(
|
|
||||||
async_get_integration(hass, "test"),
|
|
||||||
async_get_integration(hass, "test_embedded"),
|
|
||||||
async_get_integration(hass, "test_package"),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert path.normpath(
|
assert path.normpath(
|
||||||
translation.component_translation_path("test.switch", "en", int_test)
|
translation.component_translation_path("en", int_test_package)
|
||||||
) == path.normpath(
|
|
||||||
hass.config.path("custom_components", "test", "translations", "switch.en.json")
|
|
||||||
)
|
|
||||||
|
|
||||||
assert path.normpath(
|
|
||||||
translation.component_translation_path(
|
|
||||||
"test_embedded.switch", "en", int_test_embedded
|
|
||||||
)
|
|
||||||
) == path.normpath(
|
|
||||||
hass.config.path(
|
|
||||||
"custom_components", "test_embedded", "translations", "switch.en.json"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert path.normpath(
|
|
||||||
translation.component_translation_path("test_package", "en", int_test_package)
|
|
||||||
) == path.normpath(
|
) == path.normpath(
|
||||||
hass.config.path("custom_components", "test_package", "translations", "en.json")
|
hass.config.path("custom_components", "test_package", "translations", "en.json")
|
||||||
)
|
)
|
||||||
@ -86,28 +61,39 @@ def test__load_translations_files_by_language(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test the load translation files function."""
|
"""Test the load translation files function."""
|
||||||
# Test one valid and one invalid file
|
# Test one valid and one invalid file
|
||||||
file1 = hass.config.path(
|
en_file = hass.config.path("custom_components", "test", "translations", "en.json")
|
||||||
"custom_components", "test", "translations", "switch.en.json"
|
invalid_file = hass.config.path(
|
||||||
)
|
|
||||||
file2 = hass.config.path(
|
|
||||||
"custom_components", "test", "translations", "invalid.json"
|
"custom_components", "test", "translations", "invalid.json"
|
||||||
)
|
)
|
||||||
file3 = hass.config.path(
|
broken_file = hass.config.path(
|
||||||
"custom_components", "test", "translations", "_broken.en.json"
|
"custom_components", "test", "translations", "_broken.json"
|
||||||
)
|
)
|
||||||
assert translation._load_translations_files_by_language(
|
assert translation._load_translations_files_by_language(
|
||||||
{"en": {"switch.test": file1, "invalid": file2, "broken": file3}}
|
{
|
||||||
) == {
|
"en": {"test": en_file},
|
||||||
"en": {
|
"invalid": {"test": invalid_file},
|
||||||
"switch.test": {
|
"broken": {"test": broken_file},
|
||||||
"state": {"string1": "Value 1", "string2": "Value 2"},
|
|
||||||
"something": "else",
|
|
||||||
},
|
|
||||||
"invalid": {},
|
|
||||||
}
|
}
|
||||||
|
) == {
|
||||||
|
"broken": {},
|
||||||
|
"en": {
|
||||||
|
"test": {
|
||||||
|
"entity": {
|
||||||
|
"switch": {
|
||||||
|
"other1": {"name": "Other 1"},
|
||||||
|
"other2": {"name": "Other 2"},
|
||||||
|
"other3": {"name": "Other 3"},
|
||||||
|
"other4": {"name": "Other 4"},
|
||||||
|
"outlet": {"name": "Outlet " "{placeholder}"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"something": "else",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"invalid": {"test": {}},
|
||||||
}
|
}
|
||||||
assert "Translation file is unexpected type" in caplog.text
|
assert "Translation file is unexpected type" in caplog.text
|
||||||
assert "_broken.en.json" in caplog.text
|
assert "_broken.json" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@ -185,33 +171,61 @@ 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:
|
||||||
"""Test the get translations helper."""
|
"""Test the get translations helper."""
|
||||||
translations = await translation.async_get_translations(hass, "en", "state")
|
translations = await translation.async_get_translations(hass, "en", "entity")
|
||||||
assert translations == {}
|
assert translations == {}
|
||||||
|
|
||||||
assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}})
|
assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
translations = await translation.async_get_translations(hass, "en", "state")
|
translations = await translation.async_get_translations(
|
||||||
|
hass, "en", "entity", {"test"}
|
||||||
|
)
|
||||||
|
|
||||||
assert translations["component.switch.state.string1"] == "Value 1"
|
assert translations == {
|
||||||
assert translations["component.switch.state.string2"] == "Value 2"
|
"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}",
|
||||||
|
}
|
||||||
|
|
||||||
translations = await translation.async_get_translations(hass, "de", "state")
|
translations = await translation.async_get_translations(
|
||||||
assert "component.switch.something" not in translations
|
hass, "de", "entity", {"test"}
|
||||||
assert translations["component.switch.state.string1"] == "German Value 1"
|
)
|
||||||
assert translations["component.switch.state.string2"] == "German Value 2"
|
|
||||||
|
assert translations == {
|
||||||
|
"component.test.entity.switch.other1.name": "Anderes 1",
|
||||||
|
"component.test.entity.switch.other2.name": "Other 2",
|
||||||
|
"component.test.entity.switch.other3.name": "",
|
||||||
|
"component.test.entity.switch.other4.name": "Other 4",
|
||||||
|
"component.test.entity.switch.outlet.name": "Outlet {placeholder}",
|
||||||
|
}
|
||||||
|
|
||||||
# Test a partial translation
|
# Test a partial translation
|
||||||
translations = await translation.async_get_translations(hass, "es", "state")
|
translations = await translation.async_get_translations(
|
||||||
assert translations["component.switch.state.string1"] == "Spanish Value 1"
|
hass, "es", "entity", {"test"}
|
||||||
assert translations["component.switch.state.string2"] == "Value 2"
|
)
|
||||||
|
|
||||||
|
assert translations == {
|
||||||
|
"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}",
|
||||||
|
}
|
||||||
|
|
||||||
# Test that an untranslated language falls back to English.
|
# Test that an untranslated language falls back to English.
|
||||||
translations = await translation.async_get_translations(
|
translations = await translation.async_get_translations(
|
||||||
hass, "invalid-language", "state"
|
hass, "invalid-language", "entity", {"test"}
|
||||||
)
|
)
|
||||||
assert translations["component.switch.state.string1"] == "Value 1"
|
|
||||||
assert translations["component.switch.state.string2"] == "Value 2"
|
assert translations == {
|
||||||
|
"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}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_get_translations_loads_config_flows(
|
async def test_get_translations_loads_config_flows(
|
||||||
@ -348,162 +362,6 @@ async def test_get_translation_categories(hass: HomeAssistant) -> None:
|
|||||||
assert "component.light.device_automation.action_type.turn_on" in translations
|
assert "component.light.device_automation.action_type.turn_on" in translations
|
||||||
|
|
||||||
|
|
||||||
async def test_legacy_platform_translations_not_used_built_in_integrations(
|
|
||||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
||||||
) -> None:
|
|
||||||
"""Test legacy platform translations are not used for built-in integrations."""
|
|
||||||
hass.config.components.add("moon.sensor")
|
|
||||||
hass.config.components.add("sensor")
|
|
||||||
|
|
||||||
load_requests = []
|
|
||||||
|
|
||||||
def mock_load_translations_files_by_language(files):
|
|
||||||
load_requests.append(files)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"homeassistant.helpers.translation._load_translations_files_by_language",
|
|
||||||
mock_load_translations_files_by_language,
|
|
||||||
):
|
|
||||||
await translation.async_get_translations(hass, "en", "state")
|
|
||||||
|
|
||||||
assert len(load_requests) == 1
|
|
||||||
to_load = load_requests[0]
|
|
||||||
assert len(to_load) == 1
|
|
||||||
en_load = to_load["en"]
|
|
||||||
assert len(en_load) == 1
|
|
||||||
assert "sensor" in en_load
|
|
||||||
assert "moon.sensor" not in en_load
|
|
||||||
|
|
||||||
|
|
||||||
async def test_translation_merging_custom_components(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
caplog: pytest.LogCaptureFixture,
|
|
||||||
enable_custom_integrations: None,
|
|
||||||
) -> None:
|
|
||||||
"""Test we merge translations of two integrations.
|
|
||||||
|
|
||||||
Legacy state translations only used for custom integrations.
|
|
||||||
"""
|
|
||||||
hass.config.components.add("test_legacy_state_translations.sensor")
|
|
||||||
hass.config.components.add("sensor")
|
|
||||||
|
|
||||||
orig_load_translations = translation._load_translations_files_by_language
|
|
||||||
|
|
||||||
def mock_load_translations_files(files):
|
|
||||||
"""Mock loading."""
|
|
||||||
result = orig_load_translations(files)
|
|
||||||
result["en"]["test_legacy_state_translations.sensor"] = {
|
|
||||||
"state": {
|
|
||||||
"test_legacy_state_translations__phase": {
|
|
||||||
"first_quarter": "First Quarter"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"homeassistant.helpers.translation._load_translations_files_by_language",
|
|
||||||
side_effect=mock_load_translations_files,
|
|
||||||
):
|
|
||||||
translations = await translation.async_get_translations(hass, "en", "state")
|
|
||||||
|
|
||||||
assert (
|
|
||||||
"component.sensor.state.test_legacy_state_translations__phase.first_quarter"
|
|
||||||
in translations
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.config.components.add("test_legacy_state_translations_bad_data.sensor")
|
|
||||||
|
|
||||||
# Patch in some bad translation data
|
|
||||||
def mock_load_bad_translations_files(files):
|
|
||||||
"""Mock loading."""
|
|
||||||
result = orig_load_translations(files)
|
|
||||||
result["en"]["test_legacy_state_translations_bad_data.sensor"] = {
|
|
||||||
"state": "bad data"
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"homeassistant.helpers.translation._load_translations_files_by_language",
|
|
||||||
side_effect=mock_load_bad_translations_files,
|
|
||||||
):
|
|
||||||
translations = await translation.async_get_translations(hass, "en", "state")
|
|
||||||
|
|
||||||
assert (
|
|
||||||
"component.sensor.state.test_legacy_state_translations__phase.first_quarter"
|
|
||||||
in translations
|
|
||||||
)
|
|
||||||
|
|
||||||
assert (
|
|
||||||
"An integration providing translations for sensor provided invalid data:"
|
|
||||||
" bad data"
|
|
||||||
) in caplog.text
|
|
||||||
|
|
||||||
|
|
||||||
async def test_translation_merging_loaded_apart_custom_integrations(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
caplog: pytest.LogCaptureFixture,
|
|
||||||
enable_custom_integrations: None,
|
|
||||||
) -> None:
|
|
||||||
"""Test we merge translations of two integrations when they are not loaded at the same time.
|
|
||||||
|
|
||||||
Legacy state translations only used for custom integrations.
|
|
||||||
"""
|
|
||||||
orig_load_translations = translation._load_translations_files_by_language
|
|
||||||
|
|
||||||
def mock_load_translations_files(files):
|
|
||||||
"""Mock loading."""
|
|
||||||
result = orig_load_translations(files)
|
|
||||||
result["en"]["test_legacy_state_translations.sensor"] = {
|
|
||||||
"state": {
|
|
||||||
"test_legacy_state_translations__phase": {
|
|
||||||
"first_quarter": "First Quarter"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
hass.config.components.add("sensor")
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"homeassistant.helpers.translation._load_translations_files_by_language",
|
|
||||||
side_effect=mock_load_translations_files,
|
|
||||||
):
|
|
||||||
translations = await translation.async_get_translations(hass, "en", "state")
|
|
||||||
|
|
||||||
assert (
|
|
||||||
"component.sensor.state.test_legacy_state_translations__phase.first_quarter"
|
|
||||||
not in translations
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.config.components.add("test_legacy_state_translations.sensor")
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"homeassistant.helpers.translation._load_translations_files_by_language",
|
|
||||||
side_effect=mock_load_translations_files,
|
|
||||||
):
|
|
||||||
translations = await translation.async_get_translations(hass, "en", "state")
|
|
||||||
|
|
||||||
assert (
|
|
||||||
"component.sensor.state.test_legacy_state_translations__phase.first_quarter"
|
|
||||||
in translations
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"homeassistant.helpers.translation._load_translations_files_by_language",
|
|
||||||
side_effect=mock_load_translations_files,
|
|
||||||
):
|
|
||||||
translations = await translation.async_get_translations(
|
|
||||||
hass, "en", "state", integrations={"sensor"}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert (
|
|
||||||
"component.sensor.state.test_legacy_state_translations__phase.first_quarter"
|
|
||||||
in translations
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def test_translation_merging_loaded_together(
|
async def test_translation_merging_loaded_together(
|
||||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -592,14 +450,14 @@ async def test_caching(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
# Patch with same method so we can count invocations
|
# Patch with same method so we can count invocations
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.helpers.translation._merge_resources",
|
"homeassistant.helpers.translation.build_resources",
|
||||||
side_effect=translation._merge_resources,
|
side_effect=translation.build_resources,
|
||||||
) as mock_merge:
|
) as mock_build_resources:
|
||||||
load1 = await translation.async_get_translations(hass, "en", "entity_component")
|
load1 = await translation.async_get_translations(hass, "en", "entity_component")
|
||||||
assert len(mock_merge.mock_calls) == 1
|
assert len(mock_build_resources.mock_calls) == 5
|
||||||
|
|
||||||
load2 = await translation.async_get_translations(hass, "en", "entity_component")
|
load2 = await translation.async_get_translations(hass, "en", "entity_component")
|
||||||
assert len(mock_merge.mock_calls) == 1
|
assert len(mock_build_resources.mock_calls) == 5
|
||||||
|
|
||||||
assert load1 == load2
|
assert load1 == load2
|
||||||
|
|
||||||
@ -665,47 +523,58 @@ async def test_custom_component_translations(
|
|||||||
|
|
||||||
async def test_get_cached_translations(
|
async def test_get_cached_translations(
|
||||||
hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None
|
hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None
|
||||||
):
|
) -> None:
|
||||||
"""Test the get cached translations helper."""
|
"""Test the get cached translations helper."""
|
||||||
translations = translation.async_get_cached_translations(hass, "en", "state")
|
translations = await translation.async_get_translations(hass, "en", "entity")
|
||||||
assert translations == {}
|
assert translations == {}
|
||||||
|
|
||||||
assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}})
|
assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
await translation._async_get_translations_cache(hass).async_load(
|
await translation._async_get_translations_cache(hass).async_load("en", {"test"})
|
||||||
"en", hass.config.components
|
|
||||||
)
|
|
||||||
translations = translation.async_get_cached_translations(hass, "en", "state")
|
|
||||||
|
|
||||||
assert translations["component.switch.state.string1"] == "Value 1"
|
translations = translation.async_get_cached_translations(
|
||||||
assert translations["component.switch.state.string2"] == "Value 2"
|
hass, "en", "entity", "test"
|
||||||
|
|
||||||
await translation._async_get_translations_cache(hass).async_load(
|
|
||||||
"de", hass.config.components
|
|
||||||
)
|
)
|
||||||
translations = translation.async_get_cached_translations(hass, "de", "state")
|
assert translations == {
|
||||||
assert "component.switch.something" not in translations
|
"component.test.entity.switch.other1.name": "Other 1",
|
||||||
assert translations["component.switch.state.string1"] == "German Value 1"
|
"component.test.entity.switch.other2.name": "Other 2",
|
||||||
assert translations["component.switch.state.string2"] == "German Value 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}",
|
||||||
|
}
|
||||||
|
|
||||||
|
await translation._async_get_translations_cache(hass).async_load("es", {"test"})
|
||||||
|
|
||||||
# Test a partial translation
|
# Test a partial translation
|
||||||
await translation._async_get_translations_cache(hass).async_load(
|
translations = translation.async_get_cached_translations(
|
||||||
"es", hass.config.components
|
hass, "es", "entity", "test"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert translations == {
|
||||||
|
"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}",
|
||||||
|
}
|
||||||
|
|
||||||
|
await translation._async_get_translations_cache(hass).async_load(
|
||||||
|
"invalid-language", {"test"}
|
||||||
)
|
)
|
||||||
translations = translation.async_get_cached_translations(hass, "es", "state")
|
|
||||||
assert translations["component.switch.state.string1"] == "Spanish Value 1"
|
|
||||||
assert translations["component.switch.state.string2"] == "Value 2"
|
|
||||||
|
|
||||||
# Test that an untranslated language falls back to English.
|
# Test that an untranslated language falls back to English.
|
||||||
await translation._async_get_translations_cache(hass).async_load(
|
|
||||||
"invalid-language", hass.config.components
|
|
||||||
)
|
|
||||||
translations = translation.async_get_cached_translations(
|
translations = translation.async_get_cached_translations(
|
||||||
hass, "invalid-language", "state"
|
hass, "invalid-language", "entity", "test"
|
||||||
)
|
)
|
||||||
assert translations["component.switch.state.string1"] == "Value 1"
|
|
||||||
assert translations["component.switch.state.string2"] == "Value 2"
|
assert translations == {
|
||||||
|
"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}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_setup(hass: HomeAssistant):
|
async def test_setup(hass: HomeAssistant):
|
||||||
@ -784,36 +653,6 @@ async def test_translate_state(hass: HomeAssistant):
|
|||||||
mock.assert_called_once_with(hass, hass.config.language, "entity_component")
|
mock.assert_called_once_with(hass, hass.config.language, "entity_component")
|
||||||
assert result == "TRANSLATED"
|
assert result == "TRANSLATED"
|
||||||
|
|
||||||
with patch(
|
|
||||||
"homeassistant.helpers.translation.async_get_cached_translations",
|
|
||||||
return_value={"component.binary_sensor.state.device_class.on": "TRANSLATED"},
|
|
||||||
) as mock:
|
|
||||||
result = translation.async_translate_state(
|
|
||||||
hass, "on", "binary_sensor", "platform", None, "device_class"
|
|
||||||
)
|
|
||||||
mock.assert_has_calls(
|
|
||||||
[
|
|
||||||
call(hass, hass.config.language, "entity_component"),
|
|
||||||
call(hass, hass.config.language, "state", "binary_sensor"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
assert result == "TRANSLATED"
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"homeassistant.helpers.translation.async_get_cached_translations",
|
|
||||||
return_value={"component.binary_sensor.state._.on": "TRANSLATED"},
|
|
||||||
) as mock:
|
|
||||||
result = translation.async_translate_state(
|
|
||||||
hass, "on", "binary_sensor", "platform", None, None
|
|
||||||
)
|
|
||||||
mock.assert_has_calls(
|
|
||||||
[
|
|
||||||
call(hass, hass.config.language, "entity_component"),
|
|
||||||
call(hass, hass.config.language, "state", "binary_sensor"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
assert result == "TRANSLATED"
|
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.helpers.translation.async_get_cached_translations",
|
"homeassistant.helpers.translation.async_get_cached_translations",
|
||||||
return_value={},
|
return_value={},
|
||||||
@ -824,7 +663,6 @@ async def test_translate_state(hass: HomeAssistant):
|
|||||||
mock.assert_has_calls(
|
mock.assert_has_calls(
|
||||||
[
|
[
|
||||||
call(hass, hass.config.language, "entity_component"),
|
call(hass, hass.config.language, "entity_component"),
|
||||||
call(hass, hass.config.language, "state", "binary_sensor"),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert result == "on"
|
assert result == "on"
|
||||||
@ -840,7 +678,6 @@ async def test_translate_state(hass: HomeAssistant):
|
|||||||
[
|
[
|
||||||
call(hass, hass.config.language, "entity"),
|
call(hass, hass.config.language, "entity"),
|
||||||
call(hass, hass.config.language, "entity_component"),
|
call(hass, hass.config.language, "entity_component"),
|
||||||
call(hass, hass.config.language, "state", "binary_sensor"),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert result == "on"
|
assert result == "on"
|
||||||
|
@ -7,5 +7,6 @@
|
|||||||
"other4": { "name": "Other 4" },
|
"other4": { "name": "Other 4" },
|
||||||
"outlet": { "name": "Outlet {placeholder}" }
|
"outlet": { "name": "Outlet {placeholder}" }
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"something": "else"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"state": {
|
|
||||||
"string1": "German Value 1",
|
|
||||||
"string2": "German Value 2"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"state": {
|
|
||||||
"string1": "Value 1",
|
|
||||||
"string2": "Value 2"
|
|
||||||
},
|
|
||||||
"something": "else"
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"state": {
|
|
||||||
"string1": "Spanish Value 1"
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user