mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
Allow fetching translations by categories (#34329)
This commit is contained in:
parent
d1b3ed717e
commit
98f1548f2d
@ -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})
|
||||
)
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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",
|
||||
|
@ -2,5 +2,6 @@
|
||||
"state": {
|
||||
"string1": "Value 1",
|
||||
"string2": "Value 2"
|
||||
}
|
||||
},
|
||||
"something": "else"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user