Refactor translation checks (#130585)

* Refactor translation checks

* Adjust

* Improve

* Restore await

* Delay pytest.fail until the end of the test
This commit is contained in:
epenet 2024-11-14 16:26:05 +01:00 committed by GitHub
parent 472414a8d6
commit c7ee7dc880
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -26,7 +26,12 @@ from homeassistant.config_entries import (
)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowHandler, FlowManager, FlowResultType
from homeassistant.data_entry_flow import (
FlowContext,
FlowHandler,
FlowManager,
FlowResultType,
)
from homeassistant.helpers.translation import async_get_translations
if TYPE_CHECKING:
@ -557,12 +562,12 @@ def _validate_translation_placeholders(
description_placeholders is None
or placeholder not in description_placeholders
):
pytest.fail(
ignore_translations[full_key] = (
f"Description not found for placeholder `{placeholder}` in {full_key}"
)
async def _ensure_translation_exists(
async def _validate_translation(
hass: HomeAssistant,
ignore_translations: dict[str, StoreInfo],
category: str,
@ -588,7 +593,7 @@ async def _ensure_translation_exists(
ignore_translations[full_key] = "used"
return
pytest.fail(
ignore_translations[full_key] = (
f"Translation not found for {component}: `{category}.{key}`. "
f"Please add to homeassistant/components/{component}/strings.json"
)
@ -604,84 +609,106 @@ def ignore_translations() -> str | list[str]:
return []
async def _check_config_flow_result_translations(
manager: FlowManager,
flow: FlowHandler,
result: FlowResult[FlowContext, str],
ignore_translations: dict[str, str],
) -> None:
if isinstance(manager, ConfigEntriesFlowManager):
category = "config"
integration = flow.handler
elif isinstance(manager, OptionsFlowManager):
category = "options"
integration = flow.hass.config_entries.async_get_entry(flow.handler).domain
else:
return
# Check if this flow has been seen before
# Gets set to False on first run, and to True on subsequent runs
setattr(flow, "__flow_seen_before", hasattr(flow, "__flow_seen_before"))
if result["type"] is FlowResultType.FORM:
if step_id := result.get("step_id"):
# neither title nor description are required
# - title defaults to integration name
# - description is optional
for header in ("title", "description"):
await _validate_translation(
flow.hass,
ignore_translations,
category,
integration,
f"step.{step_id}.{header}",
result["description_placeholders"],
translation_required=False,
)
if errors := result.get("errors"):
for error in errors.values():
await _validate_translation(
flow.hass,
ignore_translations,
category,
integration,
f"error.{error}",
result["description_placeholders"],
)
return
if result["type"] is FlowResultType.ABORT:
# We don't need translations for a discovery flow which immediately
# aborts, since such flows won't be seen by users
if not flow.__flow_seen_before and flow.source in DISCOVERY_SOURCES:
return
await _validate_translation(
flow.hass,
ignore_translations,
category,
integration,
f"abort.{result["reason"]}",
result["description_placeholders"],
)
@pytest.fixture(autouse=True)
def check_config_translations(ignore_translations: str | list[str]) -> Generator[None]:
"""Ensure config_flow translations are available."""
def check_translations(ignore_translations: str | list[str]) -> Generator[None]:
"""Check that translation requirements are met.
Current checks:
- data entry flow results (ConfigFlow/OptionsFlow)
"""
if not isinstance(ignore_translations, list):
ignore_translations = [ignore_translations]
_ignore_translations = {k: "unused" for k in ignore_translations}
_original = FlowManager._async_handle_step
async def _async_handle_step(
# Keep reference to original functions
_original_flow_manager_async_handle_step = FlowManager._async_handle_step
# Prepare override functions
async def _flow_manager_async_handle_step(
self: FlowManager, flow: FlowHandler, *args
) -> FlowResult:
result = await _original(self, flow, *args)
if isinstance(self, ConfigEntriesFlowManager):
category = "config"
component = flow.handler
elif isinstance(self, OptionsFlowManager):
category = "options"
component = flow.hass.config_entries.async_get_entry(flow.handler).domain
else:
return result
# Check if this flow has been seen before
# Gets set to False on first run, and to True on subsequent runs
setattr(flow, "__flow_seen_before", hasattr(flow, "__flow_seen_before"))
if result["type"] is FlowResultType.FORM:
if step_id := result.get("step_id"):
# neither title nor description are required
# - title defaults to integration name
# - description is optional
for header in ("title", "description"):
await _ensure_translation_exists(
flow.hass,
_ignore_translations,
category,
component,
f"step.{step_id}.{header}",
result["description_placeholders"],
translation_required=False,
)
if errors := result.get("errors"):
for error in errors.values():
await _ensure_translation_exists(
flow.hass,
_ignore_translations,
category,
component,
f"error.{error}",
result["description_placeholders"],
)
return result
if result["type"] is FlowResultType.ABORT:
# We don't need translations for a discovery flow which immediately
# aborts, since such flows won't be seen by users
if not flow.__flow_seen_before and flow.source in DISCOVERY_SOURCES:
return result
await _ensure_translation_exists(
flow.hass,
_ignore_translations,
category,
component,
f"abort.{result["reason"]}",
result["description_placeholders"],
)
result = await _original_flow_manager_async_handle_step(self, flow, *args)
await _check_config_flow_result_translations(
self, flow, result, _ignore_translations
)
return result
# Use override functions
with patch(
"homeassistant.data_entry_flow.FlowManager._async_handle_step",
_async_handle_step,
_flow_manager_async_handle_step,
):
yield
# Run final checks
unused_ignore = [k for k, v in _ignore_translations.items() if v == "unused"]
if unused_ignore:
pytest.fail(
f"Unused ignore translations: {', '.join(unused_ignore)}. "
"Please remove them from the ignore_translations fixture."
)
for description in _ignore_translations.values():
if description not in {"used", "unused"}:
pytest.fail(description)