i18n: bilingual landing page (EN / IT) with auto-detect routing

Public landing page is now served in English and Italian via path-
prefixed URLs (/en/ and /it/), with the bare / detecting the
visitor's language and 302-ing to the right one.

Routing
-------
* GET / : if authed → dashboard (unchanged). Otherwise the visitor's
  language is resolved from (in priority order) the rtm.lang cookie,
  the Accept-Language header, the cf-ipcountry geolocation header,
  and finally DEFAULT_LANG, then a 302 redirects to /<lang>/.
* GET /en/ + /it/ : render the localised landing template, set the
  rtm.lang cookie (1-year, SameSite=Lax) so the next /-visit goes
  straight to the same translation. Logged-in users with user.lang
  set bypass detection for / (same priority chain — user.lang wins
  on every public surface they touch).

Storage
-------
* app/locales/<lang>.yaml — flat-ish nested copy files. YAML chosen
  so a future translator can edit without touching Python. Strings
  containing inline HTML (<strong>, <em>, <a>) are rendered with the
  Jinja `safe` filter in the template.
* app/services/locales.py — loads at startup, exposes get_locale()
  and detect_public_lang(). Wraps each YAML tree in a small _Dotted
  view so templates can write {{ t.hero.subhead }} (deliberately NOT
  a dict subclass — dict's built-in method names would shadow YAML
  keys like `items`).

Template + chrome
-----------------
* landing.html ported in full to {{ t.<key> }} references.
* public_base.html gets <html lang="…"> + hreflang link tags (en/it/
  x-default) when a route opts in via lang_switch=true. A tiny
  EN | IT link group lands in the public header, only visible on
  surfaces that opt in.
* public.css picks up the small lang-switch widget styles.

Scope (intentionally narrow)
----------------------------
* Only the landing page is localised. Pricing, terms, privacy,
  disclaimer, login, verify all stay English-only for now; their
  header chrome stays English too because localising labels there
  while the linked content is still EN would be a worse mismatch.
* When other public pages get translated, lang_switch=true on those
  routes will surface the same widget there with no template changes.

Tests
-----
* tests/test_locales.py covers YAML load parity (every active
  language has a file), dotted access through the tree, the
  detection precedence chain, and the unknown-locale fallback.

Deps
----
* pyyaml was already in requirements.lock as a transitive but not
  declared. Added to pyproject so it stays pinned as an explicit
  direct dependency.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-05-29 17:10:45 +02:00
parent d98a90f35e
commit ee8384f1ba
10 changed files with 721 additions and 93 deletions

128
app/locales/it.yaml Normal file
View file

