2025-07-06 21:42:45 +02:00
# If you're seeing this after cloning the Goober repo, note that this is a standalone module for translations.
# While it's used by Goober Core, it lives in its own repository and should not be modified here.
# For updates or contributions, visit: https://github.com/gooberinc/volta
2025-07-07 00:33:36 +02:00
# Also, Note to self: Add more comments it needs more love
2025-07-06 21:06:04 +02:00
import os
import json
import pathlib
import threading
import time
from dotenv import load_dotenv
2025-07-06 21:10:30 +02:00
ANSI = " \033 [ "
RED = f " { ANSI } 31m "
GREEN = f " { ANSI } 32m "
YELLOW = f " { ANSI } 33m "
DEBUG = f " { ANSI } 1;30m "
RESET = f " { ANSI } 0m "
2025-07-06 21:06:04 +02:00
load_dotenv ( )
2025-07-09 22:57:59 +02:00
LOCALE = os . getenv ( " LOCALE " )
2025-07-06 21:06:04 +02:00
module_dir = pathlib . Path ( __file__ ) . parent . parent
working_dir = pathlib . Path . cwd ( )
EXCLUDE_DIRS = { ' .git ' , ' __pycache__ ' }
locales_dirs = [ ]
2025-07-07 12:13:25 +02:00
ENGLISH_MISSING = False
2025-07-07 12:38:32 +02:00
FALLBACK_LOCALE = " en "
if os . getenv ( " fallback_locale " ) :
FALLBACK_LOCALE = os . getenv ( " fallback_locale " )
2025-07-06 21:06:04 +02:00
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
2025-07-07 16:54:19 +02:00
def find_dotenv ( start_path : pathlib . Path ) - > pathlib . Path | None :
current = start_path . resolve ( )
while current != current . parent :
candidate = current / " .env "
if candidate . exists ( ) :
return candidate
current = current . parent
return None
env_path = find_dotenv ( pathlib . Path ( __file__ ) . parent )
if env_path :
load_dotenv ( dotenv_path = env_path )
print ( f " [VOLTA] { GREEN } Loaded .env from { env_path } { RESET } " )
else :
print ( f " [VOLTA] { YELLOW } No .env file found from { __file__ } upwards. { RESET } " )
2025-07-06 21:06:04 +02:00
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 :
2025-07-07 00:16:36 +02:00
print ( f " [VOLTA] { RED } Failed loading { file_path } : { e } { RESET } " )
2025-07-06 21:06:04 +02:00
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 :
2025-07-07 00:16:36 +02:00
print ( f " [VOLTA] { RED } Translation file changed: { file_path } , reloading... { RESET } " )
2025-07-06 21:06:04 +02:00
load_translations ( )
break
except FileNotFoundError :
2025-07-07 00:16:36 +02:00
print ( f " [VOLTA] { RED } Translation file removed: { file_path } { RESET } " )
2025-07-06 21:06:04 +02:00
_file_mod_times . pop ( ( lang_code , file_path ) , None )
if lang_code in translations :
translations . pop ( lang_code , None )
def set_language ( lang : str ) :
2025-07-07 12:13:25 +02:00
global LOCALE , ENGLISH_MISSING
2025-07-06 21:06:04 +02:00
if lang in translations :
LOCALE = lang
else :
2025-07-07 00:16:36 +02:00
print ( f " [VOLTA] { RED } Language ' { lang } ' not found, defaulting to ' en ' { RESET } " )
2025-07-07 12:38:32 +02:00
if FALLBACK_LOCALE in translations :
LOCALE = FALLBACK_LOCALE
2025-07-07 12:13:25 +02:00
else :
2025-07-07 12:38:32 +02:00
print ( f " [VOLTA] { RED } The fallback translations cannot be found! No fallback available. { RESET } " )
2025-07-07 12:13:25 +02:00
ENGLISH_MISSING = True
def check_missing_translations ( ) :
global LOCALE , ENGLISH_MISSING
load_translations ( )
2025-07-07 12:38:32 +02:00
if FALLBACK_LOCALE not in translations :
2025-07-07 16:54:19 +02:00
print ( f " [VOLTA] { RED } Fallback translations ( { FALLBACK_LOCALE } .json) missing from assets/locales. { RESET } " )
2025-07-07 12:13:25 +02:00
ENGLISH_MISSING = True
return
if LOCALE == " en " :
2025-07-07 20:39:39 +02:00
print ( " [VOLTA] Locale is English, skipping missing key check. " )
2025-07-07 12:13:25 +02:00
return
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
if percent_missing == 100 :
2025-07-07 16:54:19 +02:00
print ( f " [VOLTA] { YELLOW } Warning: All keys are missing in locale ' { LOCALE } ' ! Defaulting back to { FALLBACK_LOCALE } { RESET } " )
2025-07-07 12:38:32 +02:00
set_language ( FALLBACK_LOCALE )
2025-07-07 12:13:25 +02:00
elif percent_missing > 0 :
2025-07-07 20:39:39 +02:00
print ( f " [VOLTA] { YELLOW } Warning: { missing_count } / { total_keys } keys missing in locale ' { LOCALE } ' ( { percent_missing : .1f } %)! { RESET } " )
2025-07-07 12:13:25 +02:00
for key in sorted ( missing_keys ) :
print ( f " - { key } " )
time . sleep ( 2 )
else :
2025-07-07 20:39:39 +02:00
print ( f " [VOLTA] All translation keys present for locale: { LOCALE } " )
2025-07-07 12:13:25 +02:00
2025-07-06 21:06:04 +02:00
def get_translation ( lang : str , key : str ) :
2025-07-07 12:13:25 +02:00
if ENGLISH_MISSING :
return f " [VOLTA] { RED } No fallback available! { RESET } "
2025-07-06 21:06:04 +02:00
lang_translations = translations . get ( lang , { } )
if key in lang_translations :
return lang_translations [ key ]
2025-07-07 12:24:14 +02:00
else :
2025-07-07 12:38:32 +02:00
if key not in translations . get ( FALLBACK_LOCALE , { } ) :
2025-07-07 16:54:19 +02:00
return f " [VOLTA] { YELLOW } Missing key: ' { key } ' in { FALLBACK_LOCALE } .json! { RESET } "
2025-07-07 12:38:32 +02:00
fallback = translations . get ( FALLBACK_LOCALE , { } ) . get ( key , key )
2025-07-07 16:54:19 +02:00
print ( f " [VOLTA] { YELLOW } Missing key: ' { key } ' in language ' { lang } ' , falling back to: ' { fallback } ' using { FALLBACK_LOCALE } .json { RESET } " ) # yeah probably print this
2025-07-06 21:06:04 +02:00
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 ( )
2025-07-07 12:13:25 +02:00
2025-07-07 12:38:32 +02:00
if __name__ == ' __main__ ' :
print ( " Volta should not be run directly! Please use it as a module.. " )