Skip to content

Commit f751a9e

Browse files
frenckatronCopilotfrenck
authored
Fix Spook action names in translations (#1311)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Franck Nijhof <frenck@frenck.dev>
1 parent 9e23755 commit f751a9e

4 files changed

Lines changed: 1262 additions & 17 deletions

File tree

custom_components/spook/services.py

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111
from awesomeversion import AwesomeVersion
1212
import voluptuous as vol
1313

14-
from homeassistant.const import __short_version__ as current_version
14+
from homeassistant.const import (
15+
EVENT_CORE_CONFIG_UPDATE,
16+
__short_version__ as current_version,
17+
)
1518
from homeassistant.core import (
19+
Event,
1620
HomeAssistant,
1721
Service,
1822
ServiceCall,
@@ -29,15 +33,23 @@
2933
async_register_admin_service,
3034
async_set_service_schema,
3135
)
36+
from homeassistant.helpers.translation import (
37+
_async_get_translations_cache,
38+
async_get_cached_translations,
39+
async_get_translations,
40+
)
3241
from homeassistant.loader import async_get_integration
3342

3443
from .const import DOMAIN, LOGGER
3544

3645
if TYPE_CHECKING:
46+
from collections.abc import Callable
3747
from types import ModuleType
3848

3949

4050
_EntityT = TypeVar("_EntityT", bound=Entity, default=Entity)
51+
GHOST = "👻"
52+
SERVICE_TRANSLATION_CATEGORY = "services"
4153

4254

4355
class AbstractSpookServiceBase(ABC):
@@ -252,6 +264,10 @@ class SpookServiceManager:
252264

253265
_services: set[AbstractSpookService] = field(default_factory=set)
254266
_service_schemas: dict[str, Any] = field(default_factory=dict)
267+
_service_translation_overrides: dict[tuple[str, str, str], str | None] = field(
268+
default_factory=dict
269+
)
270+
_translation_listener: Callable[[], None] | None = None
255271

256272
def __post_init__(self) -> None:
257273
"""Post initialization."""
@@ -318,6 +334,12 @@ def _load_all_service_modules() -> None:
318334

319335
self.async_register_service(service)
320336

337+
await self.async_inject_service_translations()
338+
self._translation_listener = self.hass.bus.async_listen(
339+
EVENT_CORE_CONFIG_UPDATE,
340+
self._async_core_config_updated,
341+
)
342+
321343
@callback
322344
def async_register_service(self, service: AbstractSpookService) -> None:
323345
"""Register a Spook service."""
@@ -343,10 +365,168 @@ def async_register_service(self, service: AbstractSpookService) -> None:
343365
schema=service_schema,
344366
)
345367

