"""Per-domain notification template catalogs. Every outbound notification resolves its copy through :meth:`app.notifications.service.NotificationService._load_template`, which looks the ``template_key`` up in ``_DEFAULT_TEMPLATES``. A key with no entry there falls through to whatever ``title`` / ``body_text`` the *producer* wrote on the envelope — and producers hardcode one language, so an unregistered key means every recipient gets that language regardless of what they read. Before this package 70 of the 98 live template keys were unregistered. They are declared here rather than in ``service.py`` for two reasons: that module's inline literal was already ~1000 lines, and splitting by domain lets each domain's copy be reviewed by the people who own the flow. Contract for every entry ------------------------ * ``subject`` / ``body_text`` — Chinese, the unsuffixed baseline. * ``subject_en`` / ``body_text_en``, ``subject_ja`` / ``body_text_ja`` — the other two languages Helix speaks. All three are REQUIRED; the parity test in ``server/tests/test_notifications/test_template_catalog_parity.py`` fails the build otherwise, so a new template can't quietly ship as Chinese-only. * ``card_button_label`` (+ ``_en`` / ``_ja``) — optional; the Feishu card falls back to a generic "Open in Helix" when absent. * Every ``{placeholder}`` MUST be a key the producing call site puts on ``NotificationEnvelope.context``. Placeholders are substituted per recipient at send time and again when the bell re-renders the row, so a placeholder the context doesn't carry renders literally as ``{foo}`` to the user. Adding a template is enough to localise a flow — no call-site change is needed beyond ensuring ``context`` carries the placeholders. """ from __future__ import annotations from app.notifications.templates.apikey import TEMPLATES as _APIKEY from app.notifications.templates.approval_registry import ( TEMPLATES as _APPROVAL_REGISTRY, ) from app.notifications.templates.bounty import TEMPLATES as _BOUNTY from app.notifications.templates.cloud import TEMPLATES as _CLOUD from app.notifications.templates.email_group import TEMPLATES as _EMAIL_GROUP from app.notifications.templates.feed import TEMPLATES as _FEED from app.notifications.templates.hr_leave import TEMPLATES as _HR_LEAVE from app.notifications.templates.lifecycle import TEMPLATES as _LIFECYCLE from app.notifications.templates.platform import TEMPLATES as _PLATFORM from app.notifications.templates.security_awareness import ( TEMPLATES as _SECURITY_AWARENESS, ) from app.notifications.templates.support import TEMPLATES as _SUPPORT from app.notifications.templates.system_registry import ( TEMPLATES as _SYSTEM_REGISTRY, ) _CATALOGS: tuple[tuple[str, dict[str, dict[str, str]]], ...] = ( ("apikey", _APIKEY), ("approval_registry", _APPROVAL_REGISTRY), ("bounty", _BOUNTY), ("system_registry", _SYSTEM_REGISTRY), ("cloud", _CLOUD), ("email_group", _EMAIL_GROUP), ("feed", _FEED), ("hr_leave", _HR_LEAVE), ("lifecycle", _LIFECYCLE), ("platform", _PLATFORM), ("security_awareness", _SECURITY_AWARENESS), ("support", _SUPPORT), ) def _merge() -> dict[str, dict[str, str]]: """Flatten the per-domain catalogs, refusing duplicate keys. A key defined in two catalogs is a genuine authoring mistake (two teams localising the same flow differently); failing at import time surfaces it in CI rather than letting whichever module imported last win silently. """ merged: dict[str, dict[str, str]] = {} owner: dict[str, str] = {} for domain, catalog in _CATALOGS: for key, tmpl in catalog.items(): if key in merged: raise RuntimeError( f"duplicate notification template {key!r}: defined in both " f"{owner[key]!r} and {domain!r} catalogs" ) merged[key] = tmpl owner[key] = domain return merged #: Every domain-declared template, keyed by ``template_key``. BUILTIN_TEMPLATES: dict[str, dict[str, str]] = _merge() __all__ = ["BUILTIN_TEMPLATES"]