Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing translations and descriptions for notification fields in UI #132676

Draft
wants to merge 12 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 72 additions & 15 deletions homeassistant/components/notify/legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import discovery
from homeassistant.helpers.service import async_set_service_schema
from homeassistant.helpers.translation import (
async_get_translations,
convert_to_nested_dict,
get_from_nested_dict,
translation_fields,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.loader import async_get_integration, bind_hass
from homeassistant.setup import (
Expand Down Expand Up @@ -43,6 +49,11 @@
f"{DOMAIN}_discovery_dispatcher"
)

TRANSLATION_KEYS = {
"send_via_target": "component.notify.services.send_via_target",
"send_with_service": "component.notify.services.send_with_service",
}


class LegacyNotifyPlatform(Protocol):
"""Define the format of legacy notify platforms."""
Expand Down Expand Up @@ -277,6 +288,16 @@ async def async_setup(

async def async_register_services(self) -> None:
"""Create or update the notify services."""
# Fetch translations for the service dynamically based on the current active language
current_language = self.hass.config.language
translations = await async_get_translations(
self.hass, current_language, "services"
)
nested_translations = convert_to_nested_dict(translations)

send_via_target_key = TRANSLATION_KEYS.get("send_via_target")
send_with_service_key = TRANSLATION_KEYS.get("send_with_service")

if self.targets is not None:
stale_targets = set(self.registered_targets)

Expand All @@ -297,14 +318,32 @@ async def async_register_services(self) -> None:
schema=NOTIFY_SERVICE_SCHEMA,
)
# Register the service description
service_desc = {
CONF_NAME: f"Send a notification via {target_name}",
CONF_DESCRIPTION: (
"Sends a notification message using the"
f" {target_name} integration."
),
CONF_FIELDS: self.services_dict[SERVICE_NOTIFY][CONF_FIELDS],
}
if send_via_target_key and (
send_via_target := get_from_nested_dict(
nested_translations, send_via_target_key
)
):
service_desc = {
CONF_NAME: send_via_target["name"].format(
target_name=target_name
),
CONF_DESCRIPTION: send_via_target["description"].format(
target_name=target_name
),
CONF_FIELDS: translation_fields(
send_via_target[CONF_FIELDS],
self.services_dict[SERVICE_NOTIFY][CONF_FIELDS],
),
}
else:
service_desc = {
CONF_NAME: f"Send a notification via {target_name}",
CONF_DESCRIPTION: (
"Sends a notification message using the"
f" {target_name} integration."
),
CONF_FIELDS: self.services_dict[SERVICE_NOTIFY][CONF_FIELDS],
}
async_set_service_schema(self.hass, DOMAIN, target_name, service_desc)

for stale_target_name in stale_targets:
Expand All @@ -325,13 +364,31 @@ async def async_register_services(self) -> None:
)

# Register the service description
service_desc = {
CONF_NAME: f"Send a notification with {self._service_name}",
CONF_DESCRIPTION: (
f"Sends a notification message using the {self._service_name} service."
),
CONF_FIELDS: self.services_dict[SERVICE_NOTIFY][CONF_FIELDS],
}
if send_with_service_key and (
send_with_service := get_from_nested_dict(
nested_translations, send_with_service_key
)
):
service_desc = {
CONF_NAME: send_with_service["name"].format(
service_name=self._service_name
),
CONF_DESCRIPTION: send_with_service["description"].format(
service_name=self._service_name
),
CONF_FIELDS: translation_fields(
send_with_service[CONF_FIELDS],
self.services_dict[SERVICE_NOTIFY][CONF_FIELDS],
),
}
else:
service_desc = {
CONF_NAME: f"Send a notification with {self._service_name}",
CONF_DESCRIPTION: (
f"Sends a notification message using the {self._service_name} service."
),
CONF_FIELDS: self.services_dict[SERVICE_NOTIFY][CONF_FIELDS],
}
async_set_service_schema(self.hass, DOMAIN, self._service_name, service_desc)

