Skip to content

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_title prepends a Korean severity to the English alert.title; alert_trigger.rs fans the English alert.title/alert.message to 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

  1. Detectors (acoustic_detector.rs, fusion_detector.rs) build {en,ko} title+body from their existing structured signal (AcousticSignature, label, human) and write them into details.i18n. title/description keep the EN string (back-compat + the stable dedup/audit surface).
  2. Alert bridge (alert_trigger.rs): when it creates an alert from an anomaly report, copy the anomaly's details.i18n into the alert's details.i18n.
  3. Push/SMS composition: a new alert_text::localize(alert, locale) -> (title, body) reads alert.details.i18n[locale] (fallback alert.title/message) and pairs it with a locale-keyed severity label. dispatch_vapid_push/dispatch_fcm_push call it per-subscription using sub.locale; SMS uses 'ko'.
  4. In-app (AlertsPage.tsx, both apps): render alert.details?.i18n?.[locale]?.title ?? alert.title (and body), so each operator sees their language.
  5. Model anomalies (Phase B): the rubric (services/rubric.rs) instructs the model to return title/title_ko (+ description/description_ko); inference_results.rs maps them into details.i18n at 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 use localize per-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_text unit tests: localize picks the right language, falls back when i18n absent, 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).