IRIS I18n And Error Standardization¶
Historical rollout note. Use ADR index, Terminology, and Backend Business Localization Plan for the current source of truth.
Last updated: 2026-03-15
Current iteration scope¶
This iteration covers:
- Phase 0 audit
- Phase 1 ADR alignment
- Phase 2 minimal canonical foundation
- Phase 3 API error rollout
This iteration does not attempt a big-bang migration of every backend endpoint, frontend screen or Home Assistant payload.
Phase status¶
| Phase | Status | Notes |
|---|---|---|
| 0. Audit | Complete | Repo-wide audit captured below. |
| 1. ADR alignment | Complete | Accepted ADR 0017 and ADR 0018 are now the source of truth; ADR 0016 remains draft background only. |
| 2. Base infrastructure | Complete (canonical slice) | core/i18n, core/errors, external en/ru YAML catalogs, global locale policy, registry-backed typed errors, and HA command dispatch pilot added. |
| 3. API and backend rollout | In progress | API and HA error boundaries now localize from message_key + params using the global settings locale across the migrated backend domains. |
| 4. Frontend and integration alignment | Not started | Waiting on shared contracts from phase 3. |
| 5. Quality enforcement | Not started | Waiting on broader adoption surface. |
| 6. Observability and docs | Not started | Waiting on canonical error usage in runtime paths. |
Phase 0 audit¶
Backend error handling¶
Current repo state is mixed rather than canonical:
ApiErrorFactoryis already adopted in 32 backend call sites and provides a useful transport foundation.- direct inline
HTTPException(...)text is no longer present in the migrated backend API endpoints; remaining transport debt is concentrated in operation-status surfaces and non-API runtime flows. - direct
ValueError/LookupError/RuntimeError/TypeErrorwith inline text still exists in 49 backend raise sites. - transport contracts still expose text-first fields:
backend/src/core/http/contracts.py->AcceptedResponse.messagebackend/src/core/http/operations.py->error_message, eventmessagebackend/src/apps/integrations/ha/schemas.py->HAErrorRead.messageis still present for presentation, but the contract now also carriesmessage_key,message_paramsandlocale- app-local error mapping is inconsistent:
- most backend domains now translate to registry-backed localized API errors;
- operation-store lifecycle/error persistence is now machine-first (
message_key + paramsanderror_message_key + params), but several domain result producers still only emit generic status/reason values, so some localized operation failures remain generic; - several domain/application exceptions still do not expose structured params, so the new API boundary uses generic localized top-level messages for those paths instead of fully specific localized narration.
Backend localization¶
Localization logic is duplicated and not yet platform-owned:
- locale normalization and resolution are duplicated in:
backend/src/apps/briefs/language.pybackend/src/apps/explanations/language.pybackend/src/apps/notifications/services/humanization_service.py- canonical global locale policy now exists in
backend/src/core/i18n/locale_policy.py; this iteration also added shared context-language resolution inbackend/src/core/i18n/context.py, and the old app-local helpers now act as thin bridges over that core path. - deterministic human text for
notificationsandexplanationsnow renders from shared catalogs; remaining inline deterministic narration debt is concentrated outside those two services. - operation/system read-side localization is now centralized in
backend/src/core/http/operation_localization.py, but deterministic narration outside transport boundaries still bypasses the shared translator. - persisted AI/localization artifacts no longer store row-level
language; locale now lives only inrendered_localeinside the canonical presentation envelope.
Frontend¶
Frontend currently has no i18n foundation:
frontend/package.jsonhas no i18n dependency or locale infrastructure.- UI text is hardcoded inline across Vue pages and components; representative examples:
frontend/src/pages/ControlPlane.vuefrontend/src/pages/Coins.vuefrontend/src/pages/CoinHistory.vue- frontend business/status semantics still rely on raw strings like
BUY,SELL,failed,active,shadow,throttledwithout a shared translation contract.
Home Assistant integration¶
HA is also mixed:
- static HA localization scaffolding exists:
ha/integration/custom_components/iris/strings.jsonha/integration/custom_components/iris/translations/en.jsonha/integration/custom_components/iris/translations/ru.jsonnow exists for the static integration UX surface.- HA runtime/client-side errors still use ad-hoc strings in:
ha/integration/custom_components/iris/command_bus.pyha/integration/custom_components/iris/services.pyha/integration/custom_components/iris/button.pyha/integration/custom_components/iris/switch.pyha/integration/custom_components/iris/select.py- backend HA catalog and dashboard labels now render from shared
ha.*translation keys in the centralized backend catalogs.
Config validation and magic strings¶
Representative text-first validation and status debt still exists in:
backend/src/core/settings/base.pybackend/src/apps/integrations/ha/application/control_state.pybackend/src/core/ai/prompt_policy.py- multiple app services that raise inline validation messages
- frontend/API contracts that expose raw decision/status enums without a translation boundary
These are not all defects to rewrite immediately, but they are tracked migration targets.
Logging and observability¶
Observability is not yet aligned with the target architecture:
- logging is mostly freeform and message-first;
- structured persistence logs exist, but error logs generally do not carry
error_code,message_key,domain,localeor fallback metadata; - there are no translation metrics, registry consistency metrics or locale fallback counters.
Documentation¶
Existing docs already define important direction:
docs/delivery/backend-business-localization-plan.mddocs/delivery/http-endpoint-refactor-audit.mddocs/home-assistant/notes/integration-architecture.md
One audit note: docs/delivery/backend-business-localization-plan.md says HA integration does not yet have strings.json / translations; that statement is now outdated because English HA static translation files exist.
Phase 1 ADR alignment¶
Relevant accepted architecture decisions already in force:
- ADR 0012: services return typed domain contracts, not transport payloads.
- ADR 0005: analytical API semantics require stable transport behavior and cache-aware boundary decisions.
- ADR 0015: shared platform capabilities should live in
core, not as app-specific exceptions. - ADR 0017: localization uses a global settings-driven locale model;
Accept-Languageand request-local overrides are out of scope. - ADR 0018: message keys use the accepted
error.*,ui.*,notification.*,brief.*,report.*,ha.*,doc.*,system.*taxonomy. - HA integration architecture note: backend owns catalog/runtime/business payloads; HA owns static integration UX surfaces.
docs/delivery/backend-business-localization-plan.md: boundary-localized narration, locale resolution order, and transport-safe rollout constraints.
Action taken in this iteration:
- aligned implementation to accepted ADR 0017 and ADR 0018.
- kept ADR draft
docs/architecture/adr/0016-error-taxonomy-boundary-localization.mdonly as historical rollout background, not as active source of truth. - added ADR draft
docs/architecture/adr/0021-generated-presentation-artifact-ownership.mdto capture the persisted ownership model fordescriptor_bundlevsgenerated_textartifacts, because that storage distinction is not fully specified by ADR 0017/0018.
Phase 2 minimal canonical change¶
Added foundation¶
New shared backend infrastructure:
backend/src/core/i18n/*- locale policy contracts
- global locale policy builder aligned with
Settings.language - deterministic locale resolver without request/header negotiation
- deterministic translator with fallback and interpolation checks
- external versioned translation catalogs for
enandru backend/src/core/errors/*- typed error taxonomy
- centralized error registry with duplicate protection
- registry-backed
PlatformErrorbase class carrying structured metadata, not localized user text - canonical definitions for
internal_error,resource_not_found,validation_failed, conflict/auth/policy/locked errors, HA command errors, and control-plane boundary errors backend/src/core/http/errors.py- adapter methods that localize
PlatformErroronly at the transport boundary without changing the currentApiErrorshape - localized
ApiErrorDetailmetadata withmessage_key,message_paramsandlocale
Pilot migration¶
First consumer migrated to the new standard:
backend/src/apps/integrations/ha/errors.pybackend/src/apps/integrations/ha/application/services.pybackend/src/apps/integrations/ha/api/websocket_endpoints.py
What changed in the pilot:
- HA command dispatch now raises typed registry-backed errors instead of ad-hoc inline strings for:
- command not available
- invalid payload
- message text is rendered from
message_keyviacore/i18nonly when HA transport payloads are built - machine-readable
detailsnow carry structured expectations such asexpectedandallowed_values - HA/WebSocket runtime error payloads now also carry
message_key,message_paramsandlocale
Verification¶
Verified in isolated mode without project conftest:
PYTHONPATH=backend pytest --noconftest backend/tests/core/test_i18n.py backend/tests/core/test_error_registry.py backend/tests/apps/integrations/test_ha_errors.py -q- result:
9 passed
Project-native test suites are currently blocked by environment/runtime dependencies in this workspace:
- backend
conftestexpects reachable Redis/PostgreSQL services - one fixture path also hits duplicate prompt seed state while bootstrapping test DB contents
Those failures are environment-related and not caused by the new isolated foundation tests.
Phase 3 API rollout¶
Added API locale boundary¶
Current API rollout now uses the accepted global locale model:
backend/src/core/http/deps.py
Behavior:
- request/query/header locale overrides are ignored
- effective locale resolves from
Settings.language - fallback locale remains
en
This keeps translation out of persistence and out of domain/application storage.
Enriched API error contract¶
The existing ApiError transport shape was extended, not replaced:
message_keymessage_paramslocaledomaincategoryhttp_statusseveritysafe_to_expose- detail-level
message_key - detail-level
message_params - detail-level
locale
This was done in:
backend/src/core/http/errors.py
Result:
- current clients still receive
code+message - newer clients can align on stable machine-readable metadata without parsing localized text
Migrated API endpoints¶
This wave removed the remaining direct HTTPException(detail=\"...\") endpoints and moved major backend API domains onto registry-backed localized errors:
backend/src/apps/briefs/api/*backend/src/apps/explanations/api/*backend/src/apps/notifications/api/*backend/src/apps/system/api/operation_endpoints.pybackend/src/apps/patterns/api/*backend/src/apps/signals/api/*backend/src/apps/market_data/api/*backend/src/apps/news/api/*backend/src/apps/market_structure/api/*backend/src/apps/control_plane/api/*backend/src/apps/hypothesis_engine/api/*
Boundary-localized error helpers were added in:
backend/src/apps/briefs/api/errors.pybackend/src/apps/explanations/api/errors.pybackend/src/apps/notifications/api/errors.pybackend/src/apps/patterns/api/errors.pybackend/src/apps/signals/api/errors.pybackend/src/apps/market_data/api/errors.pybackend/src/apps/news/api/errors.pybackend/src/apps/market_structure/api/errors.pybackend/src/apps/control_plane/api/errors.pybackend/src/apps/hypothesis_engine/api/errors.py
Additional canonicalization in this wave:
- central error registry now carries stable definitions for
duplicate_request,invalid_state_transition,authentication_failed,authorization_denied,policy_denied,concurrency_conflict,integration_unreachable,prompt_veil_locked,invalid_access_mode,control_mode_required,control_token_invalid - registry
message_keyvalues were renamed to ADR-0018-complianterror.*taxonomy - translation catalogs moved from Python modules to versioned external YAML files with
descriptionandmessage PlatformErrorno longer renders localized user text; transport adapters and HA bridge now localize frommessage_key + params- control-plane detail payloads are now localized and expose machine-readable detail-level translation metadata
- HA runtime error payloads now expose
message_key,message_paramsandlocalesymmetrically with backend API errors - market-structure ingest result errors no longer synthesize top-level messages from
reason.replace(...)
Operation lifecycle boundary rollout¶
This iteration also moved async operation status/event surfaces onto the same presentation-only localization model:
backend/src/core/http/operation_store.pynow persists lifecycle/error metadata asmessage_key + message_paramsanderror_message_key + error_message_paramsinstead of storing localized strings in Redis.backend/src/core/http/contracts.py,backend/src/core/http/operations.py, andbackend/src/apps/integrations/ha/schemas.pynow expose machine-readable operation fields alongside presentation text:message_keymessage_paramslocaleerror_message_keyerror_message_paramserror_localebackend/src/core/http/operation_localization.pycentralizes read-time localization for:- system operation status/result/events
- accepted background-job responses
- HA operation update/status payloads
backend/src/apps/system/api/*andbackend/src/apps/integrations/ha/application/services.pynow localize operation messages only when building transport payloads.backend/src/apps/market_data/api/job_endpoints.pywas aligned with the same locale-aware boundary after a missedmarket_data_error_to_http(..., locale=...)path was found during verification.
Deterministic narration rollout¶
This iteration also removed the last hardcoded multilingual Python branches from the deterministic fallback layer:
backend/src/core/i18n/context.pynow owns shared context-language normalization and effective-language resolution for app services.backend/src/apps/briefs/language.pyandbackend/src/apps/explanations/language.pynow delegate to the sharedcore/i18nhelper instead of maintaining parallel locale logic.backend/src/apps/notifications/services/humanization_service.pynow renders deterministic fallback notifications from catalog keys under:notification.*backend/src/apps/explanations/services/generation_service.pynow renders deterministic fallback explanation titles/body/bullets from catalog keys under:brief.explanation.*- shared
en/rucatalogs now contain deterministic fallback texts for: - notification titles/messages
- explanation titles, bodies, and bullet variants
Result:
- deterministic fallback narration for notifications and explanations is no longer source-of-truth text in Python code;
- adding another language for those surfaces now requires only catalog expansion, not business-logic edits;
- the rollout now treats persisted presentation artifacts as canonical envelopes rather than locale-specific rendered text rows.
Canonical presentation artifact storage¶
This iteration moved persisted notifications, explanations, and briefs onto an explicit canonical presentation envelope:
- ORM/storage schema now includes:
content_kindcontent_json- accepted
content_kindvalues are: descriptor_bundlegenerated_textdescriptor_bundlestoresmessage_key + paramsfield descriptors in canonical JSON form.generated_textstores a rendered presentation snapshot plusrendered_locale.- new writes no longer use
context_json.localizationas the active storage contract. - legacy text columns (
title,message,explanation,summary,bullets_json) and persisted artifactlanguagecolumns have now been physically removed from the ORM schema in favor of the envelope. - read presenters now resolve content from validated
content_json, with fallback to oldcontext_json.localizationonly for transitional rows that predate the canonical envelope backfill.
Storage identity is also now language-agnostic:
ai_notificationsare unique bysource_event_type + source_event_idai_explanationsare unique byexplain_kind + subject_idai_briefsare unique bybrief_kind + scope_keynotificationsevent handling andexplanationsgeneration no longer consume explicit per-call/per-event language overrides; effective locale comes only from global settingsbriefsgeneration and brief job deduplication/locking now also ignore explicit per-call language overrides and resolve locale only from global settings- explanation job deduplication and task locks no longer include language
- brief job deduplication and task locks no longer include language
notification_createdevent payloads no longer leaklanguageinto the event bus- brief/explanation accepted job contracts and operation result payloads now use
rendered_localeinstead of legacylanguage
Migration work included:
- backfilling existing rows into
content_json - collapsing historic locale duplicates into a single canonical row per business entity, preferring descriptor-backed rows when both existed
- replacing the old locale-scoped unique indexes with canonical unique indexes that do not include
language - dropping legacy persisted artifact
languageand rendered-text columns after the envelope backfill completed
Result:
- persisted notifications/explanations/briefs are now one-row-per-entity rather than one-row-per-locale;
- deterministic descriptor-backed content is re-rendered on the presentation boundary from shared catalogs;
- freeform AI-generated content is now explicitly governed as
generated_textpresentation snapshot content instead of accidental text-first storage; - locale changes no longer create duplicate rows for these artifact types;
- persisted artifact read contracts no longer expose legacy row language and instead expose only
rendered_localefrom the presentation envelope.
Verification¶
Additional isolated verification for this phase:
PYTHONPATH=backend pytest --noconftest backend/tests/core/test_i18n.py backend/tests/core/test_http_locale.py backend/tests/core/test_error_registry.py backend/tests/apps/integrations/test_ha_errors.py backend/tests/apps/briefs/test_brief_api_errors.py backend/tests/apps/explanations/test_explanation_api_errors.py backend/tests/apps/notifications/test_notification_api_errors.py backend/tests/apps/patterns/test_pattern_api_errors.py backend/tests/apps/signals/test_signal_api_errors.py backend/tests/apps/market_data/test_market_data_api_errors.py backend/tests/apps/news/test_news_api_errors.py backend/tests/apps/market_structure/test_market_structure_api_errors.py backend/tests/apps/control_plane/test_control_plane_api_errors.py backend/tests/apps/hypothesis_engine/test_hypothesis_api_errors.py -q- result:
22 passed PYTHONPATH=backend pytest --noconftest backend/tests/core/test_i18n.py backend/tests/core/test_error_registry.py backend/tests/core/test_operation_localization.py backend/tests/apps/integrations/test_ha_errors.py backend/tests/apps/market_data/test_views.py::test_market_data_view_branches -q- result:
15 passed PYTHONPATH=backend pytest --noconftest backend/tests/core/test_i18n.py backend/tests/core/test_i18n_context.py backend/tests/apps/notifications/test_humanization_service.py backend/tests/apps/explanations/test_generation_service_rendering.py -q- result:
11 passed PYTHONPATH=backend pytest --noconftest backend/tests/core/test_i18n.py backend/tests/core/test_i18n_context.py backend/tests/core/test_i18n_descriptors.py backend/tests/apps/notifications/test_humanization_service.py backend/tests/apps/notifications/test_notification_presenters.py backend/tests/apps/explanations/test_generation_service_rendering.py backend/tests/apps/explanations/test_explanation_presenters.py -q- result:
16 passed PYTHONPATH=backend pytest --noconftest backend/tests/core/test_i18n.py backend/tests/core/test_i18n_context.py backend/tests/core/test_i18n_descriptors.py backend/tests/apps/notifications/test_humanization_service.py backend/tests/apps/notifications/test_notification_presenters.py backend/tests/apps/notifications/test_notification_service_storage.py backend/tests/apps/explanations/test_generation_service_rendering.py backend/tests/apps/explanations/test_explanation_presenters.py backend/tests/apps/explanations/test_explanation_service_storage.py -q- result:
20 passed PYTHONPATH=backend pytest --noconftest backend/tests/core/test_i18n.py backend/tests/core/test_i18n_context.py backend/tests/core/test_i18n_descriptors.py backend/tests/core/test_i18n_presentation.py backend/tests/apps/notifications/test_humanization_service.py backend/tests/apps/notifications/test_notification_presenters.py backend/tests/apps/notifications/test_notification_service_storage.py backend/tests/apps/explanations/test_generation_service_rendering.py backend/tests/apps/explanations/test_explanation_presenters.py backend/tests/apps/explanations/test_explanation_service_storage.py -q- result:
24 passed PYTHONPATH=backend pytest --noconftest backend/tests/core/test_i18n.py backend/tests/core/test_i18n_presentation.py backend/tests/apps/briefs/test_brief_storage.py backend/tests/apps/briefs/test_brief_presenters.py -q- result:
8 passed PYTHONPATH=backend pytest --noconftest backend/tests/core/test_i18n_presentation.py backend/tests/apps/notifications/test_notification_service_storage.py backend/tests/apps/explanations/test_explanation_service_storage.py backend/tests/apps/briefs/test_brief_storage.py backend/tests/apps/notifications/test_notification_presenters.py backend/tests/apps/explanations/test_explanation_presenters.py backend/tests/apps/briefs/test_brief_presenters.py -q- result:
14 passed
Brief DB-backed integration tests still require the full project fixture stack (async_db_session, api_app_client) and were not rerun in isolated --noconftest mode.
What is done¶
- audit recorded and centralized
- ADR alignment completed
- accepted ADR 0017 and ADR 0018 are now explicitly reflected in the implementation
- minimal shared i18n and error foundation added
- first cross-layer pilot migrated in HA command dispatch
- API locale boundary now resolves language only from global settings
- API error payloads now expose shared machine fields alongside localized text
- direct string-first API
HTTPExceptionpaths across the current backend endpoint surface were migrated - remaining backend domain
api/errors.pytranslators were moved offmessage=str(exc)and onto registry-backedPlatformError ApiErrorDetailnow supports localized detail metadata and is used in control-plane concurrency/access-mode errors- control-plane header/auth/policy boundary errors now have stable registry-backed error codes
- shared catalogs are now external versioned YAML files with descriptions
- HA runtime error payloads are now aligned with backend error machine fields
- async operation storage is now machine-first, and system API / HA operation payloads localize only on read from shared message catalogs
- accepted background-job payloads now expose operation
message_keymetadata consistently with system status/event payloads - duplicated app-local language resolution for briefs/explanations/notifications now routes through shared
core/i18n - deterministic fallback narration in notifications and explanations now renders from shared message catalogs instead of inline multilingual Python branches
- deterministic notification/explanation artifacts now persist descriptor metadata and are localized again at read time instead of treating stored text as canonical
- notifications and explanations now persist canonical
content_kind + content_jsonenvelopes, and legacy rendered text fields are no longer present in the active schema - notifications/explanations storage identity is now one-row-per-entity instead of one-row-per-locale
- briefs now persist canonical
content_kind + content_jsonenvelopes, and legacy rendered text fields are no longer present in the active schema - briefs storage identity is now one-row-per-entity instead of one-row-per-locale
- explanation job deduplication/task locking no longer depends on locale
- brief job deduplication/task locking no longer depends on locale
notification_createdevent payload no longer carries locale-specific data- persisted artifact read contracts and accepted job contracts no longer expose legacy row
language; they now exposerendered_locale - canonical presentation envelope validation now exists in
core/i18n/presentation.pyand is covered by unit tests - HA catalog entity names, command names and dashboard titles now render from shared backend translation catalogs instead of hardcoded Python strings
- HA integration now includes static Russian translations for config-flow UX
- translation catalog validation/coverage tooling now exists in
core/i18n/validators.py,backend/scripts/export_i18n_coverage.py,backend/scripts/check_i18n_catalogs.py, committeddocs/_generated/i18n-coverage.md, and CImake i18n-check
What remains¶
Priority next steps for phase 3:
- start replacing text-first backend narration in high-priority deterministic surfaces with
message_key + params - migrate operation result producers that still only return generic
status/reason/error_codevalues so read-time localization can preserve richer domain-specific messages - refine domain exceptions that currently only expose exception type, so API localization can preserve domain-specific detail without falling back to generic top-level registry messages
Priority next steps for phase 4:
- add frontend i18n foundation and move hardcoded UI text onto translation catalogs
- align frontend/HA consumption with shared
error_code/message_key
Risks and legacy zones¶
- operation transport contracts still retain localized
message/error_messagefields for backward-compatible presentation, even though the backing store is now machine-first - frontend has no i18n runtime yet, so backend/shared contracts alone do not solve UI localization
- generated text remains locale-specific snapshot content by design, so locale switching for AI-generated artifacts still requires explicit regeneration/replacement if a new language snapshot is needed
- several domain exceptions still carry human English text internally and need structured params before their localized API messages can become fully domain-specific
- observability for translation fallback/error metrics is not implemented yet