diff --git a/.gitignore b/.gitignore index f0e4fa4..5768013 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ venv/ output.png .vscode/ received_memory.json +translation_report.txt +translationcompleteness.py diff --git a/assets/cogs/README.md b/assets/cogs/README.md index 6cfe875..7d2ddee 100644 --- a/assets/cogs/README.md +++ b/assets/cogs/README.md @@ -25,3 +25,4 @@ by expect (requires goober version 0.11.8 or higher) [LastFM](https://raw.githubusercontent.com/WhatDidYouExpect/goober/refs/heads/main/cogs/webserver.py) by expect (no idea what version it needs i've only tried it on 1.0.3) +- you have to add LASTFM_USERNAME and LASTFM_API_KEY to your .env \ No newline at end of file diff --git a/assets/cogs/webscraper.py b/assets/cogs/webscraper.py.disabled similarity index 100% rename from assets/cogs/webscraper.py rename to assets/cogs/webscraper.py.disabled diff --git a/assets/locales/en.json b/assets/locales/en.json index 2048776..47b86a9 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -1,4 +1,5 @@ { + "checks_disabled": "Checks are disabled!", "unhandled_exception": "An unhandled exception occurred. Please report this issue on GitHub.", "active_users:": "Active users:", "spacy_initialized": "spaCy and spacytextblob are ready.", diff --git a/assets/locales/ee.json b/assets/locales/et.json similarity index 96% rename from assets/locales/ee.json rename to assets/locales/et.json index 14caaea..9e8b20c 100644 --- a/assets/locales/ee.json +++ b/assets/locales/et.json @@ -1,4 +1,9 @@ { + "checks_disabled": "Tarkistukset on poistettu käytöstä!", + "active_users:": "Aktiiviset käyttäjät:", + "spacy_initialized": "spaCy ja spacytextblob ovat valmiita.", + "spacy_model_not_found": "spaCy mallia ei löytynyt! Ladataan se....`", + "unhandled_exception": "Käsittelemätön virhe tapahtui. Ilmoita tästä GitHubiin.", "env_file_not_found": ".env faili ei leitud! Palun loo see vajalike muutujatega.", "error_fetching_active_users": "Aktiivsete kasutajate hankimisel tekkis viga: {error}", "error_sending_alive_ping": "Elusoleku ping'i saatmisel tekkis viga: {error}", diff --git a/assets/locales/fi.json b/assets/locales/fi.json index de7cd9f..bdc1544 100644 --- a/assets/locales/fi.json +++ b/assets/locales/fi.json @@ -1,5 +1,16 @@ { + "active_users:": "Aktiiviset käyttäjät:", + "cog_fail2": "Moduulin lataaminen epäonnistui:", + "command_ran_s": "Info: {interaction.user} suoritti", + "error_fetching_active_users": "Aktiivisten käyttäjien hankkimisessa tapahtui ongelma: {error}", + "error_sending_alive_ping": "Pingin lähettäminen goober centraliin epäonnistui: {error}", + "goober_server_alert": "Viesti goober centralista!\n", + "loaded_cog2": "Ladattiin moduuli:", + "spacy_initialized": "spaCy ja spacytextblob ovat valmiita.", + "spacy_model_not_found": "spaCy mallia ei löytynyt! Ladataan se....`", + "checks_disabled": "Tarkistukset on poistettu käytöstä!", "unhandled_exception": "Käsittelemätön virhe tapahtui. Ilmoita tästä GitHubissa.", + "active_users": "Aktiiviset käyttäjät", "env_file_not_found": ".env-tiedostoa ei löytnyt! Luo tiedosto jossa on tarvittavat muuttujat", "already_started": "Olen jo käynnistynyt! Ei päivitetä...", "please_restart": "Käynnistä uudelleen, hölmö!", diff --git a/assets/locales/fr.json b/assets/locales/fr.json index 8209ba8..4561819 100644 --- a/assets/locales/fr.json +++ b/assets/locales/fr.json @@ -1,4 +1,53 @@ { + "checks_disabled": "Les vérifications sont désactivées !", + "unhandled_exception": "Une exception non gérée est survenue. Merci de rapporter ce problème sur GitHub.", + "active_users:": "Utilisateurs actifs :", + "spacy_initialized": "spaCy et spacytextblob sont prêts.", + "spacy_model_not_found": "Le modèle spaCy est introuvable ! Téléchargement en cours...", + "env_file_not_found": "Le fichier .env est introuvable ! Créez-en un avec les variables nécessaires.", + "error_fetching_active_users": "Erreur lors de la récupération des utilisateurs actifs : {error}", + "error_sending_alive_ping": "Erreur lors de l’envoi du ping actif : {error}", + "already_started": "J’ai déjà démarré ! Je ne me mets pas à jour...", + "please_restart": "Redémarre, stp !", + "local_ahead": "Local {remote}/{branch} est en avance ou à jour. Pas de mise à jour...", + "remote_ahead": "Remote {remote}/{branch} est en avance. Mise à jour en cours...", + "cant_find_local_version": "Je ne trouve pas la variable local_version ! Ou elle a été modifiée et ce n’est pas un entier !", + "running_prestart_checks": "Exécution des vérifications préalables au démarrage...", + "continuing_in_seconds": "Reprise dans {seconds} secondes... Appuie sur une touche pour passer.", + "missing_requests_psutil": "requests et psutil manquants ! Installe-les avec pip : `pip install requests psutil`", + "requirements_not_found": "requirements.txt introuvable à {path}, a-t-il été modifié ?", + "warning_failed_parse_imports": "Avertissement : Échec du parsing des imports depuis {filename} : {error}", + "cogs_dir_not_found": "Répertoire des cogs introuvable à {path}, scan ignoré.", + "std_lib_local_skipped": "LIB STD / LOCAL {package} (vérification sautée)", + "ok_installed": "OK", + "missing_package": "MANQUANT", + "missing_package2": "n’est pas installé", + "missing_packages_detected": "Packages manquants détectés :", + "telling_goober_central": "Envoi à goober central à {url}", + "failed_to_contact": "Impossible de contacter {url} : {error}", + "all_requirements_satisfied": "Toutes les dépendances sont satisfaites.", + "ping_to": "Ping vers {host} : {latency} ms", + "high_latency": "Latence élevée détectée ! Tu pourrais avoir des délais de réponse.", + "could_not_parse_latency": "Impossible d’analyser la latence.", + "ping_failed": "Ping vers {host} échoué.", + "error_running_ping": "Erreur lors du ping : {error}", + "memory_usage": "Utilisation mémoire : {used} Go / {total} Go ({percent}%)", + "memory_above_90": "Usage mémoire au-dessus de 90% ({percent}%). Pense à libérer de la mémoire.", + "total_memory": "Mémoire totale : {total} Go", + "used_memory": "Mémoire utilisée : {used} Go", + "low_free_memory": "Mémoire libre faible détectée ! Seulement {free} Go disponibles.", + "measuring_cpu": "Mesure de l’usage CPU par cœur...", + "core_usage": "Cœur {idx} : [{bar}] {usage}%", + "total_cpu_usage": "Usage total CPU : {usage}%", + "high_avg_cpu": "Moyenne CPU élevée : {usage}%", + "really_high_cpu": "Charge CPU vraiment élevée ! Le système pourrait ralentir ou planter.", + "memory_file": "Fichier mémoire : {size} Mo", + "memory_file_large": "Fichier mémoire de 1 Go ou plus, pense à le nettoyer pour libérer de l’espace.", + "memory_file_corrupted": "Fichier mémoire corrompu ! Erreur JSON : {error}", + "consider_backup_memory": "Pense à sauvegarder et recréer le fichier mémoire.", + "memory_file_encoding": "Problèmes d’encodage du fichier mémoire : {error}", + "error_reading_memory": "Erreur lecture fichier mémoire : {error}", + "memory_file_not_found": "Fichier mémoire introuvable.", "modification_warning": "Goober a été modifié ! Toutes les modifications seront perdues lors d'une mise à jour !", "reported_version": "Version rapportée :", "current_hash": "Hachage actuel :", diff --git a/assets/locales/it.json b/assets/locales/it.json index 4f6b2c8..8ff94e1 100644 --- a/assets/locales/it.json +++ b/assets/locales/it.json @@ -1,7 +1,9 @@ { + "checks_disabled": "I controlli sono disabilitati!", "unhandled_exception": "Si è verificata un'eccezione non gestita. Segnala questo problema su GitHub, per favore.", "active_users:": "Utenti attivi:", "spacy_initialized": "spaCy e spacytextblob sono pronti.", + "error_fetching_active_users": "Errore nel recupero degli utenti attivi: {error}", "spacy_model_not_found": "Il modello spaCy non è stato trovato! Lo sto scaricando...", "env_file_not_found": "Il file .env non è stato trovato! Crea un file con le variabili richieste.", "error fetching_active_users": "Errore nel recupero degli utenti attivi:", @@ -108,6 +110,7 @@ "command_help_categories_admin": "Amministrazione", "command_help_categories_custom": "Comandi personalizzati", "command_ran": "Info: {message.author.name} ha eseguito {message.content}", + "command_ran_s": "Info: {interaction.user} ha eseguito ", "command_desc_ping": "ping", "command_ping_embed_desc": "Latenza del bot:", "command_ping_footer": "Richiesto da", diff --git a/bot.py b/bot.py index 05742ae..ee82cb9 100644 --- a/bot.py +++ b/bot.py @@ -30,7 +30,7 @@ from better_profanity import profanity from discord.ext import commands from modules.central import ping_server -from modules.translations import _ +from modules.volta.main import _ from modules.markovmemory import * from modules.version import * from modules.sentenceprocessing import * @@ -368,7 +368,7 @@ async def on_message(message: discord.Message) -> None: # Process commands if message starts with a command prefix if message.content.startswith((f"{PREFIX}talk", f"{PREFIX}mem", f"{PREFIX}help", f"{PREFIX}stats", f"{PREFIX}")): - print(f"{(_('failed_generate_image')).format(message=message)}") + print(f"{(_('command_ran')).format(message=message)}") await bot.process_commands(message) return diff --git a/example.env b/example.env index 343e0c3..8785786 100644 --- a/example.env +++ b/example.env @@ -12,14 +12,15 @@ gooberTOKEN= song="War Without Reason" POSITIVE_GIFS="https://tenor.com/view/chill-guy-my-new-character-gif-2777893510283028272, https://tenor.com/view/goodnight-goodnight-friends-weezer-weezer-goodnight-gif-7322052181075806988" splashtext=" - d8b - ?88 - 88b - d888b8b d8888b d8888b 888888b d8888b 88bd88b -d8P' ?88 d8P' ?88d8P' ?88 88P `?8bd8b_,dP 88P' ` -88b ,88b 88b d8888b d88 d88, d8888b d88 -`?88P'`88b`?8888P'`?8888P'd88'`?88P'`?888P'd88' - )88 - ,88P - `?8888P + SS\ + SS | + SSSSSS\ SSSSSS\ SSSSSS\ SSSSSSS\ SSSSSS\ SSSSSS\ +SS __SS\ SS __SS\ SS __SS\ SS __SS\ SS __SS\ SS __SS\ +SS / SS |SS / SS |SS / SS |SS | SS |SSSSSSSS |SS | \__| +SS | SS |SS | SS |SS | SS |SS | SS |SS ____|SS | +\SSSSSSS |\SSSSSS |\SSSSSS |SSSSSSS |\SSSSSSS\ SS | + \____SS | \______/ \______/ \_______/ \_______|\__| +SS\ SS | +\SSSSSS | + \______/ " diff --git a/modules/central.py b/modules/central.py index 9174acf..edde8cc 100644 --- a/modules/central.py +++ b/modules/central.py @@ -1,7 +1,7 @@ import requests import os import modules.globalvars as gv -from modules.translations import _ +from modules.volta.main import _ from modules.markovmemory import get_file_info # Ping the server to check if it's alive and send some info diff --git a/modules/markovmemory.py b/modules/markovmemory.py index 23b37ed..f13b52d 100644 --- a/modules/markovmemory.py +++ b/modules/markovmemory.py @@ -3,7 +3,7 @@ import json import markovify import pickle from modules.globalvars import * -from modules.translations import _ +from modules.volta.main import _ # Get file size and line count for a given file path def get_file_info(file_path): diff --git a/modules/prestartchecks.py b/modules/prestartchecks.py index 9e4c8a0..6039a97 100644 --- a/modules/prestartchecks.py +++ b/modules/prestartchecks.py @@ -1,12 +1,16 @@ from modules.globalvars import * -from modules.translations import _ +from modules.volta.main import _, get_translation, load_translations, set_language, translations import time import os import sys import subprocess +import sysconfig import ast import json +import re +import importlib.metadata + # import shutil psutilavaliable = True try: @@ -16,30 +20,56 @@ except ImportError: psutilavaliable = False print(RED, _('missing_requests_psutil'), RESET) +def check_missing_translations(): + if LOCALE == "en": + print("Locale is English, skipping missing key check.") + return + load_translations() -import re -import importlib.metadata + en_keys = set(translations.get("en", {}).keys()) + locale_keys = set(translations.get(LOCALE, {}).keys()) + + missing_keys = en_keys - locale_keys + total_keys = len(en_keys) + missing_count = len(missing_keys) + + if missing_count > 0: + percent_missing = (missing_count / total_keys) * 100 + print(f"{YELLOW}Warning: {missing_count}/{total_keys} keys missing in locale '{LOCALE}' ({percent_missing:.1f}%)!{RESET}") + for key in sorted(missing_keys): + print(f" - {key}") + time.sleep(5) + else: + print("All translation keys present for locale:", LOCALE) +def get_stdlib_modules(): + stdlib_path = pathlib.Path(sysconfig.get_paths()['stdlib']) + modules = set() + if hasattr(sys, 'builtin_module_names'): + modules.update(sys.builtin_module_names) + for file in stdlib_path.glob('*.py'): + if file.stem != '__init__': + modules.add(file.stem) + for folder in stdlib_path.iterdir(): + if folder.is_dir() and (folder / '__init__.py').exists(): + modules.add(folder.name) + for file in stdlib_path.glob('*.*'): + if file.suffix in ('.so', '.pyd'): + modules.add(file.stem) + + return modules def check_requirements(): - STD_LIB_MODULES = { - "os", "sys", "time", "ast", "asyncio", "re", "subprocess", "json", - "datetime", "threading", "math", "logging", "functools", "itertools", - "collections", "shutil", "socket", "types", "enum", "pathlib", - "inspect", "traceback", "platform", "typing", "warnings", "email", - "http", "urllib", "argparse", "io", "copy", "pickle", "gzip", "csv", - } + STD_LIB_MODULES = get_stdlib_modules() PACKAGE_ALIASES = { "discord": "discord.py", "better_profanity": "better-profanity", - } parent_dir = os.path.dirname(os.path.abspath(__file__)) - requirements_path = os.path.join(parent_dir, '..', 'requirements.txt') - requirements_path = os.path.abspath(requirements_path) + requirements_path = os.path.abspath(os.path.join(parent_dir, '..', 'requirements.txt')) if not os.path.exists(requirements_path): print(f"{RED}{(_('requirements_not_found')).format(path=requirements_path)}{RESET}") @@ -52,7 +82,7 @@ def check_requirements(): if line.strip() and not line.startswith('#') } - cogs_dir = os.path.abspath(os.path.join(parent_dir, '..', 'cogs')) + cogs_dir = os.path.abspath(os.path.join(parent_dir, '..', 'assets', 'cogs')) if os.path.isdir(cogs_dir): for filename in os.listdir(cogs_dir): if filename.endswith('.py'): @@ -106,7 +136,7 @@ def check_requirements(): "token": gooberTOKEN } try: - response = requests.post(VERSION_URL + "/ping", json=payload) + requests.post(VERSION_URL + "/ping", json=payload) except Exception as e: print(f"{RED}{(_('failed_to_contact')).format(url=VERSION_URL, error=e)}{RESET}") sys.exit(1) @@ -247,6 +277,7 @@ def start_checks(): print(f"{YELLOW}{(_('checks_disabled'))}{RESET}") return print(_('running_prestart_checks')) + check_missing_translations() check_requirements() check_latency() check_memory() diff --git a/modules/sentenceprocessing.py b/modules/sentenceprocessing.py index 05ae1be..375dc9b 100644 --- a/modules/sentenceprocessing.py +++ b/modules/sentenceprocessing.py @@ -1,6 +1,6 @@ import re from modules.globalvars import * -from modules.translations import _ +from modules.volta.main import _ import spacy from spacy.tokens import Doc diff --git a/modules/translations.py b/modules/translations.py deleted file mode 100644 index fed4a66..0000000 --- a/modules/translations.py +++ /dev/null @@ -1,31 +0,0 @@ -import os -import json -import pathlib -from modules.globalvars import RED, RESET, LOCALE - -# Load translations at module import -def load_translations(): - translations = {} - translations_dir = pathlib.Path(__file__).parent.parent / 'assets' / 'locales' - for filename in os.listdir(translations_dir): - if filename.endswith(".json"): - lang_code = filename.replace(".json", "") - with open(translations_dir / filename, "r", encoding="utf-8") as f: - translations[lang_code] = json.load(f) - return translations - -translations = load_translations() - - -def set_language(lang: str): - global LOCALE - LOCALE = lang if lang in translations else "en" - -def get_translation(lang: str, key: str): - lang_translations = translations.get(lang, translations["en"]) - if key not in lang_translations: - print(f"{RED}Missing key: {key} in language {lang}{RESET}") - return lang_translations.get(key, key) - -def _(key: str) -> str: - return get_translation(LOCALE, key) diff --git a/modules/unhandledexception.py b/modules/unhandledexception.py index eb55b4a..9521e72 100644 --- a/modules/unhandledexception.py +++ b/modules/unhandledexception.py @@ -2,7 +2,7 @@ import sys import traceback import os from modules.globalvars import RED, RESET, splashtext -from modules.translations import _ +from modules.volta.main import _ def handle_exception(exc_type, exc_value, exc_traceback, *, context=None): os.system('cls' if os.name == 'nt' else 'clear') diff --git a/modules/version.py b/modules/version.py index f539bdd..f54f786 100644 --- a/modules/version.py +++ b/modules/version.py @@ -1,4 +1,4 @@ -from modules.translations import _ +from modules.volta.main import _ from modules.globalvars import * import requests import subprocess @@ -18,18 +18,18 @@ def is_remote_ahead(branch='main', remote='origin'): # Automatically update the local repository if the remote is ahead def auto_update(branch='main', remote='origin'): if launched == True: - print((_('already_started'))) + print(_("already_started")) return if AUTOUPDATE != "True": pass # Auto-update is disabled if is_remote_ahead(branch, remote): - print((_('remote_ahead')).format(remote=remote, branch=branch)) + print(_( "remote_ahead").format(remote=remote, branch=branch)) pull_result = run_cmd(f'git pull {remote} {branch}') print(pull_result) - print((_('please_restart'))) + print(_( "please_restart")) sys.exit(0) else: - print((_('local_ahead')).format(remote=remote, branch=branch)) + print(_( "local_ahead").format(remote=remote, branch=branch)) # Fetch the latest version info from the update server def get_latest_version_info(): @@ -38,23 +38,21 @@ def get_latest_version_info(): if response.status_code == 200: return response.json() else: - print(f"{RED}{(_('version_error'))} {response.status_code}{RESET}") + print(f"{RED}{_( 'version_error')} {response.status_code}{RESET}") return None except requests.RequestException as e: - print(f"{RED}{(_('version_error'))} {e}{RESET}") + print(f"{RED}{_( 'version_error')} {e}{RESET}") return None # Check if an update is available and perform update if needed def check_for_update(): - global latest_version, local_version, launched - launched = True if ALIVEPING != "True": - return # Update check is disabled - + return + global latest_version, local_version latest_version_info = get_latest_version_info() if not latest_version_info: - print(f"{(_('fetch_update_fail'))}") + print(f"{_('fetch_update_fail')}") return None, None latest_version = latest_version_info.get("version") @@ -62,20 +60,24 @@ def check_for_update(): download_url = latest_version_info.get("download_url") if not latest_version or not download_url: - print(f"{RED}{(_('invalid_server'))}{RESET}") + print(f"{RED}{_(LOCALE, 'invalid_server')}{RESET}") return None, None # Check if local_version is valid if local_version == "0.0.0" or None: - print(f"{RED}{(_('cant_find_local_version'))}{RESET}") + print(f"{RED}{_('cant_find_local_version')}{RESET}") return # Compare local and latest versions if local_version < latest_version: - print(f"{YELLOW}{(_('new_version')).format(latest_version=latest_version, local_version=local_version)}{RESET}") - print(f"{YELLOW}{(_('changelog')).format(VERSION_URL=VERSION_URL)}{RESET}") + print(f"{YELLOW}{_('new_version').format(latest_version=latest_version, local_version=local_version)}{RESET}") + print(f"{YELLOW}{_('changelog').format(VERSION_URL=VERSION_URL)}{RESET}") auto_update() + elif local_version > latest_version and beta == True: + print(f"{YELLOW}You are running an unstable version of Goober, do not expect it to work properly.\nVersion {local_version}{RESET}") + elif local_version > latest_version: + print(f"{YELLOW}{_('modification_warning')}{RESET}") elif local_version == latest_version: - print(f"{GREEN}{(_('latest_version'))} {local_version}{RESET}") - print(f"{(_('latest_version2')).format(VERSION_URL=VERSION_URL)}\n\n") + print(f"{GREEN}{_('latest_version')} {local_version}{RESET}") + print(f"{_('latest_version2').format(VERSION_URL=VERSION_URL)}\n\n") return latest_version \ No newline at end of file diff --git a/modules/volta/main.py b/modules/volta/main.py new file mode 100644 index 0000000..72d2b11 --- /dev/null +++ b/modules/volta/main.py @@ -0,0 +1,93 @@ +import os +import json +import pathlib +import threading +import time +from dotenv import load_dotenv +from modules.globalvars import RED, RESET + +load_dotenv() + +LOCALE = os.getenv("locale") +module_dir = pathlib.Path(__file__).parent.parent +working_dir = pathlib.Path.cwd() +EXCLUDE_DIRS = {'.git', '__pycache__'} + +locales_dirs = [] + +def find_locales_dirs(base_path): + found = [] + for root, dirs, files in os.walk(base_path): + dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS] + + if 'locales' in dirs: + locales_path = pathlib.Path(root) / 'locales' + found.append(locales_path) + dirs.remove('locales') + return found + +locales_dirs.extend(find_locales_dirs(module_dir)) +if working_dir != module_dir: + locales_dirs.extend(find_locales_dirs(working_dir)) + +translations = {} +_file_mod_times = {} + +def load_translations(): + global translations, _file_mod_times + translations.clear() + _file_mod_times.clear() + + for locales_dir in locales_dirs: + for filename in os.listdir(locales_dir): + if filename.endswith(".json"): + lang_code = filename[:-5] + file_path = locales_dir / filename + try: + with open(file_path, "r", encoding="utf-8") as f: + data = json.load(f) + if lang_code not in translations: + translations[lang_code] = {} + translations[lang_code].update(data) + _file_mod_times[(lang_code, file_path)] = file_path.stat().st_mtime + except Exception as e: + print(f"{RED}Failed loading {file_path}: {e}{RESET}") + +def reload_if_changed(): + while True: + for (lang_code, file_path), last_mtime in list(_file_mod_times.items()): + try: + current_mtime = file_path.stat().st_mtime + if current_mtime != last_mtime: + print(f"{RED}Translation file changed: {file_path}, reloading...{RESET}") + load_translations() + break + except FileNotFoundError: + print(f"{RED}Translation file removed: {file_path}{RESET}") + _file_mod_times.pop((lang_code, file_path), None) + if lang_code in translations: + translations.pop(lang_code, None) + +def set_language(lang: str): + global LOCALE + if lang in translations: + LOCALE = lang + else: + print(f"{RED}Language '{lang}' not found, defaulting to 'en'{RESET}") + LOCALE = "en" + +def get_translation(lang: str, key: str): + lang_translations = translations.get(lang, {}) + if key in lang_translations: + return lang_translations[key] + fallback = translations.get("en", {}).get(key, key) + print(f"{RED}Missing key: '{key}' in language '{lang}', falling back to: '{fallback}'{RESET}") + return fallback + +def _(key: str) -> str: + return get_translation(LOCALE, key) + +load_translations() + +watchdog_thread = threading.Thread(target=reload_if_changed, daemon=True) +watchdog_thread.start()