diff --git a/LichessClientTG_bot/bot.py b/LichessClientTG_bot/bot.py
index f8d1884..ed8054a 100644
--- a/LichessClientTG_bot/bot.py
+++ b/LichessClientTG_bot/bot.py
@@ -133,17 +133,19 @@ class LichessBot:
logger.error(f"test_admin_notify failed: {e}")
await update.message.reply_text(f"❌ Failed to trigger admin notification: {e}")
def get_user_language_from_update(self, update: Update) -> str:
- """Always return English language"""
- # Update user info in database
+ """Get user's selected bot language from database"""
user = update.effective_user
if user:
+ # Update user info in database (this will auto-detect language for new users)
self.db.add_or_get_telegram_user(
user_id=user.id,
username=user.username,
first_name=user.first_name,
last_name=user.last_name,
- language_code=None
+ language_code=user.language_code
)
+ # Get user's selected bot language from database
+ return self.db.get_user_language(user.id)
return 'en'
async def start_existing_periodic_tasks(self):
@@ -1121,58 +1123,61 @@ class LichessBot:
# Start periodic task for this gamer (send to user's personal messages)
await self.start_periodic_task(selected_gamer, user_id, period)
- async def check_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
- """Check and display current language settings"""
+ async def set_lang(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
+ """Show language selection menu"""
user = update.effective_user
- if user:
- # Обновляем язык из update
- lang = self.get_user_language_from_update(update)
-
- # Получаем язык из БД для отображения
- db_lang = self.db.get_user_language(user.id)
-
- # Получаем language_code из БД напрямую
- with sqlite3.connect(self.db.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute("SELECT language_code FROM telegram_users WHERE user_id = ?", (user.id,))
- row = cursor.fetchone()
- db_language_code = row[0] if row and row[0] else None
-
- message = (
- f"🌐 Language Info:\n"
- f"Current language: {lang}\n"
- f"DB language: {db_lang}\n"
- f"DB language_code: {db_language_code}\n"
- f"Update language_code: {user.language_code}\n\n"
- f"Language used: {lang}\n\n"
- f"Bot uses English language only."
- )
- await update.message.reply_text(message)
- self.counters.increment('lang')
- else:
+ if not user:
await update.message.reply_text("❌ Failed to get user information")
+ return
+
+ # Get current language to show menu in user's language
+ lang = self.get_user_language_from_update(update)
+
+ # Create keyboard with language buttons
+ keyboard = [
+ [
+ InlineKeyboardButton("🇬🇧 English", callback_data="lang_en"),
+ InlineKeyboardButton("🇷🇺 Русский", callback_data="lang_ru")
+ ]
+ ]
+ reply_markup = InlineKeyboardMarkup(keyboard)
+
+ await update.message.reply_text(
+ t('select_language', lang),
+ reply_markup=reply_markup,
+ parse_mode='HTML'
+ )
- async def reset_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
- """Reset user language in database - will be detected from /start"""
- user = update.effective_user
- if user:
- # Сбрасываем язык в БД (устанавливаем NULL)
- with sqlite3.connect(self.db.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- "UPDATE telegram_users SET language_code = NULL WHERE user_id = ?",
- (user.id,)
- )
- conn.commit()
-
- # Language is always English
- lang = 'en'
- await update.message.reply_text(
- "✅ Language reset! Bot uses English language only."
- )
- self.counters.increment('resetlang')
+ async def handle_language_selection(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
+ """Handle language selection callback"""
+ query = update.callback_query
+ await query.answer()
+
+ user = query.from_user
+ user_id = user.id
+ selected_lang = query.data.split('_')[1] # 'en' or 'ru'
+
+ # Update user info in database
+ self.db.add_or_get_telegram_user(
+ user_id=user_id,
+ username=user.username,
+ first_name=user.first_name,
+ last_name=user.last_name,
+ language_code=user.language_code
+ )
+
+ # Save selected language to database
+ success = self.db.set_user_language(user_id, selected_lang)
+
+ if success:
+ # Get message in selected language
+ message = t('language_set_ru', selected_lang) if selected_lang == 'ru' else t('language_set_en', selected_lang)
+ await query.edit_message_text(message)
else:
- await update.message.reply_text("❌ Failed to get user information")
+ # Use current language for error message
+ lang = self.db.get_user_language(user_id)
+ error_msg = "❌ Не удалось установить язык" if lang == 'ru' else "❌ Failed to set language"
+ await query.edit_message_text(error_msg)
async def start_periodic_task(self, gamer: Dict[str, Any], user_id: int, period_minutes: int):
"""Start periodic task for a gamer"""
@@ -1408,11 +1413,11 @@ class LichessBot:
application.add_handler(CommandHandler("lastYear_or_1000games", self.last_year_or_1000games))
application.add_handler(CommandHandler("support", self.support))
application.add_handler(CommandHandler("setperiod", self.setperiod))
- application.add_handler(CommandHandler("lang", self.check_language))
- application.add_handler(CommandHandler("resetlang", self.reset_language))
+ application.add_handler(CommandHandler("set_lang", self.set_lang))
application.add_handler(CommandHandler("test_admin_notify", self.test_admin_notify))
# Callback handlers (order matters - more specific patterns first)
+ application.add_handler(CallbackQueryHandler(self.handle_language_selection, pattern="^lang_"))
application.add_handler(CallbackQueryHandler(self.select_gamer_for_period, pattern="^select_gamer_period_"))
application.add_handler(CallbackQueryHandler(self.select_gamer, pattern="^select_"))
application.add_handler(CallbackQueryHandler(self.handle_delete_gamer, pattern="^delete_"))
diff --git a/LichessClientTG_bot/config.py b/LichessClientTG_bot/config.py
index 45678e4..ae4f780 100644
--- a/LichessClientTG_bot/config.py
+++ b/LichessClientTG_bot/config.py
@@ -49,7 +49,7 @@ DATABASE_PATH = _resolve_database_path()
PERIOD_OPTIONS = [0, 15, 30, 60, 120, 180, 360, 720, 1440] # minutes (0=disable, then: 15min, 30min, 1h, 2h, 3h, 6h, 12h, 24h)
# Bot Version
-BOT_VERSION = "1.2.0"
+BOT_VERSION = "1.3.0"
# Telegram Bot Long Polling Configuration
POLL_INTERVAL = 1.0 # seconds
diff --git a/LichessClientTG_bot/database.py b/LichessClientTG_bot/database.py
index 9317b2a..34f2ef5 100644
--- a/LichessClientTG_bot/database.py
+++ b/LichessClientTG_bot/database.py
@@ -28,13 +28,23 @@ class Database:
)
''')
- # Add language_code column if it doesn't exist
+ # Add language_code column if it doesn't exist (Telegram language)
try:
cursor.execute("ALTER TABLE telegram_users ADD COLUMN language_code TEXT")
except sqlite3.OperationalError:
# Column already exists
pass
+ # Add bot_language column if it doesn't exist (user-selected bot language)
+ try:
+ cursor.execute("ALTER TABLE telegram_users ADD COLUMN bot_language TEXT DEFAULT 'en'")
+ # Set default value for existing users
+ cursor.execute("UPDATE telegram_users SET bot_language = 'en' WHERE bot_language IS NULL")
+ conn.commit()
+ except sqlite3.OperationalError:
+ # Column already exists
+ pass
+
# Create gamers table (Lichess players only)
cursor.execute('''
CREATE TABLE IF NOT EXISTS gamers (
@@ -144,22 +154,30 @@ class Database:
def add_or_get_telegram_user(self, user_id: int, username: Optional[str] = None,
first_name: Optional[str] = None, last_name: Optional[str] = None,
language_code: Optional[str] = None) -> bool:
- """Add or update Telegram user"""
+ """
+ Add or update Telegram user.
+ For new users, automatically sets bot_language based on Telegram language_code:
+ - 'ru' -> 'ru' (Russian)
+ - anything else -> 'en' (English)
+ """
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
- cursor.execute("SELECT user_id FROM telegram_users WHERE user_id = ?", (user_id,))
+ cursor.execute("SELECT user_id, bot_language FROM telegram_users WHERE user_id = ?", (user_id,))
existing = cursor.fetchone()
if not existing:
+ # New user - determine bot language from Telegram language_code
+ bot_language = 'ru' if language_code and language_code.lower().startswith('ru') else 'en'
cursor.execute(
- "INSERT INTO telegram_users (user_id, username, first_name, last_name, language_code) VALUES (?, ?, ?, ?, ?)",
- (user_id, username, first_name, last_name, language_code)
+ "INSERT INTO telegram_users (user_id, username, first_name, last_name, language_code, bot_language) VALUES (?, ?, ?, ?, ?, ?)",
+ (user_id, username, first_name, last_name, language_code, bot_language)
)
conn.commit()
+ logger.info(f"New user {user_id} added with bot_language={bot_language} (from Telegram language_code={language_code})")
return True
else:
- # Update language_code if provided (always update, even if None to clear old value)
+ # Existing user - update language_code but keep bot_language unchanged
cursor.execute(
"UPDATE telegram_users SET language_code = ? WHERE user_id = ?",
(language_code, user_id)
@@ -168,8 +186,28 @@ class Database:
return False
def get_user_language(self, user_id: int) -> str:
- """Always return English language"""
- return 'en'
+ """Get user's selected bot language from database"""
+ with sqlite3.connect(self.db_path) as conn:
+ cursor = conn.cursor()
+ cursor.execute("SELECT bot_language FROM telegram_users WHERE user_id = ?", (user_id,))
+ result = cursor.fetchone()
+ if result and result[0]:
+ return result[0]
+ # Default to English if not set
+ return 'en'
+
+ def set_user_language(self, user_id: int, language: str) -> bool:
+ """Set user's selected bot language"""
+ if language not in ['en', 'ru']:
+ return False
+ with sqlite3.connect(self.db_path) as conn:
+ cursor = conn.cursor()
+ cursor.execute(
+ "UPDATE telegram_users SET bot_language = ? WHERE user_id = ?",
+ (language, user_id)
+ )
+ conn.commit()
+ return cursor.rowcount > 0
def add_gamer(self, username: str) -> int:
"""Add a new gamer to the database (return gamer_id)"""
diff --git a/LichessClientTG_bot/i18n.py b/LichessClientTG_bot/i18n.py
index 960bd8f..c7f2988 100644
--- a/LichessClientTG_bot/i18n.py
+++ b/LichessClientTG_bot/i18n.py
@@ -1,11 +1,11 @@
"""
Internationalization module for the Lichess Telegram Bot
-English language only
+Supports English and Russian languages
"""
from typing import Optional
-# Translation dictionary (English only)
+# Translation dictionary (English and Russian)
TRANSLATIONS = {
'en': {
# Start command
@@ -37,6 +37,7 @@ TRANSLATIONS = {
"/lastYear_or_1000games - Statistics for the last year or last 1000 rated games (whichever comes first)\n"
"/setperiod - Set up periodic notifications for the active player\n"
"(active player changes in the /getgamers menu)\n"
+ "/set_lang - Select bot language (English / Russian)\n"
"/support - Contact the developer for support and feedback"
),
@@ -117,30 +118,153 @@ TRANSLATIONS = {
# Common
'period_minutes_suffix': "m",
+
+ # Language selection
+ 'select_language': "🌐 Select language / Выберите язык:",
+ 'language_set_en': "✅ Language set to English",
+ 'language_set_ru': "✅ Язык установлен: Русский",
+ },
+ 'ru': {
+ # Start command
+ 'start_message': (
+ "Бот отслеживает одного или нескольких игроков на Lichess.\n"
+ "Показывает рейтинги, активность за сегодня, вчера и за неделю.\n"
+ "При указании user_token (с разрешением на чтение пазлов) можно также получать данные по пазлам.\n"
+ "Бот поддерживает автоматические проверки с заданными интервалами и отправляет отчет, если была активность за это время.\n\n"
+ "Пример за неделю:\n"
+ "🧩 Пазлы: 114 (✅ 81 - ❌ 33)\n\n"
+ "🔥 Блиц — 5 игр • 🔴 -10\n"
+ "Рейтинг: 2245\n"
+ "✅ Побед: 1\n"
+ "❌ Поражений: 3\n"
+ "🤝 Ничьих: 1\n\n"
+ "🐇 Рапид — 19 игр • 🟢 +20\n"
+ "Рейтинг: 2248\n"
+ "✅ Побед: 8\n"
+ "❌ Поражений: 4\n"
+ "🤝 Ничьих: 7\n\n"
+ "📋 Доступные команды:\n"
+ "/addgamer - Добавить игрока Lichess для отслеживания (только username)\n"
+ "/addtoken - Добавить игрока с токеном для получения данных по пазлам\n"
+ "/getgamers - Выбрать активного игрока (для которого будут работать /today и другие команды)\n"
+ "/delgamer - Удалить игрока из списка отслеживаемых\n"
+ "/today - Статистика за сегодня\n"
+ "/yesterday - Статистика за вчера\n"
+ "/week - Статистика за неделю\n"
+ "/lastYear_or_1000games - Статистика за последний год или последние 1000 рейтинговых игр (что наступит раньше)\n"
+ "/setperiod - Настроить периодические уведомления для активного игрока\n"
+ "(активный игрок меняется в меню /getgamers)\n"
+ "/set_lang - Выбрать язык бота\n"
+ "/support - Связаться с разработчиком для поддержки и обратной связи"
+ ),
+
+ # Add gamer commands
+ 'addgamer_prompt': "👤 Введите username игрока Lichess для отслеживания:",
+ 'addtoken_prompt': (
+ "🔑 Введите токен API Lichess для получения данных по пазлам.\n"
+ "Токен создается в настройках профиля — дайте ему только разрешение puzzle:read.\n"
+ "После этого будет добавлен игрок, соответствующий этому токену."
+ ),
+ 'token_added': "✅ Токен добавлен для игрока {username}!",
+ 'gamer_added_with_token': "✅ Игрок {username} добавлен с токеном!",
+ 'gamer_added': "✅ Игрок {username} успешно добавлен!\n\nДля добавления следующего игрока воспользуйтесь /addgamer",
+ 'invalid_token': "❌ Неверный токен. Попробуйте еще раз.",
+ 'token_username_error': "❌ Не удалось получить username из токена. Попробуйте еще раз.",
+ 'empty_username': "❌ Username не может быть пустым. Попробуйте еще раз.",
+ 'user_not_found': "❌ Игрок {username} не найден на Lichess. Проверьте правильность написания имени.",
+
+ # Get gamers
+ 'no_gamers': "📭 Нет игроков в базе данных. Используйте /addgamer для добавления.",
+ 'loading_ratings': "🔄 Загрузка рейтингов игроков...",
+ 'select_active_gamer': "👥 Выберите активного игрока:\n\n",
+ 'active_gamer_set': "✅ Активный игрок: {username}",
+ 'gamer_not_found': "❌ Игрок не найден",
+
+ # Delete gamer
+ 'no_gamers_to_delete': "📭 У вас нет отслеживаемых игроков.",
+ 'loading_gamers': "🔄 Загрузка списка игроков...",
+ 'select_gamer_to_delete': "🗑️ Выберите игрока для удаления:\n\n",
+ 'gamer_deleted': "✅ Игрок {username} удален из списка отслеживаемых.",
+ 'active_gamer_deleted': "✅ Игрок {username} удален из списка отслеживаемых.\n\n⚠️ Вы удалили активного игрока. Используйте команду /getgamers для выбора игрока, для которого будут работать команды /today, /yesterday, /week.",
+ 'last_gamer_deleted': "✅ Игрок {username} удален из списка отслеживаемых.\n\n⚠️ Вы удалили последнего игрока. Используйте команду /addgamer для добавления нового отслеживаемого игрока.",
+ 'delete_failed': "❌ Не удалось удалить игрока",
+
+ # Stats commands
+ 'no_active_gamer': "❌ Нет активного игрока. Используйте /getgamers для выбора.",
+ 'unknown_period': "❌ Неизвестный период",
+ 'no_data': "📭 Нет данных",
+ 'stats_title': "📊 Статистика {username} • {date_range}\n\n",
+ 'puzzles_section': "🧩 Пазлы: {total} (✅ {solved} - ❌ {unsolved})\n\n",
+ 'games_section': "{emoji} {game_type} — {games_count} игр • {rating_change}\nРейтинг: {rating}\n✅ Побед: {wins}\n❌ Поражений: {losses}\n🤝 Ничьих: {draws}\n\n",
+
+ # Set period
+ 'select_gamer_for_period': "⏱️ Выберите игрока для установки периода уведомлений:\n\n",
+ 'select_period': "⏱️ Выберите период для игрока {username}:\n📱 Уведомления будут отправляться в личные сообщения",
+ 'notifications_disabled': "✅ Уведомления отключены для {username}",
+ 'period_set': "✅ Период {period} минут установлен для {username}\n📱 Уведомления будут отправляться в личные сообщения",
+ 'disable_notifications': "❌ Отключить уведомления",
+ 'period_minutes': "⏰ {period} минут",
+
+ # Period notification
+ 'period_1_minute': "за 1 минуту",
+ 'period_2_3_4_minutes': "за {period} минуты",
+ 'period_minutes_text': "за {period} минут",
+ 'period_notification_title': "📊 Статистика {username} • {period_text}\n\n",
+ 'period_puzzles_section': "🧩 Пазлы: {total} (✅ {solved} - ❌ {failed})\n\n",
+ 'period_games_section': "{emoji} {game_type} — {games_count} игр • {rating_change}\nРейтинг: {rating}\n✅ Побед: {wins}\n❌ Поражений: {losses}\n🤝 Ничьих: {draws}\n\n",
+ 'no_activity': "📭 Нет активности за этот период",
+
+ # Last year or 1000 games
+ 'last_year_1000_processing': "⏳ Обработка запроса... Это может занять некоторое время, так как запросы очень медленные.",
+ 'last_year_1000_player_processing': "🔄 Запрос данных для игрока {username}...",
+
+ # Stats commands (today/yesterday/week)
+ 'stats_processing': "⏳ Обработка запроса...",
+ 'stats_player_processing': "🔄 Запрос данных для игрока {username}...",
+ 'stats_all_done': "✅ Это всё",
+
+ # Support
+ 'support_message': (
+ "💬 Поддержка и обратная связь\n\n"
+ "Вы можете написать разработчику сервиса напрямую и сообщить о проблеме, предложить новые функции или задать вопрос.\n\n"
+ "Я буду рад рассмотреть каждое сообщение и ответить на него.\n\n"
+ "Разработчик принимает сообщения на английском и русском языках.\n\n"
+ "📧 Контакт: @vrubelr\n\n"
+ "📦 Версия бота: {version}"
+ ),
+
+ # Common
+ 'period_minutes_suffix': "м",
+
+ # Language selection
+ 'select_language': "🌐 Выберите язык / Select language:",
+ 'language_set_en': "✅ Language set to English",
+ 'language_set_ru': "✅ Язык установлен: Русский",
}
}
-def get_user_language(update) -> str:
- """
- Always return English language.
- """
- return 'en'
-
-
def t(key: str, lang: str = 'en', **kwargs) -> str:
"""
- Translate a key (always English).
+ Translate a key to the specified language.
Args:
key: Translation key
- lang: Language code (ignored, always uses English)
+ lang: Language code ('en' or 'ru')
**kwargs: Format arguments for the translation string
Returns:
Translated string with formatted arguments
"""
- translation = TRANSLATIONS['en'].get(key, key)
+ # Default to English if language not supported
+ if lang not in TRANSLATIONS:
+ lang = 'en'
+
+ translation = TRANSLATIONS[lang].get(key)
+
+ # Fallback to English if translation not found
+ if translation is None:
+ translation = TRANSLATIONS['en'].get(key, key)
# Format the string if kwargs provided
if kwargs:
@@ -151,10 +275,3 @@ def t(key: str, lang: str = 'en', **kwargs) -> str:
return translation
return translation
-
-
-def get_lang_from_update(update) -> str:
- """
- Always return English.
- """
- return 'en'