Skip to content

Commit

Permalink
Refactor translation checks (home-assistant#130585)
Browse files Browse the repository at this point in the history
* Refactor translation checks

* Adjust

* Improve

* Restore await

* Delay pytest.fail until the end of the test
  • Loading branch information
epenet authored Nov 14, 2024
1 parent 472414a commit c7ee7dc
Showing 1 changed file with 91 additions and 64 deletions.
155 changes: 91 additions & 64 deletions tests/components/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@
)
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:
Expand Down Expand Up @@ -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,
Expand All @@ -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"
)
Expand All @@ -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)

0 comments on commit c7ee7dc

Please sign in to comment.