SilverElixir commited on
Commit
13103aa
·
1 Parent(s): 77e0745

Unify onboarding examples and fill missing translations across all 25 languages; add a Russian README

Browse files
Files changed (4) hide show
  1. README.md +2 -0
  2. README.ru.md +140 -0
  3. lumen_lang.py +103 -29
  4. tests/test_bot_commands.py +15 -3
README.md CHANGED
@@ -21,6 +21,8 @@ A Telegram bot styled after Claude's tone and personality (direct, warm, light o
21
 
22
  **Stack:** Python 3.13 · aiogram · FastAPI · google-genai · Docker
23
 
 
 
24
  ## Contents
25
 
26
  - [Features](#features)
 
21
 
22
  **Stack:** Python 3.13 · aiogram · FastAPI · google-genai · Docker
23
 
24
+ **Русская версия: [README.ru.md](README.ru.md).**
25
+
26
  ## Contents
27
 
28
  - [Features](#features)
README.ru.md ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lumen
2
+
3
+ Русская версия документации. Английская версия: [README.md](README.md).
4
+
5
+ Telegram-бот в стиле Claude (прямой, тёплый, без лишних оговорок). Работает на Google Gemini, бесплатных моделях OpenRouter и Groq с автоматической маршрутизацией между ними. Умеет генерировать картинки, скачивать видео из TikTok (обычно без водяных знаков, с водяным знаком — в крайнем случае) и озвучивать текст. Запускается как вебхук-сервис на Hugging Face Spaces (Docker).
6
+
7
+ **Стек:** Python 3.13 · aiogram · FastAPI · google-genai · Docker
8
+
9
+ ## Содержание
10
+
11
+ - [Возможности](#возможности)
12
+ - [Как это работает](#как-это-работает)
13
+ - [Деплой на Hugging Face Spaces](#деплой-на-hugging-face-spaces)
14
+ - [Настройка](#настройка)
15
+ - [Прокси](#прокси)
16
+ - [Диагностика](#диагностика)
17
+ - [Команды](#команды)
18
+ - [Тесты](#тесты)
19
+ - [Известные ограничения](#известные-ограничения)
20
+ - [Безопасность](#безопасность)
21
+ - [Лицензия](#лицензия)
22
+
23
+ ## Возможности
24
+
25
+ - **Автоматическая маршрутизация моделей.** Каждое сообщение уходит туда, где есть нужные возможности: обычный текст начинается с Gemma (самая большая бесплатная квота), затем Groq и OpenRouter; поиск в интернете и чтение ссылок — через Gemini. Каждый свежий запрос дополнительно получает внешние сниппеты (DuckDuckGo и Google News с датами публикации) для любой ответившей модели, включая резервные не-Gemini.
26
+ - **Стриминг ответов** с самокалибрующимся темпом печати: ответ появляется постепенно, а не крупными кусками.
27
+ - **Живой статус в стиле Claude.** В личных чатах поисковые запросы и запросы с медиа показывают таймлайн с искрой («Ищу в интернете», найденные источники, таймер ожидания) и завершаются свёрнутым блоком «Думал N с»; простые ответы получают одну курсивную строку. Ответы в личке отправляются обычными сообщениями, без Telegram-цитаты.
28
+ - **Защита от промпт-инъекций и утечки личности.** Детерминированный входной фильтр и выходные фильтры не дают боту раскрыть, какая модель или провайдер ответили.
29
+ - **Скачивание TikTok** (обычно без водяных знаков): видео, слайдшоу (включая «живые» фото-слайды) и оригинальный звук.
30
+ - **Генерация картинок** через Pollinations.ai. Модель подбирается по тексту запроса (аниме, фэнтези, реализм, быстрый набросок или общий дефолт).
31
+ - **Озвучка текста** через Gemini TTS.
32
+ - `/draw` и `/tts` работают и как обычные фразы в начале сообщения («нарисуй кота», «озвучь это»), без слэша.
33
+ - **Кнопки уточнения** для коротких вкусовых запросов без деталей («посоветуй фильм» → кнопки жанров вместо догадки).
34
+ - **Язык бота** (`/lang`): системные сообщения на 25 языках (по умолчанию английский). Ответы ИИ всегда следуют языку пользователя.
35
+ - **Персистентное состояние.** Необязательный Upstash Redis сохраняет историю чатов и счётчики квот между передеплоями.
36
+ - **Трекинг ошибок** через необязательный Sentry: секреты и локальные переменные вычищаются до отправки.
37
+
38
+ ## Как это работает
39
+
40
+ Lumen — единый сервис FastAPI + aiogram. Telegram присылает обновления на вебхук; повторные доставки с тем же `update_id` игнорируются. Каждое сообщение проходит че��ез цепочку моделей-кандидатов, собранную на лету по содержимому: вложения, ссылки и две лёгкие эвристики — «нужны ли свежие данные» и «тяжёлый ли запрос». Обычный текст начинается с Gemma, затем Groq и OpenRouter; поиск и чтение ссылок идут через Gemini; каждый свежий запрос получает внешние сниппеты DuckDuckGo/Google News для любого провайдера. Побеждает первый ответивший провайдер; если вся его цепочка упала, пробуется следующий. Модели, уже вернувшие 429 сегодня, роутер пропускает.
41
+
42
+ Код — модульный монолит. `bot.py` — точка входа и композиционный корень; остальное разбито по модулям: `lumen_admin.py` (FastAPI, секреты, admin/diag/export), `lumen_message_core.py` (ядро обработки сообщений), `lumen_routes.py` (вызовы LLM с фолбэком, `_run_route`), `lumen_streaming.py` (провайдер-агностичный стриминг), `lumen_status.py` (живой статус в стиле Claude), `lumen_search.py` (внешние сниппеты DuckDuckGo/Bing/Google News), `lumen_chat_state.py` (состояние чатов, квоты, блокировки), `lumen_commands.py` (команды, TTS/draw, кнопки выбора), `lumen_tiktok_flow.py` (отправка TikTok), `lumen_rich.py` (отправка: гости, Rich Messages, чанкинг), `lumen_media_flow.py` (скачивание вложений Telegram), `lumen_transport_calls.py` (сессии, ротация прокси, вызовы Telegram), `lumen_errors.py` (классификация ошибок моделей), `lumen_limits.py` (окно рейт-лимита, очередь кнопок), `lumen_router_config.py` (маршрутизация моделей), `lumen_formatting.py` (markdown → Telegram HTML и Rich Messages для таблиц/заголовков/математики), `lumen_security.py` (защита от инъекций и утечек), `lumen_message_parse.py` (ссылки, триггеры draw/tts, ссылки на медиа), `lumen_media.py` (mime-типы, разбор file_id), `lumen_images.py`, `lumen_tts.py`, `lumen_tiktok.py`, `lumen_telegram_transport.py`, `lumen_state_storage.py`, `lumen_typing_pace.py`, `lumen_model_speed.py` и `lumen_lang.py` (язык бота: системные сообщения на 25 языках). `lumen_devtrace.py` пишет опциональный JSONL-трейс (запросы, ответы, пассивный фон) для отладки промптов.
43
+
44
+ Подробнее о маршрутизации, стриминге, слоях защиты и загрузчике TikTok — в **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)**.
45
+
46
+ ## Деплой на Hugging Face Spaces
47
+
48
+ 1. Запушь репозиторий в Space (`sdk: docker` уже указан во фронтматтере выше).
49
+ 2. Задай обязательные секреты: `BOT_TOKEN`, `GEMINI_API_KEY` и `ADMIN_SECRET_SEED`. В проде бот не стартует без сида: админ-ключи не должны выводиться из токена бота.
50
+ 3. Hugging Face соберёт образ из `Dockerfile` и запустит `python -u bot.py`.
51
+ 4. При старте бот сам регистрирует вебхук. Если не вышло, в логах будет `[webhook] setWebhook failed`; зарегистрируй вручную:
52
+ ```bash
53
+ curl -H "Authorization: Bearer <ADMIN_PANEL_KEY>" https://<space-host>/webhook_url
54
+ ```
55
+ затем заполни `register_url_template`, который он вернёт (токен подставляй вручную, через curl — не открывай URL с токеном в браузере).
56
+ 5. `WEBHOOK_SECRET` и `ADMIN_PANEL_KEY` детерминированно выводятся из `ADMIN_SECRET_SEED`. Получить их:
57
+ ```bash
58
+ curl -H "Authorization: Bearer <BOT_TOKEN>" https://<space-host>/admin_keys
59
+ ```
60
+
61
+ Исходящие IP Hugging Face Spaces блокируются Telegram API на сетевом уровне, поэтому нужен небольшой прокси. См. [Прокси](#прокси).
62
+
63
+ ## Настройка
64
+
65
+ Локально обязательны только `BOT_TOKEN` и `GEMINI_API_KEY`; на HF Space обязателен ещё `ADMIN_SECRET_SEED`. У остального есть разумные значения по умолчанию; полный справочник (адреса прокси, таймауты, хранилище, рейт-лимиты, настройки TikTok, конкурентность медиа) — в **[docs/ENVIRONMENT.md](docs/ENVIRONMENT.md)**.
66
+
67
+ ## Прокси
68
+
69
+ Исходящие IP Hugging Face Spaces полностью блокируются Bot API Telegram, а TikWM (API TikTok, который использует бот) отвечает `403` на те же IP. Оба ограничения решает один небольшой прокси на Deno Deploy (исходник: `proxy/proxy.ts`):
70
+
71
+ ```bash
72
+ TELEGRAM_API_BASE_URL=https://<proxy-domain>/fetch/api.telegram.org
73
+ TIKWM_API_BASE_URL=https://<proxy-domain>/fetch/www.tikwm.com
74
+ LUMEN_PROXY_SECRET=<случайная строка от 32 символов: без неё прокси отвечает 503, а бот падает на старте>
75
+ ```
76
+
77
+ Прокси пропускает запросы только к явному списку хостов (`api.telegram.org`, `www.tikwm.com`, `tikwm.com`). Реализация — в `proxy/proxy.ts`, тесты — в `proxy/proxy_test.ts`.
78
+
79
+ ## Диагностика
80
+
81
+ Все эндпоинты ниже требуют `Authorization: Bearer <ADMIN_PANEL_KEY>` (`/admin_keys` из шагов деплоя — исключение, ему нужен `BOT_TOKEN`):
82
+
83
+ | Эндпоинт | Назначение |
84
+ |---|---|
85
+ | `GET /diag` | Проверка исходящей сети (Telegram, Gemini, OpenRouter, TikWM, Pollinations и др.) из контейнера. |
86
+ | `GET /webhook_url` | Вычисленный URL вебхука и готовая ссылка для регистрации. |
87
+ | `GET /export_state` | Выгрузка всех историй чатов и счётчиков квот в JSON для ручного бэкапа. |
88
+ | `GET /devtrace?n=200` | Хвост dev-трейса (запросы, ответы, пассивный фон) в JSON. Нужен `DEV_TRACE=1`. |
89
+
90
+ `/logs` (команда Telegram, только владелец) присылает текущий `bot.log` с вычищенными секретами.
91
+
92
+ ## Команды
93
+
94
+ `/start`, `/reset`, `/draw`, `/tts` и `/lang` показываются в меню команд Telegram:
95
+
96
+ | Команда | Доступ | Назначение |
97
+ |---|---|---|
98
+ | `/start` | всем | Краткое знакомство и список команд. |
99
+ | `/reset` | всем в личке; админам группы/владельцу в группах | Очищает историю диалога чата. |
100
+ | `/draw [описание]` | всем | Генерирует картинку. |
101
+ | `/tts [текст]` | всем | Озвучивает текст. |
102
+ | `/lang` | всем в личке; админам группы/владельцу в группах | Меню языка системных сообщений (ответы ИИ всегда следуют языку пользователя). |
103
+
104
+ Команды только для владельца намеренно не показываются в меню, чтобы не светить внутренние детали в группах:
105
+
106
+ | Команда | Доступ | Назначение |
107
+ |---|---|---|
108
+ | `/stats` | владелец, только личка | Число активных чатов, uptime, расход квот по моделям. |
109
+ | `/logs` | владелец, только личка | Присылает `bot.log` с вычищенными секретами. |
110
+ | `/selftest [gemini]` | владелец, только личка, не чаще раза в минуту | Живая проба голов маршрута (Groq, лёгкий OpenRouter, прокси Telegram, TikWM; Gemini — только с аргументом `gemini`). Успешные пробы тратят квоту. |
111
+ | `/ban <ID>` / `/unban <ID>` (или ответом на сообщение) | владелец, только личка | Блокировка/разблокировка пользователя по Telegram ID. Забаненные полностью игнорируются: без ответов, без фонового контекста, без счётчиков. Переживает рестарты. |
112
+ | `/banlist` | владелец, только личка | Показывает ID забаненных. |
113
+
114
+ В группах бот хранит последние сообщения (до 100, с общим лимитом символов) как контекст и отправляет их провайдерам моделей вместе с вопросом; `/reset` (админы группы/владелец) очищает их.
115
+
116
+ ## Тесты
117
+
118
+ ```bash
119
+ pip install -r requirements.txt -r requirements-dev.txt
120
+ pytest -v
121
+ ```
122
+
123
+ Тесты лежат в `tests/` и повторяют разбиение на модули: `test_lumen_formatting.py`, `test_lumen_security.py`, `test_lumen_router_config.py`, `test_lumen_typing_pace.py`, `test_lumen_images.py`, `test_lumen_media.py`, `test_lumen_model_speed.py`, `test_lumen_search.py`, `test_lumen_status.py` и `test_lumen_devtrace.py` тестируют с��ответствующие модули напрямую, а `test_bot_admin.py`, `test_bot_commands.py`, `test_bot_routes.py`, `test_bot_search.py`, `test_bot_state.py`, `test_bot_streaming.py`, `test_bot_tiktok.py`, `test_bot_transport.py` и `test_bot_tts.py` покрывают `bot.py` по доменам (общие фейки — в `bot_test_helpers.py`). `test_eval_persona.py` покрывает фикстуры оценки персоны. `conftest.py` подставляет заглушки `BOT_TOKEN`/`GEMINI_API_KEY`/`BOT_LOG_PATH`, поэтому реальные секреты для тестов не нужны.
124
+
125
+ CI (`.github/workflows/ci.yml`) на каждый push и pull request, а также по недельному расписанию запускает `pyflakes` + `pytest` + `pip-audit` + `deno test`; успешный push запускает `sync-to-hf.yml`, который переносит коммит в Hugging Face Space (и отказывается перезаписывать прямой хотфикс на Space).
126
+
127
+ ## Известные ограничения
128
+
129
+ - История чатов и счётчики квот переживают передеплой только при настроенном Upstash (см. [docs/ENVIRONMENT.md](docs/ENVIRONMENT.md)); иначе они лежат на эфемерном диске контейнера и сбрасываются при каждой пересборке.
130
+ - Скачивание YouTube не поддерживается: исходящие IP Hugging Face Spaces блокируются на уровне TLS-рукопожатия. Просмотр и анализ видео по ссылке при этом работают.
131
+ - Скачивание TikTok зависит от неофициального API TikWM, а не от официального эндпоинта TikTok, и без прокси из Hugging Face Spaces вообще не работает.
132
+ - Автотесты стриминга используют моки; живой SSE вручную прогонялся через смоук-бота, но контролируемого интеграционного теста нет. Перед правкой стриминга прочитай ручной чек-лист в [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
133
+
134
+ ## Безопасность
135
+
136
+ О том, как сообщить об уязвимости, — в [SECURITY.md](SECURITY.md). Секреты вычищаются из логов и событий Sentry до отправки из контейнера; в публичных логах Space длинные числовые id чатов и пользователей хешируются. Вебхук проверяет секретный заголовок Telegram, ограничивает размер тела и игнорирует повторные доставки `update_id`. `ADMIN_SECRET_SEED` обязателен в проде, поэтому админ-ключи никогда не выводятся из токена бота.
137
+
138
+ ## Лицензия
139
+
140
+ MIT — см. [LICENSE](LICENSE).
lumen_lang.py CHANGED
@@ -150,8 +150,7 @@ STRINGS: dict[str, dict[str, str]] = {
150
  "You can also draw and voice just with words, no commands — e.g. “draw a cat” or “read this out loud”.\n\n"
151
  "<b>TikTok</b>\n"
152
  "Send a link — I'll download video or photos without watermarks.\n\n"
153
- "Ask anything — I'm listening.\n\n"
154
- "Try right now: “who are you?”, send a photo with “what's in it?”, or “recommend a movie”."
155
  ),
156
  "es": (
157
  "<b>Lumen</b>\n\n"
@@ -190,8 +189,7 @@ STRINGS: dict[str, dict[str, str]] = {
190
  "Рисовать и озвучивать можно и просто словами, без команд — например «нарисуй кота» или «озвучь это».\n\n"
191
  "<b>TikTok</b>\n"
192
  "Пришли ссылку — скачаю видео или фото без водяных знаков.\n\n"
193
- "Спрашивай что угодно — я слушаю.\n\n"
194
- "Попробуй прямо сейчас: «кто ты?», пришли фото с вопросом «что на нём?», кинь ссылку на TikTok."
195
  ),
196
  "uk": (
197
  "<b>Lumen</b>\n\n"
@@ -423,6 +421,8 @@ STRINGS: dict[str, dict[str, str]] = {
423
  "fallback_no_live_search": {
424
  "be": "Адказваю без жывога пошуку (пошукавая квота вычарпана, знешні пошук нічога не даў) — даныя могуць быць не самымі свежымі.",
425
  "en": "Answering without live web search (search quota exhausted and fallback search returned nothing) — the details may not be fully up to date.",
 
 
426
  "ru": "Отвечаю без живого поиска (поисковая квота исчерпана, внешний поиск ничего не дал) — данные могут быть не самыми свежими.",
427
  "uk": "Відповідаю без живого пошуку (пошукова квота вичерпана, зовнішній пошук нічого не дав) — дані можуть бути не найсвіжішими.",
428
  },
@@ -909,12 +909,12 @@ STRINGS: dict[str, dict[str, str]] = {
909
  },
910
  # ── Владелец ──
911
  "owner_quota_notice": {
912
- "be": "⚠️ Квота Gemini вычарпана цалкам па ўсіх мадэлях у маршруце (гл. /stats для дэталяў).",
913
- "en": "⚠️ Gemini quota fully exhausted across all route models (see /stats for details).",
914
- "es": "⚠️ Cuota de Gemini agotada por completo en todos los modelos de la ruta (ver /stats para detalles).",
915
- "kk": "⚠️ Маршруттағы барлық модель бойынша Gemini квотасы толық таусылды (егжей-тегжей /stats).",
916
- "ru": "⚠️ Квота Gemini исчерпана целиком по всем моделям в маршруте (см. /stats для деталей).",
917
- "uk": "⚠️ Квоту Gemini вичерпано цілком за всіма моделями в маршруті (див. /stats для деталей).",
918
  },
919
  }
920
 
@@ -1022,7 +1022,9 @@ PACK_PT: dict[str, str] = {
1022
  "pick_expired": "Botões expirados — escreva em texto.",
1023
  "pick_not_yours": "Estes não são seus botões.",
1024
  "pick_choice": "Escolha: {choice}",
1025
- "owner_quota_notice": "⚠️ Cota do Gemini totalmente esgotada em todos os modelos da rota (ver /stats para detalhes).",
 
 
1026
  "user_daily_total": "Limite diário de mensagens esgotado ({used}/{limit}). Novas estarão disponíveis em {hours} h {mins} min.",
1027
  "user_daily_gemini": "Limite diário de solicitações complexas esgotado ({used}/{limit}). Poderei abrir links, vídeos e YouTube amanhã. Mas faça sua pergunta normal — responderei.",
1028
  "user_daily_tts": "Limite diário de voz esgotado ({used}/{limit}). Poderei dar voz de novo amanhã.",
@@ -1131,7 +1133,9 @@ PACK_AR: dict[str, str] = {
1131
  "pick_expired": "انتهت صلاحية الأزرار — اكتبها نصًا.",
1132
  "pick_not_yours": "هذه ليست أزرارك.",
1133
  "pick_choice": "الاختيار: {choice}",
1134
- "owner_quota_notice": "⚠️ حصة Gemini مستنفدة بالكامل عبر كل نماذج المسار (انظر /stats للتفاصيل).",
 
 
1135
  "user_daily_total": "تم استنفاد الحد اليومي للرسائل ({used}/{limit}). ستتوفر رسائل جديدة بعد {hours} س {mins} د.",
1136
  "user_daily_gemini": "تم استنفاد الحد اليومي للطلبات المعقدة ({used}/{limit}). سأتمكن من فتح الروابط والفيديو وYouTube غدًا. أما سؤالك العادي فاطرحه — وسأجيب.",
1137
  "user_daily_tts": "تم استنفاد الحد اليومي للتحويل إلى صوت ({used}/{limit}). سأتمكن من التحويل غدًا.",
@@ -1240,7 +1244,9 @@ PACK_TR: dict[str, str] = {
1240
  "pick_expired": "Düğmelerin süresi doldu — metinle yaz.",
1241
  "pick_not_yours": "Bunlar senin düğmelerin değil.",
1242
  "pick_choice": "Seçim: {choice}",
1243
- "owner_quota_notice": "⚠️ Gemini kotası rotadaki tüm modellerde tamamen doldu (detaylar için /stats).",
 
 
1244
  "user_daily_total": "Günlük mesaj limiti doldu ({used}/{limit}). Yenileri {hours} sa {mins} dk sonra kullanılabilir.",
1245
  "user_daily_gemini": "Günlük karmaşık istek limiti doldu ({used}/{limit}). Bağlantı, video ve YouTube'u yarın açabilirim. Sıradan sorunu sor — yanıtlarım.",
1246
  "user_daily_tts": "Günlük seslendirme limiti doldu ({used}/{limit}). Yarın seslendirebilirim.",
@@ -1349,7 +1355,9 @@ PACK_DE: dict[str, str] = {
1349
  "pick_expired": "Buttons abgelaufen — als Text schreiben.",
1350
  "pick_not_yours": "Das sind nicht deine Buttons.",
1351
  "pick_choice": "Wahl: {choice}",
1352
- "owner_quota_notice": "⚠️ Gemini-Kontingent über alle Routenmodelle restlos erschöpft (Details siehe /stats).",
 
 
1353
  "user_daily_total": "Tageslimit für Nachrichten erreicht ({used}/{limit}). Neue gibt es in {hours} Std. {mins} Min.",
1354
  "user_daily_gemini": "Tageslimit für komplexe Anfragen erreicht ({used}/{limit}). Links, Videos und YouTube kann ich morgen öffnen. Eine normale Frage aber gern — ich antworte.",
1355
  "user_daily_tts": "Tageslimit für Vertonung erreicht ({used}/{limit}). Morgen kann ich wieder vertonen.",
@@ -1458,7 +1466,9 @@ PACK_FR: dict[str, str] = {
1458
  "pick_expired": "Boutons expirés — écris en texte.",
1459
  "pick_not_yours": "Ce ne sont pas tes boutons.",
1460
  "pick_choice": "Choix : {choice}",
1461
- "owner_quota_notice": "⚠️ Quota Gemini entièrement épuisé sur tous les modèles de la route (voir /stats pour les détails).",
 
 
1462
  "user_daily_total": "Limite quotidienne de messages atteinte ({used}/{limit}). Les prochains seront disponibles dans {hours} h {mins} min.",
1463
  "user_daily_gemini": "Limite quotidienne de requêtes complexes atteinte ({used}/{limit}). Je pourrai ouvrir liens, vidéos et YouTube demain. Mais pose ta question ordinaire — je répondrai.",
1464
  "user_daily_tts": "Limite quotidienne de voix atteinte ({used}/{limit}). Je pourrai remettre en voix demain.",
@@ -1567,7 +1577,9 @@ PACK_IT: dict[str, str] = {
1567
  "pick_expired": "Pulsanti scaduti — scrivi in testo.",
1568
  "pick_not_yours": "Non sono i tuoi pulsanti.",
1569
  "pick_choice": "Scelta: {choice}",
1570
- "owner_quota_notice": "⚠️ Quota Gemini completamente esaurita su tutti i modelli del percorso (vedi /stats per i dettagli).",
 
 
1571
  "user_daily_total": "Limite giornaliero di messaggi raggiunto ({used}/{limit}). Nuovi disponibili tra {hours} h {mins} min.",
1572
  "user_daily_gemini": "Limite giornaliero di richieste complesse raggiunto ({used}/{limit}). Link, video e YouTube potrò aprirli domani. Ma fai pure la tua domanda normale — risponderò.",
1573
  "user_daily_tts": "Limite giornaliero di voce raggiunto ({used}/{limit}). Potrò dare voce di nuovo domani.",
@@ -1676,7 +1688,9 @@ PACK_HI: dict[str, str] = {
1676
  "pick_expired": "बटन समाप्त हो गए — टेक्स्ट में लिखो।",
1677
  "pick_not_yours": "ये तुम्हारे बटन नहीं हैं।",
1678
  "pick_choice": "चयन: {choice}",
1679
- "owner_quota_notice": "⚠️ रूट के सभी मॉडलों में Gemini कोटा पूरी तरह समाप्त (विवरण के लिए /stats)।",
 
 
1680
  "user_daily_total": "दैनिक संदेश सीमा पूरी ({used}/{limit})। नए संदेश {hours} घंटे {mins} मिनट में मिलेंगे।",
1681
  "user_daily_gemini": "जटिल अनुरोधों की दैनिक सीमा पूरी ({used}/{limit})। लिंक, वीडियो और YouTube कल खोल पाऊंगा। साधारण सवाल पूछो — जवाब दूंगा।",
1682
  "user_daily_tts": "दैनिक वॉयस सीमा पूरी ({used}/{limit})। कल फिर से सुना पाऊंगा।",
@@ -1785,7 +1799,9 @@ PACK_ID: dict[str, str] = {
1785
  "pick_expired": "Tombol kedaluwarsa — tulis sebagai teks.",
1786
  "pick_not_yours": "Itu bukan tombolmu.",
1787
  "pick_choice": "Pilihan: {choice}",
1788
- "owner_quota_notice": "⚠️ Kuota Gemini habis total di semua model rute (lihat /stats untuk detail).",
 
 
1789
  "user_daily_total": "Batas harian pesan tercapai ({used}/{limit}). Pesan baru tersedia dalam {hours} jam {mins} mnt.",
1790
  "user_daily_gemini": "Batas harian permintaan kompleks tercapai ({used}/{limit}). Link, video dan YouTube bisa kubuka besok. Pertanyaan biasa silakan — akan kujawab.",
1791
  "user_daily_tts": "Batas harian suara tercapai ({used}/{limit}). Besok bisa bersuara lagi.",
@@ -1894,7 +1910,9 @@ PACK_MS: dict[str, str] = {
1894
  "pick_expired": "Butang luput — tulis sebagai teks.",
1895
  "pick_not_yours": "Itu bukan butang awak.",
1896
  "pick_choice": "Pilihan: {choice}",
1897
- "owner_quota_notice": "⚠️ Kuota Gemini habis sepenuhnya di semua model laluan (lihat /stats untuk butiran).",
 
 
1898
  "user_daily_total": "Had harian mesej tercapai ({used}/{limit}). Mesej baharu tersedia dalam {hours} jam {mins} minit.",
1899
  "user_daily_gemini": "Had harian permintaan kompleks tercapai ({used}/{limit}). Pautan, video dan YouTube boleh kubuka esok. Soalan biasa silakan — akan kujawab.",
1900
  "user_daily_tts": "Had harian suara tercapai ({used}/{limit}). Esok boleh bersuara lagi.",
@@ -2003,7 +2021,9 @@ PACK_VI: dict[str, str] = {
2003
  "pick_expired": "Các nút đã hết hạn — viết bằng chữ.",
2004
  "pick_not_yours": "Đó không phải nút của bạn.",
2005
  "pick_choice": "Lựa chọn: {choice}",
2006
- "owner_quota_notice": "⚠️ Hạn mức Gemini đã hết sạch trên mọi model của tuyến (xem /stats để biết chi tiết).",
 
 
2007
  "user_daily_total": "Đã hết hạn mức tin nhắn trong ngày ({used}/{limit}). Tin mới sẽ có sau {hours} giờ {mins} phút.",
2008
  "user_daily_gemini": "Đã hết hạn mức yêu cầu phức tạp trong ngày ({used}/{limit}). Link, video và YouTube để mai mình mở. Cứ hỏi bình thường — mình trả lời.",
2009
  "user_daily_tts": "Đã hết hạn mức lồng tiếng trong ngày ({used}/{limit}). Mai mình lồng tiếng tiếp được.",
@@ -2112,7 +2132,9 @@ PACK_TH: dict[str, str] = {
2112
  "pick_expired": "ปุ่มหมดอายุแล้ว — พิมพ์เป็นข้อความ",
2113
  "pick_not_yours": "นั่นไม่ใช่ปุ่มของคุณ",
2114
  "pick_choice": "ตัวเลือก: {choice}",
2115
- "owner_quota_notice": "⚠️ โควต้า Gemini หมดเกลี้ยงในทุกโมเดลของเส้นทาง (ดูรายละเอียดที่ /stats)",
 
 
2116
  "user_daily_total": "โควต้ารายวันหมดแล้ว ({used}/{limit}) จะใช้ได้อีกใน {hours} ชม. {mins} นาที",
2117
  "user_daily_gemini": "โควต้าคำขอซับซ้อนรายวันหมดแล้ว ({used}/{limit}) ลิงก์ วิดีโอ และ YouTube เปิดได้พรุ่งนี้ ถามธรรมดามาได้เลย — จะตอบให้",
2118
  "user_daily_tts": "โควต้าเสียงรายวันหมดแล้ว ({used}/{limit}) พรุ่งนี้พากย์เสียงได้ใหม่",
@@ -2221,7 +2243,9 @@ PACK_FA: dict[str, str] = {
2221
  "pick_expired": "دکمه‌ها منقضی شدند — متنی بنویس.",
2222
  "pick_not_yours": "این‌ها دکمه‌های تو نیستند.",
2223
  "pick_choice": "انتخاب: {choice}",
2224
- "owner_quota_notice": "⚠️ سهمیه Gemini در همه مدل‌های مسیر کاملاً تمام شد (جزئیات: /stats).",
 
 
2225
  "user_daily_total": "سقف روزانه پیام‌ها تمام شد ({used}/{limit}). پیام‌های جدید بعد از {hours} ساعت و {mins} دقیقه.",
2226
  "user_daily_gemini": "سقف روزانه درخواست‌های پیچیده تمام شد ({used}/{limit}). لینک، ویدیو و YouTube را فردا باز می‌کنم. سؤال عادی بپرس — جواب می‌دهم.",
2227
  "user_daily_tts": "سقف روزانه صوتی تمام شد ({used}/{limit}). فردا دوباره می‌توانم بخوانم.",
@@ -2330,7 +2354,9 @@ PACK_UR: dict[str, str] = {
2330
  "pick_expired": "بٹن ختم ہو گئے — متن میں لکھو۔",
2331
  "pick_not_yours": "یہ تمہارے بٹن نہیں ہیں۔",
2332
  "pick_choice": "انتخاب: {choice}",
2333
- "owner_quota_notice": "⚠️ روٹ کے تمام ماڈلز میں Gemini کوٹا مکمل ختم (تفصیل کے لیے /stats)۔",
 
 
2334
  "user_daily_total": "روزانہ پیغامات کی حد پوری ({used}/{limit})۔ نئے پیغامات {hours} گھنٹے {mins} منٹ بعد ملیں گے۔",
2335
  "user_daily_gemini": "پیچیدہ درخواستوں کی روزانہ حد پوری ({used}/{limit})۔ لنکس، ویڈیو اور YouTube کل کھول سکوں گا۔ عام سوال پوچھو — جواب دوں گا۔",
2336
  "user_daily_tts": "روزانہ وائس کی حد پوری ({used}/{limit})۔ کل پھر سنا سکوں گا۔",
@@ -2439,7 +2465,9 @@ PACK_UZ: dict[str, str] = {
2439
  "pick_expired": "Tugmalar eskirgan — matn bilan yoz.",
2440
  "pick_not_yours": "Bular sening tugmalaring emas.",
2441
  "pick_choice": "Tanlov: {choice}",
2442
- "owner_quota_notice": "⚠️ Marshrutdagi barcha modellarda Gemini kvotasi butunlay tugadi (batafsil /stats).",
 
 
2443
  "user_daily_total": "Kunlik xabar limiti tugadi ({used}/{limit}). Yangilari {hours} soat {mins} daqiqadan keyin bo'ladi.",
2444
  "user_daily_gemini": "Murakkab so'rovlarning kunlik limiti tugadi ({used}/{limit}). Havola, video va YouTube'ni ertaga ochaman. Oddiy savolingni ber — javob beraman.",
2445
  "user_daily_tts": "Kunlik ovozlash limiti tugadi ({used}/{limit}). Ertaga yana ovozlay olaman.",
@@ -2548,7 +2576,9 @@ PACK_BN: dict[str, str] = {
2548
  "pick_expired": "বোতাম মেয়াদোত্তীর্ণ — টেক্সটে লেখো।",
2549
  "pick_not_yours": "এগুলো তোমার বোতাম নয়।",
2550
  "pick_choice": "পছন্দ: {choice}",
2551
- "owner_quota_notice": "⚠️ রুটের সব মডেলে Gemini কোটা সম্পূর্ণ শেষ (বিস্তারিত /stats)।",
 
 
2552
  "user_daily_total": "দৈনিক বার্তা সীমা শেষ ({used}/{limit})। নতুন বার্তা {hours} ঘণ্টা {mins} মিনিট পরে পাওয়া যাবে।",
2553
  "user_daily_gemini": "জটিল অনুরোধের দৈনিক সীমা শেষ ({used}/{limit})। লিংক, ভিডিও আর YouTube কাল খুলতে পারব। সাধারণ প্রশ্ন করো — উত্তর দেব।",
2554
  "user_daily_tts": "দৈনিক ভয়েস সীমা শেষ ({used}/{limit})। কাল আবার শোনাতে পারব।",
@@ -2657,7 +2687,9 @@ PACK_FIL: dict[str, str] = {
2657
  "pick_expired": "Expired na ang mga button — isulat sa text.",
2658
  "pick_not_yours": "Hindi sa iyo ang mga button na iyan.",
2659
  "pick_choice": "Pinili: {choice}",
2660
- "owner_quota_notice": "⚠️ Ubos na ubos ang Gemini quota sa lahat ng model ng ruta (tingnan ang /stats para sa detalye).",
 
 
2661
  "user_daily_total": "Naabot na ang daily limit ng mensahe ({used}/{limit}). May bago ulit sa {hours} oras {mins} minuto.",
2662
  "user_daily_gemini": "Naabot na ang daily limit ng complex request ({used}/{limit}). Bukas ko na mabubuksan ang links, videos at YouTube. Pero magtanong ka lang — sasagutin ko.",
2663
  "user_daily_tts": "Naabot na ang daily limit ng boses ({used}/{limit}). Bukas pwede ulit.",
@@ -2766,7 +2798,9 @@ PACK_PL: dict[str, str] = {
2766
  "pick_expired": "Przyciski wygasły — napisz tekstem.",
2767
  "pick_not_yours": "To nie twoje przyciski.",
2768
  "pick_choice": "Wybór: {choice}",
2769
- "owner_quota_notice": "⚠️ Limit Gemini wyczerpany w całości we wszystkich modelach trasy (szczegóły w /stats).",
 
 
2770
  "user_daily_total": "Dzienny limit wiadomości wyczerpany ({used}/{limit}). Nowe będą za {hours} godz. {mins} min.",
2771
  "user_daily_gemini": "Dzienny limit złożonych zapytań wyczerpany ({used}/{limit}). Linki, wideo i YouTube otworzę jutro. Zwykłe pytanie śmiało — odpowiem.",
2772
  "user_daily_tts": "Dzienny limit głosu wyczerpany ({used}/{limit}). Jutro znów będę mógł udźwiękowić.",
@@ -2875,7 +2909,9 @@ PACK_NL: dict[str, str] = {
2875
  "pick_expired": "Knoppen verlopen — schrijf als tekst.",
2876
  "pick_not_yours": "Dit zijn niet jouw knoppen.",
2877
  "pick_choice": "Keuze: {choice}",
2878
- "owner_quota_notice": "⚠️ Gemini-quota volledig op over alle routemodellen (details zie /stats).",
 
 
2879
  "user_daily_total": "Daglimiet voor berichten bereikt ({used}/{limit}). Nieuwe over {hours} u {mins} min.",
2880
  "user_daily_gemini": "Daglimiet voor complexe verzoeken bereikt ({used}/{limit}). Links, video's en YouTube kan ik morgen openen. Een gewone vraag mag — ik antwoord.",
2881
  "user_daily_tts": "Daglimiet voor spraak bereikt ({used}/{limit}). Morgen kan ik weer inspreken.",
@@ -2984,7 +3020,9 @@ PACK_ZU: dict[str, str] = {
2984
  "pick_expired": "Izinkinobho ziphelelwe yisikhathi — bhala ngombiko.",
2985
  "pick_not_yours": "Lezi akuzona izinkinobho zakho.",
2986
  "pick_choice": "Okukhethiwe: {choice}",
2987
- "owner_quota_notice": "⚠️ I-quot ye-Gemini iphele ngokuphelele kuwo wonke amamodeli omzila (bona /stats ngemininingwane).",
 
 
2988
  "user_daily_total": "Umkhawulo wansuku zonke wemilayezo ufinyelelwe ({used}/{limit}). Emisha izotholakala ngemuva kwamahora angu-{hours} namaminithi angu-{mins}.",
2989
  "user_daily_gemini": "Umkhawulo wansuku zonke wezicelo ezinzima ufinyelelwe ({used}/{limit}). Izixhumanisi, amavidiyo ne-YouTube ngizovavula kusasa. Buza umbuzo ojwayelekile — ngizowuphendula.",
2990
  "user_daily_tts": "Umkhawulo wansuku zonke wezwi ufinyelelwe ({used}/{limit}). Ngizokwazi ukukhuluma futhi kusasa.",
@@ -3196,6 +3234,38 @@ PICK_TABLE: dict[str, dict[str, dict[str, object]]] = {
3196
  }
3197
 
3198
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3199
  def t(lang: str | None, key: str, **kwargs: object) -> str:
3200
  """Строка key на языке lang (фолбэк — английский). Плейсхолдеры вида
3201
  {error}/{choice}/{name} подставляются из kwargs. Новые языки лежат в
@@ -3203,6 +3273,10 @@ def t(lang: str | None, key: str, **kwargs: object) -> str:
3203
  lang = normalize_lang(lang)
3204
  table = STRINGS.get(key, {})
3205
  text = LANG_PACKS.get(lang, {}).get(key) or table.get(lang) or table.get(DEFAULT_LANG) or key
 
 
 
 
3206
  if kwargs:
3207
  try:
3208
  return text.format(**kwargs)
 
150
  "You can also draw and voice just with words, no commands — e.g. “draw a cat” or “read this out loud”.\n\n"
151
  "<b>TikTok</b>\n"
152
  "Send a link — I'll download video or photos without watermarks.\n\n"
153
+ "Ask anything — I'm listening."
 
154
  ),
155
  "es": (
156
  "<b>Lumen</b>\n\n"
 
189
  "Рисовать и озвучивать можно и просто словами, без команд — например «нарисуй кота» или «озвучь это».\n\n"
190
  "<b>TikTok</b>\n"
191
  "Пришли ссылку — скачаю видео или фото без водяных знаков.\n\n"
192
+ "Спрашивай что угодно — я слушаю."
 
193
  ),
194
  "uk": (
195
  "<b>Lumen</b>\n\n"
 
421
  "fallback_no_live_search": {
422
  "be": "Адказваю без жывога пошуку (пошукавая квота вычарпана, знешні пошук нічога не даў) — даныя могуць быць не самымі свежымі.",
423
  "en": "Answering without live web search (search quota exhausted and fallback search returned nothing) — the details may not be fully up to date.",
424
+ "es": "Respondo sin búsqueda web en vivo (cuota de búsqueda agotada y la búsqueda externa no devolvió nada); los datos pueden no estar totalmente actualizados.",
425
+ "kk": "Тірі іздеусіз жауап беремін (іздеу квотасы таусылды, сыртқы іздеу ештеңе қайтармады) — деректер толық өзекті болмауы мүмкін.",
426
  "ru": "Отвечаю без живого поиска (поисковая квота исчерпана, внешний поиск ничего не дал) — данные могут быть не самыми свежими.",
427
  "uk": "Відповідаю без живого пошуку (пошукова квота вичерпана, зовнішній пошук нічого не дав) — дані можуть бути не найсвіжішими.",
428
  },
 
909
  },
910
  # ── Владелец ──
911
  "owner_quota_notice": {
912
+ "be": "Квота Gemini вычарпана цалкам па ўсіх мадэлях у маршруце (гл. /stats для дэталяў).",
913
+ "en": "Gemini quota fully exhausted across all route models (see /stats for details).",
914
+ "es": "Cuota de Gemini agotada por completo en todos los modelos de la ruta (ver /stats para detalles).",
915
+ "kk": "Маршруттағы барлық модель бойынша Gemini квотасы толық таусылды (егжей-тегжей /stats).",
916
+ "ru": "Квота Gemini исчерпана целиком по всем моделям в маршруте (см. /stats для деталей).",
917
+ "uk": "Квоту Gemini вичерпано цілком за всіма моделями в маршруті (див. /stats для деталей).",
918
  },
919
  }
920
 
 
1022
  "pick_expired": "Botões expirados — escreva em texto.",
1023
  "pick_not_yours": "Estes não são seus botões.",
1024
  "pick_choice": "Escolha: {choice}",
1025
+ "owner_quota_notice": "Cota do Gemini totalmente esgotada em todos os modelos da rota (ver /stats para detalhes).",
1026
+ "group_history_notice": "Em grupos, guardo as mensagens recentes (até 100) como contexto e as envio aos provedores de modelos junto com a tua pergunta.",
1027
+ "fallback_no_live_search": "Respondo sem busca na web ao vivo (cota de busca esgotada e a busca alternativa não retornou nada) — os detalhes podem não estar totalmente atualizados.",
1028
  "user_daily_total": "Limite diário de mensagens esgotado ({used}/{limit}). Novas estarão disponíveis em {hours} h {mins} min.",
1029
  "user_daily_gemini": "Limite diário de solicitações complexas esgotado ({used}/{limit}). Poderei abrir links, vídeos e YouTube amanhã. Mas faça sua pergunta normal — responderei.",
1030
  "user_daily_tts": "Limite diário de voz esgotado ({used}/{limit}). Poderei dar voz de novo amanhã.",
 
1133
  "pick_expired": "انتهت صلاحية الأزرار — اكتبها نصًا.",
1134
  "pick_not_yours": "هذه ليست أزرارك.",
1135
  "pick_choice": "الاختيار: {choice}",
1136
+ "owner_quota_notice": "حصة Gemini مستنفدة بالكامل عبر كل نماذج المسار (انظر /stats للتفاصيل).",
1137
+ "group_history_notice": "في المجموعات أحتفظ بالرسائل الأخيرة (حتى 100) كسياق وأرسلها إلى مزوّدي النماذج مع سؤالك.",
1138
+ "fallback_no_live_search": "أجيب دون بحث حي في الويب (انتهت حصة البحث ولم تُرجع عملية البحث البديلة شيئًا) — قد لا تكون البيانات محدثة بالكامل.",
1139
  "user_daily_total": "تم استنفاد الحد اليومي للرسائل ({used}/{limit}). ستتوفر رسائل جديدة بعد {hours} س {mins} د.",
1140
  "user_daily_gemini": "تم استنفاد الحد اليومي للطلبات المعقدة ({used}/{limit}). سأتمكن من فتح الروابط والفيديو وYouTube غدًا. أما سؤالك العادي فاطرحه — وسأجيب.",
1141
  "user_daily_tts": "تم استنفاد الحد اليومي للتحويل إلى صوت ({used}/{limit}). سأتمكن من التحويل غدًا.",
 
1244
  "pick_expired": "Düğmelerin süresi doldu — metinle yaz.",
1245
  "pick_not_yours": "Bunlar senin düğmelerin değil.",
1246
  "pick_choice": "Seçim: {choice}",
1247
+ "owner_quota_notice": "Gemini kotası rotadaki tüm modellerde tamamen doldu (detaylar için /stats).",
1248
+ "group_history_notice": "Gruplarda son mesajları (100'e kadar) bağlam olarak tutarım ve sorunla birlikte model sağlayıcılarına gönderirim.",
1249
+ "fallback_no_live_search": "Canlı web araması olmadan yanıtlıyorum (arama kotası doldu ve yedek arama hiçbir şey döndürmedi) — ayrıntılar tamamen güncel olmayabilir.",
1250
  "user_daily_total": "Günlük mesaj limiti doldu ({used}/{limit}). Yenileri {hours} sa {mins} dk sonra kullanılabilir.",
1251
  "user_daily_gemini": "Günlük karmaşık istek limiti doldu ({used}/{limit}). Bağlantı, video ve YouTube'u yarın açabilirim. Sıradan sorunu sor — yanıtlarım.",
1252
  "user_daily_tts": "Günlük seslendirme limiti doldu ({used}/{limit}). Yarın seslendirebilirim.",
 
1355
  "pick_expired": "Buttons abgelaufen — als Text schreiben.",
1356
  "pick_not_yours": "Das sind nicht deine Buttons.",
1357
  "pick_choice": "Wahl: {choice}",
1358
+ "owner_quota_notice": "Gemini-Kontingent über alle Routenmodelle restlos erschöpft (Details siehe /stats).",
1359
+ "group_history_notice": "In Gruppen behalte ich die letzten Nachrichten (bis zu 100) als Kontext und sende sie zusammen mit deiner Frage an die Modellanbieter.",
1360
+ "fallback_no_live_search": "Ich antworte ohne Live-Websuche (Suchkontingent erschöpft und die Ausweichsuche lieferte nichts) — die Angaben sind möglicherweise nicht ganz aktuell.",
1361
  "user_daily_total": "Tageslimit für Nachrichten erreicht ({used}/{limit}). Neue gibt es in {hours} Std. {mins} Min.",
1362
  "user_daily_gemini": "Tageslimit für komplexe Anfragen erreicht ({used}/{limit}). Links, Videos und YouTube kann ich morgen öffnen. Eine normale Frage aber gern — ich antworte.",
1363
  "user_daily_tts": "Tageslimit für Vertonung erreicht ({used}/{limit}). Morgen kann ich wieder vertonen.",
 
1466
  "pick_expired": "Boutons expirés — écris en texte.",
1467
  "pick_not_yours": "Ce ne sont pas tes boutons.",
1468
  "pick_choice": "Choix : {choice}",
1469
+ "owner_quota_notice": "Quota Gemini entièrement épuisé sur tous les modèles de la route (voir /stats pour les détails).",
1470
+ "group_history_notice": "Dans les groupes, je garde les messages récents (jusqu'à 100) comme contexte et les envoie aux fournisseurs de modèles avec ta question.",
1471
+ "fallback_no_live_search": "Je réponds sans recherche web en direct (quota de recherche épuisé et la recherche de secours n'a rien renvoyé) — les informations peuvent ne pas être parfaitement à jour.",
1472
  "user_daily_total": "Limite quotidienne de messages atteinte ({used}/{limit}). Les prochains seront disponibles dans {hours} h {mins} min.",
1473
  "user_daily_gemini": "Limite quotidienne de requêtes complexes atteinte ({used}/{limit}). Je pourrai ouvrir liens, vidéos et YouTube demain. Mais pose ta question ordinaire — je répondrai.",
1474
  "user_daily_tts": "Limite quotidienne de voix atteinte ({used}/{limit}). Je pourrai remettre en voix demain.",
 
1577
  "pick_expired": "Pulsanti scaduti — scrivi in testo.",
1578
  "pick_not_yours": "Non sono i tuoi pulsanti.",
1579
  "pick_choice": "Scelta: {choice}",
1580
+ "owner_quota_notice": "Quota Gemini completamente esaurita su tutti i modelli del percorso (vedi /stats per i dettagli).",
1581
+ "group_history_notice": "Nei gruppi conservo i messaggi recenti (fino a 100) come contesto e li invio ai fornitori dei modelli insieme alla tua domanda.",
1582
+ "fallback_no_live_search": "Rispondo senza ricerca web in tempo reale (quota di ricerca esaurita e la ricerca alternativa non ha restituito nulla) — i dettagli potrebbero non essere del tutto aggiornati.",
1583
  "user_daily_total": "Limite giornaliero di messaggi raggiunto ({used}/{limit}). Nuovi disponibili tra {hours} h {mins} min.",
1584
  "user_daily_gemini": "Limite giornaliero di richieste complesse raggiunto ({used}/{limit}). Link, video e YouTube potrò aprirli domani. Ma fai pure la tua domanda normale — risponderò.",
1585
  "user_daily_tts": "Limite giornaliero di voce raggiunto ({used}/{limit}). Potrò dare voce di nuovo domani.",
 
1688
  "pick_expired": "बटन समाप्त हो गए — टेक्स्ट में लिखो।",
1689
  "pick_not_yours": "ये तुम्हारे बटन नहीं हैं।",
1690
  "pick_choice": "चयन: {choice}",
1691
+ "owner_quota_notice": "रूट के सभी मॉडलों में Gemini कोटा पूरी तरह समाप्त (विवरण के लिए /stats)।",
1692
+ "group_history_notice": "समूहों में मैं हाल के संदेश (100 तक) संदर्भ के रूप में रखता हूँ और तुम्हारे सवाल के साथ मॉडल प्रदाताओं को भेजता हूँ।",
1693
+ "fallback_no_live_search": "मैं लाइव वेब खोज के बिना उत्तर दे रहा हूँ (खोज कोटा खत्म और वैकल्पिक खोज से कुछ नहीं मिला) — जानकारी पूरी तरह ताज़ा न हो सकती है।",
1694
  "user_daily_total": "दैनिक संदेश सीमा पूरी ({used}/{limit})। नए संदेश {hours} घंटे {mins} मिनट में मिलेंगे।",
1695
  "user_daily_gemini": "जटिल अनुरोधों की दैनिक सीमा पूरी ({used}/{limit})। लिंक, वीडियो और YouTube कल खोल पाऊंगा। साधारण सवाल पूछो — जवाब दूंगा।",
1696
  "user_daily_tts": "दैनिक वॉयस सीमा पूरी ({used}/{limit})। कल फिर से सुना पाऊंगा।",
 
1799
  "pick_expired": "Tombol kedaluwarsa — tulis sebagai teks.",
1800
  "pick_not_yours": "Itu bukan tombolmu.",
1801
  "pick_choice": "Pilihan: {choice}",
1802
+ "owner_quota_notice": "Kuota Gemini habis total di semua model rute (lihat /stats untuk detail).",
1803
+ "group_history_notice": "Di grup, saya menyimpan pesan terbaru (hingga 100) sebagai konteks dan mengirimkannya ke penyedia model bersama pertanyaanmu.",
1804
+ "fallback_no_live_search": "Saya menjawab tanpa pencarian web langsung (kuota pencarian habis dan pencarian cadangan tidak mengembalikan apa pun) — detailnya mungkin tidak sepenuhnya terbaru.",
1805
  "user_daily_total": "Batas harian pesan tercapai ({used}/{limit}). Pesan baru tersedia dalam {hours} jam {mins} mnt.",
1806
  "user_daily_gemini": "Batas harian permintaan kompleks tercapai ({used}/{limit}). Link, video dan YouTube bisa kubuka besok. Pertanyaan biasa silakan — akan kujawab.",
1807
  "user_daily_tts": "Batas harian suara tercapai ({used}/{limit}). Besok bisa bersuara lagi.",
 
1910
  "pick_expired": "Butang luput — tulis sebagai teks.",
1911
  "pick_not_yours": "Itu bukan butang awak.",
1912
  "pick_choice": "Pilihan: {choice}",
1913
+ "owner_quota_notice": "Kuota Gemini habis sepenuhnya di semua model laluan (lihat /stats untuk butiran).",
1914
+ "group_history_notice": "Dalam kumpulan, saya menyimpan mesej terkini (sehingga 100) sebagai konteks dan menghantarnya kepada penyedia model bersama soalan kamu.",
1915
+ "fallback_no_live_search": "Saya menjawab tanpa carian web langsung (kuota carian habis dan carian sandaran tidak mengembalikan apa-apa) — butirannya mungkin tidak sepenuhnya terkini.",
1916
  "user_daily_total": "Had harian mesej tercapai ({used}/{limit}). Mesej baharu tersedia dalam {hours} jam {mins} minit.",
1917
  "user_daily_gemini": "Had harian permintaan kompleks tercapai ({used}/{limit}). Pautan, video dan YouTube boleh kubuka esok. Soalan biasa silakan — akan kujawab.",
1918
  "user_daily_tts": "Had harian suara tercapai ({used}/{limit}). Esok boleh bersuara lagi.",
 
2021
  "pick_expired": "Các nút đã hết hạn — viết bằng chữ.",
2022
  "pick_not_yours": "Đó không phải nút của bạn.",
2023
  "pick_choice": "Lựa chọn: {choice}",
2024
+ "owner_quota_notice": "Hạn mức Gemini đã hết sạch trên mọi model của tuyến (xem /stats để biết chi tiết).",
2025
+ "group_history_notice": "Trong nhóm, tôi giữ các tin nhắn gần đây (tối đa 100) làm ngữ cảnh và gửi chúng cho các nhà cung cấp mô hình cùng với câu hỏi của bạn.",
2026
+ "fallback_no_live_search": "Tôi trả lời mà không có tìm kiếm web trực tiếp (hết hạn mức tìm kiếm và tìm kiếm dự phòng không trả về gì) — thông tin có thể không hoàn toàn mới nhất.",
2027
  "user_daily_total": "Đã hết hạn mức tin nhắn trong ngày ({used}/{limit}). Tin mới sẽ có sau {hours} giờ {mins} phút.",
2028
  "user_daily_gemini": "Đã hết hạn mức yêu cầu phức tạp trong ngày ({used}/{limit}). Link, video và YouTube để mai mình mở. Cứ hỏi bình thường — mình trả lời.",
2029
  "user_daily_tts": "Đã hết hạn mức lồng tiếng trong ngày ({used}/{limit}). Mai mình lồng tiếng tiếp được.",
 
2132
  "pick_expired": "ปุ่มหมดอายุแล้ว — พิมพ์เป็นข้อความ",
2133
  "pick_not_yours": "นั่นไม่ใช่ปุ่มของคุณ",
2134
  "pick_choice": "ตัวเลือก: {choice}",
2135
+ "owner_quota_notice": "โควต้า Gemini หมดเกลี้ยงในทุกโมเดลของเส้นทาง (ดูรายละเอียดที่ /stats)",
2136
+ "group_history_notice": "ในกลุ่ม ฉันเก็บข้อความล่าสุด (สูงสุด 100) เป็นบริบทและส่งให้ผู้ให้บริการโมเดลพร้อมคำถามของคุณ",
2137
+ "fallback_no_live_search": "ฉันตอบโดยไม่มีการค้นหาเว็บสด (โควต้าค้นหาหมดและค้นหาสำรองไม่พบอะไร) — ข้อมูลอาจไม่เป็นปัจจุบันทั้งหมด",
2138
  "user_daily_total": "โควต้ารายวันหมดแล้ว ({used}/{limit}) จะใช้ได้อีกใน {hours} ชม. {mins} นาที",
2139
  "user_daily_gemini": "โควต้าคำขอซับซ้อนรายวันหมดแล้ว ({used}/{limit}) ลิงก์ วิดีโอ และ YouTube เปิดได้พรุ่งนี้ ถามธรรมดามาได้เลย — จะตอบให้",
2140
  "user_daily_tts": "โควต้าเสียงรายวันหมดแล้ว ({used}/{limit}) พรุ่งนี้พากย์เสียงได้ใหม่",
 
2243
  "pick_expired": "دکمه‌ها منقضی شدند — متنی بنویس.",
2244
  "pick_not_yours": "این‌ها دکمه‌های تو نیستند.",
2245
  "pick_choice": "انتخاب: {choice}",
2246
+ "owner_quota_notice": "سهمیه Gemini در همه مدل‌های مسیر کاملاً تمام شد (جزئیات: /stats).",
2247
+ "group_history_notice": "در گروه‌ها پیام‌های اخیر (تا ۱۰۰) را به‌عنوان زمینه نگه می‌دارم و همراه با سؤال تو به ارائه‌دهندگان مدل می‌فرستم.",
2248
+ "fallback_no_live_search": "بدون جستجوی زندهٔ وب پاسخ می‌دهم (سهمیهٔ جستجو تمام شد و جستجوی جایگزین چیزی برنگرداند) — ممکن است اطلاعات کاملاً به‌روز نباشد.",
2249
  "user_daily_total": "سقف روزانه پیام‌ها تمام شد ({used}/{limit}). پیام‌های جدید بعد از {hours} ساعت و {mins} دقیقه.",
2250
  "user_daily_gemini": "سقف روزانه درخواست‌های پیچیده تمام شد ({used}/{limit}). لینک، ویدیو و YouTube را فردا باز می‌کنم. سؤال عادی بپرس — جواب می‌دهم.",
2251
  "user_daily_tts": "سقف روزانه صوتی تمام شد ({used}/{limit}). فردا دوباره می‌توانم بخوانم.",
 
2354
  "pick_expired": "بٹن ختم ہو گئے — متن میں لکھو۔",
2355
  "pick_not_yours": "یہ تمہارے بٹن نہیں ہیں۔",
2356
  "pick_choice": "انتخاب: {choice}",
2357
+ "owner_quota_notice": "روٹ کے تمام ماڈلز میں Gemini کوٹا مکمل ختم (تفصیل کے لیے /stats)۔",
2358
+ "group_history_notice": "گروپوں میں حالیہ پیغامات (100 تک) سیاق کے طور پر رکھتا ہوں اور آپ کے سوال کے ساتھ ماڈل فراہم کنندگان کو بھیجتا ہوں۔",
2359
+ "fallback_no_live_search": "میں لائیو ویب تلاش کے بغیر جواب دے رہا ہوں (تلاش کا کوٹہ ختم اور متبادل تلاش نے کچھ نہیں دیا) — تفصیلات مکمل طور پر تازہ نہ ہو سکیں۔",
2360
  "user_daily_total": "روزانہ پیغامات کی حد پوری ({used}/{limit})۔ نئے پیغامات {hours} گھنٹے {mins} منٹ بعد ملیں گے۔",
2361
  "user_daily_gemini": "پیچیدہ درخواستوں کی روزانہ حد پوری ({used}/{limit})۔ لنکس، ویڈیو اور YouTube کل کھول سکوں گا۔ عام سوال پوچھو — جواب دوں گا۔",
2362
  "user_daily_tts": "روزانہ وائس کی حد پوری ({used}/{limit})۔ کل پھر سنا سکوں گا۔",
 
2465
  "pick_expired": "Tugmalar eskirgan — matn bilan yoz.",
2466
  "pick_not_yours": "Bular sening tugmalaring emas.",
2467
  "pick_choice": "Tanlov: {choice}",
2468
+ "owner_quota_notice": "Marshrutdagi barcha modellarda Gemini kvotasi butunlay tugadi (batafsil /stats).",
2469
+ "group_history_notice": "Guruhlarda oxirgi xabarlarni (100 tagacha) kontekst sifatida saqlayman va ularni savoling bilan birga model provayderlariga yuboraman.",
2470
+ "fallback_no_live_search": "Jonli veb-qidiruvsiz javob beraman (qidiruv kvotasi tugadi va zaxira qidiruv hech narsa qaytarmadi) — ma'lumotlar to'liq dolzarb bo'lmasligi mumkin.",
2471
  "user_daily_total": "Kunlik xabar limiti tugadi ({used}/{limit}). Yangilari {hours} soat {mins} daqiqadan keyin bo'ladi.",
2472
  "user_daily_gemini": "Murakkab so'rovlarning kunlik limiti tugadi ({used}/{limit}). Havola, video va YouTube'ni ertaga ochaman. Oddiy savolingni ber — javob beraman.",
2473
  "user_daily_tts": "Kunlik ovozlash limiti tugadi ({used}/{limit}). Ertaga yana ovozlay olaman.",
 
2576
  "pick_expired": "বোতাম মেয়াদোত্তীর্ণ — টেক্সটে লেখো।",
2577
  "pick_not_yours": "এগুলো তোমার বোতাম নয়।",
2578
  "pick_choice": "পছন্দ: {choice}",
2579
+ "owner_quota_notice": "রুটের সব মডেলে Gemini কোটা সম্পূর্ণ শেষ (বিস্তারিত /stats)।",
2580
+ "group_history_notice": "গ্রুপে আমি সাম্প্রতিক বার্তাগুলো (সর্বোচ্চ ১০০) প্র��ঙ্গ হিসেবে রাখি এবং আপনার প্রশ্নের সাথে মডেল প্রদানকারীদের পাঠাই।",
2581
+ "fallback_no_live_search": "লাইভ ওয়েব অনুসন্ধান ছাড়াই উত্তর দিচ্ছি (অনুসন্ধান কোটা শেষ এবং বিকল্প অনুসন্ধানে কিছু পাওয়া যায়নি) — তথ্যগুলো পুরোপুরি হালনাগাদ নাও হতে পারে।",
2582
  "user_daily_total": "দৈনিক বার্তা সীমা শেষ ({used}/{limit})। নতুন বার্তা {hours} ঘণ্টা {mins} মিনিট পরে পাওয়া যাবে।",
2583
  "user_daily_gemini": "জটিল অনুরোধের দৈনিক সীমা শেষ ({used}/{limit})। লিংক, ভিডিও আর YouTube কাল খুলতে পারব। সাধারণ প্রশ্ন করো — উত্তর দেব।",
2584
  "user_daily_tts": "দৈনিক ভয়েস সীমা শেষ ({used}/{limit})। কাল আবার শোনাতে পারব।",
 
2687
  "pick_expired": "Expired na ang mga button — isulat sa text.",
2688
  "pick_not_yours": "Hindi sa iyo ang mga button na iyan.",
2689
  "pick_choice": "Pinili: {choice}",
2690
+ "owner_quota_notice": "Ubos na ubos ang Gemini quota sa lahat ng model ng ruta (tingnan ang /stats para sa detalye).",
2691
+ "group_history_notice": "Sa mga grupo, itinatago ko ang mga kamakailang mensahe (hanggang 100) bilang konteksto at ipinapadala kasama ng tanong mo sa mga provider ng modelo.",
2692
+ "fallback_no_live_search": "Sasagot ako nang walang live na web search (naubos ang quota sa paghahanap at walang nakuha ang fallback search) — maaaring hindi ganap na napapanahon ang mga detalye.",
2693
  "user_daily_total": "Naabot na ang daily limit ng mensahe ({used}/{limit}). May bago ulit sa {hours} oras {mins} minuto.",
2694
  "user_daily_gemini": "Naabot na ang daily limit ng complex request ({used}/{limit}). Bukas ko na mabubuksan ang links, videos at YouTube. Pero magtanong ka lang — sasagutin ko.",
2695
  "user_daily_tts": "Naabot na ang daily limit ng boses ({used}/{limit}). Bukas pwede ulit.",
 
2798
  "pick_expired": "Przyciski wygasły — napisz tekstem.",
2799
  "pick_not_yours": "To nie twoje przyciski.",
2800
  "pick_choice": "Wybór: {choice}",
2801
+ "owner_quota_notice": "Limit Gemini wyczerpany w całości we wszystkich modelach trasy (szczegóły w /stats).",
2802
+ "group_history_notice": "W grupach zachowuję ostatnie wiadomości (do 100) jako kontekst i wysyłam je dostawcom modeli razem z twoim pytaniem.",
2803
+ "fallback_no_live_search": "Odpowiadam bez wyszukiwania w sieci na żywo (limit wyszukiwania wyczerpany, a wyszukiwanie zastępcze nic nie zwróciło) — dane mogą nie być w pełni aktualne.",
2804
  "user_daily_total": "Dzienny limit wiadomości wyczerpany ({used}/{limit}). Nowe będą za {hours} godz. {mins} min.",
2805
  "user_daily_gemini": "Dzienny limit złożonych zapytań wyczerpany ({used}/{limit}). Linki, wideo i YouTube otworzę jutro. Zwykłe pytanie śmiało — odpowiem.",
2806
  "user_daily_tts": "Dzienny limit głosu wyczerpany ({used}/{limit}). Jutro znów będę mógł udźwiękowić.",
 
2909
  "pick_expired": "Knoppen verlopen — schrijf als tekst.",
2910
  "pick_not_yours": "Dit zijn niet jouw knoppen.",
2911
  "pick_choice": "Keuze: {choice}",
2912
+ "owner_quota_notice": "Gemini-quota volledig op over alle routemodellen (details zie /stats).",
2913
+ "group_history_notice": "In groepen bewaar ik recente berichten (tot 100) als context en stuur ze samen met je vraag naar de modelproviders.",
2914
+ "fallback_no_live_search": "Ik antwoord zonder live webzoekopdracht (zoekquotum uitgeput en de fallbackzoekopdracht leverde niets op) — de details zijn mogelijk niet helemaal actueel.",
2915
  "user_daily_total": "Daglimiet voor berichten bereikt ({used}/{limit}). Nieuwe over {hours} u {mins} min.",
2916
  "user_daily_gemini": "Daglimiet voor complexe verzoeken bereikt ({used}/{limit}). Links, video's en YouTube kan ik morgen openen. Een gewone vraag mag — ik antwoord.",
2917
  "user_daily_tts": "Daglimiet voor spraak bereikt ({used}/{limit}). Morgen kan ik weer inspreken.",
 
3020
  "pick_expired": "Izinkinobho ziphelelwe yisikhathi — bhala ngombiko.",
3021
  "pick_not_yours": "Lezi akuzona izinkinobho zakho.",
3022
  "pick_choice": "Okukhethiwe: {choice}",
3023
+ "owner_quota_notice": "I-quot ye-Gemini iphele ngokuphelele kuwo wonke amamodeli omzila (bona /stats ngemininingwane).",
3024
+ "group_history_notice": "Emaqenjini ngigcina imilayezo yakamuva (kuze kufike ku-100) njengomongo futhi ngiyithumela kubaphovidi bamamodeli kanye nombuzo wakho.",
3025
+ "fallback_no_live_search": "Ngiphendula ngaphandle kokusesha kuwebhu bukhoma (isabelo sokususha siphelile futhi ukusesha okulandelayo akubuyisanga lutho) — imininingwane kungenzeka ingabi sesikhathini ngokuphelele.",
3026
  "user_daily_total": "Umkhawulo wansuku zonke wemilayezo ufinyelelwe ({used}/{limit}). Emisha izotholakala ngemuva kwamahora angu-{hours} namaminithi angu-{mins}.",
3027
  "user_daily_gemini": "Umkhawulo wansuku zonke wezicelo ezinzima ufinyelelwe ({used}/{limit}). Izixhumanisi, amavidiyo ne-YouTube ngizovavula kusasa. Buza umbuzo ojwayelekile — ngizowuphendula.",
3028
  "user_daily_tts": "Umkhawulo wansuku zonke wezwi ufinyelelwe ({used}/{limit}). Ngizokwazi ukukhuluma futhi kusasa.",
 
3234
  }
3235
 
3236
 
3237
+ # Один и тот же блок живых примеров во всех языках: состав приветствия не
3238
+ # должен различаться между переводами (аудит 28.10.2026). Хранится отдельно,
3239
+ # чтобы не дублировать одну и ту же строку в 25 значениях start_text.
3240
+ _START_EXAMPLES: dict[str, str] = {
3241
+ "ar": "جرّب الآن: «من أنت؟»، أرسل صورة مع «ماذا فيها؟» أو «اقترح فيلمًا».",
3242
+ "be": "Паспрабуй прама зараз: «хто ты?», дашлі фота з пытаннем «што на ім?» або «парай фільм».",
3243
+ "bn": "এখনই চেষ্টা করো: «তুমি কে?», «এতে কী আছে?» লিখে ছবি পাঠাও বা «একটি সিনেমা সুপারিশ করো»।",
3244
+ "de": "Probier es gleich aus: „Wer bist du?“, schick ein Foto mit „Was ist darauf?“ oder „Empfiehl mir einen Film“.",
3245
+ "en": "Try right now: “who are you?”, send a photo with “what's in it?”, or “recommend a movie”.",
3246
+ "es": "Pruébalo ahora: «¿quién eres?», envía una foto con «¿qué hay en ella?» o «recomiéndame una película».",
3247
+ "fa": "همین حالا امتحان کن: «تو کی هستی؟»، عکسی با «چه چیزی در آن است؟» بفرست یا «یک فیلم پیشنهاد بده».",
3248
+ "fil": "Subukan ngayon: «sino ka?», magpadala ng larawan na may «ano ang nasa loob?» o «magrekomenda ng pelikula».",
3249
+ "fr": "Essaie tout de suite : « qui es-tu ? », envoie une photo avec « qu'y a-t-il dessus ? » ou « recommande-moi un film ».",
3250
+ "hi": "अभी आज़माओ: «तुम कौन हो?», «इसमें क्या है?» के साथ फोटो भेजो या «कोई फिल्म सुझाओ»।",
3251
+ "id": "Coba sekarang: «siapa kamu?», kirim foto dengan «ada apa di dalamnya?» atau «rekomendasikan film».",
3252
+ "it": "Provalo subito: «chi sei?», invia una foto con «cosa c'è dentro?» o «consigliami un film».",
3253
+ "kk": "Дәл қазір байқап көр: «сен кімсің?», сурет жіберіп «онда не бар?» деп сұра немесе «фильм ұсын».",
3254
+ "ms": "Cuba sekarang: «siapa kamu?», hantar foto dengan «apa di dalamnya?» atau «syorkan filem».",
3255
+ "nl": "Probeer het nu: 'wie ben je?', stuur een foto met 'wat staat erop?' of 'raad een film aan'.",
3256
+ "pl": "Wypróbuj teraz: „kim jesteś?”, wyślij zdjęcie z „co na nim jest?” albo „poleć film”.",
3257
+ "pt": "Experimente agora: «quem é você?», envie uma foto com «o que há nela?» ou «recomende um filme».",
3258
+ "ru": "Попробуй прямо сейчас: «кто ты?», пришли фото с вопросом «что на нём?», или «посоветуй фильм».",
3259
+ "th": "ลองเลย: «คุณเป็นใคร?», ส่งรูปพร้อม «ในรูปมีอะไร?» หรือ «แนะนำหนัง»",
3260
+ "tr": "Hemen dene: «sen kimsin?», «üzerinde ne var?» diye bir fotoğraf gönder veya «film öner».",
3261
+ "uk": "Спробуй прямо зараз: «хто ти?», надішли фото із запитанням «що на ньому?» або «порадь фільм».",
3262
+ "ur": "ابھی آزمائیں: «آپ کون ہیں؟»، «اس میں کیا ہے؟» کے ساتھ تصویر بھیجیں یا «کوئی فلم تجویز کریں»۔",
3263
+ "uz": "Hoziroq sinab ko'r: «sen kimsan?», «unda nima bor?» deb rasm yubor yoki «film tavsiya qil».",
3264
+ "vi": "Thử ngay: «bạn là ai?», gửi ảnh với «có gì trong đó?» hoặc «gợi ý một bộ phim».",
3265
+ "zu": "Zama manje: «ungubani?», thumela isithombe esinombuzo «kukhona ini kuso?» noma «phakamisa ifilimu».",
3266
+ }
3267
+
3268
+
3269
  def t(lang: str | None, key: str, **kwargs: object) -> str:
3270
  """Строка key на языке lang (фолбэк — английский). Плейсхолдеры вида
3271
  {error}/{choice}/{name} подставляются из kwargs. Новые языки лежат в
 
3273
  lang = normalize_lang(lang)
3274
  table = STRINGS.get(key, {})
3275
  text = LANG_PACKS.get(lang, {}).get(key) or table.get(lang) or table.get(DEFAULT_LANG) or key
3276
+ if key == "start_text":
3277
+ examples = _START_EXAMPLES.get(lang)
3278
+ if examples:
3279
+ text = text.rstrip() + "\n\n" + examples
3280
  if kwargs:
3281
  try:
3282
  return text.format(**kwargs)
tests/test_bot_commands.py CHANGED
@@ -1147,6 +1147,17 @@ def test_expired_picks_purged_on_new_pick(rate_guard_setup, monkeypatch):
1147
  bot._pending_picks.clear()
1148
 
1149
 
 
 
 
 
 
 
 
 
 
 
 
1150
  def test_lang_table_covers_all_keys_in_all_languages():
1151
  import lumen_lang
1152
  assert tuple(lumen_lang.SUPPORTED_LANGS) == tuple(sorted(lumen_lang.SUPPORTED_LANGS))
@@ -1157,9 +1168,10 @@ def test_lang_table_covers_all_keys_in_all_languages():
1157
  for key, table in lumen_lang.STRINGS.items():
1158
  for lang in lumen_lang.SUPPORTED_LANGS:
1159
  explicit = lumen_lang.LANG_PACKS.get(lang, {}).get(key) or table.get(lang)
1160
- resolved = explicit or table.get("en")
1161
- assert resolved, f"missing {key}[{lang}]"
1162
- if explicit is not None and lang != "en" and explicit == table.get("en"):
 
1163
  assert (key, lang) in allowed_en_duplicates, (
1164
  f"untranslated duplicate {key}[{lang}]: delete it, English fallback covers display"
1165
  )
 
1147
  bot._pending_picks.clear()
1148
 
1149
 
1150
+ def test_start_text_examples_cover_every_language():
1151
+ # Приветствие должно иметь одинаковый состав во всех языках, а не только
1152
+ # в en/ru (аудит 28.10.2026).
1153
+ import lumen_lang
1154
+ assert set(lumen_lang._START_EXAMPLES) == set(lumen_lang.SUPPORTED_LANGS)
1155
+ for lang in lumen_lang.SUPPORTED_LANGS:
1156
+ examples = lumen_lang._START_EXAMPLES[lang]
1157
+ assert examples
1158
+ assert lumen_lang.t(lang, "start_text").endswith(examples)
1159
+
1160
+
1161
  def test_lang_table_covers_all_keys_in_all_languages():
1162
  import lumen_lang
1163
  assert tuple(lumen_lang.SUPPORTED_LANGS) == tuple(sorted(lumen_lang.SUPPORTED_LANGS))
 
1168
  for key, table in lumen_lang.STRINGS.items():
1169
  for lang in lumen_lang.SUPPORTED_LANGS:
1170
  explicit = lumen_lang.LANG_PACKS.get(lang, {}).get(key) or table.get(lang)
1171
+ # Фолбэк на английский скрывал пропуски в переводе; теперь каждый
1172
+ # ключ обязан иметь явный перевод во всех 25 языках (аудит 28.10.2026).
1173
+ assert explicit, f"missing translation {key}[{lang}]"
1174
+ if lang != "en" and explicit == table.get("en"):
1175
  assert (key, lang) in allowed_en_duplicates, (
1176
  f"untranslated duplicate {key}[{lang}]: delete it, English fallback covers display"
1177
  )