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": {
"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!"

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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."
}
}
}

View File

@ -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."
}
}
}

View File

@ -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}"
},

View File

@ -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}"
}
}
}

View File

@ -24,6 +24,7 @@ REMOVED = 2
RE_REFERENCE = r"\[\%key:(.+)\%\]"
RE_TRANSLATION_KEY = re.compile(r"^(?!.+[_-]{2})(?![_-])[a-z0-9-_]+(?<![_-])$")
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
ALLOW_NAME_TRANSLATION = {
@ -128,14 +129,25 @@ def translation_value_validator(value: Any) -> 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,

View File

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

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)