368+
@callback
369+
def _service_schema_key(self, service: AbstractSpookService) -> str:
370+
"""Return the services.yaml key for a Spook service."""
371+
if service.domain == DOMAIN:
372+
return service.service
373+
return f"{service.domain}_{service.service}"
374+
375+
@callback
376+
def _service_translation_strings(
377+
self,
378+
service: AbstractSpookService,
379+
cached_spook_translations: dict[str, str],
380+
) -> dict[str, str]:
381+
"""Return service translation strings mapped to the target domain."""
382+
schema_key = self._service_schema_key(service)
383+
spook_prefix = f"component.{DOMAIN}.services.{schema_key}."
384+
target_prefix = f"component.{service.domain}.services.{service.service}."
385+
386+
return {
387+
f"{target_prefix}{key.removeprefix(spook_prefix)}": (
388+
f"{value} {GHOST}"
389+
if key == f"{spook_prefix}name" and GHOST not in value
390+
else value
391+
)
392+
for key, value in cached_spook_translations.items()
393+
if key.startswith(spook_prefix)
394+
}
395+
396+
@callback
397+
def _translation_component_cache(
398+
self,
399+
language: str,
400+
domain: str,
401+
*,
402+
create: bool = False,
403+
) -> dict[str, str] | None:
404+
"""Return the Home Assistant translation cache for a component."""
405+
translations_cache = _async_get_translations_cache(self.hass)
406+
407+
try:
408+
cache = translations_cache.cache_data.cache
409+
except AttributeError:
410+
LOGGER.warning(
411+
"Unable to access Home Assistant's translation cache, "
412+
"skipping Spook service translation update"
413+
)
414+
return None
415+
416+
if not isinstance(cache, dict):
417+
LOGGER.warning(
418+
"Home Assistant's translation cache has an unexpected structure, "
419+
"skipping Spook service translation update"
420+
)
421+
return None
422+
423+
if create:
424+
return (
425+
cache.setdefault(language, {})
426+
.setdefault(
427+
SERVICE_TRANSLATION_CATEGORY,
428+
{},
429+
)
430+
.setdefault(domain, {})
431+
)
432+
433+
return cache.get(language, {}).get(SERVICE_TRANSLATION_CATEGORY, {}).get(domain)
434+
435+
@callback
436+
def _inject_service_translation_strings(
437+
self,
438+
service: AbstractSpookService,
439+
cached_spook_translations: dict[str, str],
440+
) -> None:
441+
"""Inject service translation strings into Home Assistant's cache."""
442+
language = self.hass.config.language
443+
component_cache = self._translation_component_cache(
444+
language,
445+
service.domain,
446+
create=True,
447+
)
448+
if component_cache is None:
449+
return
450+
451+
cached_translations = async_get_cached_translations(
452+
self.hass,
453+
language,
454+
SERVICE_TRANSLATION_CATEGORY,
455+
service.domain,
456+
)
457+
458+
for key, value in self._service_translation_strings(
459+
service,
460+
cached_spook_translations,
461+
).items():
462+
self._service_translation_overrides.setdefault(
463+
(language, service.domain, key), cached_translations.get(key)
464+
)
465+
component_cache[key] = value
466+
467+
async def async_inject_service_translations(self) -> None:
468+
"""Inject Spook service strings into Home Assistant translations."""
469+
services = [
470+
service
471+
for service in self._services
472+
if self._service_schema_key(service) in self._service_schemas
473+
]
474+
475+
if not services:
476+
return
477+
478+
await async_get_translations(
479+
self.hass,
480+
self.hass.config.language,
481+
SERVICE_TRANSLATION_CATEGORY,
482+
{DOMAIN, *(service.domain for service in services)},
483+
)
484+
cached_spook_translations = async_get_cached_translations(
485+
self.hass,
486+
self.hass.config.language,
487+
SERVICE_TRANSLATION_CATEGORY,
488+
DOMAIN,
489+
)
490+
491+
for service in services:
492+
self._inject_service_translation_strings(
493+
service,
494+
cached_spook_translations,
495+
)
496+
497+
async def _async_core_config_updated(self, event: Event) -> None:
498+
"""Re-inject service translations when the language changes."""
499+
if "language" not in event.data:
500+
return
501+
await self.async_inject_service_translations()
502+
503+
@callback
504+
def async_clear_service_translation_overrides(self) -> None:
505+
"""Restore translation strings that were overridden by Spook."""
506+
for (
507+
language,
508+
domain,
509+
key,
510+
), original_value in self._service_translation_overrides.items():
511+
component_cache = self._translation_component_cache(language, domain)
512+
if component_cache is None:
513+
continue
514+
515+
if original_value is None:
516+
component_cache.pop(key, None)
517+
else:
518+
component_cache[key] = original_value
519+
520+
self._service_translation_overrides.clear()
521+
346522
@callback
347523
def async_on_unload(self) -> None:
348524
"""Tear down the Spook services."""
349525
LOGGER.debug("Tearing down Spook services")
526+
if self._translation_listener:
527+
self._translation_listener()
528+
self._translation_listener = None
529+
350530
for service in self._services:
351531
LOGGER.debug(
352532
"Unregistering service: %s.%s",
@@ -373,3 +553,5 @@ def async_on_unload(self) -> None:
373553

374554
# Flush service description schema cache
375555
self.hass.data.pop(SERVICE_DESCRIPTION_CACHE, None)
556+
557+
self.async_clear_service_translation_overrides()

custom_components/spook/services.yaml

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ homeassistant_add_alias_to_area:
5959
area:
6060
alias:
6161
name: Alias
62-
description: The alias (or list of aliasses) to add to the area.
62+
description: The alias (or list of aliases) to add to the area.
6363
required: true
6464
selector:
6565
object:
@@ -77,15 +77,15 @@ homeassistant_remove_alias_from_area:
7777
area:
7878
alias:
7979
name: Alias
80-
description: The alias (or list of aliasses) to remove from the area.
80+
description: The alias (or list of aliases) to remove from the area.
8181
required: true
8282
selector:
8383
object:
8484

8585
homeassistant_set_area_aliases:
8686
name: Sets aliases for an area 👻
8787
description: >-
88-
Sets aliases for an area. Overwrite and removed any existing aliases,
88+
Sets aliases for an area. Overwrites and removes any existing aliases,
8989
fully replacing them with the new ones.
9090
fields:
9191
area_id:
@@ -104,7 +104,7 @@ homeassistant_set_area_aliases:
104104
homeassistant_add_device_to_area:
105105
name: Add a device to an area 👻
106106
description: >-
107-
Adds an device to an area. Please note, if the device is already in an area,
107+
Adds a device to an area. Please note, if the device is already in an area,
108108
it will be removed from the previous area.
109109
fields:
110110
area_id:
@@ -138,7 +138,7 @@ homeassistant_remove_device_from_area:
138138
homeassistant_add_entity_to_area:
139139
name: Add an entity to an area 👻
140140
description: >-
141-
Adds an entity to an area. Please note, if the enity is already in an area,
141+
Adds an entity to an area. Please note, if the entity is already in an area,
142142
it will be removed from the previous area. This will override the area
143143
the device, that provides this entity, is in.
144144
fields:
@@ -174,7 +174,7 @@ homeassistant_remove_entity_from_area:
174174
homeassistant_delete_area:
175175
name: Delete an area 👻
176176
description: >-
177-
Deletes a new area on the fly.
177+
Deletes an area on the fly.
178178
fields:
179179
area_id:
180180
name: Area ID
@@ -271,7 +271,7 @@ homeassistant_update_entity_id:
271271
entity:
272272
new_entity_id:
273273
name: New Entity ID
274-
description: The new ID for the entity
274+
description: The new ID for the entity.
275275
required: true
276276
selector:
277277
text:
@@ -392,7 +392,7 @@ homeassistant_add_alias_to_floor:
392392
floor:
393393
alias:
394394
name: Alias
395-
description: The alias (or list of aliasses) to add to the floor.
395+
description: The alias (or list of aliases) to add to the floor.
396396
required: true
397397
selector:
398398
object:
@@ -410,15 +410,15 @@ homeassistant_remove_alias_from_floor:
410410
floor:
411411
alias:
412412
name: Alias
413-
description: The alias (or list of aliasses) to remove from the floor.
413+
description: The alias (or list of aliases) to remove from the floor.
414414
required: true
415415
selector:
416416
object:
417417

418418
homeassistant_set_floor_aliases:
419419
name: Sets aliases for a floor 👻
420420
description: >-
421-
Sets aliases for a floor. Overwrite and removed any existing aliases,
421+
Sets aliases for a floor. Overwrites and removes any existing aliases,
422422
fully replacing them with the new ones.
423423
fields:
424424
floor_id:
@@ -625,7 +625,7 @@ homeassistant_add_label_to_entity:
625625
homeassistant_remove_label_from_area:
626626
name: Remove a label from an area 👻
627627
description: >-
628-
Removes a label to an area. If multiple labels or multiple areas are
628+
Removes a label from an area. If multiple labels or multiple areas are
629629
provided, all combinations will be removed.
630630
fields:
631631
label_id:
@@ -830,7 +830,7 @@ recorder_import_statistics:
830830
select_random:
831831
name: Select random option 👻
832832
description: >-
833-
Select an random option for a select entity.
833+
Select a random option for a select entity.
834834
target:
835835
entity:
836836
domain: select
@@ -847,7 +847,7 @@ select_random:
847847
input_select_random:
848848
name: Select random option 👻
849849
description: >-
850-
Select an random option for an input_select entity.
850+
Select a random option for an input_select entity.
851851
target:
852852
entity:
853853
domain: input_select
@@ -1126,7 +1126,7 @@ person_remove_device_tracker:
11261126
repairs_create:
11271127
name: Create issue 👻
11281128
description: >-
1129-
Manually create and raise a issue in Home Assistant repairs.
1129+
Manually create and raise an issue in Home Assistant repairs.
11301130
fields:
11311131
title:
11321132
name: Title
@@ -1145,7 +1145,7 @@ repairs_create:
11451145
name: Issue ID
11461146
description: >-
11471147
The issue can have an identifier, which allows you to cancel it
1148-
later with that ID if needed. It also prevent duplicate issues
1148+
later with that ID if needed. It also prevents duplicate issues
11491149
to be created. If not provided, a random ID will be generated.
11501150
required: false
11511151
selector:
@@ -1165,7 +1165,7 @@ repairs_create:
11651165
name: Severity
11661166
description: >-
11671167
The severity of the issue. This will be used to determine the
1168-
priority of the issue. If not set, "warning" will be used
1168+
priority of the issue. If not set, "warning" will be used.
11691169
required: false
11701170
selector:
11711171
select:

0 commit comments

Comments
 (0)