Multilingual

One app, three languages — UI and the Sage.

MiFamilias is built so a family that spans languages can use it together: the interface, the personality questions, and even the AI Sage's spoken replies follow the member's chosen language. English, Spanish and Tagalog ship today. Every diagram below maps to real handlers, services and routes in the codebase — frontend and backend.

The Problem We Solve The app is in English, but Lola only speaks Tagalog — and Abuela, Spanish.
UI in English only questions in English Sage answers in English now: each in their language
01The Whole Picture

Two layers, one language signal

A single source of truth — LanguageService.current() — drives everything. The frontend swaps UI strings instantly with ngx-translate; the backend receives the language on each request and localizes data (the question bank) and AI (chat reply, voice, transcription).

flowchart TB
    subgraph FE["fa:fa-mobile-screen Mobile (Ionic / Angular)"]
        LS["fa:fa-language LanguageService
current() signal · persisted"] UI["fa:fa-table-cells UI strings
TranslatePipe + en/es/tl.json"] LS --> UI end subgraph BE["fa:fa-server Backend (Lambda handlers)"] Q["fa:fa-list-check getQuestions?lang
question-bank overlay"] C["fa:fa-comments chat / chatVoice / chatVideo
languageDirective in prompt"] T["fa:fa-microphone transcribe/upload
Whisper language hint"] end subgraph ML["fa:fa-robot Self-hosted AI"] LLM["fa:fa-brain LLM (Ollama→OpenAI→Claude)"] TTS["fa:fa-volume-high OpenVoice + MeloTTS"] STT["fa:fa-waveform Faster-Whisper"] end LS ==>|"lang on every call"| Q LS ==>|lang| C LS ==>|lang| T C --> LLM C --> TTS T --> STT style LS fill:#0e3b2e,stroke:#4ade80,stroke-width:2px,color:#e8f4f0 style UI fill:#14262a,stroke:#34d399,color:#e8f4f0 style Q fill:#14262a,stroke:#34d399,color:#e8f4f0 style C fill:#14262a,stroke:#34d399,color:#e8f4f0 style T fill:#14262a,stroke:#34d399,color:#e8f4f0 style LLM fill:#101f23,stroke:#8aa6a0,color:#e8f4f0 style TTS fill:#101f23,stroke:#8aa6a0,color:#e8f4f0 style STT fill:#101f23,stroke:#8aa6a0,color:#e8f4f0

The language code (en | es | tl) is the only thing that crosses the wire — small, cacheable, and never PII.

02Choosing a Language

Follow the phone — or ask, Apple-style

On first run the app reads the device locale (navigator.language; fil maps to tl). If it's a language we ship, we use it; if not, the very first onboarding step is a simple language picker. Either way it's saved and changeable later in Profile → Appearance and Settings → Preferences.

flowchart LR
    A["fa:fa-power-off App start"] --> B{"Saved choice in
localStorage?"} B -->|yes| USE["fa:fa-check Use it"] B -->|no| D["fa:fa-mobile Detect device locale
fil → tl"] D --> E{"Supported
(en/es/tl)?"} E -->|yes| USE E -->|no| P["fa:fa-list Onboarding
language picker (step 1)"] P --> USE USE --> S["fa:fa-language LanguageService.current()
signal · persisted"] S -.->|"change anytime"| PR["fa:fa-user Profile → Appearance"] S -.->|"change anytime"| ST["fa:fa-gear Settings → Preferences"] style USE fill:#0e3b2e,stroke:#4ade80,stroke-width:2px,color:#e8f4f0 style S fill:#0e3b2e,stroke:#4ade80,stroke-width:2px,color:#e8f4f0 style P fill:#14262a,stroke:#34d399,color:#e8f4f0 style PR fill:#14262a,stroke:#34d399,color:#e8f4f0 style ST fill:#14262a,stroke:#34d399,color:#e8f4f0

The picker shows native names — English · Español · Tagalog — and switching is instant (no reload).

03The UI Pipeline

Keys in, the right language out

Templates render translation keys, not text. TranslatePipe looks each key up in the active locale file (assets/i18n/<lang>.json). For text computed in TypeScript — status labels, the countdown badge, PIN-setup titles — the method returns a key and the template pipes it. Bold copy uses [innerHTML] so emphasis survives translation.

flowchart LR
    K["fa:fa-key 'capsule.unlocked'
(in template)"] --> P["fa:fa-filter TranslatePipe"] TS["fa:fa-code getStatusLabel()
returns a key, not text"] --> P P --> J{"Active locale"} J -->|en| EN["fa:fa-flag en.json"] J -->|es| ES["fa:fa-flag es.json"] J -->|tl| TL["fa:fa-flag tl.json"] EN --> R["fa:fa-eye Rendered text"] ES --> R TL --> R style P fill:#0e3b2e,stroke:#4ade80,stroke-width:2px,color:#e8f4f0 style R fill:#14262a,stroke:#34d399,color:#e8f4f0 style EN fill:#101f23,stroke:#8aa6a0,color:#e8f4f0 style ES fill:#101f23,stroke:#8aa6a0,color:#e8f4f0 style TL fill:#101f23,stroke:#8aa6a0,color:#e8f4f0

Plurals are handled with paired …One/…Many keys chosen by a ternary, so grammar stays right in every language. Coverage: tabs, full Auth, Home, Families, Capsules, the whole Settings tree, Onboarding, and all of Transcend.

04The Question Bank

112 questions, served in your language

The personality questions live server-side in backend/src/data/transcend-questions.ts. The mobile app sends ?lang=; getQuestions overlays the localized question + follow-up text from transcend-questions.i18n.ts at serve time. English falls straight through to the source — no overlay, no risk.

sequenceDiagram
    autonumber
    participant M as Mobile
    participant H as getQuestions
    participant B as Question bank (en)
    participant I as i18n overlay (es/tl)
    M->>H: GET /transcend/questions?lang=es
    H->>B: load + filter + sort
    B-->>H: 112 questions (English)
    H->>I: localizeQuestions(qs, 'es')
    I-->>H: question + followUp swapped to Spanish
    H-->>M: localized list
    Note over M,H: lang = LanguageService.current()
en → no overlay (source as-is)

Category labels and the importance badge are localized client-side; the question text is localized server-side so it's identical across every device.

05Your Own Questions

Add what the bank never asks

A member can add their own question + answer — a signature story, a family in-joke, a piece of advice the standard set never covers. It's stored with a custom-<id>, flagged isCustom, and — crucially — embedded into the same RAG store the Sage retrieves from. The Sage genuinely learns from it. Custom answers never count toward "core" progress.

flowchart LR
    U["fa:fa-pen-to-square Add your own
(question + answer)"] --> S["fa:fa-paper-plane submitResponse
{ customQuestion, customCategory }"] S --> ID["fa:fa-hashtag mint custom-<id>
isCustom = true"] ID --> DB["fa:fa-database Response record"] ID --> V["fa:fa-vector-square Embed Q+A → RAG"] V --> SAGE["fa:fa-brain Sage draws on it
in conversation"] DB --> LIST["fa:fa-list My Own Questions
(getResponses · isCustom)"] style S fill:#0e3b2e,stroke:#4ade80,stroke-width:2px,color:#e8f4f0 style V fill:#0e3b2e,stroke:#4ade80,stroke-width:2px,color:#e8f4f0 style SAGE fill:#14262a,stroke:#34d399,color:#e8f4f0 style LIST fill:#14262a,stroke:#34d399,color:#e8f4f0

Re-sending the same custom-<id> updates in place (no duplicates). Verified by smoke-test-custom-questions.ts (33/33).

06Talking to the Sage

Reply, voice, and transcription — all in language

The chat handlers take lang and inject a directive (languageDirective()) telling the model to answer in the member's language. The reply is then voiced and the member's speech transcribed in the same language. Spanish runs the Spanish MeloTTS voice; Whisper handles speech-to-text for all three.

sequenceDiagram
    autonumber
    participant U as Family member
    participant V as Whisper (STT)
    participant C as chatVoice (+lang)
    participant L as LLM
    participant T as MeloTTS (TTS)
    U->>V: speaks (es)
    V-->>C: text + language=es hint
    C->>L: prompt + "Respond ENTIRELY in Spanish"
    L-->>C: Spanish reply
    C->>T: synthesize (language=ES)
    T-->>U: Spanish voice in the Sage's cloned tone
    Note over C,T: en → English accents (GB/US/AU…)
es → MeloTTS ES · tl → MMS-TTS + OpenVoice clone

English chats are byte-for-byte unchanged: languageDirective('en') is empty, so no directive is added.

07Where It Stands

What's covered — honestly

Every layer follows the language across all three — UI, questions, chat text, voice, and transcription. Spanish voice runs MeloTTS; Tagalog voice runs a self-hosted MMS-TTS + OpenVoice clone (see §08), with ElevenLabs as the safety net.

UI strings en · es · tl

Every screen — tabs, auth, onboarding, the full settings tree, all Transcend screens.

Question bank en · es · tl

112 questions + follow-ups localized server-side via ?lang=.

Sage reply (text) en · es · tl

System-prompt directive steers the language; mobile passes the choice.

Voice-to-text en · es · tl

Whisper language hint; live dictation follows the device language.

Sage voice — Spanish es

MeloTTS ES model + es.pth base speaker; voice cloning preserved.

Sage voice — Tagalog tl

Self-hosted Meta MMS-TTS base → OpenVoice clone (verified end-to-end). ElevenLabs auto-fallback.

Legal

The Transcend consent translations (es/tl) are a machine-translated straw-man for demo, pending counsel review — see docs/legal/CONSENT_TRANSLATION_REVIEW.md.

08The Tagalog Voice

Self-hosted base + cloned tone — with a cloud safety net

MeloTTS has no Tagalog model, so tl uses a different base engine and still clones the user's voice. Meta MMS-TTS (facebook/mms-tts-tgl) generates the Tagalog speech; OpenVoice's language-agnostic tone-color converter then re-paints it in the member's cloned voice. If the self-hosted path errors, the voice-synthesis factory automatically falls back to ElevenLabs multilingual. Verified end-to-end (HTTP 200, 7 s @ 22 kHz).

flowchart LR
    TL["fa:fa-flag tl reply text"] --> P{"local TTS
available?"} P -->|yes| MMS["fa:fa-robot MMS-TTS (tgl)
Tagalog base audio"] MMS --> SE["fa:fa-fingerprint base SE (no-VAD)
+ user voice SE"] SE --> OV["fa:fa-wand-magic-sparkles OpenVoice
tone-color convert"] OV --> DONE["fa:fa-circle-check Tagalog in the
Sage's cloned voice"] P -->|"error / unavailable"| EL["fa:fa-cloud ElevenLabs
multilingual (fallback)"] EL --> DONE style MMS fill:#0e3b2e,stroke:#4ade80,stroke-width:2px,color:#e8f4f0 style OV fill:#0e3b2e,stroke:#4ade80,stroke-width:2px,color:#e8f4f0 style DONE fill:#14262a,stroke:#34d399,color:#e8f4f0 style EL fill:#101f23,stroke:#8aa6a0,color:#e8f4f0

Keeps the platform's zero-API-dependency principle by default (MMS + OpenVoice are self-hosted); ElevenLabs is opt-in insurance. Trade-off still worth tracking: MMS Tagalog voice-clone fidelity vs. cloud quality — easy to A/B now that both paths exist.

North Star A grandchild and a grandparent use the same app — and the same Sage — each in their own language.