async def async_unregister_services(self) -> None:
Expand Down
44 changes: 44 additions & 0 deletions homeassistant/components/notify/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,50 @@
}
}
},
"send_via_target": {
"name": "Send a notification via {target_name}",
"description": "Sends a notification message using the {target_name} integration.",
"fields": {
"message": {
"name": "Message",
"description": "Message body of the notification."
},
"title": {
"name": "Title",
"description": "Title for your notification."
},
"target": {
"name": "Target",
"description": "The recipient of the notification."
},
"data": {
"name": "Data",
"description": "Custom data to be sent with the notification."
}
}
},
"send_with_service": {
"name": "Send a notification with {service_name}",
"description": "Sends a notification message using the {service_name} service.",
"fields": {
"message": {
"name": "Message",
"description": "Message body of the notification."
},
"title": {
"name": "Title",
"description": "Title for your notification."
},
"target": {
"name": "Target",
"description": "The recipient of the notification."
},
"data": {
"name": "Data",
"description": "Custom data to be sent with the notification."
}
}
},
"send_message": {
"name": "Send a notification message",
"description": "Sends a notification message.",
Expand Down
46 changes: 46 additions & 0 deletions homeassistant/helpers/translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,3 +492,49 @@ def async_translate_state(
return translations[localize_key]

return state


def convert_to_nested_dict(input_dict: dict[str, str]) -> dict[str, Any]:
"""Convert a flat translation dictionary into nested format."""
nested_dict: dict[str, Any] = {}
for key, value in input_dict.items():
keys = key.split(".")
d = nested_dict

for part in keys[:-1]:
if part not in d:
d[part] = {}
d = d[part]

d[keys[-1]] = value

return nested_dict


def get_from_nested_dict(nested_dict: dict[str, Any], path: str) -> Any:
"""Fetch specific translation paths from the nested dictionary using a dot-separated path."""
keys = path.split(".")
for key in keys:
if isinstance(nested_dict, dict) and key in nested_dict:
nested_dict = nested_dict[key]
else:
return None
return nested_dict


def translation_fields(
fields: dict[str, dict[str, str]], fields_schema: dict[str, dict[str, Any]]
) -> dict[str, dict[str, Any]]:
"""Update a schema with translated field names and descriptions from the provided translations."""
updated_schema = {}
for field_name, field_info in fields_schema.items():
translation = fields.get(field_name, {})
updated_field = field_info.copy()
updated_field.update(
{
"name": translation.get("name", ""),
"description": translation.get("description", ""),
}
)
updated_schema[field_name] = updated_field
return updated_schema
56 changes: 56 additions & 0 deletions tests/helpers/test_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
from unittest.mock import Mock, call, patch

import pytest
import voluptuous as vol

from homeassistant import loader
from homeassistant.const import EVENT_CORE_CONFIG_UPDATE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import translation
import homeassistant.helpers.config_validation as cv
from homeassistant.setup import async_setup_component


Expand Down Expand Up @@ -712,3 +714,57 @@ async def test_get_translations_still_has_title_without_translations_files(
assert translations == {
"component.component1.title": "Component 1",
}


@pytest.fixture
async def mock_integration(hass: HomeAssistant):
"""Set up a mock notification service."""

async def test_service(call):
pass

hass.services.async_register(
"test_notify",
"send_message",
test_service,
schema=vol.Schema(
{
vol.Required("message"): cv.string,
vol.Optional("title"): cv.string,
}
),
)


async def test_translation_for_notify_service(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we want to test the notify integration service descriptions I'd expect this test to be part of the notify integration (legacy) tests.

hass: HomeAssistant, mock_integration
) -> None:
"""Test that the notification service description is translated correctly."""
translations = {
"component.test_notify.services.send_message.description": "Send a translated test notification",
"component.test_notify.services.send_message.fields.message.description": "Translated message description",
"component.test_notify.services.send_message.fields.title.description": "Translated title description",
}

with patch(
"homeassistant.helpers.translation.async_get_translations",
return_value=translations,
):
descriptions = await translation.async_get_translations(hass, "en", "services")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't testing any production code. The call here is calling the mock patched above.


assert (
descriptions["component.test_notify.services.send_message.description"]
== "Send a translated test notification"
)
assert (
descriptions[
"component.test_notify.services.send_message.fields.message.description"
]
== "Translated message description"
)
assert (
descriptions[
"component.test_notify.services.send_message.fields.title.description"
]
== "Translated title description"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""notify."""

DOMAIN = "test_notify"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"domain": "test_notify",
"name": "Test Notify",
"documentation": "https://example.com",
"dependencies": [],
"codeowners": [],
"requirements": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
send_message:
description: "Sends a test notification"
fields:
message:
description: "The message to send"
title:
description: "The title of the message"
15 changes: 15 additions & 0 deletions tests/testing_config/custom_components/test_notify/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"services": {
"send_message": {
"description": "Send a translated test notification",
"fields": {
"message": {
"description": "Translated message description"
},
"title": {
"description": "Translated title description"
}
}
}
}
}