@ -0,0 +1,128 @@
# Italian copy for the public landing page.
#
# Note: brand names ("Read the Markets") and proper nouns
# (tickers, currency codes) stay verbatim — they aren't translated.
hero:
tagline: "Capisci i mercati. Non scommetterci sopra."
subhead: >-
Pensato per investitori che vogliono <strong>ragionare con
lucidità</strong> e tagliare fuori il rumore ad alta frequenza
che nasce dal trattare i mercati come un casinò. Aggreghiamo
notizie cross-asset e segnali macro, e ne scriviamo una lettura
in prosa chiara: cosa giustificano i fondamentali sottostanti,
rispetto a cosa sta facendo la folla. Aggiornato durante la
giornata di trading. Un servizio editoriale, non finanziario.
cta_dashboard: "Apri dashboard"
cta_pricing: "Vedi i piani"
cta_signup: "Iscriviti gratis"
shot_dashboard:
alt: "Anteprima dashboard"
zoom_hint: "Clicca per ingrandire"
caption: >-
La dashboard. In alto una lettura cross-asset aggregata, sotto
gruppi di indicatori selezionati a mano. Il toggle del livello
di lettura (Novice / Pro) cambia il taglio di ogni pannello
generato dall'IA, da prosa accessibile a registro tecnico
sintetico.
features:
news:
tag: "Notizie, aggregate"
title: "Titoli dall'intero universo macro"
body: >-
Feed RSS e per-ticker che coprono azioni, tassi, credito, FX,
materie prime e geopolitica. Ogni titolo è etichettato
automaticamente per tema, così il rumore resta rumore e il
materiale rilevante per i fondamentali è facile da trovare.
L'ingestione segue il calendario di trading — fuori orario
tutto resta in pausa.
shot_alt: "Feed notizie con tag automatici"
shot_caption: >-
Il feed delle notizie. Ogni titolo porta uno o più tag tematici
(tassi, AI, energia, geopolitica, …) per tenere i fili che ti
interessano e silenziare gli altri. Click su un tag per
includerlo; shift-click per escluderlo.
indicators:
tag: "Segnali macro"
title: "Una tape cross-asset curata"
body: >-
Un set di indicatori selezionato a mano su ogni classe di
attivo, aggiornato ogni ora durante le sessioni di mercato.
Ogni gruppo ha una lettura breve che spiega cosa il movimento
<em>significa</em>, non cosa è stato. Ancorata a utili,
politica monetaria, valutazione — non a pattern grafici.
shot_alt: "Pannello indicatori con commento IA"
shot_caption: >-
Il pannello indicatori. Tab per classe di attivo (azionario,
tassi, materie prime, FX, obbligazioni, …); ogni tab porta una
'lettura' di un paragrafo scritta dal modello sopra i prezzi
live. I numeri ancorano la prosa, così il commento è
verificabile, non sospeso nel vuoto.
strategic:
tag: "Lettura strategica"
title: "Razionale vs irrazionale, paragrafo per paragrafo"
body: >-
Leghiamo i titoli della giornata e i segnali cross-asset in
un'unica interpretazione breve. Ogni paragrafo distingue i
<strong>driver razionali</strong> (utili, politica, valutazione)
dagli <strong>irrazionali</strong> (posizionamento, narrativa,
flussi) e dà un nome al divario. Due livelli di lettura: Novice
e Pro. È commento editoriale su dati pubblici — non una
previsione e non un consiglio su nessuna decisione di
investimento.
shot_alt: "Log strategico — la lettura editoriale IA"
shot_caption: >-
Il log strategico. Il modello scrive un'interpretazione
aggiornata durante la giornata, partendo dalla versione
precedente come contesto, in modo da aggiornare invece di
ricominciare. Gli utenti paganti ricevono un refresh ogni ora;
gli utenti gratuiti uno ogni sei.
multilang_callout: >-
Ogni superficie generata dall'IA — log strategico, letture degli
indicatori, analisi di portafoglio, chat, digest giornaliero — è
disponibile in <strong>Inglese</strong> e <strong>Italiano</strong>.
Usa il selettore di lingua nell'header e i pannelli si aggiornano
in tempo reale; simboli ticker, codici valuta e numeri restano
invariati tra le lingue.
more_views:
head: "Altre viste"
chat:
alt: "Fai domande di approfondimento su qualunque log passato"
caption: >-
Fai domande di approfondimento su qualunque log passato. Il
pannello chat eredita tutto il contesto del log, così puoi
tirare un filo senza re-incollare titoli o re-spiegare la
situazione.
caption_strong: "Chiedi qualsiasi cosa su un log"
caption_span: "Domande conversazionali con il contesto della giornata già caricato."
portfolio_blurb: >-
Gli utenti paganti possono anche caricare un CSV di portafoglio
dal loro broker per un sense-check IA su concentrazione,
coerenza con il regime di mercato ed esposizione valutaria. Le
posizioni restano nel browser per impostazione predefinita;
l'attivazione opzionale del cloud sync (crittografato)
permette di ritrovarle su un altro dispositivo.
not_strip:
head: "Cosa questo NON è."
items:
- "Non è consulenza finanziaria."
- "Non sono segnali di trading."
- "Non è uno strumento per day-trading."
- "Nessuna chiamata buy/sell, mai."
- "Nessuna previsione basata su pattern grafici."
- "Non è un servizio regolamentato."
footer:
legal: >-
Iscrivendoti accetti i nostri <a href="/terms">Termini</a> e
l'<a href="/privacy">Informativa privacy</a>, e confermi di aver
letto il <a href="/disclaimer">disclaimer finanziario</a>.
meta:
description: "Capisci i mercati. Non scommetterci sopra."