diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index 09bbba4c226..2260acede1c 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -106,7 +106,7 @@ }, "exceptions": { "integration_not_found": { - "message": "Integration '{target}' not found in registry" + "message": "Integration \"{target}\" not found in registry" }, "no_path": { "message": "Can't write to directory {target}, no access to path!" diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index a21f57a7a24..50afd21deb3 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -227,7 +227,7 @@ }, "deprecated_yaml_import_issue_continent_not_match": { "title": "The Ecovacs YAML configuration import failed", - "description": "Configuring Ecovacs using YAML is being removed but there is an unexpected continent specified in the YAML configuration.\n\nFrom the given country, the continent '{continent}' is expected. Change the continent and restart Home Assistant to try again or remove the Ecovacs YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually.\n\nIf the contintent '{continent}' is not applicable, please open an issue on [GitHub]({github_issue_url})." + "description": "Configuring Ecovacs using YAML is being removed but there is an unexpected continent specified in the YAML configuration.\n\nFrom the given country, the continent \"{continent}\" is expected. Change the continent and restart Home Assistant to try again or remove the Ecovacs YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually.\n\nIf the contintent \"{continent}\" is not applicable, please open an issue on [GitHub]({github_issue_url})." } }, "selector": { diff --git a/homeassistant/components/homeworks/strings.json b/homeassistant/components/homeworks/strings.json index 46c58515f39..b0d0f6e61e1 100644 --- a/homeassistant/components/homeworks/strings.json +++ b/homeassistant/components/homeworks/strings.json @@ -41,7 +41,7 @@ }, "exceptions": { "invalid_controller_id": { - "message": "Invalid controller_id '{controller_id}', expected one of '{controller_ids}'" + "message": "Invalid controller_id \"{controller_id}\", expected one of \"{controller_ids}\"" } }, "options": { diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json index 8c06889361c..378a1172788 100644 --- a/homeassistant/components/imap/strings.json +++ b/homeassistant/components/imap/strings.json @@ -37,13 +37,13 @@ }, "exceptions": { "copy_failed": { - "message": "Copying the message failed with '{error}'." + "message": "Copying the message failed with \"{error}\"." }, "delete_failed": { - "message": "Marking the the message for deletion failed with '{error}'." + "message": "Marking the the message for deletion failed with \"{error}\"." }, "expunge_failed": { - "message": "Expungling the the message failed with '{error}'." + "message": "Expungling the the message failed with \"{error}\"." }, "invalid_entry": { "message": "No valid IMAP entry was found." @@ -58,7 +58,7 @@ "message": "The IMAP server failed to connect: {error}." }, "seen_failed": { - "message": "Marking message as seen failed with '{error}'." + "message": "Marking message as seen failed with \"{error}\"." } }, "options": { diff --git a/homeassistant/components/mailbox/strings.json b/homeassistant/components/mailbox/strings.json index 44f1ad08d39..01746e3e98d 100644 --- a/homeassistant/components/mailbox/strings.json +++ b/homeassistant/components/mailbox/strings.json @@ -3,7 +3,7 @@ "issues": { "deprecated_mailbox": { "title": "The mailbox platform is being removed", - "description": "The mailbox platform is being removed. Please report it to the author of the '{integration_domain}' custom integration." + "description": "The mailbox platform is being removed. Please report it to the author of the \"{integration_domain}\" custom integration." } } } diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 87fe0bd033a..2bd47db63bc 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -264,10 +264,10 @@ "message": "Unable to publish: topic template `{topic_template}` produced an invalid topic `{topic}` after rendering ({error})" }, "mqtt_not_setup_cannot_subscribe": { - "message": "Cannot subscribe to topic '{topic}', make sure MQTT is set up correctly." + "message": "Cannot subscribe to topic \"{topic}\", make sure MQTT is set up correctly." }, "mqtt_not_setup_cannot_publish": { - "message": "Cannot publish to topic '{topic}', make sure MQTT is set up correctly." + "message": "Cannot publish to topic \"{topic}\", make sure MQTT is set up correctly." } } } diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 2282289bdbc..ec81893d846 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -28,7 +28,7 @@ "api_error": "API error occurred", "cannot_connect": "Failed to connect, check the IP address of the camera", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "not_admin": "User needs to be admin, user ''{username}'' has authorisation level ''{userlevel}''", + "not_admin": "User needs to be admin, user \"{username}\" has authorisation level \"{userlevel}\"", "unknown": "[%key:common::config_flow::error::unknown%]", "webhook_exception": "Home Assistant URL is not available, go to Settings > System > Network > Home Assistant URL and correct the URLs, see {more_info}" }, diff --git a/homeassistant/components/rest_command/strings.json b/homeassistant/components/rest_command/strings.json index 2d658ad8b20..fd0b26f6499 100644 --- a/homeassistant/components/rest_command/strings.json +++ b/homeassistant/components/rest_command/strings.json @@ -7,13 +7,13 @@ }, "exceptions": { "timeout": { - "message": "Timeout when calling resource '{request_url}'" + "message": "Timeout when calling resource \"{request_url}\"" }, "client_error": { - "message": "Client error occurred when calling resource '{request_url}'" + "message": "Client error occurred when calling resource \"{request_url}\"" }, "decoding_error": { - "message": "The response of '{request_url}' could not be decoded as {decoding_type}" + "message": "The response of \"{request_url}\" could not be decoded as {decoding_type}" } } } diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 6c20246b396..b893902af69 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -24,6 +24,7 @@ REMOVED = 2 RE_REFERENCE = r"\[\%key:(.+)\%\]" RE_TRANSLATION_KEY = re.compile(r"^(?!.+[_-]{2})(?![_-])[a-z0-9-_]+(? str: """Validate that the value is a valid translation. - prevents string with HTML + - prevents strings with single quoted placeholders - prevents combined translations """ value = cv.string_with_no_html(value) + value = string_no_single_quoted_placeholders(value) if RE_COMBINED_REFERENCE.search(value): raise vol.Invalid("the string should not contain combined translations") return str(value) +def string_no_single_quoted_placeholders(value: str) -> str: + """Validate that the value does not contain placeholders inside single quotes.""" + if RE_PLACEHOLDER_IN_SINGLE_QUOTES.search(value): + raise vol.Invalid( + "the string should not contain placeholders inside single quotes" + ) + return value + + def gen_data_entry_schema( *, config: Config, diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index 567391a4b32..4f88e1b9d34 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -68,7 +68,7 @@ async def test_rest_command_timeout( with pytest.raises(HomeAssistantError) as exc: await hass.services.async_call(DOMAIN, "get_test", {}, blocking=True) - assert str(exc.value) == "Timeout when calling resource 'https://example.com/'" + assert str(exc.value) == 'Timeout when calling resource "https://example.com/"' assert len(aioclient_mock.mock_calls) == 1 @@ -88,7 +88,7 @@ async def test_rest_command_aiohttp_error( assert ( str(exc.value) - == "Client error occurred when calling resource 'https://example.com/'" + == 'Client error occurred when calling resource "https://example.com/"' ) assert len(aioclient_mock.mock_calls) == 1 @@ -341,7 +341,7 @@ async def test_rest_command_get_response_malformed_json( ) assert ( str(exc.value) - == "The response of 'https://example.com/' could not be decoded as JSON" + == 'The response of "https://example.com/" could not be decoded as JSON' ) @@ -375,7 +375,7 @@ async def test_rest_command_get_response_none( ) assert ( str(exc.value) - == "The response of 'https://example.com/' could not be decoded as text" + == 'The response of "https://example.com/" could not be decoded as text' ) assert not response diff --git a/tests/hassfest/test_translations.py b/tests/hassfest/test_translations.py new file mode 100644 index 00000000000..526320a5044 --- /dev/null +++ b/tests/hassfest/test_translations.py @@ -0,0 +1,21 @@ +"""Tests for hassfest translations.""" + +import pytest +import voluptuous as vol + +from script.hassfest import translations + + +def test_string_with_no_placeholders_in_single_quotes() -> None: + """Test string with no placeholders in single quotes.""" + schema = vol.Schema(translations.string_no_single_quoted_placeholders) + + with pytest.raises(vol.Invalid): + schema("This has '{placeholder}' in single quotes") + + for value in ( + 'This has "{placeholder}" in double quotes', + "Simple {placeholder}", + "No placeholder", + ): + schema(value)