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:
J. Nick Koston 2024-04-14 06:13:17 -05:00 committed by GitHub
parent 0200d1aa66
commit 33412dd9f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 139 additions and 514 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -7,5 +7,6 @@
"other4": { "name": "Other 4" }, "other4": { "name": "Other 4" },
"outlet": { "name": "Outlet {placeholder}" } "outlet": { "name": "Outlet {placeholder}" }
} }
} },
"something": "else"
} }

View File

@ -1,6 +0,0 @@
{
"state": {
"string1": "German Value 1",
"string2": "German Value 2"
}
}

View File

@ -1,7 +0,0 @@
{
"state": {
"string1": "Value 1",
"string2": "Value 2"
},
"something": "else"
}

View File

@ -1,5 +0,0 @@
{
"state": {
"string1": "Spanish Value 1"
}
}