From d26160a5098a4be9d3bd2d7fe4f97f06cc1b8eb1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 17 Apr 2023 09:36:25 +0200 Subject: [PATCH] Prevent combined translations in strings.json (#91334) --- script/hassfest/translations.py | 80 ++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index bf2697644daf..9efe01cf9623 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -22,6 +22,7 @@ REMOVED = 2 RE_REFERENCE = r"\[\%key:(.+)\%\]" RE_TRANSLATION_KEY = re.compile(r"^(?!.+[_-]{2})(?![_-])[a-z0-9-_]+(? str: 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( *, config: Config, @@ -127,24 +140,24 @@ def gen_data_entry_schema( """Generate a data entry schema.""" step_title_class = vol.Required if require_step_title else vol.Optional schema = { - vol.Optional("flow_title"): cv.string_with_no_html, + vol.Optional("flow_title"): translation_value_validator, vol.Required("step"): { str: { - step_title_class("title"): cv.string_with_no_html, - vol.Optional("description"): cv.string_with_no_html, - vol.Optional("data"): {str: cv.string_with_no_html}, - vol.Optional("data_description"): {str: cv.string_with_no_html}, - vol.Optional("menu_options"): {str: cv.string_with_no_html}, - vol.Optional("submit"): cv.string_with_no_html, + step_title_class("title"): translation_value_validator, + vol.Optional("description"): translation_value_validator, + vol.Optional("data"): {str: translation_value_validator}, + vol.Optional("data_description"): {str: translation_value_validator}, + vol.Optional("menu_options"): {str: translation_value_validator}, + vol.Optional("submit"): translation_value_validator, } }, - vol.Optional("error"): {str: cv.string_with_no_html}, - vol.Optional("abort"): {str: cv.string_with_no_html}, - vol.Optional("progress"): {str: cv.string_with_no_html}, - vol.Optional("create_entry"): {str: cv.string_with_no_html}, + vol.Optional("error"): {str: translation_value_validator}, + vol.Optional("abort"): {str: translation_value_validator}, + vol.Optional("progress"): {str: translation_value_validator}, + vol.Optional("create_entry"): {str: translation_value_validator}, } if flow_title == REQUIRED: - schema[vol.Required("title")] = cv.string_with_no_html + schema[vol.Required("title")] = translation_value_validator elif flow_title == REMOVED: schema[vol.Optional("title", msg=REMOVED_TITLE_MSG)] = partial( removed_title_validator, config, integration @@ -201,7 +214,7 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: """Generate a strings 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( config=config, integration=integration, @@ -220,40 +233,43 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: vol.Optional("selector"): 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), ), vol.Optional("device_automation"): { - vol.Optional("action_type"): {str: cv.string_with_no_html}, - vol.Optional("condition_type"): {str: cv.string_with_no_html}, - vol.Optional("trigger_type"): {str: cv.string_with_no_html}, - vol.Optional("trigger_subtype"): {str: cv.string_with_no_html}, + vol.Optional("action_type"): {str: translation_value_validator}, + vol.Optional("condition_type"): {str: translation_value_validator}, + vol.Optional("trigger_type"): {str: translation_value_validator}, + vol.Optional("trigger_subtype"): {str: translation_value_validator}, }, vol.Optional("system_health"): { 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( 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), ), vol.Optional("application_credentials"): { - vol.Optional("description"): cv.string_with_no_html, + vol.Optional("description"): translation_value_validator, }, vol.Optional("issues"): { str: vol.All( cv.has_at_least_one_key("description", "fix_flow"), vol.Schema( { - vol.Required("title"): cv.string_with_no_html, + vol.Required("title"): translation_value_validator, vol.Exclusive( "description", "fixable" - ): cv.string_with_no_html, + ): translation_value_validator, vol.Exclusive("fix_flow", "fixable"): gen_data_entry_schema( config=config, integration=integration, @@ -268,14 +284,14 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: { vol.Optional("name"): str, vol.Optional("state"): cv.schema_with_slug_keys( - cv.string_with_no_html, + translation_value_validator, slug_validator=translation_key_validator, ), vol.Optional("state_attributes"): cv.schema_with_slug_keys( { vol.Optional("name"): str, vol.Optional("state"): cv.schema_with_slug_keys( - cv.string_with_no_html, + translation_value_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( 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( - cv.string_with_no_html, + translation_value_validator, slug_validator=translation_key_validator, ), 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( - cv.string_with_no_html, + translation_value_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 @@ -415,7 +433,7 @@ def validate_translation_file( # noqa: C901 strings_schema = gen_strings_schema(config, integration).extend( { 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) ) } )