2018-03-01 03:31:38 +00:00
|
|
|
"""Translation string lookup helpers."""
|
|
|
|
import logging
|
2019-04-13 00:10:19 +00:00
|
|
|
from typing import Any, Dict, Iterable, Optional
|
2018-03-01 03:31:38 +00:00
|
|
|
|
2019-07-08 23:19:37 +00:00
|
|
|
from homeassistant.loader import (
|
2019-07-31 19:25:30 +00:00
|
|
|
async_get_integration,
|
|
|
|
bind_hass,
|
|
|
|
async_get_config_flows,
|
|
|
|
)
|
2018-03-01 03:31:38 +00:00
|
|
|
from homeassistant.util.json import load_json
|
2018-10-28 19:12:52 +00:00
|
|
|
from .typing import HomeAssistantType
|
2018-03-01 03:31:38 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
TRANSLATION_STRING_CACHE = "translation_string_cache"
|
2018-03-01 03:31:38 +00:00
|
|
|
|
|
|
|
|
2018-10-28 19:12:52 +00:00
|
|
|
def recursive_flatten(prefix: Any, data: Dict) -> Dict[str, Any]:
|
2018-03-01 03:31:38 +00:00
|
|
|
"""Return a flattened representation of dict data."""
|
|
|
|
output = {}
|
|
|
|
for key, value in data.items():
|
|
|
|
if isinstance(value, dict):
|
2019-08-23 16:53:33 +00:00
|
|
|
output.update(recursive_flatten(f"{prefix}{key}.", value))
|
2018-03-01 03:31:38 +00:00
|
|
|
else:
|
2019-08-23 16:53:33 +00:00
|
|
|
output[f"{prefix}{key}"] = value
|
2018-03-01 03:31:38 +00:00
|
|
|
return output
|
|
|
|
|
|
|
|
|
2018-10-28 19:12:52 +00:00
|
|
|
def flatten(data: Dict) -> Dict[str, Any]:
|
2018-03-01 03:31:38 +00:00
|
|
|
"""Return a flattened representation of dict data."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return recursive_flatten("", data)
|
2018-03-01 03:31:38 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def component_translation_file(
|
|
|
|
hass: HomeAssistantType, component: str, language: str
|
|
|
|
) -> Optional[str]:
|
2019-01-16 00:06:04 +00:00
|
|
|
"""Return the translation json file location for a component.
|
|
|
|
|
2019-04-13 00:10:19 +00:00
|
|
|
For component:
|
|
|
|
- components/hue/.translations/nl.json
|
2019-01-16 00:06:04 +00:00
|
|
|
|
2019-04-13 00:10:19 +00:00
|
|
|
For platform:
|
2019-01-16 00:06:04 +00:00
|
|
|
- components/hue/.translations/light.nl.json
|
|
|
|
|
2019-04-13 00:10:19 +00:00
|
|
|
If component is just a single file, will return None.
|
|
|
|
"""
|
2019-07-31 19:25:30 +00:00
|
|
|
parts = component.split(".")
|
2019-04-13 00:10:19 +00:00
|
|
|
domain = parts[-1]
|
|
|
|
is_platform = len(parts) == 2
|
2019-01-16 00:06:04 +00:00
|
|
|
|
2019-04-13 00:10:19 +00:00
|
|
|
integration = await async_get_integration(hass, domain)
|
|
|
|
assert integration is not None, domain
|
2018-03-01 03:31:38 +00:00
|
|
|
|
2019-04-13 00:10:19 +00:00
|
|
|
if is_platform:
|
2019-01-16 00:06:04 +00:00
|
|
|
filename = "{}.{}.json".format(parts[0], language)
|
2019-07-31 19:25:30 +00:00
|
|
|
return str(integration.file_path / ".translations" / filename)
|
2018-03-01 03:31:38 +00:00
|
|
|
|
2019-04-13 00:10:19 +00:00
|
|
|
# If it's a component that is just one file, we don't support translations
|
|
|
|
# Example custom_components/my_component.py
|
2019-04-18 18:11:43 +00:00
|
|
|
if integration.file_path.name != domain:
|
2019-04-13 00:10:19 +00:00
|
|
|
return None
|
|
|
|
|
2019-08-23 16:53:33 +00:00
|
|
|
filename = f"{language}.json"
|
2019-07-31 19:25:30 +00:00
|
|
|
return str(integration.file_path / ".translations" / filename)
|
2018-03-01 03:31:38 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def load_translations_files(
|
|
|
|
translation_files: Dict[str, str]
|
|
|
|
) -> Dict[str, Dict[str, Any]]:
|
2018-03-01 03:31:38 +00:00
|
|
|
"""Load and parse translation.json files."""
|
|
|
|
loaded = {}
|
|
|
|
for component, translation_file in translation_files.items():
|
2018-10-28 19:12:52 +00:00
|
|
|
loaded_json = load_json(translation_file)
|
|
|
|
assert isinstance(loaded_json, dict)
|
|
|
|
loaded[component] = loaded_json
|
2018-03-01 03:31:38 +00:00
|
|
|
|
|
|
|
return loaded
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def build_resources(
|
|
|
|
translation_cache: Dict[str, Dict[str, Any]], components: Iterable[str]
|
|
|
|
) -> Dict[str, Dict[str, Any]]:
|
2018-03-01 03:31:38 +00:00
|
|
|
"""Build the resources response for the given components."""
|
|
|
|
# Build response
|
2018-10-28 19:12:52 +00:00
|
|
|
resources = {} # type: Dict[str, Dict[str, Any]]
|
2018-03-01 03:31:38 +00:00
|
|
|
for component in components:
|
2019-07-31 19:25:30 +00:00
|
|
|
if "." not in component:
|
2018-03-01 03:31:38 +00:00
|
|
|
domain = component
|
|
|
|
else:
|
2019-07-31 19:25:30 +00:00
|
|
|
domain = component.split(".", 1)[0]
|
2018-03-01 03:31:38 +00:00
|
|
|
|
|
|
|
if domain not in resources:
|
|
|
|
resources[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
|
|
|
|
|
|
|
|
|
|
|
|
@bind_hass
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_get_component_resources(
|
|
|
|
hass: HomeAssistantType, language: str
|
|
|
|
) -> Dict[str, Any]:
|
2018-03-01 03:31:38 +00:00
|
|
|
"""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]
|
|
|
|
|
|
|
|
# Get the set of components
|
2019-07-31 19:25:30 +00:00
|
|
|
components = hass.config.components | await async_get_config_flows(hass)
|
2018-03-01 03:31:38 +00:00
|
|
|
|
|
|
|
# Calculate the missing components
|
|
|
|
missing_components = components - set(translation_cache)
|
|
|
|
missing_files = {}
|
|
|
|
for component in missing_components:
|
2019-04-13 00:10:19 +00:00
|
|
|
path = await component_translation_file(hass, component, language)
|
|
|
|
# No translation available
|
|
|
|
if path is None:
|
|
|
|
translation_cache[component] = {}
|
|
|
|
else:
|
|
|
|
missing_files[component] = path
|
2018-03-01 03:31:38 +00:00
|
|
|
|
|
|
|
# Load missing files
|
|
|
|
if missing_files:
|
2018-10-28 19:12:52 +00:00
|
|
|
load_translations_job = hass.async_add_job(
|
2019-07-31 19:25:30 +00:00
|
|
|
load_translations_files, missing_files
|
|
|
|
)
|
2018-10-28 19:12:52 +00:00
|
|
|
assert load_translations_job is not None
|
|
|
|
loaded_translations = await load_translations_job
|
2018-03-01 03:31:38 +00:00
|
|
|
|
|
|
|
# Update cache
|
2018-10-28 19:12:52 +00:00
|
|
|
translation_cache.update(loaded_translations)
|
2018-03-01 03:31:38 +00:00
|
|
|
|
|
|
|
resources = build_resources(translation_cache, components)
|
|
|
|
|
|
|
|
# Return the component translations resources under the 'component'
|
|
|
|
# translation namespace
|
2019-07-31 19:25:30 +00:00
|
|
|
return flatten({"component": resources})
|
2018-03-01 03:31:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
@bind_hass
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_get_translations(
|
|
|
|
hass: HomeAssistantType, language: str
|
|
|
|
) -> Dict[str, Any]:
|
2018-03-01 03:31:38 +00:00
|
|
|
"""Return all backend translations."""
|
|
|
|
resources = await async_get_component_resources(hass, language)
|
2019-07-31 19:25:30 +00:00
|
|
|
if language != "en":
|
2018-03-01 03:31:38 +00:00
|
|
|
# Fetch the English resources, as a fallback for missing keys
|
2019-07-31 19:25:30 +00:00
|
|
|
base_resources = await async_get_component_resources(hass, "en")
|
2018-03-01 03:31:38 +00:00
|
|
|
resources = {**base_resources, **resources}
|
|
|
|
|
|
|
|
return resources
|