From 379f5dfb50c79917dc2c81c671cc4f6c236b8668 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sat, 14 Sep 2024 15:42:05 +0200 Subject: [PATCH] added i18n + minor improvements / cleanup --- src/beebot.py | 162 ++++++++++++++++++++++++++++++-------------------- src/lang.json | 41 +++++++++++++ 2 files changed, 140 insertions(+), 63 deletions(-) create mode 100644 src/lang.json diff --git a/src/beebot.py b/src/beebot.py index 01cb76a..8310ebb 100644 --- a/src/beebot.py +++ b/src/beebot.py @@ -6,13 +6,13 @@ import os import sqlite3 import subprocess from sqlite3 import Connection -from typing import Optional +from typing import Optional, Callable import requests import telegram.constants from bs4 import BeautifulSoup from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton -from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes, CallbackQueryHandler +from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes, CallbackQueryHandler, Application log_dir = os.getenv("BEEBOT_LOGS") if not log_dir: @@ -65,27 +65,27 @@ class BeeBot: } CATEGORIES = { "E": { - "name": "Étudiant" + "name": "category.student" }, "D": { - "name": "Doctorant" + "name": "category.phd_student" }, "C": { - "name": "Campus" + "name": "category.campus" }, "V": { - "name": "Visiteur" + "name": "category.visitor" } } def __init__(self, token: str): self.tg_token: str = token - self.tg_app = None + self.tg_app: Optional[Application] = None self.cache: dict[str, str] = {} self.db_con: Optional[Connection] = None - self.locks: dict[str, bool] = {} - self.fetch_lock = asyncio.Lock() - self.gen_lock = asyncio.Lock() + self.langs: Optional[dict[str, dict[str, str]]] = None + self.fetch_lock: asyncio.Lock = asyncio.Lock() + self.gen_lock: asyncio.Lock = asyncio.Lock() def mainloop(self): logger.info("Initialization") @@ -93,15 +93,27 @@ class BeeBot: self.db_con = sqlite3.connect(self.DB_PATH) self.check_database() self.load_cache() + self.load_i18n() - app = ApplicationBuilder().token(self.tg_token).build() - app.add_handler(CommandHandler("week", self.cmd_week)) - app.add_handler(CommandHandler("today", self.cmd_today)) - app.add_handler(CommandHandler("settings", self.cmd_settings)) - app.add_handler(CallbackQueryHandler(self.cb_handler)) + self.tg_app = ApplicationBuilder().token(self.tg_token).build() + self.tg_app.add_handler(CommandHandler("week", self.cmd_week)) + self.tg_app.add_handler(CommandHandler("today", self.cmd_today)) + self.tg_app.add_handler(CommandHandler("settings", self.cmd_settings)) + self.tg_app.add_handler(CallbackQueryHandler(self.cb_handler)) logger.info("Starting bot") - app.run_polling() + self.tg_app.run_polling() + + def load_i18n(self) -> None: + with open(os.path.join(self.BASE_DIR, "lang.json"), "r") as f: + self.langs = json.load(f) + + def i18n(self, lang: str, key: str) -> str: + if lang not in self.langs: + lang = "en" + if key not in self.langs[lang]: + return f"[{key}]" + return self.langs[lang][key] async def cmd_week(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: logger.debug("Received /week") @@ -113,9 +125,10 @@ class BeeBot: async def cmd_settings(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: logger.debug("Received /settings") - menu = self.get_settings_menu(update) + lang = self.get_user_pref(update, context)["lang"] + menu = self.get_settings_menu(context) reply_markup = InlineKeyboardMarkup(menu) - await update.effective_chat.send_message(text="Your preferences", reply_markup=reply_markup) + await update.effective_chat.send_message(text=self.i18n(lang, "menu.settings"), reply_markup=reply_markup) async def cb_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: query = update.callback_query @@ -128,15 +141,22 @@ class BeeBot: elif query.data.startswith("toggle_category"): await self.cb_toggle_category(update, context) elif query.data == "back_to_settings": - menu = self.get_settings_menu(update) - reply_markup = InlineKeyboardMarkup(menu) - await update.effective_message.edit_text(text="Your preferences", reply_markup=reply_markup) + await self.show_menu(update, context, "menu.settings", self.get_settings_menu) + + async def show_menu(self, + update: Update, + context: ContextTypes.DEFAULT_TYPE, + text_key: str, + menu_func: Callable[[ContextTypes.DEFAULT_TYPE], list[list[InlineKeyboardButton]]]) -> None: + reply_markup = InlineKeyboardMarkup(menu_func(context)) + await update.effective_message.edit_text( + text=self.i18n(context.user_data["lang"], text_key), + reply_markup=reply_markup + ) async def cb_change_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: logger.debug("Clicked 'Change language'") - menu = self.get_language_menu(update) - reply_markup = InlineKeyboardMarkup(menu) - await update.effective_message.edit_text(text="Choose a language", reply_markup=reply_markup) + await self.show_menu(update, context, "menu.languages", self.get_language_menu) async def cb_set_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: lang = update.callback_query.data.split(":")[1] @@ -148,21 +168,17 @@ class BeeBot: ) self.db_con.commit() cur.close() - menu = self.get_language_menu(update) - reply_markup = InlineKeyboardMarkup(menu) - await update.effective_message.edit_text(text="Choose a language", reply_markup=reply_markup) + self.get_user_pref(update, context) + await self.show_menu(update, context, "menu.languages", self.get_language_menu) async def cb_change_categories(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: logger.debug("Clicked 'Change categories'") - menu = self.get_categories_menu(update) - reply_markup = InlineKeyboardMarkup(menu) - await update.effective_message.edit_text(text="Choose the price categories to display", reply_markup=reply_markup) + await self.show_menu(update, context, "menu.categories", self.get_categories_menu) async def cb_toggle_category(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: category = update.callback_query.data.split(":")[1] logger.debug(f"Clicked 'Toggle category {category}'") - categories: set[str] = self.get_user_pref(update)["categories"] - + categories: set[str] = context.user_data["categories"] if category in categories: categories.remove(category) else: @@ -175,9 +191,8 @@ class BeeBot: ) self.db_con.commit() cur.close() - menu = self.get_categories_menu(update) - reply_markup = InlineKeyboardMarkup(menu) - await update.effective_message.edit_text(text="Choose the price categories to display", reply_markup=reply_markup) + self.get_user_pref(update, context) + await self.show_menu(update, context, "menu.categories", self.get_categories_menu) async def request_menu(self, update: Update, context: ContextTypes.DEFAULT_TYPE, today_only: bool) -> None: chat_id = update.effective_chat.id @@ -196,17 +211,20 @@ class BeeBot: # If menu needs to be fetched if not os.path.exists(menu_path) or self.is_outdated(menu_id, today_only): # Notify user - msg = await update.message.reply_text("The menu is being updated, please wait...") + msg = await update.message.reply_text(self.i18n(prefs["lang"], "notif.wait_updating")) async with self.fetch_lock: - if today_only: - await self.fetch_today_menu() - else: - await self.fetch_week_menu() + if not os.path.exists(menu_path) or self.is_outdated(menu_id, today_only): + if today_only: + await self.fetch_today_menu() + else: + await self.fetch_week_menu() await msg.delete() # If image needs to be (re)generated if not os.path.exists(img_path) or self.is_outdated(img_id, today_only): - await self.gen_image(today_only, categories, img_path, img_id) + async with self.gen_lock: + if not os.path.exists(img_path) or self.is_outdated(img_id, today_only): + await self.gen_image(today_only, categories, img_path, img_id) await context.bot.send_photo(chat_id=chat_id, photo=img_path) @@ -225,25 +243,31 @@ class BeeBot: self.db_con.commit() cur.close() - def get_user_pref(self, update: Update) -> dict: + def get_user_pref(self, update: Update, context: Optional[ContextTypes.DEFAULT_TYPE] = None) -> dict: user_id = update.effective_user.id cur = self.db_con.cursor() res = cur.execute("SELECT categories, lang FROM user WHERE telegram_id=?", (user_id,)) user = res.fetchone() cur.close() + prefs = { + "categories": {"E", "D", "C", "V"}, + "lang": "fr" + } if user is None: self.create_user(user_id) - return { - "categories": {"E", "D", "C", "V"}, - "lang": "fr" + else: + categories = set(user[0].split(",")) + prefs = { + "categories": categories, + "lang": user[1] } - categories = set(user[0].split(",")) - return { - "categories": categories, - "lang": user[1] - } + if context is not None: + context.user_data["categories"] = prefs["categories"] + context.user_data["lang"] = prefs["lang"] + + return prefs def create_user(self, telegram_id: int) -> None: logger.debug(f"New user with id {telegram_id}") @@ -377,23 +401,29 @@ class BeeBot: return menus - def get_settings_menu(self, update: Update) -> list[list[InlineKeyboardButton]]: - prefs = self.get_user_pref(update) + def get_settings_menu(self, context: ContextTypes.DEFAULT_TYPE) -> list[list[InlineKeyboardButton]]: + lang = context.user_data["lang"] menu = [ [ - InlineKeyboardButton(f"Language: {prefs['lang']}", callback_data="change_language") + InlineKeyboardButton( + self.i18n(lang, "setting.language").format(lang), + callback_data="change_language" + ) ], [ - InlineKeyboardButton(f"Categories: {' / '.join(prefs['categories'])}", callback_data="change_categories") + InlineKeyboardButton( + self.i18n(lang, "setting.categories").format(" / ".join(context.user_data["categories"])), + callback_data="change_categories" + ) ] ] return menu - def get_language_menu(self, update: Update) -> list[list[InlineKeyboardButton]]: - prefs = self.get_user_pref(update) + def get_language_menu(self, context: ContextTypes.DEFAULT_TYPE) -> list[list[InlineKeyboardButton]]: + user_lang = context.user_data["lang"] buttons = [] for lang, data in self.LANGUAGES.items(): - extra = " ✅" if prefs["lang"] == lang else "" + extra = " ✅" if user_lang == lang else "" buttons.append( InlineKeyboardButton( data["emoji"] + extra, @@ -402,24 +432,30 @@ class BeeBot: ) menu = [buttons[i:i+2] for i in range(0, len(buttons), 2)] menu.append([ - InlineKeyboardButton("Back to settings", callback_data="back_to_settings") + InlineKeyboardButton( + self.i18n(user_lang, "menu.back_to_settings"), + callback_data="back_to_settings" + ) ]) return menu - def get_categories_menu(self, update: Update) -> list[list[InlineKeyboardButton]]: - prefs = self.get_user_pref(update) + def get_categories_menu(self, context: ContextTypes.DEFAULT_TYPE) -> list[list[InlineKeyboardButton]]: + lang = context.user_data["lang"] buttons = [] for categ, data in self.CATEGORIES.items(): - extra = " ✅" if categ in prefs["categories"] else " ❌" + extra = " ✅" if categ in context.user_data["categories"] else " ❌" buttons.append( InlineKeyboardButton( - data["name"] + extra, + self.i18n(lang, data["name"]) + extra, callback_data=f"toggle_category:{categ}" ) ) menu = [buttons[i:i+2] for i in range(0, len(buttons), 2)] menu.append([ - InlineKeyboardButton("Back to settings", callback_data="back_to_settings") + InlineKeyboardButton( + self.i18n(lang, "menu.back_to_settings"), + callback_data="back_to_settings" + ) ]) return menu diff --git a/src/lang.json b/src/lang.json new file mode 100644 index 0000000..8f2ab0c --- /dev/null +++ b/src/lang.json @@ -0,0 +1,41 @@ +{ + "fr": { + "category.student": "Étudiant", + "category.phd_student": "Doctorant", + "category.campus": "Campus", + "category.visitor": "Visiteur", + "menu.settings": "Vos préférences", + "menu.languages": "Choisissez une langue\n(Ceci n'affectera pas la langue des menus)", + "menu.categories": "Choisissez les catégories de prix à afficher", + "menu.back_to_settings": "Retour aux paramètres", + "setting.language": "Langue: {}", + "setting.categories": "Catégories: {}", + "notif.wait_updating": "Le menu est en train d'être mis à jour, veuillez patienter..." + }, + "en": { + "category.student": "Student", + "category.phd_student": "PhD Student", + "category.campus": "Campus", + "category.visitor": "Visitor", + "menu.settings": "Your preferences", + "menu.languages": "Choose a language\n(This will not affect the menu language)", + "menu.categories": "Choose the price categories to display", + "menu.back_to_settings": "Back to settings", + "setting.language": "Language: {}", + "setting.categories": "Categories: {}", + "notif.wait_updating": "The menu is being updated, please wait..." + }, + "de": { + "category.student": "Student", + "category.phd_student": "Doktorand", + "category.campus": "Campus", + "category.visitor": "Besucher", + "menu.settings": "Ihre Präferenzen", + "menu.languages": "Wählen Sie eine Sprache\n(Dies hat keinen Einfluss auf die Menüsprache)", + "menu.categories": "Wählen Sie die anzuzeigenden Preiskategorien", + "menu.back_to_settings": "Zurück zu Einstellungen", + "setting.language": "Sprache: {}", + "setting.categories": "Kategorien: {}", + "notif.wait_updating": "Das Menü wird gerade aktualisiert, bitte warten Sie..." + } +} \ No newline at end of file