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 app is in English, but Lola only speaks Tagalog — and Abuela, Spanish.
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.
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).
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.
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.
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).
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.
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.
Every screen — tabs, auth, onboarding, the full settings tree, all Transcend screens.
112 questions + follow-ups localized server-side via ?lang=.
System-prompt directive steers the language; mobile passes the choice.
Whisper language hint; live dictation follows the device language.
MeloTTS ES model + es.pth base speaker; voice cloning preserved.
Self-hosted Meta MMS-TTS base → OpenVoice clone (verified end-to-end). ElevenLabs auto-fallback.
The Transcend consent translations (es/tl) are a machine-translated
straw-man for demo, pending counsel review — see
docs/legal/CONSENT_TRANSLATION_REVIEW.md.
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.