From 5730aad73cf3a0a3ff13447f45fdc25bc219b25a Mon Sep 17 00:00:00 2001 From: Giorgio Gilestro Date: Wed, 27 May 2026 16:46:32 +0200 Subject: [PATCH] i18n: add LANGUAGES, ACTIVE_LANGUAGES, respond_in_clause helper Co-Authored-By: Claude Opus 4.7 --- app/services/i18n.py | 48 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_i18n.py | 44 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 app/services/i18n.py create mode 100644 tests/test_i18n.py diff --git a/app/services/i18n.py b/app/services/i18n.py new file mode 100644 index 0000000..742373d --- /dev/null +++ b/app/services/i18n.py @@ -0,0 +1,48 @@ +"""Language registry + prompt helpers for localized AI output. + +Two surfaces consume this module: +- Per-user LLM call sites (portfolio analysis only at this stage) call + ``respond_in_clause(user.lang)`` and append the result to their + system prompt. +- The settings dropdown + its PATCH endpoint consult ``ACTIVE_LANGUAGES`` + to decide which options are selectable. The strategic-log and digest + translation fan-outs also consult it to decide which languages to + spend tokens on. + +Adding Spanish/French/German support later is a one-line constant +change: extend ``ACTIVE_LANGUAGES`` to include the new code. No other +code change is required — the rest of the system already treats them +as first-class via ``LANGUAGES``. +""" +from __future__ import annotations + + +# Display labels for every language the system knows about. ES/FR/DE +# are kept here so labels still render in the dropdown (as disabled +# options) without requiring code changes to enable them later. +LANGUAGES: dict[str, str] = { + "en": "English", + "it": "Italian", + "es": "Spanish", + "fr": "French", + "de": "German", +} + + +# Languages users can actually select. Settings POST validates against +# this; the strategic-log + digest translation fan-outs only consider +# these. +ACTIVE_LANGUAGES: set[str] = {"en", "it"} + + +def respond_in_clause(lang: str | None) -> str: + """Suffix appended to per-user LLM system prompts. + + Returns an empty string for ``en`` (no nudge needed), an unknown + code, or ``None``/empty input — those callers want the default + English path. Otherwise returns ``"\\n\\nRespond in ."`` + keyed off ``LANGUAGES``. + """ + if not lang or lang == "en" or lang not in LANGUAGES: + return "" + return f"\n\nRespond in {LANGUAGES[lang]}." diff --git a/tests/test_i18n.py b/tests/test_i18n.py new file mode 100644 index 0000000..f0edce0 --- /dev/null +++ b/tests/test_i18n.py @@ -0,0 +1,44 @@ +"""Unit tests for app.services.i18n.""" +from __future__ import annotations + +import pytest + + +def test_languages_contains_all_four_plus_english(): + from app.services.i18n import LANGUAGES + assert set(LANGUAGES.keys()) == {"en", "it", "es", "fr", "de"} + assert LANGUAGES["en"] == "English" + assert LANGUAGES["it"] == "Italian" + assert LANGUAGES["es"] == "Spanish" + assert LANGUAGES["fr"] == "French" + assert LANGUAGES["de"] == "German" + + +def test_active_languages_is_en_and_it_only(): + from app.services.i18n import ACTIVE_LANGUAGES + assert ACTIVE_LANGUAGES == {"en", "it"} + + +def test_respond_in_clause_empty_for_english(): + from app.services.i18n import respond_in_clause + assert respond_in_clause("en") == "" + + +def test_respond_in_clause_empty_for_none_or_empty(): + from app.services.i18n import respond_in_clause + assert respond_in_clause("") == "" + assert respond_in_clause(None) == "" + + +def test_respond_in_clause_italian(): + from app.services.i18n import respond_in_clause + result = respond_in_clause("it") + assert "Italian" in result + assert result.startswith("\n\n") + + +def test_respond_in_clause_unknown_lang_falls_back_to_english(): + """Defensive: a raw POST or stale lang code should not crash the + prompt assembly. Unknown codes map to no-suffix (English default).""" + from app.services.i18n import respond_in_clause + assert respond_in_clause("xx") == ""