1
mirror of https://github.com/home-assistant/core synced 2024-09-15 17:29:45 +02:00

Allow fetching translations by categories (#34329)

This commit is contained in:
Paulus Schoutsen 2020-04-18 17:13:13 -07:00 committed by GitHub
parent d1b3ed717e
commit 98f1548f2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 140 additions and 51 deletions

View File

@ -536,7 +536,13 @@ def websocket_get_themes(hass, connection, msg):
@websocket_api.websocket_command(
{"type": "frontend/get_translations", vol.Required("language"): str}
{
"type": "frontend/get_translations",
vol.Required("language"): str,
vol.Required("category"): str,
vol.Optional("integration"): str,
vol.Optional("config_flow"): bool,
}
)
@websocket_api.async_response
async def websocket_get_translations(hass, connection, msg):
@ -544,7 +550,13 @@ async def websocket_get_translations(hass, connection, msg):
Async friendly.
"""
resources = await async_get_translations(hass, msg["language"])
resources = await async_get_translations(
hass,
msg["language"],
msg["category"],
msg.get("integration"),
msg.get("config_flow"),
)
connection.send_message(
websocket_api.result_message(msg["id"], {"resources": resources})
)

View File

@ -117,7 +117,7 @@ class UserOnboardingView(_BaseOnboardingView):
# Create default areas using the users supplied language.
translations = await hass.helpers.translation.async_get_translations(
data["language"]
data["language"], integration=DOMAIN
)
area_registry = await hass.helpers.area_registry.async_get_registry()

View File

@ -1,7 +1,7 @@
"""Translation string lookup helpers."""
import asyncio
import logging
from typing import Any, Dict, Iterable, Optional
from typing import Any, Dict, Optional, Set
from homeassistant.core import callback
from homeassistant.loader import (
@ -16,6 +16,7 @@ from .typing import HomeAssistantType
_LOGGER = logging.getLogger(__name__)
TRANSLATION_LOAD_LOCK = "translation_load_lock"
TRANSLATION_STRING_CACHE = "translation_string_cache"
@ -36,7 +37,7 @@ def flatten(data: Dict) -> Dict[str, Any]:
@callback
def component_translation_file(
def component_translation_path(
component: str, language: str, integration: Integration
) -> Optional[str]:
"""Return the translation json file location for a component.
@ -80,7 +81,9 @@ def load_translations_files(
def build_resources(
translation_cache: Dict[str, Dict[str, Any]], components: Iterable[str]
translation_cache: Dict[str, Dict[str, Any]],
components: Set[str],
category: Optional[str],
) -> Dict[str, Dict[str, Any]]:
"""Build the resources response for the given components."""
# Build response
@ -91,40 +94,43 @@ def build_resources(
else:
domain = component.split(".", 1)[0]
if domain not in resources:
resources[domain] = {}
domain_resources = resources.setdefault(domain, {})
# Add the translations for this component to the domain resources.
# Since clients cannot determine which platform an entity belongs to,
# all translations for a domain will be returned together.
resources[domain].update(translation_cache[component])
return resources
if category is None:
domain_resources.update(translation_cache[component])
continue
if category not in translation_cache[component]:
continue
domain_resources.setdefault(category, {}).update(
translation_cache[component][category]
)
return {"component": resources}
@bind_hass
async def async_get_component_resources(
hass: HomeAssistantType, language: str
async def async_get_component_cache(
hass: HomeAssistantType, language: str, components: Set[str]
) -> Dict[str, Any]:
"""Return translation resources for all components.
We go through all loaded components and platforms:
- see if they have already been loaded (exist in translation_cache)
- load them if they have not been loaded yet
- write them to cache
- flatten the cache and return
"""
"""Return translation cache that includes all specified components."""
# Get cache for this language
cache = hass.data.setdefault(TRANSLATION_STRING_CACHE, {})
translation_cache = cache.setdefault(language, {})
# Get the set of components to check
components = hass.config.components | await async_get_config_flows(hass)
cache: Dict[str, Dict[str, Any]] = hass.data.setdefault(
TRANSLATION_STRING_CACHE, {}
)
translation_cache: Dict[str, Any] = cache.setdefault(language, {})
# Calculate the missing components and platforms
missing_loaded = components - set(translation_cache)
missing_domains = {loaded.split(".")[-1] for loaded in missing_loaded}
if not missing_loaded:
return translation_cache
missing_domains = list({loaded.split(".")[-1] for loaded in missing_loaded})
missing_integrations = dict(
zip(
missing_domains,
@ -141,7 +147,7 @@ async def async_get_component_resources(
domain = parts[-1]
integration = missing_integrations[domain]
path = component_translation_file(loaded, language, integration)
path = component_translation_path(loaded, language, integration)
# No translation available
if path is None:
translation_cache[loaded] = {}
@ -167,22 +173,47 @@ async def async_get_component_resources(
# Update cache
translation_cache.update(loaded_translations)
resources = build_resources(translation_cache, components)
# Return the component translations resources under the 'component'
# translation namespace
return flatten({"component": resources})
return translation_cache
@bind_hass
async def async_get_translations(
hass: HomeAssistantType, language: str
hass: HomeAssistantType,
language: str,
category: Optional[str] = None,
integration: Optional[str] = None,
config_flow: Optional[bool] = None,
) -> Dict[str, Any]:
"""Return all backend translations."""
resources = await async_get_component_resources(hass, language)
"""Return all backend translations.
If integration specified, load it for that one.
Otherwise default to loaded intgrations combined with config flow
integrations if config_flow is true.
"""
if integration is not None:
components = {integration}
elif config_flow:
components = hass.config.components | await async_get_config_flows(hass)
else:
components = set(hass.config.components)
lock = hass.data.get(TRANSLATION_LOAD_LOCK)
if lock is None:
lock = hass.data[TRANSLATION_LOAD_LOCK] = asyncio.Lock()
tasks = [async_get_component_cache(hass, language, components)]
# Fetch the English resources, as a fallback for missing keys
if language != "en":
# Fetch the English resources, as a fallback for missing keys
base_resources = await async_get_component_resources(hass, "en")
tasks.append(async_get_component_cache(hass, "en", components))
async with lock:
results = await asyncio.gather(*tasks)
resources = flatten(build_resources(results[0], components, category))
if language != "en":
base_resources = flatten(build_resources(results[1], components, category))
resources = {**base_resources, **resources}
return resources

View File

@ -16,7 +16,7 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT
from homeassistant.const import HTTP_NOT_FOUND
from homeassistant.setup import async_setup_component
from tests.common import async_capture_events, mock_coro
from tests.common import async_capture_events
CONFIG_THEMES = {DOMAIN: {CONF_THEMES: {"happy": {"primary-color": "red"}}}}
@ -283,10 +283,17 @@ async def test_get_translations(hass, hass_ws_client):
with patch(
"homeassistant.components.frontend.async_get_translations",
side_effect=lambda hass, lang: mock_coro({"lang": lang}),
side_effect=lambda hass, lang, category, integration, config_flow: {
"lang": lang
},
):
await client.send_json(
{"id": 5, "type": "frontend/get_translations", "language": "nl"}
{
"id": 5,
"type": "frontend/get_translations",
"language": "nl",
"category": "lang",
}
)
msg = await client.receive_json()

View File

@ -35,7 +35,7 @@ def test_flatten():
}
async def test_component_translation_file(hass):
async def test_component_translation_path(hass):
"""Test the component translation file function."""
assert await async_setup_component(
hass,
@ -58,13 +58,13 @@ async def test_component_translation_file(hass):
)
assert path.normpath(
translation.component_translation_file("switch.test", "en", int_test)
translation.component_translation_path("switch.test", "en", int_test)
) == path.normpath(
hass.config.path("custom_components", "test", ".translations", "switch.en.json")
)
assert path.normpath(
translation.component_translation_file(
translation.component_translation_path(
"switch.test_embedded", "en", int_test_embedded
)
) == path.normpath(
@ -74,14 +74,14 @@ async def test_component_translation_file(hass):
)
assert (
translation.component_translation_file(
translation.component_translation_path(
"test_standalone", "en", int_test_standalone
)
is None
)
assert path.normpath(
translation.component_translation_file("test_package", "en", int_test_package)
translation.component_translation_path("test_package", "en", int_test_package)
) == path.normpath(
hass.config.path(
"custom_components", "test_package", ".translations", "en.json"
@ -101,7 +101,10 @@ def test_load_translations_files(hass):
assert translation.load_translations_files(
{"switch.test": file1, "invalid": file2}
) == {
"switch.test": {"state": {"string1": "Value 1", "string2": "Value 2"}},
"switch.test": {
"state": {"string1": "Value 1", "string2": "Value 2"},
"something": "else",
},
"invalid": {},
}
@ -115,10 +118,12 @@ async def test_get_translations(hass, mock_config_flows):
translations = await translation.async_get_translations(hass, "en")
assert translations["component.switch.something"] == "else"
assert translations["component.switch.state.string1"] == "Value 1"
assert translations["component.switch.state.string2"] == "Value 2"
translations = await translation.async_get_translations(hass, "de")
translations = await translation.async_get_translations(hass, "de", "state")
assert "component.switch.something" not in translations
assert translations["component.switch.state.string1"] == "German Value 1"
assert translations["component.switch.state.string2"] == "German Value 2"
@ -140,7 +145,7 @@ async def test_get_translations_loads_config_flows(hass, mock_config_flows):
integration.name = "Component 1"
with patch.object(
translation, "component_translation_file", return_value=mock_coro("bla.json")
translation, "component_translation_path", return_value=mock_coro("bla.json")
), patch.object(
translation,
"load_translations_files",
@ -149,7 +154,40 @@ async def test_get_translations_loads_config_flows(hass, mock_config_flows):
"homeassistant.helpers.translation.async_get_integration",
return_value=integration,
):
translations = await translation.async_get_translations(hass, "en")
translations = await translation.async_get_translations(
hass, "en", config_flow=True
)
assert translations == {
"component.component1.title": "Component 1",
"component.component1.hello": "world",
}
assert "component1" not in hass.config.components
async def test_get_translations_while_loading_components(hass):
"""Test the get translations helper loads config flow translations."""
integration = Mock(file_path=pathlib.Path(__file__))
integration.name = "Component 1"
hass.config.components.add("component1")
async def mock_load_translation_files(files):
"""Mock load translation files."""
# Mimic race condition by loading a component during setup
await async_setup_component(hass, "persistent_notification", {})
return {"component1": {"hello": "world"}}
with patch.object(
translation, "component_translation_path", return_value=mock_coro("bla.json")
), patch.object(
translation, "load_translations_files", side_effect=mock_load_translation_files,
), patch(
"homeassistant.helpers.translation.async_get_integration",
return_value=integration,
):
translations = await translation.async_get_translations(hass, "en")
assert translations == {
"component.component1.title": "Component 1",
"component.component1.hello": "world",

View File

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