diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 620207c14bf..498a9605833 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -1,5 +1,4 @@ { - "title": "Philips Hue", "config": { "step": { "init": { diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index e58c6379e95..6989e7770b9 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -1,8 +1,11 @@ """Translation string lookup helpers.""" +import asyncio import logging from typing import Any, Dict, Iterable, Optional +from homeassistant.core import callback from homeassistant.loader import ( + Integration, async_get_config_flows, async_get_integration, bind_hass, @@ -32,8 +35,9 @@ def flatten(data: Dict) -> Dict[str, Any]: return recursive_flatten("", data) -async def component_translation_file( - hass: HomeAssistantType, component: str, language: str +@callback +def component_translation_file( + component: str, language: str, integration: Integration ) -> Optional[str]: """Return the translation json file location for a component. @@ -49,9 +53,6 @@ async def component_translation_file( domain = parts[-1] is_platform = len(parts) == 2 - integration = await async_get_integration(hass, domain) - assert integration is not None, domain - if is_platform: filename = f"{parts[0]}.{language}.json" return str(integration.file_path / ".translations" / filename) @@ -105,26 +106,47 @@ def build_resources( async def async_get_component_resources( hass: HomeAssistantType, language: str ) -> Dict[str, Any]: - """Return translation resources for all components.""" - if TRANSLATION_STRING_CACHE not in hass.data: - hass.data[TRANSLATION_STRING_CACHE] = {} - if language not in hass.data[TRANSLATION_STRING_CACHE]: - hass.data[TRANSLATION_STRING_CACHE][language] = {} - translation_cache = hass.data[TRANSLATION_STRING_CACHE][language] + """Return translation resources for all components. - # Get the set of 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 + """ + # 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) - # Calculate the missing components - missing_components = components - set(translation_cache) + # Calculate the missing components and platforms + missing_loaded = components - set(translation_cache) + missing_domains = {loaded.split(".")[-1] for loaded in missing_loaded} + + missing_integrations = dict( + zip( + missing_domains, + await asyncio.gather( + *[async_get_integration(hass, domain) for domain in missing_domains] + ), + ) + ) + + # Determine paths of missing components/platforms missing_files = {} - for component in missing_components: - path = await component_translation_file(hass, component, language) + for loaded in missing_loaded: + parts = loaded.split(".") + domain = parts[-1] + integration = missing_integrations[domain] + + path = component_translation_file(loaded, language, integration) # No translation available if path is None: - translation_cache[component] = {} + translation_cache[loaded] = {} else: - missing_files[component] = path + missing_files[loaded] = path # Load missing files if missing_files: @@ -134,6 +156,14 @@ async def async_get_component_resources( assert load_translations_job is not None loaded_translations = await load_translations_job + # Translations that miss "title" will get integration put in. + for loaded, translations in loaded_translations.items(): + if "." in loaded: + continue + + if "title" not in translations: + translations["title"] = missing_integrations[loaded].name + # Update cache translation_cache.update(loaded_translations) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 6b846703914..f85425c575f 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -1,12 +1,14 @@ """Test the translation helper.""" -# pylint: disable=protected-access +import asyncio from os import path -from unittest.mock import patch +import pathlib +from asynctest import Mock, patch import pytest from homeassistant.generated import config_flows import homeassistant.helpers.translation as translation +from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component from tests.common import mock_coro @@ -43,14 +45,28 @@ async def test_component_translation_file(hass): assert await async_setup_component(hass, "test_standalone", {"test_standalone"}) assert await async_setup_component(hass, "test_package", {"test_package"}) + ( + int_test, + int_test_embedded, + int_test_standalone, + int_test_package, + ) = await asyncio.gather( + async_get_integration(hass, "test"), + async_get_integration(hass, "test_embedded"), + async_get_integration(hass, "test_standalone"), + async_get_integration(hass, "test_package"), + ) + assert path.normpath( - await translation.component_translation_file(hass, "switch.test", "en") + translation.component_translation_file("switch.test", "en", int_test) ) == path.normpath( hass.config.path("custom_components", "test", ".translations", "switch.en.json") ) assert path.normpath( - await translation.component_translation_file(hass, "switch.test_embedded", "en") + translation.component_translation_file( + "switch.test_embedded", "en", int_test_embedded + ) ) == path.normpath( hass.config.path( "custom_components", "test_embedded", ".translations", "switch.en.json" @@ -58,12 +74,14 @@ async def test_component_translation_file(hass): ) assert ( - await translation.component_translation_file(hass, "test_standalone", "en") + translation.component_translation_file( + "test_standalone", "en", int_test_standalone + ) is None ) assert path.normpath( - await translation.component_translation_file(hass, "test_package", "en") + translation.component_translation_file("test_package", "en", int_test_package) ) == path.normpath( hass.config.path( "custom_components", "test_package", ".translations", "en.json" @@ -118,6 +136,8 @@ async def test_get_translations(hass, mock_config_flows): async def test_get_translations_loads_config_flows(hass, mock_config_flows): """Test the get translations helper loads config flow translations.""" mock_config_flows.append("component1") + integration = Mock(file_path=pathlib.Path(__file__)) + integration.name = "Component 1" with patch.object( translation, "component_translation_file", return_value=mock_coro("bla.json") @@ -125,6 +145,12 @@ async def test_get_translations_loads_config_flows(hass, mock_config_flows): translation, "load_translations_files", return_value={"component1": {"hello": "world"}}, + ), patch( + "homeassistant.helpers.translation.async_get_integration", + return_value=integration, ): translations = await translation.async_get_translations(hass, "en") - assert translations == {"component.component1.hello": "world"} + assert translations == { + "component.component1.title": "Component 1", + "component.component1.hello": "world", + }