# Libraries import json import os import re import shutil import util.dazedwrap as dazedwrap import threading import time import traceback import openai from pathlib import Path from colorama import Fore from dotenv import load_dotenv from retry import retry from tqdm import tqdm from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost # Open AI load_dotenv() if os.getenv("api").replace(" ", "") != "": openai.base_url = os.getenv("api") openai.organization = os.getenv("org") openai.api_key = os.getenv("key") # Globals MODEL = os.getenv("model") TIMEOUT = int(os.getenv("timeout")) LANGUAGE = os.getenv("language").capitalize() PROMPT = Path("prompt.txt").read_text(encoding="utf-8") VOCAB = Path("vocab.txt").read_text(encoding="utf-8") THREADS = int(os.getenv("threads")) LOCK = threading.Lock() VOCAB_LOCK = threading.Lock() # Dedicated lock for vocab.txt updates WIDTH = int(os.getenv("width")) LISTWIDTH = int(os.getenv("listWidth")) NOTEWIDTH = int(os.getenv("noteWidth")) MAXHISTORY = 10 ESTIMATE = "" TOKENS = [0, 0] MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) NAMESLIST = [] # List of speaker names and their translations PBAR = None FILENAME = None TIMETOTAL = 0 # Total Time Taken for all translations # Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex LANGREGEX = r"[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uFF00-\uFF5D\uFF5F-\uFFEF]+" # Get pricing configuration based on the model PRICING_CONFIG = getPricingConfig(MODEL) INPUTAPICOST = PRICING_CONFIG["inputAPICost"] OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"] BATCHSIZE = PRICING_CONFIG["batchSize"] FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"] # tqdm Globals BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" POSITION = 0 LEAVE = False # Initialize Translation Config TRANSLATION_CONFIG = TranslationConfig( model=MODEL, language=LANGUAGE, prompt=PROMPT, vocab=VOCAB, langRegex=LANGREGEX, batchSize=BATCHSIZE, maxHistory=MAXHISTORY, estimateMode=False # Will be set dynamically based on ESTIMATE ) # Config (Default) FIXTEXTWRAP = True # Rewrap text to WIDTH IGNORETLTEXT = False # Skip Translated Text # List of file patterns that use parseGeneric # Add more patterns here as needed GENERIC_FILES = [ "quests", "shops", "shoplayout", "bonuses", "base", "battleprep", "manage", "mapcommands", "title", "classes", "classesgroups", "classtypes", "races", "skills", "weapons", "states", "difficulties", "fonts", "fusionsettings", "transformations", "placeevents", "talkevents", "characters", "glossary", "npc", "screens", "strings", "originalterrains", "runtimeterrains", "archers", "fighters", "mages", "items", ] # List of file patterns that use parseMap # Be specific to avoid catching non-map files like CommandLayout/mapcommands.json # SRPG Studio map files in this project follow the pattern Maps/map_XXX.json MAP_FILES = [ "map_", # e.g., Maps/map_000.json ] def update_vocab_section(category: str, pairs: list[tuple[str, str]]): """Update or insert a section in vocab.txt for the given category with provided pairs. Only writes when there's an actual translation (dst is non-empty and differs from src after normalization). - category: e.g., "Items", "Weapons", "Speakers", etc. Section header will be "# {category}". - pairs: list of (source, translated) strings. Duplicates by source are deduped (last wins). The existing section is replaced entirely; other sections are preserved. """ try: vocab_path = Path("vocab.txt") # Helper: normalized comparison to detect no-op translations def _norm(s: str) -> str: if s is None: return "" # Collapse whitespace and case-fold; leave punctuation to avoid over-matching return re.sub(r"\s+", " ", str(s)).strip().casefold() # Filter and deduplicate by source term (last mapping wins) dedup: dict[str, str] = {} for src, dst in pairs: if not src: continue # Skip when no destination or no actual change if dst is None or _norm(dst) == "" or _norm(dst) == _norm(src): continue dedup[src] = dst # If nothing to add after filtering, skip touching the file if not dedup: return # Guard the read-modify-write with a dedicated lock to avoid races with VOCAB_LOCK: existing = vocab_path.read_text(encoding="utf-8") if vocab_path.exists() else "" lines = [f"{src} ({dst})" for src, dst in dedup.items()] # Always terminate a section with a blank line to separate from next header new_block = f"# {category}\n" + "\n".join(lines) if not new_block.endswith("\n\n"): if not new_block.endswith("\n"): new_block += "\n" new_block += "\n" # Regex to find the specific section starting at the header for this category # and ending right before the next header (any number of '#') or EOF. # - Handles headers like '#Category', '# Category', '## Category', etc. # - Uses non-greedy matching for the body to avoid spanning multiple sections. pattern = re.compile( rf"^[\t ]*#+\s*{re.escape(category)}\s*$\r?\n.*?(?=^[\t ]*#|\Z)", re.MULTILINE | re.DOTALL, ) if pattern.search(existing): # Replace only the first matching section for this category. updated = pattern.sub(lambda m: new_block, existing, count=1) else: updated = existing if updated and not updated.endswith("\n\n"): # Ensure a blank line before appending new section if file not empty if not updated.endswith("\n"): updated += "\n" updated += "\n" updated += new_block # Avoid writing if nothing changed if updated == existing: return # Atomic write: write to unique temp and replace with retries on Windows tmp_path = vocab_path.with_suffix(vocab_path.suffix + f".{os.getpid()}.{threading.get_ident()}.tmp") tmp_path.write_text(updated, encoding="utf-8") attempts = 6 delay = 0.1 last_err = None for attempt in range(attempts): try: os.replace(tmp_path, vocab_path) last_err = None break except PermissionError as e: last_err = e # Try relaxing permissions then retry try: if vocab_path.exists(): os.chmod(vocab_path, 0o666) except Exception: pass time.sleep(delay) delay = min(1.0, delay * 2) except Exception as e: last_err = e break if last_err is not None: try: shutil.move(str(tmp_path), str(vocab_path)) except Exception: try: if tmp_path.exists(): tmp_path.unlink(missing_ok=True) except Exception: pass raise last_err except Exception: traceback.print_exc() def handleSRPG(filename, estimate): """ Main handler function for SRPG Studio files. Args: filename: Name of the file to translate estimate: Boolean indicating if this is an estimate run Returns: String with translation results or error message """ global ESTIMATE, TOKENS, FILENAME, TIMETOTAL ESTIMATE = estimate FILENAME = filename # Translate start = time.time() translatedData = openFiles(filename) # Write output file if not in estimate mode if not estimate: try: with open("translated/" + filename, "w", encoding="utf-8", newline="\n") as outFile: json.dump(translatedData[0], outFile, ensure_ascii=False, indent=4) except Exception: traceback.print_exc() return "Fail" # Print File end = time.time() tqdm.write(getResultString(translatedData, end - start, filename)) with LOCK: TOKENS[0] += translatedData[1][0] TOKENS[1] += translatedData[1][1] # Print Total totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") # Print any errors if len(MISMATCH) > 0: return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET else: return totalString def openFiles(filename): """ Opens and routes SRPG Studio files to appropriate parsing functions. Args: filename: Name of the file to open and parse Returns: Tuple of (translated data, token counts, error) """ with open("files/" + filename, "r", encoding="utf-8-sig") as f: data = json.load(f) # Check if filename matches recollection pattern if "recollection" in filename.lower(): translatedData = parseRecollection(data, filename) # mapcommonevents is a list of events like recollection elif "mapcommonevents" in filename.lower(): translatedData = parseRecollection(data, filename) # Bookmark events use the same structure as recollection (list of events with pages/commands) elif "bookmarkevents" in filename.lower(): translatedData = parseRecollection(data, filename) # Check if filename matches bookmark pattern (top-level entries with name/desc + events) elif "bookmark" in filename.lower(): translatedData = parseBookmark(data, filename) # Players have the same shape as bookmark entries elif "players" in filename.lower(): translatedData = parseBookmark(data, filename) # Titles.json is a small dict of title strings elif os.path.basename(filename).lower() == "titles.json": translatedData = parseTitles(data, filename) # Check if filename matches map pattern elif any(pattern in filename.lower() for pattern in MAP_FILES): translatedData = parseMap(data, filename) # Check if filename matches any pattern in GENERIC_FILES elif any(pattern in filename.lower() for pattern in GENERIC_FILES): translatedData = parseGeneric(data, filename) # TODO: Add other SRPG Studio file types here else: raise NameError(filename + " Not Supported") return translatedData def parseBookmark(data, filename): """ Parser for SRPG Studio bookmark.json files. Structure: List of entries, each with id, name, desc, and events containing pages -> commands where commands have data (array of strings) and optional speaker. Args: data: Parsed JSON data (list of bookmark entries) filename: Name of the file being parsed Returns: Tuple of (data, token counts, error) """ totalTokens = [0, 0] pbar = None try: # Count work units: names, descs, dialogue lines, and speakers total_units = 0 for entry in data: if not entry: continue # name and desc for field in ["name", "desc"]: if field in entry and entry[field]: total_units += 1 # events -> pages -> commands if "events" in entry and isinstance(entry["events"], list): for event in entry["events"]: if not event: continue # Count event-level name and desc fields for field in ["name", "desc"]: if field in event and event[field]: total_units += 1 if "pages" not in event or not isinstance(event["pages"], list): continue for page in event["pages"]: if not page or "commands" not in page or not isinstance(page["commands"], list): continue for command in page["commands"]: if not command: continue if "data" in command and isinstance(command["data"], list): for text in command["data"]: if text: total_units += 1 if "speaker" in command and command["speaker"]: total_units += 1 # Setup progress bar with LOCK: pbar = tqdm( desc=filename, total=total_units, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, ) # Translate using two-pass approach result = translateBookmark(data, filename, pbar=pbar) totalTokens[0] += result[0] totalTokens[1] += result[1] return (data, totalTokens, None) except Exception as e: traceback.print_exc() return (data, totalTokens, e) finally: try: if pbar is not None: pbar.close() except Exception: pass def translateBookmark(data, filename, translatedDataList=None, pbar=None): """ Translates bookmark.json data structure. Two-pass approach via recursion: - Pass 1: Collect strings (names, descs, dialogue data with speaker prefix, speakers) - Pass 2: Apply translations back into data Returns: [input tokens, output tokens] """ totalTokens = [0, 0] # Initialize or extract lists if translatedDataList is None: nameList = [] descList = [] dataList = [] speakerList = [] else: nameList = translatedDataList[0] descList = translatedDataList[1] dataList = translatedDataList[2] speakerList = translatedDataList[3] for entry in data: if not entry: continue # name if "name" in entry and entry["name"]: if translatedDataList is None: nameList.append(entry["name"]) else: if nameList: entry["name"] = nameList[0] nameList.pop(0) # desc if "desc" in entry and entry["desc"]: if translatedDataList is None: # Remove newlines for translation descList.append(entry["desc"].replace("\n", " ")) else: if descList: translatedText = descList[0] # Apply text wrapping translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) entry["desc"] = translatedText descList.pop(0) # events -> pages -> commands if "events" in entry and isinstance(entry["events"], list): for event in entry["events"]: if not event: continue # Handle event-level name field if "name" in event and event["name"]: if translatedDataList is None: nameList.append(event["name"]) else: if nameList: event["name"] = nameList[0] nameList.pop(0) # Handle event-level desc field if "desc" in event and event["desc"]: if translatedDataList is None: # Remove newlines for translation descList.append(event["desc"].replace("\n", " ")) else: if descList: translatedText = descList[0] # Apply text wrapping translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) event["desc"] = translatedText descList.pop(0) if "pages" not in event or not isinstance(event["pages"], list): continue for page in event["pages"]: if not page or "commands" not in page or not isinstance(page["commands"], list): continue for command in page["commands"]: if not command: continue speaker = command.get("speaker", "") # data array if "data" in command and isinstance(command["data"], list): for i, text in enumerate(command["data"]): if text: if translatedDataList is None: text = text.replace("\n", " ") if speaker: dataList.append(f"[{speaker}]: {text}") else: dataList.append(text) else: if dataList: translated = dataList[0] if speaker: match = re.search(r'(^\[.+?\]\s?[|:]\s?)', translated) if match: translated = translated.replace(match.group(1), "") translated = dazedwrap.wrapText(translated, width=WIDTH) command["data"][i] = translated dataList.pop(0) # speaker field if "speaker" in command and command["speaker"]: if translatedDataList is None: speakerList.append(command["speaker"]) else: if speakerList: command["speaker"] = speakerList[0] speakerList.pop(0) # If this was Pass 1, perform translations and recurse if translatedDataList is None: originalNameCount = len(nameList) originalDescCount = len(descList) originalDataCount = len(dataList) originalSpeakerCount = len(speakerList) if nameList: response = translateAI( nameList, "Reply with only the " + LANGUAGE + " translation of the bookmark name.", True, filename, pbar, ) nameList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] if descList: response = translateAI( descList, "Reply with only the " + LANGUAGE + " translation of the bookmark description.", True, filename, pbar, ) descList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] if dataList: response = translateAI( dataList, "Reply with only the " + LANGUAGE + " translation of the dialogue text.", True, filename, pbar, ) dataList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] if speakerList: response = translateAI( speakerList, "Reply with only the " + LANGUAGE + " translation of the speaker name.", True, filename, pbar, ) speakerList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Mismatch checks if len(nameList) != originalNameCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(descList) != originalDescCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(dataList) != originalDataCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(speakerList) != originalSpeakerCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) # PASS 2 translateBookmark(data, filename, [nameList, descList, dataList, speakerList], pbar) return totalTokens def parseGeneric(data, filename): """ Generic parser for SRPG Studio files with id, name, desc structure. Handles files like quests.json. Uses a two-pass approach: first pass collects all strings to translate, then batch translates them, then second pass applies translations. Args: data: Parsed JSON data (list of objects with id, name, desc) filename: Name of the file being parsed Returns: Tuple of (data, token counts, error) """ global PBAR totalTokens = [0, 0] pbar = None try: # Count work units (all translatable fields that need translation) total_units = 0 translatable_fields = ["name", "desc", "commandName", "command"] for entry in data: if entry: for field in translatable_fields: if field in entry and entry[field]: # If command is a list (e.g., fusionsettings), count per element if field == "command" and isinstance(entry[field], list): total_units += sum(1 for x in entry[field] if x) # Otherwise simple increment elif not isinstance(entry[field], list): total_units += 1 # Handle pages array separately if "pages" in entry and entry["pages"] and isinstance(entry["pages"], list): for page in entry["pages"]: if page: total_units += 1 # Handle msg arrays (e.g., shoplayout) if "msg" in entry and isinstance(entry["msg"], list): total_units += sum(1 for m in entry["msg"] if m) # Handle terrains array (nested structure) if "terrains" in entry and entry["terrains"] and isinstance(entry["terrains"], list): for terrain in entry["terrains"]: if terrain: for field in ["name", "desc"]: if field in terrain and terrain[field]: total_units += 1 # Setup progress bar (use a per-file instance to avoid cross-thread clashes) with LOCK: pbar = tqdm( desc=filename, total=total_units, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, ) # Translate the data using two-pass approach result = translateGeneric(data, filename, pbar=pbar) totalTokens[0] += result[0] totalTokens[1] += result[1] return (data, totalTokens, None) except Exception as e: traceback.print_exc() return (data, totalTokens, e) finally: # Ensure progress bar is closed try: if pbar is not None: pbar.close() except Exception: pass def translateGeneric(data, filename, translatedDataList=None, pbar=None): """ Translates generic SRPG Studio data with id, name, desc, commandName, command, pages structure. Uses two-pass approach via recursion: - Pass 1 (translatedDataList=None): Collect strings and batch translate - Pass 2 (translatedDataList set): Apply translations back to the data Args: data: List of objects with id, name, desc, commandName, command, pages keys filename: Name of the file being translated translatedDataList: List containing [nameList, descList, commandNameList, commandList, pagesList] - Pass 1: Empty lists to collect originals - Pass 2: Filled lists with translations Returns: Tuple of [input tokens, output tokens] """ global PBAR totalTokens = [0, 0] # Initialize or extract lists if translatedDataList is None: # PASS 1: Create empty lists to collect strings nameList = [] descList = [] commandNameList = [] commandList = [] pagesList = [] msgList = [] commandArrayList = [] else: # PASS 2: Use provided translated lists nameList = translatedDataList[0] descList = translatedDataList[1] commandNameList = translatedDataList[2] commandList = translatedDataList[3] pagesList = translatedDataList[4] msgList = translatedDataList[5] commandArrayList = translatedDataList[6] # Single loop - behavior depends on which pass we're in for entry in data: if not entry: continue # Handle name field if "name" in entry and entry["name"]: # PASS 1: Collect original if translatedDataList is None: nameList.append(entry["name"]) # PASS 2: Apply translation else: if nameList: entry["name"] = nameList[0] nameList.pop(0) # Handle desc field if "desc" in entry and entry["desc"]: # PASS 1: Collect original if translatedDataList is None: # Nuke Wordwrap descList.append(entry["desc"].replace("\n", " ")) # PASS 2: Apply translation else: if descList: translatedText = descList[0] # Wordwrap translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) # Set Data entry["desc"] = translatedText descList.pop(0) # Handle commandName field if "commandName" in entry and entry["commandName"]: # PASS 1: Collect original if translatedDataList is None: commandNameList.append(entry["commandName"]) # PASS 2: Apply translation else: if commandNameList: entry["commandName"] = commandNameList[0] commandNameList.pop(0) # Handle command field (string or list) if "command" in entry and entry["command"] is not None: # If list of strings if isinstance(entry["command"], list): for i, val in enumerate(entry["command"]): if val: if translatedDataList is None: # Nuke Wrap commandArrayList.append(val.replace("\n", " ")) else: if commandArrayList: translatedText = dazedwrap.wrapText(commandArrayList[0], width=WIDTH) entry["command"][i] = translatedText commandArrayList.pop(0) # If simple string elif isinstance(entry["command"], str): if translatedDataList is None: commandList.append(entry["command"]) else: if commandList: entry["command"] = commandList[0] commandList.pop(0) # Handle pages field (array of strings) if "pages" in entry and entry["pages"] and isinstance(entry["pages"], list): for i, page in enumerate(entry["pages"]): if page: # PASS 1: Collect original if translatedDataList is None: # Nuke Wordwrap page = page.replace("\n", " ") pagesList.append(page) # PASS 2: Apply translation else: if pagesList: translatedText = pagesList[0] # Wordwrap translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) # Set Data entry["pages"][i] = translatedText pagesList.pop(0) # Handle msg field (array of strings) if "msg" in entry and isinstance(entry["msg"], list): for i, val in enumerate(entry["msg"]): if val is not None: if translatedDataList is None: msgList.append(str(val).replace("\n", " ")) else: if msgList: translatedText = dazedwrap.wrapText(msgList[0], width=WIDTH) entry["msg"][i] = translatedText msgList.pop(0) # Handle terrains field (nested array with name and desc) if "terrains" in entry and entry["terrains"] and isinstance(entry["terrains"], list): for terrain in entry["terrains"]: if not terrain: continue # Handle terrain name if "name" in terrain and terrain["name"]: # PASS 1: Collect original if translatedDataList is None: nameList.append(terrain["name"]) # PASS 2: Apply translation else: if nameList: terrain["name"] = nameList[0] nameList.pop(0) # Handle terrain desc if "desc" in terrain and terrain["desc"]: # PASS 1: Collect original if translatedDataList is None: descList.append(terrain["desc"]).replace("\n", " ") # PASS 2: Apply translation else: if descList: translatedText = descList[0] # Wordwrap translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) # Set Data terrain["desc"] = translatedText descList.pop(0) # If this was Pass 1, do the translation and recurse for Pass 2 if translatedDataList is None: # Store original counts for mismatch checking originalNameCount = len(nameList) originalDescCount = len(descList) originalCommandNameCount = len(commandNameList) originalCommandCount = len(commandList) originalPagesCount = len(pagesList) originalMsgCount = len(msgList) originalCommandArrayCount = len(commandArrayList) # Keep a copy of original names for vocab update (for characters/items/skills/classes/weapons) vocab_name_files = ["characters", "items", "skills", "classes", "weapons"] originalNameList = nameList.copy() if any(tag in filename.lower() for tag in vocab_name_files) else [] # Batch translate names if nameList: response = translateAI( nameList, "Reply with only the " + LANGUAGE + " translation of the quest name.", True, filename, pbar ) nameList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Update vocab.txt for name-bearing files if originalNameList and nameList: try: file_lower = filename.lower() section = None if "characters" in file_lower: section = "Speakers" elif "items" in file_lower: section = "Items" elif "skills" in file_lower: section = "Skills" elif "classes" in file_lower: section = "Classes" elif "weapons" in file_lower: section = "Weapons" if section: vocab_pairs = list(zip(originalNameList, nameList)) update_vocab_section(section, vocab_pairs) except Exception: traceback.print_exc() # Batch translate descriptions if descList: response = translateAI( descList, "Reply with only the " + LANGUAGE + " translation of the quest description.", True, filename, pbar ) descList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate command names if commandNameList: response = translateAI( commandNameList, "Reply with only the " + LANGUAGE + " translation of the command name.", True, filename, pbar ) commandNameList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate commands if commandList: response = translateAI( commandList, "Reply with only the " + LANGUAGE + " translation of the command.", True, filename, pbar ) commandList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate pages if pagesList: response = translateAI( pagesList, "Reply with only the " + LANGUAGE + " translation of the page content.", True, filename, pbar ) pagesList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate msg arrays if msgList: response = translateAI( msgList, "Reply with only the " + LANGUAGE + " translation of the message text.", True, filename, pbar ) msgList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate command arrays if commandArrayList: response = translateAI( commandArrayList, "Reply with only the " + LANGUAGE + " translation of the command text.", True, filename, pbar ) commandArrayList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Check for mismatch errors if len(nameList) != originalNameCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(descList) != originalDescCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(commandNameList) != originalCommandNameCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(commandList) != originalCommandCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(pagesList) != originalPagesCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(msgList) != originalMsgCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(commandArrayList) != originalCommandArrayCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) # PASS 2: Recursively call to apply translations translateGeneric( data, filename, [nameList, descList, commandNameList, commandList, pagesList, msgList, commandArrayList], pbar, ) return totalTokens def parseTitles(data, filename): """Parser for titles.json (small dict of title strings).""" totalTokens = [0, 0] pbar = None try: # Collect fields to translate keys = [k for k in ["windowTitle", "gameTitle", "saveFileTitle"] if k in data and data[k]] values = [data[k] for k in keys] with LOCK: pbar = tqdm( desc=filename, total=len(values), bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, ) if values: response = translateAI( values, "Reply with only the " + LANGUAGE + " translation of the title text.", True, filename, pbar, ) translations = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Apply for i, k in enumerate(keys): data[k] = translations[i] return (data, totalTokens, None) except Exception as e: traceback.print_exc() return (data, totalTokens, e) finally: try: if pbar is not None: pbar.close() except Exception: pass def parseRecollection(data, filename): """ Parser for SRPG Studio recollection.json files. Structure: List of entries, each with pages containing commands with data arrays and speakers. Args: data: Parsed JSON data (list of objects with id, pages structure) filename: Name of the file being parsed Returns: Tuple of (data, token counts, error) """ global PBAR totalTokens = [0, 0] pbar = None try: # Count work units (data entries and speakers that need translation) total_units = 0 # Iterate through structure and count for entry in data: if not entry: continue # Count name and desc fields at entry level for field in ["name", "desc"]: if field in entry and entry[field]: total_units += 1 # Count customParameters hints if "customParameters" in entry and entry["customParameters"]: match = re.search(r'hint:"((?:[^"\\]|\\.)*)"', entry["customParameters"]) if match: total_units += 1 if "pages" not in entry or not isinstance(entry["pages"], list): continue for page in entry["pages"]: if not page or "commands" not in page or not isinstance(page["commands"], list): continue for command in page["commands"]: if not command: continue # Count data array items (count any non-empty text; do not gate on LANGREGEX) if "data" in command and isinstance(command["data"], list): for text in command["data"]: if text: total_units += 1 # Count speaker field (count any non-empty speaker; do not gate on LANGREGEX) if "speaker" in command and command["speaker"]: total_units += 1 # Setup progress bar (per-file instance) with LOCK: pbar = tqdm( desc=filename, total=total_units, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, ) # Translate the data using two-pass approach result = translateRecollection(data, filename, pbar=pbar) totalTokens[0] += result[0] totalTokens[1] += result[1] return (data, totalTokens, None) except Exception as e: traceback.print_exc() return (data, totalTokens, e) finally: try: if pbar is not None: pbar.close() except Exception: pass def translateRecollection(data, filename, translatedDataList=None, pbar=None): """ Translates recollection.json data structure. Uses two-pass approach via recursion: - Pass 1 (translatedDataList=None): Collect strings and batch translate - Pass 2 (translatedDataList set): Apply translations back to the data Args: data: List of objects with name, desc, customParameters, and pages->commands->data structure filename: Name of the file being translated translatedDataList: List containing [nameList, descList, dataList, speakerList, customParamsList] - Pass 1: Empty lists to collect originals - Pass 2: Filled lists with translations Returns: Tuple of [input tokens, output tokens] """ global PBAR totalTokens = [0, 0] # Initialize or extract lists if translatedDataList is None: # PASS 1: Create empty lists to collect strings nameList = [] descList = [] dataList = [] speakerList = [] customParamsList = [] originalSpeakerList = [] # For vocab update else: # PASS 2: Use provided translated lists nameList = translatedDataList[0] descList = translatedDataList[1] dataList = translatedDataList[2] speakerList = translatedDataList[3] customParamsList = translatedDataList[4] # Single loop - behavior depends on which pass we're in for entry in data: if not entry: continue # Handle name field if "name" in entry and entry["name"]: # PASS 1: Collect original if translatedDataList is None: nameList.append(entry["name"]) # PASS 2: Apply translation else: if nameList: entry["name"] = nameList[0] nameList.pop(0) # Handle desc field if "desc" in entry and entry["desc"]: # PASS 1: Collect original if translatedDataList is None: # Remove newlines for translation descList.append(entry["desc"].replace("\n", " ")) # PASS 2: Apply translation else: if descList: translatedText = descList[0] # Apply text wrapping translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) entry["desc"] = translatedText descList.pop(0) # Handle customParameters at entry level if "customParameters" in entry and entry["customParameters"]: # PASS 1: Collect hint text if translatedDataList is None: # Extract hint value using regex (captures content between hint:" and ") match = re.search(r'hint:"((?:[^"\\]|\\.)*)"', entry["customParameters"]) if match: hintText = match.group(1) hintText = hintText.replace("\\n", " ") customParamsList.append(hintText) else: # No hint found, add empty string as placeholder customParamsList.append("") # PASS 2: Apply translation else: if customParamsList: translatedHint = customParamsList[0] customParamsList.pop(0) if translatedHint: # Only replace if we translated something # Replace double quotes with single quotes since \" is used as delimiter translatedHint = translatedHint.replace('"', "'") # Wrap text using dazedwrap with WIDTH and replace newlines with \\n translatedHint = dazedwrap.wrapText(translatedHint, width=WIDTH) translatedHint = translatedHint.replace("\n", "\\\\n") # Replace the hint value in customParameters entry["customParameters"] = re.sub( r'(hint:")((?:[^"\\]|\\.)*)"', r'\1' + translatedHint + '"', entry["customParameters"] ) # Check if pages exists before processing if "pages" not in entry or not isinstance(entry["pages"], list): continue for page in entry["pages"]: if not page or "commands" not in page: continue if not isinstance(page["commands"], list): continue for command in page["commands"]: if not command: continue # Get the speaker for this command (if available) speaker = command.get("speaker", "") # Handle data array if "data" in command and isinstance(command["data"], list): for i, text in enumerate(command["data"]): if text: # PASS 1: Collect original with speaker prefix if translatedDataList is None: # Remove Wrap text = text.replace("\n", " ") # Attach speaker to data for translation if speaker: dataList.append(f"[{speaker}]: {text}") else: dataList.append(text) # PASS 2: Apply translation and strip speaker prefix else: if dataList: translated = dataList[0] # Remove speaker if speaker: match = re.search(r'(^\[.+?\]\s?[|:]\s?)', translated) if match: translated = translated.replace(match.group(1), "") # Textwrap translated = dazedwrap.wrapText(translated, width=WIDTH) # Set Data command["data"][i] = translated dataList.pop(0) # Handle speaker field if "speaker" in command and command["speaker"]: # PASS 1: Collect original if translatedDataList is None: originalSpeakerList.append(command["speaker"]) speakerList.append(command["speaker"]) # PASS 2: Apply translation else: if speakerList: command["speaker"] = speakerList[0] speakerList.pop(0) # If this was Pass 1, do the translation and recurse for Pass 2 if translatedDataList is None: # Store original counts for mismatch checking originalNameCount = len(nameList) originalDescCount = len(descList) originalDataCount = len(dataList) originalSpeakerCount = len(speakerList) originalCustomParamsCount = len(customParamsList) # Batch translate names if nameList: response = translateAI( nameList, "Reply with only the " + LANGUAGE + " translation of the name.", True, filename, pbar ) nameList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate descriptions if descList: response = translateAI( descList, "Reply with only the " + LANGUAGE + " translation of the description.", True, filename, pbar ) descList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate data text if dataList: response = translateAI( dataList, "Reply with only the " + LANGUAGE + " translation of the dialogue text.", True, filename, pbar ) dataList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate speakers if speakerList: response = translateAI( speakerList, "Reply with only the " + LANGUAGE + " translation of the speaker name.", True, filename, pbar ) speakerList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate customParameters hints (only non-empty ones) if customParamsList: # Filter out empty strings for translation hintsToTranslate = [h for h in customParamsList if h] if hintsToTranslate: response = translateAI( hintsToTranslate, "Reply with only the " + LANGUAGE + " translation of the hint text.", True, filename, pbar ) translatedHints = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Reconstruct customParamsList with translations in place translatedIndex = 0 for i, hint in enumerate(customParamsList): if hint: # If it was non-empty, use the translation customParamsList[i] = translatedHints[translatedIndex] translatedIndex += 1 # Check for mismatch errors if len(nameList) != originalNameCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(descList) != originalDescCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(dataList) != originalDataCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(speakerList) != originalSpeakerCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(customParamsList) != originalCustomParamsCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) # PASS 2: Recursively call to apply translations translateRecollection(data, filename, [nameList, descList, dataList, speakerList, customParamsList], pbar) return totalTokens def parseMap(data, filename): """ Parser for SRPG Studio map.json files. Structure: Single object with: - id, name, desc, mapName - victoryConds (array of strings) - defeatConds (array of strings) - EnemyUnits (array of objects with id, name, desc, events structure similar to recollection) Args: data: Parsed JSON data (single map object) filename: Name of the file being parsed Returns: Tuple of (data, token counts, error) """ global PBAR totalTokens = [0, 0] pbar = None try: # Count work units (all translatable fields) total_units = 0 # Count top-level fields: desc, mapName (name is an identifier and should not be translated) for field in ["desc", "mapName"]: if field in data and data[field]: total_units += 1 # Count victoryConds array items if "victoryConds" in data and isinstance(data["victoryConds"], list): for cond in data["victoryConds"]: if cond: total_units += 1 # Count defeatConds array items if "defeatConds" in data and isinstance(data["defeatConds"], list): for cond in data["defeatConds"]: if cond: total_units += 1 # Count units with events (EnemyUnits, EvEnemyUnits) for unitsKey in ["EnemyUnits", "EvEnemyUnits"]: if unitsKey in data and isinstance(data[unitsKey], list): for unit in data[unitsKey]: if not unit: continue # Count unit name and desc for field in ["name", "desc"]: if field in unit and unit[field]: total_units += 1 # Count events->pages->commands->data and speaker if "events" in unit and isinstance(unit["events"], list): for event in unit["events"]: if not event or "pages" not in event: continue if not isinstance(event["pages"], list): continue for page in event["pages"]: if not page or "commands" not in page: continue if not isinstance(page["commands"], list): continue for command in page["commands"]: if not command: continue # Count data array items if "data" in command and isinstance(command["data"], list): for text in command["data"]: if text: total_units += 1 # Count speaker field if "speaker" in command and command["speaker"]: total_units += 1 # Count events (placeEvents, autoEvents, openingEvents, communicationEvents) for eventsKey in ["placeEvents", "autoEvents", "openingEvents", "communicationEvents"]: if eventsKey in data and isinstance(data[eventsKey], list): for event in data[eventsKey]: if not event or "pages" not in event: continue if not isinstance(event["pages"], list): continue for page in event["pages"]: if not page or "commands" not in page: continue if not isinstance(page["commands"], list): continue for command in page["commands"]: if not command: continue # Count data array items if "data" in command and isinstance(command["data"], list): for text in command["data"]: if text: total_units += 1 # Count speaker field if "speaker" in command and command["speaker"]: total_units += 1 # Setup progress bar (per-file instance) with LOCK: pbar = tqdm( desc=filename, total=total_units, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, ) # Translate the data using two-pass approach result = translateMap(data, filename, pbar=pbar) totalTokens[0] += result[0] totalTokens[1] += result[1] return (data, totalTokens, None) except Exception as e: traceback.print_exc() return (data, totalTokens, e) finally: try: if pbar is not None: pbar.close() except Exception: pass def translateMap(data, filename, translatedDataList=None, pbar=None): """ Translates map.json data structure. Uses two-pass approach via recursion: - Pass 1 (translatedDataList=None): Collect strings and batch translate - Pass 2 (translatedDataList set): Apply translations back to the data Args: data: Single map object with name, desc, mapName, victoryConds, defeatConds, and EnemyUnits filename: Name of the file being translated translatedDataList: List containing translation lists - Pass 1: Empty lists to collect originals - Pass 2: Filled lists with translations Returns: Tuple of [input tokens, output tokens] """ global PBAR totalTokens = [0, 0] # Initialize or extract lists if translatedDataList is None: # PASS 1: Create empty lists to collect strings descList = [] mapNameList = [] victoryCondsList = [] defeatCondsList = [] unitNameList = [] unitDescList = [] dataList = [] speakerList = [] originalSpeakerList = [] # For vocab update else: # PASS 2: Use provided translated lists descList = translatedDataList[0] mapNameList = translatedDataList[1] victoryCondsList = translatedDataList[2] defeatCondsList = translatedDataList[3] unitNameList = translatedDataList[4] unitDescList = translatedDataList[5] dataList = translatedDataList[6] speakerList = translatedDataList[7] # Note: name field is not translated as it's an identifier (e.g., "ch3_瘴気の森") # Handle desc field if "desc" in data and data["desc"]: # PASS 1: Collect original if translatedDataList is None: descList.append(data["desc"].replace("\n", " ")) # PASS 2: Apply translation else: if descList: translatedText = descList[0] # Wordwrap translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) data["desc"] = translatedText descList.pop(0) # Handle mapName field if "mapName" in data and data["mapName"]: # PASS 1: Collect original if translatedDataList is None: mapNameList.append(data["mapName"]) # PASS 2: Apply translation else: if mapNameList: data["mapName"] = mapNameList[0] mapNameList.pop(0) # Handle victoryConds array if "victoryConds" in data and isinstance(data["victoryConds"], list): for i, cond in enumerate(data["victoryConds"]): if cond: # PASS 1: Collect original if translatedDataList is None: victoryCondsList.append(cond) # PASS 2: Apply translation else: if victoryCondsList: data["victoryConds"][i] = victoryCondsList[0] victoryCondsList.pop(0) # Handle defeatConds array if "defeatConds" in data and isinstance(data["defeatConds"], list): for i, cond in enumerate(data["defeatConds"]): if cond: # PASS 1: Collect original if translatedDataList is None: defeatCondsList.append(cond) # PASS 2: Apply translation else: if defeatCondsList: data["defeatConds"][i] = defeatCondsList[0] defeatCondsList.pop(0) # Process all unit arrays (EnemyUnits, EvEnemyUnits) for unitsKey in ["EnemyUnits", "EvEnemyUnits"]: if unitsKey in data and isinstance(data[unitsKey], list): for unit in data[unitsKey]: if not unit: continue # Handle unit name if "name" in unit and unit["name"]: if translatedDataList is None: unitNameList.append(unit["name"]) else: if unitNameList: unit["name"] = unitNameList[0] unitNameList.pop(0) # Handle unit desc if "desc" in unit and unit["desc"]: if translatedDataList is None: unitDescList.append(unit["desc"].replace("\n", " ")) else: if unitDescList: translatedText = unitDescList[0] # Wordwrap translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) unit["desc"] = translatedText unitDescList.pop(0) # Process events within units if "events" in unit and isinstance(unit["events"], list): for event in unit["events"]: if not event or "pages" not in event: continue if not isinstance(event["pages"], list): continue for page in event["pages"]: if not page or "commands" not in page: continue if not isinstance(page["commands"], list): continue for command in page["commands"]: if not command: continue speaker = command.get("speaker", "") # Handle data array if "data" in command and isinstance(command["data"], list): for i, text in enumerate(command["data"]): if text: if translatedDataList is None: text = text.replace("\n", " ") if speaker: dataList.append(f"[{speaker}]: {text}") else: dataList.append(text) else: if dataList: translated = dataList[0] if speaker: match = re.search(r'(^\[.+?\]\s?[|:]\s?)', translated) if match: translated = translated.replace(match.group(1), "") translated = dazedwrap.wrapText(translated, width=WIDTH) command["data"][i] = translated dataList.pop(0) # Handle speaker field if "speaker" in command and command["speaker"]: if translatedDataList is None: originalSpeakerList.append(command["speaker"]) speakerList.append(command["speaker"]) else: if speakerList: command["speaker"] = speakerList[0] speakerList.pop(0) # Process all event arrays (placeEvents, autoEvents, openingEvents, communicationEvents) for eventsKey in ["placeEvents", "autoEvents", "openingEvents", "communicationEvents"]: if eventsKey in data and isinstance(data[eventsKey], list): for event in data[eventsKey]: if not event or "pages" not in event: continue if not isinstance(event["pages"], list): continue for page in event["pages"]: if not page or "commands" not in page: continue if not isinstance(page["commands"], list): continue for command in page["commands"]: if not command: continue speaker = command.get("speaker", "") # Handle data array if "data" in command and isinstance(command["data"], list): for i, text in enumerate(command["data"]): if text: if translatedDataList is None: text = text.replace("\n", " ") if speaker: dataList.append(f"[{speaker}]: {text}") else: dataList.append(text) else: if dataList: translated = dataList[0] if speaker: match = re.search(r'(^\[.+?\]\s?[|:]\s?)', translated) if match: translated = translated.replace(match.group(1), "") translated = dazedwrap.wrapText(translated, width=WIDTH) command["data"][i] = translated dataList.pop(0) # Handle speaker field if "speaker" in command and command["speaker"]: if translatedDataList is None: originalSpeakerList.append(command["speaker"]) speakerList.append(command["speaker"]) else: if speakerList: command["speaker"] = speakerList[0] speakerList.pop(0) # If this was Pass 1, do the translation and recurse for Pass 2 if translatedDataList is None: # Store original counts for mismatch checking originalDescCount = len(descList) originalMapNameCount = len(mapNameList) originalVictoryCondsCount = len(victoryCondsList) originalDefeatCondsCount = len(defeatCondsList) originalUnitNameCount = len(unitNameList) originalUnitDescCount = len(unitDescList) originalDataCount = len(dataList) originalSpeakerCount = len(speakerList) # Batch translate map descriptions if descList: response = translateAI( descList, "Reply with only the " + LANGUAGE + " translation of the map description.", True, filename, pbar ) descList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate map display names if mapNameList: response = translateAI( mapNameList, "Reply with only the " + LANGUAGE + " translation of the map display name.", True, filename, pbar ) mapNameList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate victory conditions if victoryCondsList: response = translateAI( victoryCondsList, "Reply with only the " + LANGUAGE + " translation of the victory condition.", True, filename, pbar ) victoryCondsList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate defeat conditions if defeatCondsList: response = translateAI( defeatCondsList, "Reply with only the " + LANGUAGE + " translation of the defeat condition.", True, filename, pbar ) defeatCondsList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate unit names if unitNameList: response = translateAI( unitNameList, "Reply with only the " + LANGUAGE + " translation of the enemy unit name.", True, filename, pbar ) unitNameList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate unit descriptions if unitDescList: response = translateAI( unitDescList, "Reply with only the " + LANGUAGE + " translation of the enemy unit description.", True, filename, pbar ) unitDescList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate dialogue data if dataList: response = translateAI( dataList, "Reply with only the " + LANGUAGE + " translation of the dialogue text.", True, filename, pbar ) dataList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Batch translate speakers if speakerList: response = translateAI( speakerList, "Reply with only the " + LANGUAGE + " translation of the speaker name.", True, filename, pbar ) speakerList = response[0] totalTokens[0] += response[1][0] totalTokens[1] += response[1][1] # Check for mismatch errors if len(descList) != originalDescCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(mapNameList) != originalMapNameCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(victoryCondsList) != originalVictoryCondsCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(defeatCondsList) != originalDefeatCondsCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(unitNameList) != originalUnitNameCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(unitDescList) != originalUnitDescCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(dataList) != originalDataCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) if len(speakerList) != originalSpeakerCount: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) # PASS 2: Recursively call to apply translations translateMap(data, filename, [descList, mapNameList, victoryCondsList, defeatCondsList, unitNameList, unitDescList, dataList, speakerList], pbar) return totalTokens def getResultString(translatedData, translationTime, filename): """ Formats the translation result string with token counts, cost, and time. Args: translatedData: Tuple of (data, tokens, error) translationTime: Time taken for translation filename: Name of the file Returns: Formatted result string """ global TIMETOTAL # Calculate cost cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL) totalTokenstring = ( Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" "[Output: " + str(translatedData[1][1]) + "]" "[Cost: ${:,.4f}".format(cost) + "]" ) # Format time string if filename != "TOTAL": timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" TIMETOTAL += round(translationTime, 1) else: timeString = Fore.BLUE + "[" + str(round(TIMETOTAL, 1)) + "s]" # Return success or failure string if translatedData[2] is None: # Success return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET else: # Fail try: raise translatedData[2] except Exception as e: traceback.print_exc() return ( filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + Fore.RESET ) def getSpeaker(speaker): """ Translates speaker/character names with caching to avoid redundant translations. Args: speaker: The original speaker name to translate Returns: List containing [translated name, [input tokens, output tokens]] """ if speaker == "": return ["", [0, 0]] # Check if speaker has already been translated for i in range(len(NAMESLIST)): if speaker == NAMESLIST[i][0]: return [NAMESLIST[i][1], [0, 0]] # Translate and Store Speaker response = translateAI( speaker, "Reply with the " + LANGUAGE + " translation of the NPC name.", False, ) response[0] = response[0].title() response[0] = response[0].replace("'S", "'s") response[0] = response[0].replace("Speaker: ", "") # Retry if name doesn't translate for some reason if re.search(r"([a-zA-Z??])", response[0]) is None: response = translateAI( speaker, "Reply with the " + LANGUAGE + " translation of the NPC name.", False, ) response[0] = response[0].title() response[0] = response[0].replace("'S", "'s") speakerList = [speaker, response[0]] NAMESLIST.append(speakerList) return response def translateAI(text, history, fullPromptFlag, filename_param=None, pbar_param=None): """ Legacy wrapper function for the new shared translation utility. This maintains compatibility with existing code while using the new shared implementation. Args: text: Text to translate (can be string or list) history: History/context for the translation fullPromptFlag: Whether to use the full prompt with vocab Returns: List containing [translated text, [input tokens, output tokens]] """ global PBAR, MISMATCH, FILENAME # Update config estimate mode based on global ESTIMATE TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE) # Call the new shared translation function return sharedtranslateAI( text=text, history=history, fullPromptFlag=fullPromptFlag, config=TRANSLATION_CONFIG, filename=(filename_param if filename_param is not None else FILENAME), pbar=(pbar_param if pbar_param is not None else PBAR), lock=LOCK, mismatchList=MISMATCH )