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.const import STATE_OFF, STATE_ON
|
||||||
from homeassistant.core import HomeAssistant
|
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
|
from homeassistant.helpers.translation import async_get_translations
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -557,12 +562,12 @@ def _validate_translation_placeholders(
|
||||||
description_placeholders is None
|
description_placeholders is None
|
||||||
or placeholder not in description_placeholders
|
or placeholder not in description_placeholders
|
||||||
):
|
):
|
||||||
pytest.fail(
|
ignore_translations[full_key] = (
|
||||||
f"Description not found for placeholder `{placeholder}` in {full_key}"
|
f"Description not found for placeholder `{placeholder}` in {full_key}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _ensure_translation_exists(
|
async def _validate_translation(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
ignore_translations: dict[str, StoreInfo],
|
ignore_translations: dict[str, StoreInfo],
|
||||||
category: str,
|
category: str,
|
||||||
|
@ -588,7 +593,7 @@ async def _ensure_translation_exists(
|
||||||
ignore_translations[full_key] = "used"
|
ignore_translations[full_key] = "used"
|
||||||
return
|
return
|
||||||
|
|
||||||
pytest.fail(
|
ignore_translations[full_key] = (
|
||||||
f"Translation not found for {component}: `{category}.{key}`. "
|
f"Translation not found for {component}: `{category}.{key}`. "
|
||||||
f"Please add to homeassistant/components/{component}/strings.json"
|
f"Please add to homeassistant/components/{component}/strings.json"
|
||||||
)
|
)
|
||||||
|
@ -604,84 +609,106 @@ def ignore_translations() -> str | list[str]:
|
||||||
return []
|
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)
|
@pytest.fixture(autouse=True)
|
||||||
def check_config_translations(ignore_translations: str | list[str]) -> Generator[None]:
|
def check_translations(ignore_translations: str | list[str]) -> Generator[None]:
|
||||||
"""Ensure config_flow translations are available."""
|
"""Check that translation requirements are met.
|
||||||
|
|
||||||
|
Current checks:
|
||||||
|
- data entry flow results (ConfigFlow/OptionsFlow)
|
||||||
|
"""
|
||||||
if not isinstance(ignore_translations, list):
|
if not isinstance(ignore_translations, list):
|
||||||
ignore_translations = [ignore_translations]
|
ignore_translations = [ignore_translations]
|
||||||
|
|
||||||
_ignore_translations = {k: "unused" for k in 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
|
self: FlowManager, flow: FlowHandler, *args
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
result = await _original(self, flow, *args)
|
result = await _original_flow_manager_async_handle_step(self, flow, *args)
|
||||||
if isinstance(self, ConfigEntriesFlowManager):
|
await _check_config_flow_result_translations(
|
||||||
category = "config"
|
self, flow, result, _ignore_translations
|
||||||
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"],
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
# Use override functions
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.data_entry_flow.FlowManager._async_handle_step",
|
"homeassistant.data_entry_flow.FlowManager._async_handle_step",
|
||||||
_async_handle_step,
|
_flow_manager_async_handle_step,
|
||||||
):
|
):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
# Run final checks
|
||||||
unused_ignore = [k for k, v in _ignore_translations.items() if v == "unused"]
|
unused_ignore = [k for k, v in _ignore_translations.items() if v == "unused"]
|
||||||
if unused_ignore:
|
if unused_ignore:
|
||||||
pytest.fail(
|
pytest.fail(
|
||||||
f"Unused ignore translations: {', '.join(unused_ignore)}. "
|
f"Unused ignore translations: {', '.join(unused_ignore)}. "
|
||||||
"Please remove them from the ignore_translations fixture."
|
"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