Fix placeholder quotes (#114974)

* When quoting placeholders, always use double quotes so Lokalise recognizes the placeholder.

* Ensure that strings does not contain placeholders in single quotes.

* Avoid redefining value

* Moved string_with_no_placeholders_in_single_quotes

* Define regex once

* Fix tests
This commit is contained in:
Øyvind Matheson Wergeland 2024-04-06 13:01:56 +02:00 committed by GitHub
parent bd9070be11
commit fdef3ece13
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 51 additions and 18 deletions

View File

@ -106,7 +106,7 @@
}, },
"exceptions": { "exceptions": {
"integration_not_found": { "integration_not_found": {
"message": "Integration '{target}' not found in registry" "message": "Integration \"{target}\" not found in registry"
}, },
"no_path": { "no_path": {
"message": "Can't write to directory {target}, no access to path!" "message": "Can't write to directory {target}, no access to path!"

View File

@ -227,7 +227,7 @@
}, },
"deprecated_yaml_import_issue_continent_not_match": { "deprecated_yaml_import_issue_continent_not_match": {
"title": "The Ecovacs YAML configuration import failed", "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": { "selector": {

View File

@ -41,7 +41,7 @@
}, },
"exceptions": { "exceptions": {
"invalid_controller_id": { "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": { "options": {

View File

@ -37,13 +37,13 @@
}, },
"exceptions": { "exceptions": {
"copy_failed": { "copy_failed": {
"message": "Copying the message failed with '{error}'." "message": "Copying the message failed with \"{error}\"."
}, },
"delete_failed": { "delete_failed": {
"message": "Marking the the message for deletion failed with '{error}'." "message": "Marking the the message for deletion failed with \"{error}\"."
}, },
"expunge_failed": { "expunge_failed": {
"message": "Expungling the the message failed with '{error}'." "message": "Expungling the the message failed with \"{error}\"."
}, },
"invalid_entry": { "invalid_entry": {
"message": "No valid IMAP entry was found." "message": "No valid IMAP entry was found."
@ -58,7 +58,7 @@
"message": "The IMAP server failed to connect: {error}." "message": "The IMAP server failed to connect: {error}."
}, },
"seen_failed": { "seen_failed": {
"message": "Marking message as seen failed with '{error}'." "message": "Marking message as seen failed with \"{error}\"."
} }
}, },
"options": { "options": {

View File

@ -3,7 +3,7 @@
"issues": { "issues": {
"deprecated_mailbox": { "deprecated_mailbox": {
"title": "The mailbox platform is being removed", "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."
} }
} }
} }

View File

@ -264,10 +264,10 @@
"message": "Unable to publish: topic template `{topic_template}` produced an invalid topic `{topic}` after rendering ({error})" "message": "Unable to publish: topic template `{topic_template}` produced an invalid topic `{topic}` after rendering ({error})"
}, },
"mqtt_not_setup_cannot_subscribe": { "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": { "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."
} }
} }
} }

View File

@ -28,7 +28,7 @@
"api_error": "API error occurred", "api_error": "API error occurred",
"cannot_connect": "Failed to connect, check the IP address of the camera", "cannot_connect": "Failed to connect, check the IP address of the camera",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "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%]", "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}" "webhook_exception": "Home Assistant URL is not available, go to Settings > System > Network > Home Assistant URL and correct the URLs, see {more_info}"
}, },

View File

@ -7,13 +7,13 @@
}, },
"exceptions": { "exceptions": {
"timeout": { "timeout": {
"message": "Timeout when calling resource '{request_url}'" "message": "Timeout when calling resource \"{request_url}\""
}, },
"client_error": { "client_error": {
"message": "Client error occurred when calling resource '{request_url}'" "message": "Client error occurred when calling resource \"{request_url}\""
}, },
"decoding_error": { "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}"
} }
} }
} }

View File

@ -24,6 +24,7 @@ REMOVED = 2
RE_REFERENCE = r"\[\%key:(.+)\%\]" RE_REFERENCE = r"\[\%key:(.+)\%\]"
RE_TRANSLATION_KEY = re.compile(r"^(?!.+[_-]{2})(?![_-])[a-z0-9-_]+(?<![_-])$") RE_TRANSLATION_KEY = re.compile(r"^(?!.+[_-]{2})(?![_-])[a-z0-9-_]+(?<![_-])$")
RE_COMBINED_REFERENCE = re.compile(r"(.+\[%)|(%\].+)") RE_COMBINED_REFERENCE = re.compile(r"(.+\[%)|(%\].+)")
RE_PLACEHOLDER_IN_SINGLE_QUOTES = re.compile(r"'{\w+}'")
# Only allow translation of integration names if they contain non-brand names # Only allow translation of integration names if they contain non-brand names
ALLOW_NAME_TRANSLATION = { ALLOW_NAME_TRANSLATION = {
@ -128,14 +129,25 @@ def translation_value_validator(value: Any) -> str:
"""Validate that the value is a valid translation. """Validate that the value is a valid translation.
- prevents string with HTML - prevents string with HTML
- prevents strings with single quoted placeholders
- prevents combined translations - prevents combined translations
""" """
value = cv.string_with_no_html(value) value = cv.string_with_no_html(value)
value = string_no_single_quoted_placeholders(value)
if RE_COMBINED_REFERENCE.search(value): if RE_COMBINED_REFERENCE.search(value):
raise vol.Invalid("the string should not contain combined translations") raise vol.Invalid("the string should not contain combined translations")
return str(value) 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( def gen_data_entry_schema(
*, *,
config: Config, config: Config,

View File

@ -68,7 +68,7 @@ async def test_rest_command_timeout(
with pytest.raises(HomeAssistantError) as exc: with pytest.raises(HomeAssistantError) as exc:
await hass.services.async_call(DOMAIN, "get_test", {}, blocking=True) 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 assert len(aioclient_mock.mock_calls) == 1
@ -88,7 +88,7 @@ async def test_rest_command_aiohttp_error(
assert ( assert (
str(exc.value) 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 assert len(aioclient_mock.mock_calls) == 1
@ -341,7 +341,7 @@ async def test_rest_command_get_response_malformed_json(
) )
assert ( assert (
str(exc.value) 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 ( assert (
str(exc.value) 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 assert not response

View File

@ -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)