Prevent combined translations in strings.json (#91334)

This commit is contained in:
epenet 2023-04-17 09:36:25 +02:00 committed by GitHub
parent 42b0602190
commit d26160a509
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -22,6 +22,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"(.+\[%)|(%\].+)")
# 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 = {
@ -116,6 +117,18 @@ def translation_key_validator(value: str) -> str:
return value return value
def translation_value_validator(value: Any) -> str:
"""Validate that the value is a valid translation.
- prevents string with HTML
- prevents combined translations
"""
value = cv.string_with_no_html(value)
if RE_COMBINED_REFERENCE.search(value):
raise vol.Invalid("the string should not contain combined translations")
return str(value)
def gen_data_entry_schema( def gen_data_entry_schema(
*, *,
config: Config, config: Config,
@ -127,24 +140,24 @@ def gen_data_entry_schema(
"""Generate a data entry schema.""" """Generate a data entry schema."""
step_title_class = vol.Required if require_step_title else vol.Optional step_title_class = vol.Required if require_step_title else vol.Optional
schema = { schema = {
vol.Optional("flow_title"): cv.string_with_no_html, vol.Optional("flow_title"): translation_value_validator,
vol.Required("step"): { vol.Required("step"): {
str: { str: {
step_title_class("title"): cv.string_with_no_html, step_title_class("title"): translation_value_validator,
vol.Optional("description"): cv.string_with_no_html, vol.Optional("description"): translation_value_validator,
vol.Optional("data"): {str: cv.string_with_no_html}, vol.Optional("data"): {str: translation_value_validator},
vol.Optional("data_description"): {str: cv.string_with_no_html}, vol.Optional("data_description"): {str: translation_value_validator},
vol.Optional("menu_options"): {str: cv.string_with_no_html}, vol.Optional("menu_options"): {str: translation_value_validator},
vol.Optional("submit"): cv.string_with_no_html, vol.Optional("submit"): translation_value_validator,
} }
}, },
vol.Optional("error"): {str: cv.string_with_no_html}, vol.Optional("error"): {str: translation_value_validator},
vol.Optional("abort"): {str: cv.string_with_no_html}, vol.Optional("abort"): {str: translation_value_validator},
vol.Optional("progress"): {str: cv.string_with_no_html}, vol.Optional("progress"): {str: translation_value_validator},
vol.Optional("create_entry"): {str: cv.string_with_no_html}, vol.Optional("create_entry"): {str: translation_value_validator},
} }
if flow_title == REQUIRED: if flow_title == REQUIRED:
schema[vol.Required("title")] = cv.string_with_no_html schema[vol.Required("title")] = translation_value_validator
elif flow_title == REMOVED: elif flow_title == REMOVED:
schema[vol.Optional("title", msg=REMOVED_TITLE_MSG)] = partial( schema[vol.Optional("title", msg=REMOVED_TITLE_MSG)] = partial(
removed_title_validator, config, integration removed_title_validator, config, integration
@ -201,7 +214,7 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema:
"""Generate a strings schema.""" """Generate a strings schema."""
return vol.Schema( return vol.Schema(
{ {
vol.Optional("title"): cv.string_with_no_html, vol.Optional("title"): translation_value_validator,
vol.Optional("config"): gen_data_entry_schema( vol.Optional("config"): gen_data_entry_schema(
config=config, config=config,
integration=integration, integration=integration,
@ -220,40 +233,43 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema:
vol.Optional("selector"): cv.schema_with_slug_keys( vol.Optional("selector"): cv.schema_with_slug_keys(
{ {
"options": cv.schema_with_slug_keys( "options": cv.schema_with_slug_keys(
cv.string_with_no_html, slug_validator=translation_key_validator translation_value_validator,
slug_validator=translation_key_validator,
) )
}, },
slug_validator=vol.Any("_", cv.slug), slug_validator=vol.Any("_", cv.slug),
), ),
vol.Optional("device_automation"): { vol.Optional("device_automation"): {
vol.Optional("action_type"): {str: cv.string_with_no_html}, vol.Optional("action_type"): {str: translation_value_validator},
vol.Optional("condition_type"): {str: cv.string_with_no_html}, vol.Optional("condition_type"): {str: translation_value_validator},
vol.Optional("trigger_type"): {str: cv.string_with_no_html}, vol.Optional("trigger_type"): {str: translation_value_validator},
vol.Optional("trigger_subtype"): {str: cv.string_with_no_html}, vol.Optional("trigger_subtype"): {str: translation_value_validator},
}, },
vol.Optional("system_health"): { vol.Optional("system_health"): {
vol.Optional("info"): cv.schema_with_slug_keys( vol.Optional("info"): cv.schema_with_slug_keys(
cv.string_with_no_html, slug_validator=translation_key_validator translation_value_validator,
slug_validator=translation_key_validator,
), ),
}, },
vol.Optional("config_panel"): cv.schema_with_slug_keys( vol.Optional("config_panel"): cv.schema_with_slug_keys(
cv.schema_with_slug_keys( cv.schema_with_slug_keys(
cv.string_with_no_html, slug_validator=translation_key_validator translation_value_validator,
slug_validator=translation_key_validator,
), ),
slug_validator=vol.Any("_", cv.slug), slug_validator=vol.Any("_", cv.slug),
), ),
vol.Optional("application_credentials"): { vol.Optional("application_credentials"): {
vol.Optional("description"): cv.string_with_no_html, vol.Optional("description"): translation_value_validator,
}, },
vol.Optional("issues"): { vol.Optional("issues"): {
str: vol.All( str: vol.All(
cv.has_at_least_one_key("description", "fix_flow"), cv.has_at_least_one_key("description", "fix_flow"),
vol.Schema( vol.Schema(
{ {
vol.Required("title"): cv.string_with_no_html, vol.Required("title"): translation_value_validator,
vol.Exclusive( vol.Exclusive(
"description", "fixable" "description", "fixable"
): cv.string_with_no_html, ): translation_value_validator,
vol.Exclusive("fix_flow", "fixable"): gen_data_entry_schema( vol.Exclusive("fix_flow", "fixable"): gen_data_entry_schema(
config=config, config=config,
integration=integration, integration=integration,
@ -268,14 +284,14 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema:
{ {
vol.Optional("name"): str, vol.Optional("name"): str,
vol.Optional("state"): cv.schema_with_slug_keys( vol.Optional("state"): cv.schema_with_slug_keys(
cv.string_with_no_html, translation_value_validator,
slug_validator=translation_key_validator, slug_validator=translation_key_validator,
), ),
vol.Optional("state_attributes"): cv.schema_with_slug_keys( vol.Optional("state_attributes"): cv.schema_with_slug_keys(
{ {
vol.Optional("name"): str, vol.Optional("name"): str,
vol.Optional("state"): cv.schema_with_slug_keys( vol.Optional("state"): cv.schema_with_slug_keys(
cv.string_with_no_html, translation_value_validator,
slug_validator=translation_key_validator, slug_validator=translation_key_validator,
), ),
}, },
@ -287,16 +303,16 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema:
vol.Optional("entity"): cv.schema_with_slug_keys( vol.Optional("entity"): cv.schema_with_slug_keys(
cv.schema_with_slug_keys( cv.schema_with_slug_keys(
{ {
vol.Optional("name"): cv.string_with_no_html, vol.Optional("name"): translation_value_validator,
vol.Optional("state"): cv.schema_with_slug_keys( vol.Optional("state"): cv.schema_with_slug_keys(
cv.string_with_no_html, translation_value_validator,
slug_validator=translation_key_validator, slug_validator=translation_key_validator,
), ),
vol.Optional("state_attributes"): cv.schema_with_slug_keys( vol.Optional("state_attributes"): cv.schema_with_slug_keys(
{ {
vol.Optional("name"): cv.string_with_no_html, vol.Optional("name"): translation_value_validator,
vol.Optional("state"): cv.schema_with_slug_keys( vol.Optional("state"): cv.schema_with_slug_keys(
cv.string_with_no_html, translation_value_validator,
slug_validator=translation_key_validator, slug_validator=translation_key_validator,
), ),
}, },
@ -386,7 +402,9 @@ def gen_platform_strings_schema(config: Config, integration: Integration) -> vol
) )
ONBOARDING_SCHEMA = vol.Schema({vol.Required("area"): {str: cv.string_with_no_html}}) ONBOARDING_SCHEMA = vol.Schema(
{vol.Required("area"): {str: translation_value_validator}}
)
def validate_translation_file( # noqa: C901 def validate_translation_file( # noqa: C901
@ -415,7 +433,7 @@ def validate_translation_file( # noqa: C901
strings_schema = gen_strings_schema(config, integration).extend( strings_schema = gen_strings_schema(config, integration).extend(
{ {
vol.Optional("device_class"): cv.schema_with_slug_keys( vol.Optional("device_class"): cv.schema_with_slug_keys(
cv.string_with_no_html, slug_validator=vol.Any("_", cv.slug) translation_value_validator, slug_validator=vol.Any("_", cv.slug)
) )
} }
) )