switch ru/en

This commit is contained in:
vrubelroman 2025-11-20 12:43:00 +03:00
parent c16a11cf63
commit 123749415a
4 changed files with 241 additions and 81 deletions

View file

@ -133,17 +133,19 @@ class LichessBot:
logger.error(f"test_admin_notify failed: {e}") logger.error(f"test_admin_notify failed: {e}")
await update.message.reply_text(f"❌ Failed to trigger admin notification: {e}") await update.message.reply_text(f"❌ Failed to trigger admin notification: {e}")
def get_user_language_from_update(self, update: Update) -> str: def get_user_language_from_update(self, update: Update) -> str:
"""Always return English language""" """Get user's selected bot language from database"""
# Update user info in database
user = update.effective_user user = update.effective_user
if user: if user:
# Update user info in database (this will auto-detect language for new users)
self.db.add_or_get_telegram_user( self.db.add_or_get_telegram_user(
user_id=user.id, user_id=user.id,
username=user.username, username=user.username,
first_name=user.first_name, first_name=user.first_name,
last_name=user.last_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' return 'en'
async def start_existing_periodic_tasks(self): 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) # Start periodic task for this gamer (send to user's personal messages)
await self.start_periodic_task(selected_gamer, user_id, period) await self.start_periodic_task(selected_gamer, user_id, period)
async def check_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def set_lang(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Check and display current language settings""" """Show language selection menu"""
user = update.effective_user user = update.effective_user
if user: if not 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:
await update.message.reply_text("❌ Failed to get user information") 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): async def handle_language_selection(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Reset user language in database - will be detected from /start""" """Handle language selection callback"""
user = update.effective_user query = update.callback_query
if user: await query.answer()
# Сбрасываем язык в БД (устанавливаем NULL)
with sqlite3.connect(self.db.db_path) as conn: user = query.from_user
cursor = conn.cursor() user_id = user.id
cursor.execute( selected_lang = query.data.split('_')[1] # 'en' or 'ru'
"UPDATE telegram_users SET language_code = NULL WHERE user_id = ?",
(user.id,) # Update user info in database
) self.db.add_or_get_telegram_user(
conn.commit() user_id=user_id,
username=user.username,
# Language is always English first_name=user.first_name,
lang = 'en' last_name=user.last_name,
await update.message.reply_text( language_code=user.language_code
"✅ Language reset! Bot uses English language only." )
)
self.counters.increment('resetlang') # 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: 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): async def start_periodic_task(self, gamer: Dict[str, Any], user_id: int, period_minutes: int):
"""Start periodic task for a gamer""" """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("lastYear_or_1000games", self.last_year_or_1000games))
application.add_handler(CommandHandler("support", self.support)) application.add_handler(CommandHandler("support", self.support))
application.add_handler(CommandHandler("setperiod", self.setperiod)) application.add_handler(CommandHandler("setperiod", self.setperiod))
application.add_handler(CommandHandler("lang", self.check_language)) application.add_handler(CommandHandler("set_lang", self.set_lang))
application.add_handler(CommandHandler("resetlang", self.reset_language))
application.add_handler(CommandHandler("test_admin_notify", self.test_admin_notify)) application.add_handler(CommandHandler("test_admin_notify", self.test_admin_notify))
# Callback handlers (order matters - more specific patterns first) # 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_for_period, pattern="^select_gamer_period_"))
application.add_handler(CallbackQueryHandler(self.select_gamer, pattern="^select_")) application.add_handler(CallbackQueryHandler(self.select_gamer, pattern="^select_"))
application.add_handler(CallbackQueryHandler(self.handle_delete_gamer, pattern="^delete_")) application.add_handler(CallbackQueryHandler(self.handle_delete_gamer, pattern="^delete_"))

View file

@ -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) 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
BOT_VERSION = "1.2.0" BOT_VERSION = "1.3.0"
# Telegram Bot Long Polling Configuration # Telegram Bot Long Polling Configuration
POLL_INTERVAL = 1.0 # seconds POLL_INTERVAL = 1.0 # seconds

View file

@ -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: try:
cursor.execute("ALTER TABLE telegram_users ADD COLUMN language_code TEXT") cursor.execute("ALTER TABLE telegram_users ADD COLUMN language_code TEXT")
except sqlite3.OperationalError: except sqlite3.OperationalError:
# Column already exists # Column already exists
pass 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) # Create gamers table (Lichess players only)
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS gamers ( 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, def add_or_get_telegram_user(self, user_id: int, username: Optional[str] = None,
first_name: Optional[str] = None, last_name: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None,
language_code: Optional[str] = None) -> bool: 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: with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor() 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() existing = cursor.fetchone()
if not existing: 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( cursor.execute(
"INSERT INTO telegram_users (user_id, username, first_name, last_name, language_code) VALUES (?, ?, ?, ?, ?)", "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) (user_id, username, first_name, last_name, language_code, bot_language)
) )
conn.commit() conn.commit()
logger.info(f"New user {user_id} added with bot_language={bot_language} (from Telegram language_code={language_code})")
return True return True
else: 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( cursor.execute(
"UPDATE telegram_users SET language_code = ? WHERE user_id = ?", "UPDATE telegram_users SET language_code = ? WHERE user_id = ?",
(language_code, user_id) (language_code, user_id)
@ -168,8 +186,28 @@ class Database:
return False return False
def get_user_language(self, user_id: int) -> str: def get_user_language(self, user_id: int) -> str:
"""Always return English language""" """Get user's selected bot language from database"""
return 'en' 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: def add_gamer(self, username: str) -> int:
"""Add a new gamer to the database (return gamer_id)""" """Add a new gamer to the database (return gamer_id)"""

View file

@ -1,11 +1,11 @@
""" """
Internationalization module for the Lichess Telegram Bot Internationalization module for the Lichess Telegram Bot
English language only Supports English and Russian languages
""" """
from typing import Optional from typing import Optional
# Translation dictionary (English only) # Translation dictionary (English and Russian)
TRANSLATIONS = { TRANSLATIONS = {
'en': { 'en': {
# Start command # Start command
@ -37,6 +37,7 @@ TRANSLATIONS = {
"/lastYear_or_1000games - Statistics for the last year or last 1000 rated games (whichever comes first)\n" "/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" "/setperiod - Set up periodic notifications for the active player\n"
"(active player changes in the /getgamers menu)\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" "/support - Contact the developer for support and feedback"
), ),
@ -117,30 +118,153 @@ TRANSLATIONS = {
# Common # Common
'period_minutes_suffix': "m", 'period_minutes_suffix': "m",
# Language selection
'select_language': "🌐 <b>Select language / Выберите язык:</b>",
'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': "👥 <b>Выберите активного игрока:</b>\n\n",
'active_gamer_set': "✅ Активный игрок: {username}",
'gamer_not_found': "❌ Игрок не найден",
# Delete gamer
'no_gamers_to_delete': "📭 У вас нет отслеживаемых игроков.",
'loading_gamers': "🔄 Загрузка списка игроков...",
'select_gamer_to_delete': "🗑️ <b>Выберите игрока для удаления:</b>\n\n",
'gamer_deleted': "✅ Игрок <b>{username}</b> удален из списка отслеживаемых.",
'active_gamer_deleted': "✅ Игрок <b>{username}</b> удален из списка отслеживаемых.\n\n⚠️ Вы удалили активного игрока. Используйте команду /getgamers для выбора игрока, для которого будут работать команды /today, /yesterday, /week.",
'last_gamer_deleted': "✅ Игрок <b>{username}</b> удален из списка отслеживаемых.\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': "⏱️ <b>Выберите игрока для установки периода уведомлений:</b>\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': "🔄 Запрос данных для игрока <b>{username}</b>...",
# Stats commands (today/yesterday/week)
'stats_processing': "⏳ Обработка запроса...",
'stats_player_processing': "🔄 Запрос данных для игрока <b>{username}</b>...",
'stats_all_done': "✅ Это всё",
# Support
'support_message': (
"💬 <b>Поддержка и обратная связь</b>\n\n"
"Вы можете написать разработчику сервиса напрямую и сообщить о проблеме, предложить новые функции или задать вопрос.\n\n"
"Я буду рад рассмотреть каждое сообщение и ответить на него.\n\n"
"Разработчик принимает сообщения на <b>английском</b> и <b>русском</b> языках.\n\n"
"📧 Контакт: <a href=\"https://t.me/vrubelr\">@vrubelr</a>\n\n"
"📦 Версия бота: <b>{version}</b>"
),
# Common
'period_minutes_suffix': "м",
# Language selection
'select_language': "🌐 <b>Выберите язык / Select language:</b>",
'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: def t(key: str, lang: str = 'en', **kwargs) -> str:
""" """
Translate a key (always English). Translate a key to the specified language.
Args: Args:
key: Translation key key: Translation key
lang: Language code (ignored, always uses English) lang: Language code ('en' or 'ru')
**kwargs: Format arguments for the translation string **kwargs: Format arguments for the translation string
Returns: Returns:
Translated string with formatted arguments 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 # Format the string if kwargs provided
if kwargs: if kwargs:
@ -151,10 +275,3 @@ def t(key: str, lang: str = 'en', **kwargs) -> str:
return translation return translation
return translation return translation
def get_lang_from_update(update) -> str:
"""
Always return English.
"""
return 'en'