diff --git a/LichessClientTG_bot/bot.py b/LichessClientTG_bot/bot.py
index e91c763..8f5872c 100644
--- a/LichessClientTG_bot/bot.py
+++ b/LichessClientTG_bot/bot.py
@@ -1,5 +1,6 @@
import asyncio
import logging
+import sqlite3
from datetime import datetime, timedelta
from typing import Dict, Any, Optional
@@ -17,6 +18,7 @@ from config import (
from database import Database
from lichess_api import LichessAPI
from formatters import StatsFormatter
+from i18n import t
# Configure logging
logging.basicConfig(
@@ -35,6 +37,20 @@ class LichessBot:
self.periodic_tasks = {} # Store periodic tasks
self.period_start_times = {} # Store start times for each gamer
self.application = None # Will be set when application is created
+
+ def get_user_language_from_update(self, update: Update) -> str:
+ """Always return English language"""
+ # Update user info in database
+ user = update.effective_user
+ if user:
+ 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
+ )
+ return 'en'
async def start_existing_periodic_tasks(self):
"""Start periodic tasks for all user-gamer pairs that have periods set"""
@@ -56,41 +72,17 @@ class LichessBot:
"""Start command handler"""
# Register user in database
user = update.effective_user
+ lang_code = user.language_code if user else None
self.db.add_or_get_telegram_user(
user_id=user.id,
username=user.username,
first_name=user.first_name,
- last_name=user.last_name
+ last_name=user.last_name,
+ language_code=lang_code
)
- await update.message.reply_text(
- "Бот отслеживает игру одного или нескольких игроков на Lichess.\n"
- "Он показывает рейтинги, дневную, вчерашнюю и недельную активность.\n"
- "При указании user_token (с правом чтения задач) можно также получать данные по задачам.\n"
- "Бот поддерживает автоматические проверки с заданным интервалом и отправляет отчёт, если за это время была активность.\n\n"
- "Пример за неделю:\n"
- "🧩 Задачи: 114 (✅ 81 - ❌ 33)\n\n"
- "🔥 Blitz — 5 игр • 🔴 -10\n"
- "Рейтинг: 2245\n"
- "✅ Победы: 1\n"
- "❌ Поражения: 3\n"
- "🤝 Ничьи: 1\n\n"
- "🐇 Rapid — 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"
- "/setperiod - Настроить периодические уведомления активного игрока\n"
- "(активный игрок меняется в меню команды /getgamers)"
- )
+ lang = self.get_user_language_from_update(update)
+ await update.message.reply_text(t('start_message', lang))
async def start_and_addgamer(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Start command that automatically launches addgamer"""
@@ -101,16 +93,14 @@ class LichessBot:
async def addgamer_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Start addgamer command - simple username only"""
- await update.message.reply_text("👤 Введите Lichess username игрока для отслеживания:")
+ lang = self.get_user_language_from_update(update)
+ await update.message.reply_text(t('addgamer_prompt', lang))
return WAITING_FOR_USERNAME
async def addtoken_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Start addtoken command - token required"""
- await update.message.reply_text(
- "🔑 Введите Lichess API токен, чтобы получать данные по задачам.\n"
- "Токен создаётся в настройках профиля — дайте ему только право puzzle:read.\n"
- "После этого будет добавлен игрок, соответствующий этому токену."
- )
+ lang = self.get_user_language_from_update(update)
+ await update.message.reply_text(t('addtoken_prompt', lang))
return WAITING_FOR_TOKEN
async def handle_token(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -127,11 +117,12 @@ class LichessBot:
user_gamers = self.db.get_user_gamers(user_id)
existing_gamer = next((g for g in user_gamers if g['username'] == username), None)
+ lang = self.get_user_language_from_update(update)
if existing_gamer:
# Update token for existing gamer
self.db.add_user_gamer(user_id, existing_gamer['id'], token)
await update.message.reply_text(
- f"✅ Токен добавлен для игрока {username}!"
+ t('token_added', lang, username=username)
)
else:
# Add new gamer and link with token
@@ -147,17 +138,19 @@ class LichessBot:
self.db.set_user_active_gamer(user_id, gamer_id)
await update.message.reply_text(
- f"✅ Игрок {username} добавлен с токеном!"
+ t('gamer_added_with_token', lang, username=username)
)
return ConversationHandler.END
else:
+ lang = self.get_user_language_from_update(update)
await update.message.reply_text(
- "❌ Не удалось получить username из токена. Попробуйте еще раз."
+ t('token_username_error', lang)
)
return WAITING_FOR_TOKEN
else:
+ lang = self.get_user_language_from_update(update)
await update.message.reply_text(
- "❌ Неверный токен. Попробуйте еще раз."
+ t('invalid_token', lang)
)
return WAITING_FOR_TOKEN
@@ -166,9 +159,10 @@ class LichessBot:
username = update.message.text.strip()
user_id = update.effective_user.id
+ lang = self.get_user_language_from_update(update)
if not username:
await update.message.reply_text(
- "❌ Username не может быть пустым. Попробуйте еще раз."
+ t('empty_username', lang)
)
return WAITING_FOR_USERNAME
@@ -176,7 +170,7 @@ class LichessBot:
user_exists = await self.lichess_api.check_user_exists(username)
if not user_exists:
await update.message.reply_text(
- f"❌ Игрок {username} не найден на Lichess. Проверьте правильность написания имени."
+ t('user_not_found', lang, username=username)
)
return WAITING_FOR_USERNAME
@@ -193,8 +187,9 @@ class LichessBot:
if len(user_gamers) == 1:
self.db.set_user_active_gamer(user_id, gamer_id)
+ lang = self.get_user_language_from_update(update)
await update.message.reply_text(
- f"✅ Игрок {username} успешно добавлен!"
+ t('gamer_added', lang, username=username)
)
return ConversationHandler.END
@@ -206,12 +201,13 @@ class LichessBot:
logger.info(f"getgamers: user_id={user_id}, found {len(gamers)} gamers")
+ lang = self.get_user_language_from_update(update)
if not gamers:
- await update.message.reply_text("📭 В базе нет игроков. Используйте /adduser для добавления.")
+ await update.message.reply_text(t('no_gamers', lang))
return
# Show loading message
- loading_msg = await update.message.reply_text("🔄 Загружаем рейтинги игроков...")
+ loading_msg = await update.message.reply_text(t('loading_ratings', lang))
# Prepare data for each gamer
gamers_data = []
@@ -240,7 +236,8 @@ class LichessBot:
# Add period information if period > 0
period_minutes = gamer.get('period_minutes', 0)
- period_text = f" · {period_minutes}м" if period_minutes > 0 else ""
+ period_suffix = t('period_minutes_suffix', lang)
+ period_text = f" · {period_minutes}{period_suffix}" if period_minutes > 0 else ""
gamers_data.append({
'id': gamer['id'],
@@ -257,6 +254,9 @@ class LichessBot:
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
# Still add the gamer with N/A ratings
+ period_minutes = gamer.get('period_minutes', 0)
+ period_suffix = t('period_minutes_suffix', lang)
+ period_text = f" · {period_minutes}{period_suffix}" if period_minutes > 0 else ""
gamers_data.append({
'id': gamer['id'],
'status': "🟢" if gamer['is_active'] else "⚪",
@@ -264,7 +264,7 @@ class LichessBot:
'bullet': 'N/A',
'blitz': 'N/A',
'rapid': 'N/A',
- 'period': f" · {gamer.get('period_minutes', 0)}м" if gamer.get('period_minutes', 0) > 0 else ""
+ 'period': period_text
})
logger.info(f"Added gamer {gamer['username']} with N/A ratings due to error")
@@ -277,7 +277,7 @@ class LichessBot:
)
logger.info(f"getgamers: prepared {len(gamers_data)} gamers for display")
- gamers_text = "👥 Выберите активного игрока:\n\n" + "\n".join(text_lines)
+ gamers_text = t('select_active_gamer', lang) + "\n".join(text_lines)
logger.info(f"getgamers: message length: {len(gamers_text)} characters")
@@ -325,26 +325,37 @@ class LichessBot:
gamers = self.db.get_user_gamers(user_id)
selected_gamer = next((g for g in gamers if g['id'] == gamer_id), None)
+ # Для callback query обновляем язык пользователя если есть в update
+ if update.effective_user:
+ self.db.add_or_get_telegram_user(
+ user_id=update.effective_user.id,
+ username=update.effective_user.username,
+ first_name=update.effective_user.first_name,
+ last_name=update.effective_user.last_name,
+ language_code=update.effective_user.language_code
+ )
+ lang = self.db.get_user_language(user_id)
if selected_gamer:
# Set active gamer for this user
self.db.set_user_active_gamer(user_id, gamer_id)
await query.edit_message_text(
- f"✅ Активный игрок: {selected_gamer['username']}"
+ t('active_gamer_set', lang, username=selected_gamer['username'])
)
else:
- await query.edit_message_text("❌ Игрок не найден")
+ await query.edit_message_text(t('gamer_not_found', lang))
async def delgamer(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Show gamers list for deletion"""
user_id = update.effective_user.id
gamers = self.db.get_user_gamers(user_id)
+ lang = self.get_user_language_from_update(update)
if not gamers:
- await update.message.reply_text("📭 У вас нет отслеживаемых игроков.")
+ await update.message.reply_text(t('no_gamers_to_delete', lang))
return
# Show loading message
- loading_msg = await update.message.reply_text("🔄 Загружаем список игроков...")
+ loading_msg = await update.message.reply_text(t('loading_gamers', lang))
# Create text message with stats
text_lines = []
@@ -370,7 +381,8 @@ class LichessBot:
bullet_rating = blitz_rating = rapid_rating = 'N/A'
period_minutes = gamer.get('period_minutes', 0)
- period_text = f" · {period_minutes}м" if period_minutes > 0 else ""
+ period_suffix = t('period_minutes_suffix', lang)
+ period_text = f" · {period_minutes}{period_suffix}" if period_minutes > 0 else ""
text_lines.append(
f"{status} {username} "
@@ -383,7 +395,7 @@ class LichessBot:
callback_data=f"delete_{gamer['id']}"
)])
- gamers_text = "🗑️ Выберите игрока для удаления:\n\n" + "\n".join(text_lines)
+ gamers_text = t('select_gamer_to_delete', lang) + "\n".join(text_lines)
reply_markup = InlineKeyboardMarkup(keyboard)
@@ -418,19 +430,29 @@ class LichessBot:
gamers = self.db.get_user_gamers(user_id)
gamer_to_delete = next((g for g in gamers if g['id'] == gamer_id), None)
+ # Для callback query получаем язык из БД
+ if update.effective_user:
+ self.db.add_or_get_telegram_user(
+ user_id=update.effective_user.id,
+ username=update.effective_user.username,
+ first_name=update.effective_user.first_name,
+ last_name=update.effective_user.last_name,
+ language_code=update.effective_user.language_code
+ )
+ lang = self.db.get_user_language(user_id)
if gamer_to_delete:
username = gamer_to_delete['username']
deleted = self.db.remove_user_gamer(user_id, gamer_id)
if deleted:
await query.edit_message_text(
- f"✅ Игрок {username} удален из списка отслеживаемых.",
+ t('gamer_deleted', lang, username=username),
parse_mode='HTML'
)
else:
- await query.edit_message_text("❌ Не удалось удалить игрока")
+ await query.edit_message_text(t('delete_failed', lang))
else:
- await query.edit_message_text("❌ Игрок не найден")
+ await query.edit_message_text(t('gamer_not_found', lang))
async def get_stats(self, update: Update, context: ContextTypes.DEFAULT_TYPE, period: str):
"""Get statistics for a period"""
@@ -439,9 +461,10 @@ class LichessBot:
# Get active gamer for this user
active_gamer = self.db.get_user_active_gamer(user_id)
+ lang = self.get_user_language_from_update(update)
if not active_gamer:
await update.message.reply_text(
- "❌ Нет активного игрока. Используйте /getgamers для выбора."
+ t('no_active_gamer', lang)
)
return
@@ -455,11 +478,11 @@ class LichessBot:
elif period == "week":
data = await self.lichess_api.get_week_stats(username)
else:
- await update.message.reply_text("❌ Неизвестный период")
+ await update.message.reply_text(t('unknown_period', lang))
return
# Format and send response
- formatted_response = StatsFormatter.format_stats_response(data, username, period)
+ formatted_response = StatsFormatter.format_stats_response(data, username, period, lang)
await update.message.reply_text(formatted_response)
async def today(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -481,24 +504,24 @@ class LichessBot:
# Get active gamer for this user
active_gamer = self.db.get_user_active_gamer(user_id)
+ lang = self.get_user_language_from_update(update)
if not active_gamer:
await update.message.reply_text(
- "❌ Нет активного игрока. Используйте /getgamers для выбора."
+ t('no_active_gamer', lang)
)
return
keyboard = []
for period in PERIOD_OPTIONS:
if period == 0:
- button_text = "❌ Отключить уведомления"
+ button_text = t('disable_notifications', lang)
else:
- button_text = f"⏰ {period} минут"
+ button_text = t('period_minutes', lang, period=period)
keyboard.append([InlineKeyboardButton(button_text, callback_data=f"period_{period}")])
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
- f"⏱️ Выберите период для игрока {active_gamer['username']}:\n"
- f"📱 Уведомления будут приходить в личные сообщения",
+ t('select_period', lang, username=active_gamer['username']),
reply_markup=reply_markup
)
@@ -513,23 +536,83 @@ class LichessBot:
# Get active gamer for this user
active_gamer = self.db.get_user_active_gamer(user_id)
+ # Для callback query получаем язык из БД
+ if update.effective_user:
+ self.db.add_or_get_telegram_user(
+ user_id=update.effective_user.id,
+ username=update.effective_user.username,
+ first_name=update.effective_user.first_name,
+ last_name=update.effective_user.last_name,
+ language_code=update.effective_user.language_code
+ )
+ lang = self.db.get_user_language(user_id)
if active_gamer:
# Set period for this user-gamer pair
self.db.set_user_gamer_period(user_id, active_gamer['id'], period)
if period == 0:
await query.edit_message_text(
- f"✅ Уведомления для {active_gamer['username']} отключены"
+ t('notifications_disabled', lang, username=active_gamer['username'])
)
else:
await query.edit_message_text(
- f"✅ Период {period} минут установлен для {active_gamer['username']}\n"
- f"📱 Уведомления будут приходить в личные сообщения"
+ t('period_set', lang, period=period, username=active_gamer['username'])
)
# Start periodic task for this gamer (send to user's personal messages)
await self.start_periodic_task(active_gamer, user_id, period)
+ async def check_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
+ """Check and display current language settings"""
+ 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)
+ else:
+ await update.message.reply_text("❌ Failed to get user information")
+
+ 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."
+ )
+ else:
+ await update.message.reply_text("❌ Failed to get user information")
+
async def start_periodic_task(self, gamer: Dict[str, Any], user_id: int, period_minutes: int):
"""Start periodic task for a gamer"""
task_key = f"{gamer['id']}_{user_id}"
@@ -622,8 +705,10 @@ class LichessBot:
# Отправляем уведомление только если есть реальная активность
if has_games or has_puzzles:
try:
+ # Get user language from database
+ user_lang = self.db.get_user_language(user_id)
notification = StatsFormatter.format_period_notification(
- gamer['username'], games_data, puzzles_data, period_minutes
+ gamer['username'], games_data, puzzles_data, period_minutes, lang=user_lang
)
if self.application:
@@ -690,6 +775,8 @@ class LichessBot:
application.add_handler(CommandHandler("yesterday", self.yesterday))
application.add_handler(CommandHandler("week", self.week))
application.add_handler(CommandHandler("setperiod", self.setperiod))
+ application.add_handler(CommandHandler("lang", self.check_language))
+ application.add_handler(CommandHandler("resetlang", self.reset_language))
# Callback handlers
application.add_handler(CallbackQueryHandler(self.select_gamer, pattern="^select_"))
diff --git a/LichessClientTG_bot/database.py b/LichessClientTG_bot/database.py
index 595a839..af966cf 100644
--- a/LichessClientTG_bot/database.py
+++ b/LichessClientTG_bot/database.py
@@ -22,10 +22,18 @@ class Database:
username TEXT,
first_name TEXT,
last_name TEXT,
+ language_code TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
+ # Add language_code column if it doesn't exist
+ try:
+ cursor.execute("ALTER TABLE telegram_users ADD COLUMN language_code TEXT")
+ except sqlite3.OperationalError:
+ # Column already exists
+ pass
+
# Create gamers table (Lichess players only)
cursor.execute('''
CREATE TABLE IF NOT EXISTS gamers (
@@ -104,7 +112,8 @@ class Database:
logger.info(f"Migrated {migrated} tokens from gamers to user_gamers")
def add_or_get_telegram_user(self, user_id: int, username: Optional[str] = None,
- first_name: Optional[str] = None, last_name: Optional[str] = None) -> bool:
+ first_name: Optional[str] = None, last_name: Optional[str] = None,
+ language_code: Optional[str] = None) -> bool:
"""Add or update Telegram user"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
@@ -114,13 +123,24 @@ class Database:
if not existing:
cursor.execute(
- "INSERT INTO telegram_users (user_id, username, first_name, last_name) VALUES (?, ?, ?, ?)",
- (user_id, username, first_name, last_name)
+ "INSERT INTO telegram_users (user_id, username, first_name, last_name, language_code) VALUES (?, ?, ?, ?, ?)",
+ (user_id, username, first_name, last_name, language_code)
)
conn.commit()
return True
+ else:
+ # Update language_code if provided (always update, even if None to clear old value)
+ cursor.execute(
+ "UPDATE telegram_users SET language_code = ? WHERE user_id = ?",
+ (language_code, user_id)
+ )
+ conn.commit()
return False
+ def get_user_language(self, user_id: int) -> str:
+ """Always return English language"""
+ return 'en'
+
def add_gamer(self, username: str) -> int:
"""Add a new gamer to the database (return gamer_id)"""
with sqlite3.connect(self.db_path) as conn:
diff --git a/LichessClientTG_bot/formatters.py b/LichessClientTG_bot/formatters.py
index 872f32b..b0abbcd 100644
--- a/LichessClientTG_bot/formatters.py
+++ b/LichessClientTG_bot/formatters.py
@@ -1,5 +1,6 @@
from typing import Dict, Any, Optional
from datetime import datetime
+from i18n import t
class StatsFormatter:
@staticmethod
@@ -13,10 +14,10 @@ class StatsFormatter:
return f"0"
@staticmethod
- def format_stats_response(data: Dict[str, Any], username: str, period: str) -> str:
+ def format_stats_response(data: Dict[str, Any], username: str, period: str, lang: str = 'en') -> str:
"""Format statistics response according to the template"""
if not data or data.get('data') is None:
- message = data.get('message', 'Нет данных') if data else 'Нет данных'
+ message = data.get('message', t('no_data', lang)) if data else t('no_data', lang)
return f"📭 {message}"
# Extract data from API response
@@ -25,7 +26,7 @@ class StatsFormatter:
games = api_data.get('games', {})
# Format date range
- date_range = StatsFormatter._get_date_range(period)
+ date_range = StatsFormatter._get_date_range(period, lang)
# Format tasks section
task_text = ""
@@ -33,7 +34,7 @@ class StatsFormatter:
total_tasks = tasks.get('total', 0)
solved = tasks.get('solved', 0)
unsolved = tasks.get('unsolved', 0)
- task_text = f"🧩 Задачи: {total_tasks} (✅ {solved} - ❌ {unsolved})\n\n"
+ task_text = t('puzzles_section', lang, total=total_tasks, solved=solved, unsolved=unsolved)
# Format games section
games_text = ""
@@ -55,21 +56,29 @@ class StatsFormatter:
# Format rating change
rating_change_str = StatsFormatter._format_rating_change(rating_change)
- games_text += f"{emoji} {game_type.title()} — {games_count} игр • {rating_change_str}\n"
- games_text += f"Рейтинг: {rating}\n"
- games_text += f"✅ Победы: {wins}\n"
- games_text += f"❌ Поражения: {losses}\n"
- games_text += f"🤝 Ничьи: {draws}\n\n"
+ # Get game type name (capitalize first letter)
+ game_type_name = game_type.title()
+
+ games_text += t('games_section', lang,
+ emoji=emoji,
+ game_type=game_type_name,
+ games_count=games_count,
+ rating_change=rating_change_str,
+ rating=rating,
+ wins=wins,
+ losses=losses,
+ draws=draws
+ )
# Combine all parts
- result = f"📊 Статистика {username} • {date_range}\n\n"
+ result = t('stats_title', lang, username=username, date_range=date_range)
result += task_text
result += games_text.rstrip()
return result
@staticmethod
- def _get_date_range(period: str) -> str:
+ def _get_date_range(period: str, lang: str = 'en') -> str:
"""Get date range string for the period"""
from datetime import datetime, timedelta
@@ -99,19 +108,19 @@ class StatsFormatter:
return emoji_map.get(game_type.lower(), '🎯')
@staticmethod
- def format_period_notification(username: str, games_data: Optional[Dict], puzzles_data: Optional[Dict], period_minutes: int) -> str:
+ def format_period_notification(username: str, games_data: Optional[Dict], puzzles_data: Optional[Dict], period_minutes: int, lang: str = 'en') -> str:
"""Format notification for periodic checks"""
from datetime import datetime
# Format period text
if period_minutes == 1:
- period_text = "за 1 минуту"
+ period_text = t('period_1_minute', lang)
elif period_minutes in [2, 3, 4]:
- period_text = f"за {period_minutes} минуты"
+ period_text = t('period_2_3_4_minutes', lang, period=period_minutes)
else:
- period_text = f"за {period_minutes} минут"
+ period_text = t('period_minutes_text', lang, period=period_minutes)
- result = f"📊 Статистика {username} • {period_text}\n\n"
+ result = t('period_notification_title', lang, username=username, period_text=period_text)
# Format puzzles first (if available and there's actual activity)
if puzzles_data and puzzles_data.get('data'):
@@ -122,7 +131,7 @@ class StatsFormatter:
# Only show tasks section if there's actual activity (not all zeros)
if total_puzzles > 0 or solved > 0 or failed > 0:
- result += f"🧩 Задачи: {total_puzzles} (✅ {solved} - ❌ {failed})\n\n"
+ result += t('period_puzzles_section', lang, total=total_puzzles, solved=solved, failed=failed)
# Format games
if games_data and games_data.get('data'):
@@ -142,14 +151,21 @@ class StatsFormatter:
draws = game_data.get('draws', 0)
rating_change_str = StatsFormatter._format_rating_change(rating_change)
- result += f"{emoji} {game_type.title()} — {games_count} игр • {rating_change_str}\n"
- result += f"Рейтинг: {rating}\n"
- result += f"✅ Победы: {wins}\n"
- result += f"❌ Поражения: {losses}\n"
- result += f"🤝 Ничьи: {draws}\n\n"
+ game_type_name = game_type.title()
+
+ result += t('period_games_section', lang,
+ emoji=emoji,
+ game_type=game_type_name,
+ games_count=games_count,
+ rating_change=rating_change_str,
+ rating=rating,
+ wins=wins,
+ losses=losses,
+ draws=draws
+ )
# If no activity
if not (games_data and games_data.get('data')) and not (puzzles_data and puzzles_data.get('data')):
- result += "📭 Нет активности за этот период"
+ result += t('no_activity', lang)
return result.rstrip()
diff --git a/LichessClientTG_bot/i18n.py b/LichessClientTG_bot/i18n.py
new file mode 100644
index 0000000..db279ee
--- /dev/null
+++ b/LichessClientTG_bot/i18n.py
@@ -0,0 +1,136 @@
+"""
+Internationalization module for the Lichess Telegram Bot
+English language only
+"""
+
+from typing import Optional
+
+# Translation dictionary (English only)
+TRANSLATIONS = {
+ 'en': {
+ # Start command
+ 'start_message': (
+ "The bot tracks one or more players on Lichess.\n"
+ "It shows ratings, daily, yesterday and weekly activity.\n"
+ "When specifying a user_token (with puzzle read permission), you can also get puzzle data.\n"
+ "The bot supports automatic checks at specified intervals and sends a report if there was activity during that time.\n\n"
+ "Example for a week:\n"
+ "🧩 Puzzles: 114 (✅ 81 - ❌ 33)\n\n"
+ "🔥 Blitz — 5 games • 🔴 -10\n"
+ "Rating: 2245\n"
+ "✅ Wins: 1\n"
+ "❌ Losses: 3\n"
+ "🤝 Draws: 1\n\n"
+ "🐇 Rapid — 19 games • 🟢 +20\n"
+ "Rating: 2248\n"
+ "✅ Wins: 8\n"
+ "❌ Losses: 4\n"
+ "🤝 Draws: 7\n\n"
+ "📋 Available commands:\n"
+ "/addgamer - Add a Lichess player to track (username only)\n"
+ "/addtoken - Add a player with a token to get puzzle data\n"
+ "/getgamers - Select active player (for which /today and other commands will work)\n"
+ "/delgamer - Remove a player from the tracked list\n"
+ "/today - Statistics for today\n"
+ "/yesterday - Statistics for yesterday\n"
+ "/week - Statistics for the week\n"
+ "/setperiod - Set up periodic notifications for the active player\n"
+ "(active player changes in the /getgamers menu)"
+ ),
+
+ # Add gamer commands
+ 'addgamer_prompt': "👤 Enter the Lichess username of the player to track:",
+ 'addtoken_prompt': (
+ "🔑 Enter the Lichess API token to get puzzle data.\n"
+ "The token is created in profile settings — give it only puzzle:read permission.\n"
+ "After that, the player corresponding to this token will be added."
+ ),
+ 'token_added': "✅ Token added for player {username}!",
+ 'gamer_added_with_token': "✅ Player {username} added with token!",
+ 'gamer_added': "✅ Player {username} successfully added!",
+ 'invalid_token': "❌ Invalid token. Please try again.",
+ 'token_username_error': "❌ Failed to get username from token. Please try again.",
+ 'empty_username': "❌ Username cannot be empty. Please try again.",
+ 'user_not_found': "❌ Player {username} not found on Lichess. Check the spelling of the name.",
+
+ # Get gamers
+ 'no_gamers': "📭 No players in database. Use /addgamer to add.",
+ 'loading_ratings': "🔄 Loading player ratings...",
+ 'select_active_gamer': "👥 Select active player:\n\n",
+ 'active_gamer_set': "✅ Active player: {username}",
+ 'gamer_not_found': "❌ Player not found",
+
+ # Delete gamer
+ 'no_gamers_to_delete': "📭 You have no tracked players.",
+ 'loading_gamers': "🔄 Loading player list...",
+ 'select_gamer_to_delete': "🗑️ Select player to delete:\n\n",
+ 'gamer_deleted': "✅ Player {username} removed from tracked list.",
+ 'delete_failed': "❌ Failed to delete player",
+
+ # Stats commands
+ 'no_active_gamer': "❌ No active player. Use /getgamers to select.",
+ 'unknown_period': "❌ Unknown period",
+ 'no_data': "📭 No data",
+ 'stats_title': "📊 Statistics {username} • {date_range}\n\n",
+ 'puzzles_section': "🧩 Puzzles: {total} (✅ {solved} - ❌ {unsolved})\n\n",
+ 'games_section': "{emoji} {game_type} — {games_count} games • {rating_change}\nRating: {rating}\n✅ Wins: {wins}\n❌ Losses: {losses}\n🤝 Draws: {draws}\n\n",
+
+ # Set period
+ 'select_period': "⏱️ Select period for player {username}:\n📱 Notifications will be sent to personal messages",
+ 'notifications_disabled': "✅ Notifications disabled for {username}",
+ 'period_set': "✅ Period {period} minutes set for {username}\n📱 Notifications will be sent to personal messages",
+ 'disable_notifications': "❌ Disable notifications",
+ 'period_minutes': "⏰ {period} minutes",
+
+ # Period notification
+ 'period_1_minute': "for 1 minute",
+ 'period_2_3_4_minutes': "for {period} minutes",
+ 'period_minutes_text': "for {period} minutes",
+ 'period_notification_title': "📊 Statistics {username} • {period_text}\n\n",
+ 'period_puzzles_section': "🧩 Puzzles: {total} (✅ {solved} - ❌ {failed})\n\n",
+ 'period_games_section': "{emoji} {game_type} — {games_count} games • {rating_change}\nRating: {rating}\n✅ Wins: {wins}\n❌ Losses: {losses}\n🤝 Draws: {draws}\n\n",
+ 'no_activity': "📭 No activity for this period",
+
+ # Common
+ 'period_minutes_suffix': "m",
+ }
+}
+
+
+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).
+
+ Args:
+ key: Translation key
+ lang: Language code (ignored, always uses English)
+ **kwargs: Format arguments for the translation string
+
+ Returns:
+ Translated string with formatted arguments
+ """
+ translation = TRANSLATIONS['en'].get(key, key)
+
+ # Format the string if kwargs provided
+ if kwargs:
+ try:
+ return translation.format(**kwargs)
+ except KeyError:
+ # If formatting fails, return the translation as-is
+ return translation
+
+ return translation
+
+
+def get_lang_from_update(update) -> str:
+ """
+ Always return English.
+ """
+ return 'en'