Bilingual, Locale-Aware Notifications — Design
Goal: Every alert/anomaly notification is produced in both Korean and English, and each recipient sees it in their own UI language — across the in-app alert list, web push (VAPID + FCM), and SMS. Today the content is generated in English by the server detectors and the inference model, then shown verbatim in a Korean UI.
Owner decisions (2026-07-13): - Generate both KO + EN; deliver each user their UI language (not hardcode Korean). - Inference-model English titles are localized via the Inference Ops rubric instructing bilingual output (Phase B).
Root cause (verified)
acoustic_detector.rs→"Acoustic signature on {label}: {human}"(English).fusion_detector.rs→"Multi-sensor anomaly on {label}"(English).- inference model output (
body.title, e.g."Thermal runaway detected") — English. alert_text.rs::push_notification_titleprepends a Korean severity to the Englishalert.title;alert_trigger.rsfans the Englishalert.title/alert.messageto VAPID + FCM + SMS verbatim.- Operator-app toasts/errors are already locale-aware (
friendlyErrorMessage(err, locale)), so only the alert content leaks English.
Architecture — localize at display time from stored bilingual content
Store both languages in the existing details JSONB (no content-column migration): details.i18n = { "en": { "title": …, "body": … }, "ko": { "title": …, "body": … } }. Every surface reads details.i18n[locale] and falls back to the legacy title/message when a language (or the whole i18n block) is absent — so old rows and not-yet-migrated model output degrade gracefully to today's behavior.
Recipient locale:
- In-app (both SPAs): the client already knows its UI locale → render details.i18n[locale].
- Web push / FCM (server-rendered): add a locale column to push_subscriptions and device_push_tokens (browser/device sends its UI locale on register; default 'ko'). Compose per-subscription.
- SMS: facility staff are Korean → render 'ko' (no per-recipient locale row exists; revisit only if EN SMS recipients appear).
severity_label is already Korean; make it locale-keyed too so an EN recipient gets "Critical: …" not "치명적: …".
Data flow
- Detectors (
acoustic_detector.rs,fusion_detector.rs) build{en,ko}title+body from their existing structured signal (AcousticSignature,label,human) and write them intodetails.i18n.title/descriptionkeep the EN string (back-compat + the stable dedup/audit surface). - Alert bridge (
alert_trigger.rs): when it creates an alert from an anomaly report, copy the anomaly'sdetails.i18ninto the alert'sdetails.i18n. - Push/SMS composition: a new
alert_text::localize(alert, locale) -> (title, body)readsalert.details.i18n[locale](fallbackalert.title/message) and pairs it with a locale-keyed severity label.dispatch_vapid_push/dispatch_fcm_pushcall it per-subscription usingsub.locale; SMS uses'ko'. - In-app (
AlertsPage.tsx, both apps): renderalert.details?.i18n?.[locale]?.title ?? alert.title(and body), so each operator sees their language. - Model anomalies (Phase B): the rubric (
services/rubric.rs) instructs the model to returntitle/title_ko(+ description/description_ko);inference_results.rsmaps them intodetails.i18nat ingestion. Until then, model anomalies fall back to the stored (English)title— unchanged from today.
Migration
push_subscriptions.locale TEXT NOT NULL DEFAULT 'ko' and device_push_tokens.locale TEXT NOT NULL DEFAULT 'ko'. Strictly-monotonic YYYYMMDDHHMMSS prefix, greater than all existing. No change to anomaly_reports/alerts columns (i18n rides in existing details JSONB).
Phasing (each independently shippable, behavioral no-op for absent i18n)
- A1 Migration +
severity_label(locale)+alert_text::localize. - A2 Detectors emit
details.i18n; alert bridge copies it; push/SMS uselocalizeper-recipient locale. - A3 Frontends: register subscription with UI locale; in-app AlertsPage renders
details.i18n[locale]. - B Rubric bilingual instruction + inference ingestion maps model bilingual output →
details.i18n.
Testing
alert_textunit tests:localizepicks the right language, falls back wheni18nabsent, EN vs KO severity label.- Detector tests assert
details.i18n.{en,ko}populated. - Migration monotonicity check.
- Frontend: AlertsPage renders localized title when
details.i18n[locale]present, falls back otherwise (both apps).