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>
52 lines
1.1 KiB
TOML
52 lines
1.1 KiB
TOML
[project]
|
|
name = "cassandra"
|
|
version = "0.1.0"
|
|
description = "Containerised macro-strategy dashboard — market data, news, portfolios, AI daily log."
|
|
requires-python = ">=3.13"
|
|
dependencies = [
|
|
"fastapi>=0.115",
|
|
"uvicorn[standard]>=0.32",
|
|
"jinja2>=3.1",
|
|
"python-multipart>=0.0.12",
|
|
"sqlalchemy[asyncio]>=2.0.36",
|
|
"aiomysql>=0.2.0",
|
|
"alembic>=1.14",
|
|
"pydantic>=2.9",
|
|
"pydantic-settings>=2.6",
|
|
"httpx>=0.28",
|
|
"apscheduler>=3.10",
|
|
"tenacity>=9.0",
|
|
"structlog>=24.4",
|
|
"argon2-cffi>=23.1",
|
|
"cryptography>=43.0",
|
|
"itsdangerous>=2.2",
|
|
"email-validator>=2.2",
|
|
"aiosmtplib>=3.0",
|
|
"redis[hiredis]>=5.2",
|
|
"stripe>=11.0",
|
|
"pyyaml>=6.0",
|
|
]
|
|
|
|
[project.optional-dependencies]
|
|
dev = [
|
|
"pytest>=8.3",
|
|
"pytest-asyncio>=0.24",
|
|
"pytest-httpx>=0.34",
|
|
"aiosqlite>=0.20",
|
|
"ruff>=0.7",
|
|
]
|
|
|
|
[tool.pytest.ini_options]
|
|
asyncio_mode = "auto"
|
|
testpaths = ["tests"]
|
|
|
|
[tool.ruff]
|
|
line-length = 100
|
|
target-version = "py313"
|
|
|
|
[build-system]
|
|
requires = ["setuptools>=68"]
|
|
build-backend = "setuptools.build_meta"
|
|
|
|
[tool.setuptools]
|
|
packages = ["app", "app.services", "app.jobs", "app.routers"]
|