"""Тексты системных уведомлений (ТЗ 3.21).
Строки собирает сервер, а не service worker: воркер живёт между сессиями и не
знает ни языка пользователя, ни его настроек — у него нет ни бандла локалей, ни
запроса, из которого язык можно взять. Поэтому ru/en/uk живут здесь и осознанно
дублируют фронтовые `gamify.claimed`/`gamify.review`: тост и системное
уведомление говорят об одном и том же разными словами.
Язык разрешается той же цепочкой, что и на фронте (`i18n/locale.ts`): ручное
переопределение в настройках → локаль SSO → «ru».
"""
from sqlalchemy.orm import Session
from app.models import AppSetting, User
# Тот же ключ, что `LANGUAGE_KEY` в api/settings.py: импортировать оттуда значило бы
# развернуть зависимость «сервис → API» ради одного строкового литерала
LANGUAGE_KEY = "language"
DEFAULT_LANGUAGE = "ru"
# Языки, для которых есть строки; всё остальное — «ru»
SUPPORTED = ("ru", "en", "uk")
AGENT_FALLBACK: dict[str, str] = {
"ru": "ИИ-агент",
"en": "the AI agent",
"uk": "ШІ-агент",
}
# kind → язык → (заголовок, тело). Плейсхолдеры подставляются через str.format
TEXTS: dict[str, dict[str, tuple[str, str]]] = {
"task.claimed": {
"ru": (
"Агент взял задачу в работу",
"«{title}» — {name} взялась за дело, не трогайте её, пока работает",
),
"en": (
"An agent took the task",
"“{title}” — {name} got to work, leave it alone while she works",
),
"uk": (
"Агент узяв завдання в роботу",
"«{title}» — {name} взялася до справи, не чіпайте його, поки працює",
),
},
"task.review": {
"ru": (
"Работа ждёт приёмки",
"«{title}» — посмотрите, что сделано, и примите или верните",
),
"en": (
"Work awaits review",
"“{title}” — check what was done, then accept or return it",
),
"uk": (
"Робота чекає на приймання",
"«{title}» — подивіться, що зроблено, і прийміть або поверніть",
),
},
}
# Тексты уведомления живут секунды на экране блокировки — длинное тело в нём не
# читают, а усекать его по дороге обиднее, чем обрезать здесь
MAX_TITLE = 60
MAX_BODY = 140
def user_language(db: Session, user_id: str) -> str:
"""Язык уведомлений пользователя: настройка → локаль SSO → «ru».
Настройка читается напрямую, а не через `api/settings.py`: импортировать роутер
в сервис значило бы завести обратную зависимость, а правило — три строки.
"""
row = db.get(AppSetting, (user_id, LANGUAGE_KEY))
if row is not None and row.value in SUPPORTED:
return row.value
user = db.get(User, user_id)
locale = (user.locale or "") if user is not None else ""
# SSO отдаёт локали вида «ru-RU»: берём язык, отбрасывая регион
language = locale.replace("_", "-").split("-")[0].lower()
return language if language in SUPPORTED else DEFAULT_LANGUAGE
def _clip(text: str, limit: int) -> str:
if len(text) <= limit:
return text
return text[: limit - 1].rstrip() + "…"
def render(kind: str, language: str, **fields: str) -> tuple[str, str] | None:
"""Собрать (заголовок, тело) для события; неизвестный kind → None.
Заголовок задачи усекаем: он приходит от пользователя (до 500 символов), а
системное уведомление длиннее пары строк всё равно не показывает.
"""
variants = TEXTS.get(kind)
if variants is None:
return None
title, body = variants.get(language, variants[DEFAULT_LANGUAGE])
fields.setdefault("name", AGENT_FALLBACK.get(language, AGENT_FALLBACK[DEFAULT_LANGUAGE]))
if "title" in fields:
fields["title"] = _clip(fields["title"], MAX_TITLE)
return title, _clip(body.format(**fields), MAX_BODY)