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:
parent
472414a8d6
commit
c7ee7dc880
1 changed files with 91 additions and 64 deletions
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue