log: serve translated content when available; English fallback
Adds module-level _resolve_log_content(session, log_id, lang) helper to app/routers/pages.py: looks up StrategicLogTranslation by (log_id, lang) when lang != 'en'; falls back silently to the English original when no translation row exists yet (the expected case for the first hour after a new language activates, or when translation fails for a specific log). log_page / log_page_day pull cu.user.lang and thread it through _log_page_context so the template renders the right variant. Two tests cover both branches. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
924f37548b
commit
1ea71bc160
2 changed files with 92 additions and 4 deletions
|
|
@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from app.auth import CurrentUser, maybe_current_user, require_auth, require_token
|
from app.auth import CurrentUser, maybe_current_user, require_auth, require_token
|
||||||
from app.config import get_settings, load_groups
|
from app.config import get_settings, load_groups
|
||||||
from app.db import get_session
|
from app.db import get_session
|
||||||
from app.models import EmailSend, Referral, StrategicLog, User
|
from app.models import EmailSend, Referral, StrategicLog, StrategicLogTranslation, User
|
||||||
from app.services.access import is_paid_active, paid_status
|
from app.services.access import is_paid_active, paid_status
|
||||||
from app.services.referral_service import assign_code_if_missing
|
from app.services.referral_service import assign_code_if_missing
|
||||||
from app.templates_env import templates
|
from app.templates_env import templates
|
||||||
|
|
@ -75,7 +75,29 @@ async def _resolve_log_date(session: AsyncSession, day: str | None) -> date:
|
||||||
return datetime.now(timezone.utc).date()
|
return datetime.now(timezone.utc).date()
|
||||||
|
|
||||||
|
|
||||||
def _log_page_context(target: date, paid: bool) -> dict:
|
async def _resolve_log_content(
|
||||||
|
session: AsyncSession, log_id: int, lang: str | None,
|
||||||
|
) -> str:
|
||||||
|
"""Return the strategic log content in the user's preferred language.
|
||||||
|
|
||||||
|
If ``lang`` is 'en'/None or no translation exists for the requested
|
||||||
|
language, returns the English original from StrategicLog.content.
|
||||||
|
A missing translation is the expected case for hours where
|
||||||
|
translation hasn't yet run; the fallback is silent.
|
||||||
|
"""
|
||||||
|
if lang and lang != "en":
|
||||||
|
row = (await session.execute(
|
||||||
|
select(StrategicLogTranslation)
|
||||||
|
.where(StrategicLogTranslation.log_id == log_id)
|
||||||
|
.where(StrategicLogTranslation.lang == lang)
|
||||||
|
)).scalar_one_or_none()
|
||||||
|
if row is not None:
|
||||||
|
return row.content_md
|
||||||
|
log_row = await session.get(StrategicLog, log_id)
|
||||||
|
return log_row.content if log_row is not None else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _log_page_context(target: date, paid: bool, user_lang: str = "en") -> dict:
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
return {
|
return {
|
||||||
"selected_iso": target.isoformat(),
|
"selected_iso": target.isoformat(),
|
||||||
|
|
@ -83,6 +105,7 @@ def _log_page_context(target: date, paid: bool) -> dict:
|
||||||
"current_tone": s.CASSANDRA_TONE.upper(),
|
"current_tone": s.CASSANDRA_TONE.upper(),
|
||||||
"current_analysis": s.CASSANDRA_ANALYSIS.upper(),
|
"current_analysis": s.CASSANDRA_ANALYSIS.upper(),
|
||||||
"paid": paid,
|
"paid": paid,
|
||||||
|
"user_lang": user_lang,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -93,8 +116,9 @@ async def log_page(
|
||||||
cu: CurrentUser = Depends(require_auth),
|
cu: CurrentUser = Depends(require_auth),
|
||||||
):
|
):
|
||||||
target = await _resolve_log_date(session, None)
|
target = await _resolve_log_date(session, None)
|
||||||
|
user_lang = cu.user.lang if cu.user else "en"
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request, "log.html", _log_page_context(target, is_paid_active(cu)),
|
request, "log.html", _log_page_context(target, is_paid_active(cu), user_lang),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -106,8 +130,9 @@ async def log_page_day(
|
||||||
cu: CurrentUser = Depends(require_auth),
|
cu: CurrentUser = Depends(require_auth),
|
||||||
):
|
):
|
||||||
target = await _resolve_log_date(session, day)
|
target = await _resolve_log_date(session, day)
|
||||||
|
user_lang = cu.user.lang if cu.user else "en"
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request, "log.html", _log_page_context(target, is_paid_active(cu)),
|
request, "log.html", _log_page_context(target, is_paid_active(cu), user_lang),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -332,3 +332,66 @@ def test_digest_pick_variant_uses_user_lang():
|
||||||
assert ed._pick_variant(table, tone="NOVICE", lang="de") == "novice en"
|
assert ed._pick_variant(table, tone="NOVICE", lang="de") == "novice en"
|
||||||
# Missing tone → fallback to INTERMEDIATE/en (the safe default).
|
# Missing tone → fallback to INTERMEDIATE/en (the safe default).
|
||||||
assert ed._pick_variant(table, tone="UNKNOWN", lang="en") == "intermediate en"
|
assert ed._pick_variant(table, tone="UNKNOWN", lang="en") == "intermediate en"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_log_endpoint_serves_italian_when_user_is_italian(tmp_path):
|
||||||
|
"""When a user with lang='it' opens /log, the served content is the
|
||||||
|
Italian translation, not the English original."""
|
||||||
|
from app.db import utcnow
|
||||||
|
from app.models import StrategicLog, StrategicLogTranslation, User
|
||||||
|
|
||||||
|
_, factory, setup = _build_session_factory(tmp_path)
|
||||||
|
await setup()
|
||||||
|
|
||||||
|
async with factory() as session:
|
||||||
|
session.add(User(id=10, email="it@x", tier="paid", lang="it"))
|
||||||
|
slog = StrategicLog(
|
||||||
|
generated_at=utcnow(), content="# Open\n\nDown 0.4%.",
|
||||||
|
model="test-model",
|
||||||
|
tone="INTERMEDIATE", analysis="NORMAL",
|
||||||
|
)
|
||||||
|
session.add(slog)
|
||||||
|
await session.commit()
|
||||||
|
session.add(StrategicLogTranslation(
|
||||||
|
log_id=slog.id, lang="it",
|
||||||
|
content_md="# Apertura\n\nIn calo 0,4%.",
|
||||||
|
generated_at=utcnow(), llm_model="m", llm_cost_usd=0.0,
|
||||||
|
))
|
||||||
|
await session.commit()
|
||||||
|
log_id = slog.id
|
||||||
|
|
||||||
|
# Test the resolver directly.
|
||||||
|
from app.routers.pages import _resolve_log_content
|
||||||
|
async with factory() as session:
|
||||||
|
user = await session.get(User, 10)
|
||||||
|
content = await _resolve_log_content(session, log_id, user.lang)
|
||||||
|
assert "Apertura" in content
|
||||||
|
assert "Open" not in content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_log_endpoint_falls_back_to_english_when_no_translation(tmp_path):
|
||||||
|
"""User lang='it' but no IT translation exists → English fallback."""
|
||||||
|
from app.db import utcnow
|
||||||
|
from app.models import StrategicLog, User
|
||||||
|
|
||||||
|
_, factory, setup = _build_session_factory(tmp_path)
|
||||||
|
await setup()
|
||||||
|
|
||||||
|
async with factory() as session:
|
||||||
|
session.add(User(id=11, email="it2@x", tier="paid", lang="it"))
|
||||||
|
slog = StrategicLog(
|
||||||
|
generated_at=utcnow(), content="# Open\n\nDown 0.4%.",
|
||||||
|
model="test-model",
|
||||||
|
tone="INTERMEDIATE", analysis="NORMAL",
|
||||||
|
)
|
||||||
|
session.add(slog)
|
||||||
|
await session.commit()
|
||||||
|
log_id = slog.id
|
||||||
|
|
||||||
|
from app.routers.pages import _resolve_log_content
|
||||||
|
async with factory() as session:
|
||||||
|
user = await session.get(User, 11)
|
||||||
|
content = await _resolve_log_content(session, log_id, user.lang)
|
||||||
|
assert "Open" in content
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue