spun off the translation into its own thing that works with anything

This commit is contained in:
WhatDidYouExpect 2025-07-06 21:06:04 +02:00
parent cd6ce96b36
commit b89390e713
18 changed files with 248 additions and 80 deletions

2
.gitignore vendored
View file

@ -9,3 +9,5 @@ venv/
output.png output.png
.vscode/ .vscode/
received_memory.json received_memory.json
translation_report.txt
translationcompleteness.py

View file

@ -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) [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) 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

View file

@ -1,4 +1,5 @@
{ {
"checks_disabled": "Checks are disabled!",
"unhandled_exception": "An unhandled exception occurred. Please report this issue on GitHub.", "unhandled_exception": "An unhandled exception occurred. Please report this issue on GitHub.",
"active_users:": "Active users:", "active_users:": "Active users:",
"spacy_initialized": "spaCy and spacytextblob are ready.", "spacy_initialized": "spaCy and spacytextblob are ready.",

View file

@ -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.", "env_file_not_found": ".env faili ei leitud! Palun loo see vajalike muutujatega.",
"error_fetching_active_users": "Aktiivsete kasutajate hankimisel tekkis viga: {error}", "error_fetching_active_users": "Aktiivsete kasutajate hankimisel tekkis viga: {error}",
"error_sending_alive_ping": "Elusoleku ping'i saatmisel tekkis viga: {error}", "error_sending_alive_ping": "Elusoleku ping'i saatmisel tekkis viga: {error}",

View file

@ -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.", "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", "env_file_not_found": ".env-tiedostoa ei löytnyt! Luo tiedosto jossa on tarvittavat muuttujat",
"already_started": "Olen jo käynnistynyt! Ei päivitetä...", "already_started": "Olen jo käynnistynyt! Ei päivitetä...",
"please_restart": "Käynnistä uudelleen, hölmö!", "please_restart": "Käynnistä uudelleen, hölmö!",

View file

@ -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 lenvoi du ping actif : {error}",
"already_started": "Jai 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 nest 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": "nest 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 danalyser 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 lusage 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 lespace.",
"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 dencodage 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 !", "modification_warning": "Goober a été modifié ! Toutes les modifications seront perdues lors d'une mise à jour !",
"reported_version": "Version rapportée :", "reported_version": "Version rapportée :",
"current_hash": "Hachage actuel :", "current_hash": "Hachage actuel :",

View file

@ -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.", "unhandled_exception": "Si è verificata un'eccezione non gestita. Segnala questo problema su GitHub, per favore.",
"active_users:": "Utenti attivi:", "active_users:": "Utenti attivi:",
"spacy_initialized": "spaCy e spacytextblob sono pronti.", "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...", "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.", "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:", "error fetching_active_users": "Errore nel recupero degli utenti attivi:",
@ -108,6 +110,7 @@
"command_help_categories_admin": "Amministrazione", "command_help_categories_admin": "Amministrazione",
"command_help_categories_custom": "Comandi personalizzati", "command_help_categories_custom": "Comandi personalizzati",
"command_ran": "Info: {message.author.name} ha eseguito {message.content}", "command_ran": "Info: {message.author.name} ha eseguito {message.content}",
"command_ran_s": "Info: {interaction.user} ha eseguito ",
"command_desc_ping": "ping", "command_desc_ping": "ping",
"command_ping_embed_desc": "Latenza del bot:", "command_ping_embed_desc": "Latenza del bot:",
"command_ping_footer": "Richiesto da", "command_ping_footer": "Richiesto da",

4
bot.py
View file

@ -30,7 +30,7 @@ from better_profanity import profanity
from discord.ext import commands from discord.ext import commands
from modules.central import ping_server from modules.central import ping_server
from modules.translations import _ from modules.volta.main import _
from modules.markovmemory import * from modules.markovmemory import *
from modules.version import * from modules.version import *
from modules.sentenceprocessing 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 # 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}")): 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) await bot.process_commands(message)
return return

View file

@ -12,14 +12,15 @@ gooberTOKEN=
song="War Without Reason" 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" 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=" splashtext="
d8b SS\
?88 SS |
88b SSSSSS\ SSSSSS\ SSSSSS\ SSSSSSS\ SSSSSS\ SSSSSS\
d888b8b d8888b d8888b 888888b d8888b 88bd88b SS __SS\ SS __SS\ SS __SS\ SS __SS\ SS __SS\ SS __SS\
d8P' ?88 d8P' ?88d8P' ?88 88P `?8bd8b_,dP 88P' ` SS / SS |SS / SS |SS / SS |SS | SS |SSSSSSSS |SS | \__|
88b ,88b 88b d8888b d88 d88, d8888b d88 SS | SS |SS | SS |SS | SS |SS | SS |SS ____|SS |
`?88P'`88b`?8888P'`?8888P'd88'`?88P'`?888P'd88' \SSSSSSS |\SSSSSS |\SSSSSS |SSSSSSS |\SSSSSSS\ SS |
)88 \____SS | \______/ \______/ \_______/ \_______|\__|
,88P SS\ SS |
`?8888P \SSSSSS |
\______/
" "

View file

@ -1,7 +1,7 @@
import requests import requests
import os import os
import modules.globalvars as gv import modules.globalvars as gv
from modules.translations import _ from modules.volta.main import _
from modules.markovmemory import get_file_info from modules.markovmemory import get_file_info
# Ping the server to check if it's alive and send some info # Ping the server to check if it's alive and send some info

View file

@ -3,7 +3,7 @@ import json
import markovify import markovify
import pickle import pickle
from modules.globalvars import * from modules.globalvars import *
from modules.translations import _ from modules.volta.main import _
# Get file size and line count for a given file path # Get file size and line count for a given file path
def get_file_info(file_path): def get_file_info(file_path):

View file

@ -1,12 +1,16 @@
from modules.globalvars import * from modules.globalvars import *
from modules.translations import _ from modules.volta.main import _, get_translation, load_translations, set_language, translations
import time import time
import os import os
import sys import sys
import subprocess import subprocess
import sysconfig
import ast import ast
import json import json
import re
import importlib.metadata
# import shutil # import shutil
psutilavaliable = True psutilavaliable = True
try: try:
@ -16,30 +20,56 @@ except ImportError:
psutilavaliable = False psutilavaliable = False
print(RED, _('missing_requests_psutil'), RESET) 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 en_keys = set(translations.get("en", {}).keys())
import importlib.metadata 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(): def check_requirements():
STD_LIB_MODULES = { STD_LIB_MODULES = get_stdlib_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",
}
PACKAGE_ALIASES = { PACKAGE_ALIASES = {
"discord": "discord.py", "discord": "discord.py",
"better_profanity": "better-profanity", "better_profanity": "better-profanity",
} }
parent_dir = os.path.dirname(os.path.abspath(__file__)) parent_dir = os.path.dirname(os.path.abspath(__file__))
requirements_path = os.path.join(parent_dir, '..', 'requirements.txt') requirements_path = os.path.abspath(os.path.join(parent_dir, '..', 'requirements.txt'))
requirements_path = os.path.abspath(requirements_path)
if not os.path.exists(requirements_path): if not os.path.exists(requirements_path):
print(f"{RED}{(_('requirements_not_found')).format(path=requirements_path)}{RESET}") 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('#') 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): if os.path.isdir(cogs_dir):
for filename in os.listdir(cogs_dir): for filename in os.listdir(cogs_dir):
if filename.endswith('.py'): if filename.endswith('.py'):
@ -106,7 +136,7 @@ def check_requirements():
"token": gooberTOKEN "token": gooberTOKEN
} }
try: try:
response = requests.post(VERSION_URL + "/ping", json=payload) requests.post(VERSION_URL + "/ping", json=payload)
except Exception as e: except Exception as e:
print(f"{RED}{(_('failed_to_contact')).format(url=VERSION_URL, error=e)}{RESET}") print(f"{RED}{(_('failed_to_contact')).format(url=VERSION_URL, error=e)}{RESET}")
sys.exit(1) sys.exit(1)
@ -247,6 +277,7 @@ def start_checks():
print(f"{YELLOW}{(_('checks_disabled'))}{RESET}") print(f"{YELLOW}{(_('checks_disabled'))}{RESET}")
return return
print(_('running_prestart_checks')) print(_('running_prestart_checks'))
check_missing_translations()
check_requirements() check_requirements()
check_latency() check_latency()
check_memory() check_memory()

View file

@ -1,6 +1,6 @@
import re import re
from modules.globalvars import * from modules.globalvars import *
from modules.translations import _ from modules.volta.main import _
import spacy import spacy
from spacy.tokens import Doc from spacy.tokens import Doc

View file

@ -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)

View file

@ -2,7 +2,7 @@ import sys
import traceback import traceback
import os import os
from modules.globalvars import RED, RESET, splashtext 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): def handle_exception(exc_type, exc_value, exc_traceback, *, context=None):
os.system('cls' if os.name == 'nt' else 'clear') os.system('cls' if os.name == 'nt' else 'clear')

View file

@ -1,4 +1,4 @@
from modules.translations import _ from modules.volta.main import _
from modules.globalvars import * from modules.globalvars import *
import requests import requests
import subprocess import subprocess
@ -18,18 +18,18 @@ def is_remote_ahead(branch='main', remote='origin'):
# Automatically update the local repository if the remote is ahead # Automatically update the local repository if the remote is ahead
def auto_update(branch='main', remote='origin'): def auto_update(branch='main', remote='origin'):
if launched == True: if launched == True:
print((_('already_started'))) print(_("already_started"))
return return
if AUTOUPDATE != "True": if AUTOUPDATE != "True":
pass # Auto-update is disabled pass # Auto-update is disabled
if is_remote_ahead(branch, remote): 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}') pull_result = run_cmd(f'git pull {remote} {branch}')
print(pull_result) print(pull_result)
print((_('please_restart'))) print(_( "please_restart"))
sys.exit(0) sys.exit(0)
else: 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 # Fetch the latest version info from the update server
def get_latest_version_info(): def get_latest_version_info():
@ -38,23 +38,21 @@ def get_latest_version_info():
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
else: else:
print(f"{RED}{(_('version_error'))} {response.status_code}{RESET}") print(f"{RED}{_( 'version_error')} {response.status_code}{RESET}")
return None return None
except requests.RequestException as e: except requests.RequestException as e:
print(f"{RED}{(_('version_error'))} {e}{RESET}") print(f"{RED}{_( 'version_error')} {e}{RESET}")
return None return None
# Check if an update is available and perform update if needed # Check if an update is available and perform update if needed
def check_for_update(): def check_for_update():
global latest_version, local_version, launched
launched = True
if ALIVEPING != "True": if ALIVEPING != "True":
return # Update check is disabled return
global latest_version, local_version
latest_version_info = get_latest_version_info() latest_version_info = get_latest_version_info()
if not latest_version_info: if not latest_version_info:
print(f"{(_('fetch_update_fail'))}") print(f"{_('fetch_update_fail')}")
return None, None return None, None
latest_version = latest_version_info.get("version") latest_version = latest_version_info.get("version")
@ -62,20 +60,24 @@ def check_for_update():
download_url = latest_version_info.get("download_url") download_url = latest_version_info.get("download_url")
if not latest_version or not 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 return None, None
# Check if local_version is valid # Check if local_version is valid
if local_version == "0.0.0" or None: 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 return
# Compare local and latest versions # Compare local and latest versions
if local_version < latest_version: if local_version < latest_version:
print(f"{YELLOW}{(_('new_version')).format(latest_version=latest_version, local_version=local_version)}{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}") print(f"{YELLOW}{_('changelog').format(VERSION_URL=VERSION_URL)}{RESET}")
auto_update() 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: elif local_version == latest_version:
print(f"{GREEN}{(_('latest_version'))} {local_version}{RESET}") print(f"{GREEN}{_('latest_version')} {local_version}{RESET}")
print(f"{(_('latest_version2')).format(VERSION_URL=VERSION_URL)}\n\n") print(f"{_('latest_version2').format(VERSION_URL=VERSION_URL)}\n\n")
return latest_version return latest_version

93
modules/volta/main.py Normal file
View file

@ -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()