EN is the source-of-truth language. PT-BR is the translation. Every new user-facing string must be wired through the i18n layer at write-time — never hardcoded inline, never "I'll translate it later". Retrofitting i18n costs ~10x the original write and has burned us repeatedly (PT outliers in snp_database.py, analyzer.py, GENE_CONTEXT, wellness panels leaked into the EN-default product).
Concretely, when you add a new string:
- UI label / button / error / table header → add EN + PT entries to
STRINGSinsrc/i18n.py, reference viat["key"]in the template. Never{{ "Save" }}in a Jinja file. - Domain data (gene description, SNP text, hereditary condition, family-planning detail) → store EN as the bare key (
note,description,text), PT under the same key +_ptsuffix (note_pt). Accessor function (get_gene_context(gene, lang),_t(item, key, lang), etc.) resolves at call time. - AI prompts → ship
SYSTEM_PROMPT_ENandSYSTEM_PROMPT_PTas a pair insrc/local_ai.py, switch bylanguagearg. - New analysis function that emits user-facing text → accept
lang: Lang(fromsrc/i18n.py), default"en", branch internally on it.
The Lang type. Lang = Literal["en", "pt"] lives in src/i18n.py alongside LANGS, DEFAULT_LANG = "en", and normalize_lang(value). Use lang: Lang on every public signature that consumes language. At any untrusted boundary (URL segment, cookie, query param), coerce with normalize_lang(raw) — never compare against literals like if lang == "pt" before normalization.
UI ordering. EN-first everywhere — language toggles, dropdowns, dashboard headers: EN | PT-BR, in that order.
Fallback direction. If a PT translation is missing, fall back to EN. Never the inverse. If translate_medical (Argos neural EN→PT) is unavailable, show the EN source — graceful degradation.
Decision rule — where does a new string live? Before reflexively adding field + field_en to a data dict, pick the right channel:
| Kind of text | Channel | Why |
|---|---|---|
| UI label, button, error, table header, status phrase | STRINGS in src/i18n.py, referenced via t["key"] in templates |
Single source of truth, no duplication across templates |
| Clinically sensitive long-form text (hereditary condition narrative, family-planning guidance, prescriptive-language-adjacent copy) | Hardcoded EN + PT pair in the domain dict (text_F/text_F_en, condition_pt/condition_en) |
Neural translation can shift medical meaning ("mastectomy profilática" → "preventive mastectomy" is fine; subtler clinical idioms aren't) |
| Short trait/category labels ("Eye color", "Skin pigmentation") | Either STRINGS or hardcoded pair — caller's call |
Low risk either way; prefer STRINGS if reused in multiple places |
| Dynamic clinical annotations from external corpora (ClinVar significance text, PharmGKB drug notes, gene descriptions ingested in bulk) | Argos neural EN→PT at runtime via src/translator.py |
Corpus too large to hardcode; manual translation doesn't scale |
| AI system prompts | SYSTEM_PROMPT_EN + SYSTEM_PROMPT_PT pair in src/local_ai.py |
Prompt engineering is fragile; an Argos retranslation could break instruction following |
Default for new code: if the text is shorter than a sentence, lives in a STRINGS-able UI surface, or could be reused → src/i18n.py. If it's domain data with safety implications → hardcoded EN+PT. If it's coming from an external feed → Argos. Only invent a new _en/_pt pair on a dict when none of the above fit.
Definition of done. A PR adding a new string is not done until both languages render correctly. CI doesn't enforce this — code review does. If you see a hardcoded literal in a PR diff, block it.
-
i18n from the start — see the dedicated section above; this is rule #1.
-
Use background agents for large writes: Translation files, large data dicts, and repetitive code generation should be delegated to background agents. Keep the main context for architecture decisions and integration.
-
Test with
python -cbefore restarting Flask: Verify code compiles and functions work correctly via inline Python before killing/restarting the server. Avoid restart cycles for syntax errors. -
Batch related edits: Group related changes (e.g., new panel + i18n strings + template update + sample genome SNPs) into a single focused operation instead of editing the same files across multiple rounds.
- Privacy first:
NetworkBlockermust wrap ALL analysis code. Translation runs inside the blocker. - NetworkBlocker is THREAD-LOCAL (since the gray-screen-loading bug): the monkey-patch of
socket.socket/getaddrinfois installed globally but only RAISES on threads insidewith NetworkBlocker():. Other threads (Flask serving static assets, Argos translator install) keep networking normally. Do NOT callNetworkBlockerfrom a Flask request handler — only from background analysis threads. Nesting is supported via a per-thread depth counter; inner__exit__does not unblock the outer scope. Regression locked bytests/test_privacy.py::TestNetworkBlockerThreadIsolation. - AI chat hits local Ollama via HTTP loopback only:
chat_about_analysisposts tohttp://127.0.0.1:11434/api/chat(NOTollama runsubprocess). The CLI rendering was soft-wrapping mid-word and truncating PT replies. HTTP gives us deterministicnum_predict/num_ctx. Privacy posture identical (same daemon, pure loopback). Locked bytest_request_targets_local_ollama_loopback. Streaming (chat_about_analysis_stream) still uses subprocess; migrate only if it surfaces in the UI path. - No prescriptive language: Use "associated with", "may indicate", "discuss with your physician" — never "you have", "you should take", "recommended dose".
- Sex-aware routing is CHROMOSOMAL, not clinical:
infer_sex()counts chromosome Y SNPs and emitsM/F/None. Known miscalls: Klinefelter (XXY), Turner (X0), complete androgen insensitivity (XY female), XX male (SRY translocation), mosaicism. Downstream code MUST honorprofile.get("sex")over the inferred value when both exist (this is already wired inanalyze_hereditary_conditions). Hereditary conditions usetext_F,text_M,text_neutralvariants — when no sex is known, fall back totext_neutral, never guess. If a condition hastext_M=None(e.g. polyposis, mostly studied in women), the matcher must fall back to neutral text, not crash onNone. - Hereditary condition matrix integrity: every entry in
HEREDITARY_CONDITIONSmust have both PT and EN variants forname,text_neutral,evidence,confirm. Sex-specific text fields (text_F/text_M) are optional but, if PT is set, EN must be set too (and vice-versa). Locked by parametrized test intests/test_hereditary_conditions.py. - Secure deletion: Always use
secure_delete()for genetic data files, neverunlink(). - CSRF protection: All POST routes are protected via Origin/Referer check in
_csrf_check().
Gene Lens is a desktop-first product and we don't pretend otherwise. Reasoning:
- The whole runtime lives on the user's machine:
pip install,python run.py, Ollama serving a ~5 GB local model, ClinVar's 290 MB index loaded into RAM. None of this works on mobile. - The browser that views the dashboard is on the same machine that ran the analysis — they're not on different devices.
- An analysis session is a deep reading task: 200-SNP tables, ancestry maps, donut charts with multiple categories, AI chat next to a panel. That's mouse-and-monitor work.
- The plausible mobile case ("I want to show my dad my Factor V Leiden on a phone") is niche.
What this means for changes:
- Don't accept feature requests framed as "make X mobile-friendly" without checking who actually uses it on mobile. The answer is usually "nobody".
- A media query for narrow viewports is OK if it prevents the worst layout breaks (header wrap, table overflow), but don't redesign components for touch / small screens.
- The
/reportroute is the one exception: it has its own A4-paper layout because the user does sometimes want to share a PDF with a doctor. That's not mobile, that's print. - If a future contributor wants real mobile support, that's a re-platform conversation (PWA + worker offload + remote inference + auth), not a CSS pass.
python main.py webboots in EN by default.--lang pt(orpt-BR) switches startup banner, DB loader messages, and the initial dashboard language until the user's cookie overrides.- DB loader messages go through
src.databases.set_lang(code)+_t(key). Both_MSGS["en"]and_MSGS["pt"]must carry the same key set — paridade enforced bytest_every_pt_key_exists_in_en_and_vice_versa. - The startup banner title is
"Dashboard Gene Lens"in both languages.
dashboard.py— Flask app, routes, conclusions builders, human conclusions (bilingual via_HCand_TCdicts).run_dashboard(port, lang)is the public entrypoint called frommain.py.src/wellness_panels.py— 14 panels with PT text, EN translations insrc/wellness_i18n.pysrc/phenotype_i18n.py— EN translations for phenotype + ancestrysrc/hereditary_conditions.py— Sex-aware condition matrix (15 conditions).analyze_hereditary_conditions(disease_findings, profile, lang)is the only consumer; matrix entries follow strict bilingual schema.src/sex_inference.py—infer_sex(genome_by_rsid, threshold=50)→M/F/None. Chromosomal heuristic only; honorprofile.sexover it.src/local_ai.py—chat_about_analysis(blocking, HTTP),chat_about_analysis_stream(streaming, subprocess),_build_chat_messages(single source of truth for the Ollamamessagesarray).src/privacy.py—NetworkBlocker(thread-local),secure_delete,sanitize_report_metadata.src/databases.py—load_clinvar,load_pharmgkb,set_langfor console messages.src/reports.py— Protocol builders with_PROTOCOL_PT/_PROTOCOL_ENdicts
- Run the suite:
.venv/bin/python -m pytest tests/(~4s, 228 tests at last count). Systempythonis missing Flask — always use the venv.
When deciding what to test first, classify by what a regression actually causes. Same framework we used to triage the original test backlog:
- 🔴 Critical — regression causes a safety/clinical/privacy harm.
Examples: hereditary-condition sex routing (man receives BRCA female-only guidance),
NetworkBlocker(data leak during analysis, or the gray-screen loading bug). Must have tests and the test must fail before the fix lands. - 🟠 High — regression silently corrupts analysis output or core data path.
Examples:
databasesloaders (mutates every report),sex_inference(cascades into the hereditary matrix),local_ai._build_chat_messages(system prompt / role sanitization). Should have tests; bug here is hard to spot without them. - 🟡 Medium — regression degrades UX or content, but doesn't break correctness. Examples: i18n parity (missing label = blank UI element), bilingual protocol dicts (report section without header). Tests are valuable as drift guards, not urgent.
When triaging: hit reds first, then oranges, then yellows. Don't write yellow tests when red gaps exist.
- Manual smoke:
sample/sample_genome.csv(synthetic, 200+ SNPs) through the dashboard. Verify both languages render after any text change. - Privacy guarantee:
python main.py privacy-checkafter touchingsrc/privacy.pyor anything that runs insideNetworkBlocker. - Don't hit the network in tests. AI chat tests mock
urllib.request.urlopen; streaming tests mocksubprocess.Popen; DB loader tests write synthetic TSVs totmp_path. A test that times out for 80+ seconds is almost certainly trying to reach a real Ollama daemon — fix the mock. - When to add a test (the bar):
- Sex-aware routing change in
hereditary_conditions.py→ extendtests/test_hereditary_conditions.py. - New condition added to the matrix → the parametrized integrity test catches missing bilingual fields automatically.
- New i18n key in
databases._MSGS→ paridade test catches half-translations automatically. - Fix for a user-reported bug → write the regression test FIRST, watch it fail, then fix.
- New public function in
src/local_ai.py,src/privacy.py,src/hereditary_conditions.py,src/sex_inference.py,src/databases.py→ must ship with tests in the matchingtests/test_*.py.
- Sex-aware routing change in
- Tracked + safe:
sample/*(synthetic only; CI enforces "SYNTHETIC" header),src/snp_database.pyand othersrc/*.py(public reference data: rsid+gene+description, no genotypes), reports/templates with no rsid+genotype pairs. - Tracked + forbidden: any file outside
sample/matchingrs[0-9]+[,\t][0-9XYMT]+[,\t][0-9]+[,\t][AGCT]{1,2}. CI rejects. - Untracked (gitignored):
input/,output/,history/,data/*.tsv,*.log,.genetics_consent. Nevergit add -fthese. - Adding new fixtures: put in
sample/, prefix header# SYNTHETIC SAMPLE - NOT REAL GENETIC DATA, updatesample/README.md.