1 """Translation string lookup helpers."""
3 from __future__
import annotations
6 from collections.abc
import Iterable, Mapping
7 from contextlib
import suppress
8 from dataclasses
import dataclass
12 from typing
import Any
15 EVENT_CORE_CONFIG_UPDATE,
22 async_get_config_flows,
23 async_get_integrations,
28 from .
import singleton
30 _LOGGER = logging.getLogger(__name__)
32 TRANSLATION_FLATTEN_CACHE =
"translation_flatten_cache"
37 prefix: str, data: dict[str, dict[str, Any] | str]
39 """Return a flattened representation of dict data."""
40 output: dict[str, str] = {}
41 for key, value
in data.items():
42 if isinstance(value, dict):
45 output[f
"{prefix}{key}"] = value
50 translation_files: dict[str, dict[str, pathlib.Path]],
51 ) -> dict[str, dict[str, Any]]:
52 """Load and parse translation.json files."""
53 loaded: dict[str, dict[str, Any]] = {}
54 for language, component_translation_file
in translation_files.items():
55 loaded_for_language: dict[str, Any] = {}
56 loaded[language] = loaded_for_language
58 for component, translation_file
in component_translation_file.items():
61 if not isinstance(loaded_json, dict):
63 "Translation file is unexpected type %s. Expected dict for %s",
69 loaded_for_language[component] = loaded_json
75 translation_strings: dict[str, dict[str, dict[str, Any] | str]],
78 ) -> dict[str, dict[str, Any] | str]:
79 """Build the resources response for the given components."""
82 component: category_strings
83 for component
in components
84 if (component_strings := translation_strings.get(component))
85 and (category_strings := component_strings.get(category))
91 languages: Iterable[str],
93 integrations: dict[str, Integration],
94 ) -> dict[str, dict[str, Any]]:
95 """Load translations."""
96 translations_by_language: dict[str, dict[str, Any]] = {}
98 files_to_load_by_language: dict[str, dict[str, pathlib.Path]] = {}
99 loaded_translations_by_language: dict[str, dict[str, Any]] = {}
100 has_files_to_load =
False
101 for language
in languages:
102 file_name = f
"{language}.json"
103 files_to_load: dict[str, pathlib.Path] = {
104 domain: integration.file_path /
"translations" / file_name
105 for domain
in components
107 (integration := integrations.get(domain))
108 and integration.has_translations
111 files_to_load_by_language[language] = files_to_load
112 has_files_to_load |= bool(files_to_load)
114 if has_files_to_load:
115 loaded_translations_by_language = await hass.async_add_executor_job(
116 _load_translations_files_by_language, files_to_load_by_language
119 for language
in languages:
120 loaded_translations = loaded_translations_by_language.setdefault(language, {})
121 for domain
in components:
123 component_translations = loaded_translations.setdefault(domain, {})
124 if "title" not in component_translations
and (
125 integration := integrations.get(domain)
127 component_translations[
"title"] = integration.name
129 translations_by_language.setdefault(language, {}).
update(loaded_translations)
131 return translations_by_language
134 @dataclass(slots=True)
136 """Data for the translation cache.
138 This class contains data that is designed to be shared
139 between multiple instances of the translation cache so
140 we only have to load the data once.
143 loaded: dict[str, set[str]]
144 cache: dict[str, dict[str, dict[str, dict[str, str]]]]
148 """Cache for flattened translations."""
150 __slots__ = (
"hass",
"cache_data",
"lock")
153 """Initialize the cache."""
160 """Return if the given components are loaded for the language."""
161 return components.issubset(self.
cache_datacache_data.loaded.get(language, set()))
166 components: set[str],
168 """Load resources into the cache."""
169 loaded = self.
cache_datacache_data.loaded.setdefault(language, set())
170 if components_to_load := components - loaded:
175 async
with self.
locklock:
178 if components_to_load := components - loaded:
179 await self.
_async_load_async_load(language, components_to_load)
185 components: set[str],
187 """Load resources into the cache and return them."""
188 await self.
async_loadasync_load(language, components)
190 return self.
get_cachedget_cached(language, category, components)
196 components: set[str],
198 """Read resources from the cache."""
199 category_cache = self.
cache_datacache_data.cache.get(language, {}).
get(category, {})
203 if len(components) == 1
and (component := next(iter(components))):
204 return category_cache.get(component, {})
206 result: dict[str, str] = {}
207 for component
in components.intersection(category_cache):
208 result.update(category_cache[component])
211 async
def _async_load(self, language: str, components: set[str]) ->
None:
212 """Populate the cache for a given set of components."""
215 "Cache miss for %s: %s",
220 languages = [LOCALE_EN]
if language == LOCALE_EN
else [LOCALE_EN, language]
222 integrations: dict[str, Integration] = {}
224 for domain, int_or_exc
in ints_or_excs.items():
225 if isinstance(int_or_exc, Exception):
227 "Failed to load integration for translation: %s", int_or_exc
230 integrations[domain] = int_or_exc
233 self.
hasshass, languages, components, integrations
238 language, components, translation_by_language_strings[LOCALE_EN]
241 if language != LOCALE_EN:
244 language, components, translation_by_language_strings[language]
247 loaded_english_components = loaded.setdefault(LOCALE_EN, set())
250 if loaded_english_components.isdisjoint(components):
252 LOCALE_EN, components, translation_by_language_strings[LOCALE_EN]
254 loaded_english_components.update(components)
256 loaded[language].
update(components)
261 updated_resources: dict[str, str],
262 cached_resources: dict[str, str] |
None =
None,
264 """Validate if updated resources have same placeholders as cached resources."""
265 if cached_resources
is None:
266 return updated_resources
268 mismatches: set[str] = set()
270 for key, value
in updated_resources.items():
271 if key
not in cached_resources:
274 tuples =
list(string.Formatter().parse(value))
277 (
"Error while parsing localized (%s) string %s"), language, key
280 updated_placeholders = {tup[1]
for tup
in tuples
if tup[1]
is not None}
282 tuples =
list(string.Formatter().parse(cached_resources[key]))
283 cached_placeholders = {tup[1]
for tup
in tuples
if tup[1]
is not None}
284 if updated_placeholders != cached_placeholders:
287 "Validation of translation placeholders for localized (%s) string "
288 "%s failed: (%s != %s)"
292 updated_placeholders,
297 for mismatch
in mismatches:
298 del updated_resources[mismatch]
300 return updated_resources
306 components: set[str],
307 translation_strings: dict[str, dict[str, Any]],
309 """Extract resources into the cache."""
310 resource: dict[str, Any] | str
311 cached = self.
cache_datacache_data.cache.setdefault(language, {})
314 for component
in translation_strings.values()
315 for category
in component
318 for category
in categories:
319 new_resources =
build_resources(translation_strings, components, category)
320 category_cache = cached.setdefault(category, {})
322 for component, resource
in new_resources.items():
323 component_cache = category_cache.setdefault(component, {})
325 if not isinstance(resource, dict):
326 component_cache[f
"component.{component}.{category}"] = resource
329 prefix = f
"component.{component}.{category}."
332 component_cache.update(flat)
340 integrations: Iterable[str] |
None =
None,
341 config_flow: bool |
None =
None,
343 """Return all backend translations.
345 If integration is specified, load it for that one.
346 Otherwise, default to loaded integrations combined with config flow
347 integrations if config_flow is true.
349 if integrations
is None and config_flow:
351 elif integrations
is not None:
352 components = set(integrations)
354 components = hass.config.top_level_components
357 language, category, components
366 integration: str |
None =
None,
368 """Return all cached backend translations.
370 If integration is specified, return translations for it.
371 Otherwise, default to all loaded integrations.
373 components = {integration}
if integration
else hass.config.top_level_components
375 language, category, components
379 @singleton.singleton(TRANSLATION_FLATTEN_CACHE)
381 """Return the translation cache."""
387 """Create translation cache and register listeners for translation loaders.
389 Listeners load translations for every loaded component and after config change.
392 current_language = hass.config.language
396 def _async_load_translations_filter(event_data: Mapping[str, Any]) -> bool:
397 """Filter out unwanted events."""
398 nonlocal current_language
400 new_language := event_data.get(
"language")
401 )
and new_language != current_language:
402 current_language = new_language
406 async
def _async_load_translations(event: Event) ->
None:
407 new_language = event.data[
"language"]
408 _LOGGER.debug(
"Loading translations for language: %s", new_language)
409 await cache.async_load(new_language, hass.config.components)
411 hass.bus.async_listen(
412 EVENT_CORE_CONFIG_UPDATE,
413 _async_load_translations,
414 event_filter=_async_load_translations_filter,
419 """Load translations for integrations."""
421 hass.config.language, integrations
427 """Return if the given components are loaded for the language."""
429 hass.config.language, components
435 translation_domain: str,
436 translation_key: str,
437 translation_placeholders: dict[str, str] |
None =
None,
439 """Return a translated exception message.
441 Defaults to English, requires translations to already be cached.
446 f
"component.{translation_domain}.exceptions.{translation_key}.message"
449 if localize_key
in translations:
450 if message := translations[localize_key]:
451 message = message.rstrip(
".")
452 if not translation_placeholders:
454 with suppress(KeyError):
455 message = message.format(**translation_placeholders)
459 return translation_key
467 platform: str |
None,
468 translation_key: str |
None,
469 device_class: str |
None,
471 """Translate provided state using cached translations for currently selected language."""
472 if state
in [STATE_UNAVAILABLE, STATE_UNKNOWN]:
474 language = hass.config.language
475 if platform
is not None and translation_key
is not None:
477 f
"component.{platform}.entity.{domain}.{translation_key}.state.{state}"
480 if localize_key
in translations:
481 return translations[localize_key]
484 if device_class
is not None:
486 f
"component.{domain}.entity_component.{device_class}.state.{state}"
488 if localize_key
in translations:
489 return translations[localize_key]
490 localize_key = f
"component.{domain}.entity_component._.state.{state}"
491 if localize_key
in translations:
492 return translations[localize_key]
None __init__(self, HomeAssistant hass)
None _async_load(self, str language, set[str] components)
None _build_category_cache(self, str language, set[str] components, dict[str, dict[str, Any]] translation_strings)
dict[str, str] _validate_placeholders(self, str language, dict[str, str] updated_resources, dict[str, str]|None cached_resources=None)
dict[str, str] async_fetch(self, str language, str category, set[str] components)
bool async_is_loaded(self, str language, set[str] components)
dict[str, str] get_cached(self, str language, str category, set[str] components)
None async_load(self, str language, set[str] components)
web.Response get(self, web.Request request, str config_key)
IssData update(pyiss.ISS iss)
HomeAssistant async_get_hass()
None async_load(HomeAssistant hass)
None async_setup(HomeAssistant hass)
str async_get_exception_message(str translation_domain, str translation_key, dict[str, str]|None translation_placeholders=None)
bool async_translations_loaded(HomeAssistant hass, set[str] components)
dict[str, str] recursive_flatten(str prefix, dict[str, dict[str, Any]|str] data)
str async_translate_state(HomeAssistant hass, str state, str domain, str|None platform, str|None translation_key, str|None device_class)
_TranslationCache _async_get_translations_cache(HomeAssistant hass)
dict[str, dict[str, Any]] _load_translations_files_by_language(dict[str, dict[str, pathlib.Path]] translation_files)
dict[str, dict[str, Any]|str] build_resources(dict[str, dict[str, dict[str, Any]|str]] translation_strings, set[str] components, str category)
dict[str, str] async_get_cached_translations(HomeAssistant hass, str language, str category, str|None integration=None)
dict[str, str] async_get_translations(HomeAssistant hass, str language, str category, Iterable[str]|None integrations=None, bool|None config_flow=None)
dict[str, dict[str, Any]] _async_get_component_strings(HomeAssistant hass, Iterable[str] languages, set[str] components, dict[str, Integration] integrations)
None async_load_integrations(HomeAssistant hass, set[str] integrations)
set[str] async_get_config_flows(HomeAssistant hass, Literal["device", "helper", "hub", "service"]|None type_filter=None)
dict[str, Integration|Exception] async_get_integrations(HomeAssistant hass, Iterable[str] domains)
JsonValueType load_json(str|PathLike[str] filename, JsonValueType default=_SENTINEL)