# Libraries import json import os import re import util.dazedwrap as dazedwrap import threading import time import traceback import tiktoken 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 import tempfile # Globals MODEL = os.getenv("model") TIMEOUT = int(os.getenv("timeout")) LANGUAGE = os.getenv("language").capitalize() from util.paths import PROMPT_PATH, VOCAB_PATH PROMPT = PROMPT_PATH.read_text(encoding="utf-8") VOCAB = VOCAB_PATH.read_text(encoding="utf-8") LOCK = threading.Lock() WIDTH = int(os.getenv("width")) LISTWIDTH = int(os.getenv("listWidth")) NOTEWIDTH = 70 MAXHISTORY = 10 ESTIMATE = "" TOKENS = [0, 0] NAMESLIST = [] NAMES = False # Output a list of all the character names found BRFLAG = False # If the game uses
instead FIXTEXTWRAP = True # Overwrites textwrap IGNORETLTEXT = False # Ignores all translated text. MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) FILENAME = None # tqdm Globals BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" POSITION = 0 LEAVE = False PBAR = None # Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+" # 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"] # 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 ) LEAVE = False def handlePlugin(filename, estimate): global ESTIMATE, PBAR, FILENAME ESTIMATE = estimate FILENAME = filename if ESTIMATE: start = time.time() translatedData = openFiles(filename) # Print Result 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 on maps if len(MISMATCH) > 0: return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET else: return totalString else: try: # Perform translation first; incremental progress writes happen during translation. # We no longer keep the destination file open simultaneously to avoid Windows locking issues # during atomic os.replace operations inside save_progress_lines. start = time.time() translatedData = openFiles(filename) # Ensure final state is flushed (in case no incremental writes occurred for some reason). save_progress_lines(translatedData[0], filename) # Print Result end = time.time() tqdm.write(getResultString(translatedData, end - start, filename)) with LOCK: TOKENS[0] += translatedData[1][0] TOKENS[1] += translatedData[1][1] except Exception: traceback.print_exc() return "Fail" return getResultString(["", TOKENS, None], end - start, "TOTAL") def getResultString(translatedData, translationTime, filename): # File Print String 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) + "]" ) timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" if translatedData[2] == None: # Success return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET else: # Fail try: raise translatedData[2] except Exception as e: traceback.print_exc() errorString = str(e) + Fore.RED return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET def openFiles(filename): with open("files/" + filename, "r", encoding="utf_8") as readFile: translatedData = parsePlugin(readFile, filename) # Delete lines marked for deletion finalData = [] for line in translatedData[0]: if line != "\\d\n": finalData.append(line) translatedData[0] = finalData return translatedData def parsePlugin(readFile, filename): global PBAR totalTokens = [0, 0] # Read File into data data = readFile.readlines() # Create Progress Bar with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: pbar.desc = filename PBAR = pbar try: result = translatePlugin(data, pbar, filename, []) totalTokens[0] += result[0] totalTokens[1] += result[1] except Exception as e: traceback.print_exc() return [data, totalTokens, e] return [data, totalTokens, None] def save_progress_lines(lines, filename, encoding="utf_8"): """Atomically save current line-based translation progress.""" try: if ESTIMATE: return global LOCK os.makedirs("translated", exist_ok=True) # Use a single lock to prevent concurrent replace attempts on Windows (which causes PermissionError) with LOCK: tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated") try: with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file: tmp_file.writelines(lines) dest_path = os.path.join("translated", filename) # Retry a few times in case another process/thread still has the file open momentarily. for attempt in range(5): try: os.replace(tmp_path, dest_path) break except PermissionError: if attempt == 4: raise time.sleep(0.1 * (attempt + 1)) finally: if os.path.exists(tmp_path): try: os.remove(tmp_path) except OSError: pass except Exception: traceback.print_exc() def saveCheckLines(lines, filename, tokens=None, encoding="utf_8"): """Save progress only when tokens indicate work or when explicitly called after a mutation.""" try: if tokens is not None: if not (isinstance(tokens, (list, tuple)) and len(tokens) >= 2 and (tokens[0] or tokens[1])): return save_progress_lines(lines, filename, encoding=encoding) except Exception: traceback.print_exc() def translatePlugin(data, pbar, filename, translatedList): if len(translatedList) > 0: questList = translatedList[0] custom = translatedList[1] sceneMenuText = translatedList[2] if len(translatedList) > 2 else [] sceneMenuCommonHelpText = translatedList[3] if len(translatedList) > 3 else [] sceneMenuHelpText = translatedList[4] if len(translatedList) > 4 else [] sceneMenuDrawText = translatedList[5] if len(translatedList) > 5 else [] sceneMenuStatLabel = translatedList[6] if len(translatedList) > 6 else [] saveParamName = translatedList[7] if len(translatedList) > 7 else [] setData = True else: questList = [[], [], [], [], [], []] custom = [] sceneMenuText = [] sceneMenuCommonHelpText = [] sceneMenuHelpText = [] sceneMenuDrawText = [] sceneMenuStatLabel = [] saveParamName = [] setData = False currentGroup = [] tokens = [0, 0] speaker = "" voice = False global LOCK, ESTIMATE i = 0 in_disp_list = False brace_count = 0 _pending_drawtext = {} # Deferred drawTextEx brace label replacements (longest-first) _pending_statlabel = {} # Deferred drawTextEx stat label replacements (longest-first) # Category with open("log/translations.txt", "a+", encoding="utf-8") as tlFile: tlFile.write(f"\nCustom:\n") tlFile.close() while i < len(data): voice = False speaker = "" # Track if we're inside CBR_travel_data block if 'data_map_name_list' in data[i]: in_disp_list = True brace_count = 0 if in_disp_list: # Count braces to know when we exit the object brace_count += data[i].count('{') - data[i].count('}') if brace_count < 0: in_disp_list = False # Nested array strings (e.g., this.disp_list = { key: { subkey: ["string1", "string2"] } }) # Matches strings inside arrays within object literals if in_disp_list: regex_nested = r'"([^"]*[一-龠ぁ-ゔァ-ヴー]+[^"]*)"' matchList = re.findall(regex_nested, data[i]) if len(matchList) > 0: for match in matchList: # Save Original String jaString = match originalString = jaString # Replace \n for translation jaStringClean = jaString.replace("\\n", " ") if jaStringClean.strip(): # Pass 1 if setData == False: custom.append(jaStringClean.strip()) # Pass 2 else: if custom: # Grab and Pop translatedText = custom[0] custom.pop(0) # Set to None if empty list if len(custom) <= 0: custom = [] # Restore original newlines but keep translated text # Count original newlines newline_count = jaString.count("\\n") if newline_count > 0: # Textwrap the translation translatedText = dazedwrap.wrapText(translatedText, 50) translatedText = translatedText.replace("\n", "\\n") # Escape quotes in translation translatedText = translatedText.replace('"', '\\"') # Set Data data[i] = data[i].replace(f'"{originalString}"', f'"{translatedText}"') saveCheckLines(data, filename) # Custom # Useful Regex's # r'"Text[\\]+":[\\]+"(.+?)[\\]+",' # r'"HelpText[\\]+":[\\]+"(.+?)[\\]+",' # r"this.drawTextEx\(\\'(.+?)\'," # r'txtSubject.+?"(.+)"' regex = r'txtSubject.+?"(.+)"' matchList = re.findall(regex, data[i]) if len(matchList) > 0: for match in matchList: # Save Original String jaString = match originalString = jaString newline = None colorCode = None # Make sure it contains Japanese if not re.search(LANGREGEX, jaString): continue # Make sure didn't grab \\ if re.search(r"^[\\]+$", jaString): continue # Replace \n and \c if re.search(r"\\+n", jaString): newline = re.search(r"\\+n", jaString).group(0) jaString = re.sub(r"\\+n", r"\\n", jaString) if re.search(r"\\+C", jaString): colorCode = re.search(r"\\+C", jaString).group(0) jaString = re.sub(r"\\+C", r"\\C", jaString) # Remove any textwrap jaString = jaString.replace("\\n", " ") if jaString.replace("\u3000", "") and jaString: # Pass 1 if setData == False and jaString.strip(): custom.append(jaString.strip()) # Pass 2 else: if custom: # Grab and Pop translatedText = custom[0] custom.pop(0) # Set to None if empty list if len(translatedList) <= 0: translatedList = None # Replace Single Quotes translatedText = re.sub(r"([^\\'])'", r"\1՚", translatedText) translatedText = re.sub(r"([^\\'])\"", r"\1՚", translatedText) # Replace \n and \c if newline: # Textwrap # translatedText = dazedwrap.wrapText(translatedText, WIDTH) translatedText = re.sub(r"\n", re.escape(newline), translatedText) if colorCode: translatedText = re.sub(r"\n", re.escape(colorCode), translatedText) # Set Data with open("log/translations.txt", "a+", encoding="utf-8") as tlFile: tlFile.write(f"{originalString} ({translatedText})\n") data[i] = data[i].replace(originalString, translatedText) saveCheckLines(data, filename) # Quest Name regex = r'[\\]+"QuestName[\\]+":[\\]+"(.*?)[\\]+"' matchList = re.findall(regex, data[i]) if len(matchList) > 0: for match in matchList: if match: # Save Original String originalString = match # Remove any textwrap match = match.replace(newline, " ") # Pass 1 if setData == False: # Add String if match != "\\\\\\\\": questList[0].append(match.strip()) # Pass 2 else: if questList[0]: # Grab and Pop translatedText = questList[0][0] questList[0].pop(0) # Set to None if empty list if len(translatedList) <= 0: translatedList = None # Replace Single Quotes translatedText = translatedText.replace('"', "'") translatedText = re.sub(r"([^\\'])'", r"\1\\'", translatedText) # Set Data data[i] = data[i].replace(originalString, translatedText) saveCheckLines(data, filename) # Quest Client regex = r'QuestClientName[\\]+":[\\]+"(.*?)[\\]+"' matchList = re.findall(regex, data[i]) if len(matchList) > 0: for match in matchList: if match: # Save Original String originalString = match # Pass 1 if setData == False: # Add String if match != "\\\\\\\\": questList[1].append(match.strip()) # Pass 2 else: if questList[1]: # Grab and Pop translatedText = questList[1][0] questList[1].pop(0) # Set to None if empty list if len(translatedList) <= 0: translatedList = None # Replace Single Quotes translatedText = translatedText.replace('"', "'") translatedText = re.sub(r"([^\\'])'", r"\1\\'", translatedText) # Set Data data[i] = data[i].replace(originalString, translatedText) saveCheckLines(data, filename) # Quest Location regex = r'QuestLocation[\\]+":[\\]+"(.*?)[\\]+"' matchList = re.findall(regex, data[i]) if len(matchList) > 0: for match in matchList: if match: # Save Original String originalString = match # Pass 1 if setData == False: # Add String if match != "\\\\\\\\": questList[2].append(match.strip()) # Pass 2 else: if questList[2]: # Grab and Pop translatedText = questList[2][0] questList[2].pop(0) # Set to None if empty list if len(translatedList) <= 0: translatedList = None # Replace Single Quotes translatedText = translatedText.replace('"', "'") translatedText = re.sub(r"([^\\'])'", r"\1\\'", translatedText) # Set Data data[i] = data[i].replace(originalString, translatedText) saveCheckLines(data, filename) # Quest Target regex = r'PlaceInformation[\\]+":[\\]+"(.*?)[\\]+"' matchList = re.findall(regex, data[i]) if len(matchList) > 0: for match in matchList: if match: # Save Original String originalString = match # Pass 1 if setData == False: # Add String if match != "\\\\\\\\": questList[3].append(match.strip()) # Pass 2 else: if questList[3]: # Grab and Pop translatedText = questList[3][0] questList[3].pop(0) # Set to None if empty list if len(translatedList) <= 0: translatedList = None # Replace Single Quotes translatedText = translatedText.replace('"', "'") translatedText = re.sub(r"([^\\'])'", r"\1\\'", translatedText) # Set Data data[i] = data[i].replace(originalString, translatedText) saveCheckLines(data, filename) # Quest Summary regex = r'[\\]+"QuestContent[\\]+":[\\]+"[\\]+"(.*?)[\\]+"[\\]+"' matchList = re.findall(regex, data[i]) if len(matchList) > 0: for match in matchList: if match: # Save Original String originalString = match # Remove any textwrap match = match.replace(r"\\\\\\\\n", " ") match = match.replace(r"\\\\\\\\\\\\\\\\c", "\\c") # Pass 1 if setData == False: # Add String if match != "\\\\\\\\": questList[4].append(match.strip()) # Pass 2 else: if questList[4]: # Grab and Pop translatedText = questList[4][0] questList[4].pop(0) # Set to None if empty list if len(translatedList) <= 0: translatedList = None # Textwrap translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) translatedText = translatedText.replace("\n", r"\\\\\\\\n") match = match.replace("\\c", r"\\\\\\\\\\\\\\\\c") # Replace Single Quotes translatedText = translatedText.replace('"', "'") translatedText = re.sub(r"([^\\'])'", r"\1\\'", translatedText) # Set Data data[i] = data[i].replace(originalString, translatedText) saveCheckLines(data, filename) # Quest Goal 1 regex = r'ObjectiveContent[\\]+":[\\]+"[\\]+"(.*?)[\\]+"' matchList = re.findall(regex, data[i]) if len(matchList) > 0: for match in matchList: if match: # Save Original String originalString = match # Remove any textwrap match = match.replace(r"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n", " ") # Pass 1 if setData == False: # Add String if match != "\\\\\\\\": questList[5].append(match.strip()) # Pass 2 else: if questList[5]: # Grab and Pop translatedText = questList[5][0] questList[5].pop(0) # Set to None if empty list if len(translatedList) <= 0: translatedList = None # Textwrap translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) translatedText = translatedText.replace("\n", r"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n") # Replace Single Quotes translatedText = translatedText.replace('"', "'") translatedText = re.sub(r"([^\\'])'", r"\1\\'", translatedText) # Set Data data[i] = data[i].replace(originalString, translatedText) saveCheckLines(data, filename) # SceneCustomMenu - Text (Command Labels) regex = r'[\\]+"Text[\\]+":[\\]+"(.+?)[\\]+"' matchList = re.findall(regex, data[i]) if len(matchList) > 0: for match in matchList: if match: jaString = match originalString = jaString # Make sure it contains Japanese if not re.search(LANGREGEX, jaString): continue # Make sure didn't grab only backslashes if re.search(r"^[\\]+$", jaString): continue if jaString.replace("\u3000", "").strip(): # Pass 1 if setData == False: sceneMenuText.append(jaString.strip()) # Pass 2 else: if sceneMenuText: # Grab and Pop translatedText = sceneMenuText[0] sceneMenuText.pop(0) # Replace quotes translatedText = translatedText.replace('"', "'") translatedText = re.sub(r"([^\\'])'" , r"\1\\'", translatedText) # Set Data data[i] = data[i].replace(originalString, translatedText) saveCheckLines(data, filename) # SceneCustomMenu - CommonHelpText regex = r'[\\]+"CommonHelpText[\\]+":[\\]+"(.+?)[\\]+"' matchList = re.findall(regex, data[i]) if len(matchList) > 0: for match in matchList: if match: jaString = match originalString = jaString # Make sure it contains Japanese if not re.search(LANGREGEX, jaString): continue # Make sure didn't grab only backslashes if re.search(r"^[\\]+$", jaString): continue if jaString.replace("\u3000", "").strip(): # Pass 1 if setData == False: sceneMenuCommonHelpText.append(jaString.strip()) # Pass 2 else: if sceneMenuCommonHelpText: # Grab and Pop translatedText = sceneMenuCommonHelpText[0] sceneMenuCommonHelpText.pop(0) # Replace quotes translatedText = translatedText.replace('"', "'") translatedText = re.sub(r"([^\\'])'" , r"\1\\'", translatedText) # Set Data data[i] = data[i].replace(originalString, translatedText) saveCheckLines(data, filename) # SceneCustomMenu - HelpText (Command Help) regex = r'[\\]+"HelpText[\\]+":[\\]+"(.+?)[\\]+"' matchList = re.findall(regex, data[i]) if len(matchList) > 0: for match in matchList: if match: jaString = match originalString = jaString # Make sure it contains Japanese if not re.search(LANGREGEX, jaString): continue # Make sure didn't grab only backslashes if re.search(r"^[\\]+$", jaString): continue if jaString.replace("\u3000", "").strip(): # Pass 1 if setData == False: sceneMenuHelpText.append(jaString.strip()) # Pass 2 else: if sceneMenuHelpText: # Grab and Pop translatedText = sceneMenuHelpText[0] sceneMenuHelpText.pop(0) # Replace quotes translatedText = translatedText.replace('"', "'") translatedText = re.sub(r"([^\\'])'" , r"\1\\'", translatedText) # Set Data data[i] = data[i].replace(originalString, translatedText) saveCheckLines(data, filename) # NUUN_SaveScreen - ParamName (save screen labels like 現在地, プレイ時間, 所持金) regex = r'[\\]+"ParamName[\\]+":[\\]+"(.+?)[\\]+"' matchList = re.findall(regex, data[i]) if len(matchList) > 0: for match in matchList: if match: jaString = match originalString = jaString # Make sure it contains Japanese if not re.search(LANGREGEX, jaString): continue # Make sure didn't grab only backslashes if re.search(r"^[\\]+$", jaString): continue if jaString.replace("\u3000", "").strip(): # Pass 1 if setData == False: saveParamName.append(jaString.strip()) # Pass 2 else: if saveParamName: # Grab and Pop translatedText = saveParamName[0] saveParamName.pop(0) # Replace quotes translatedText = translatedText.replace('"', "'") translatedText = re.sub(r"([^\\'])'" , r"\1\\'", translatedText) # Set Data data[i] = data[i].replace(originalString, translatedText) saveCheckLines(data, filename) # DrawTextEx - Brace-delimited labels (e.g., \}【口づけ】\{, \}回数:\{) # Only process lines containing drawTextEx calls if 'drawTextEx' in data[i]: regex = r'[\\]+\}(.+?)[\\]+\{' matchList = re.findall(regex, data[i]) if len(matchList) > 0: for match in matchList: if match: jaString = match originalString = jaString # Skip game variable references (handled separately) if '$gameVariables' in jaString: continue # Make sure it contains Japanese if not re.search(LANGREGEX, jaString): continue if jaString.replace("\u3000", "").strip(): # Pass 1 (deduplicated) if setData == False: if jaString.strip() not in sceneMenuDrawText: sceneMenuDrawText.append(jaString.strip()) # Pass 2 (collect mapping, skip duplicates) else: if sceneMenuDrawText and originalString not in _pending_drawtext: # Grab and Pop translatedText = sceneMenuDrawText[0] sceneMenuDrawText.pop(0) # Replace quotes translatedText = translatedText.replace('"', "'") # Queue for deferred replacement _pending_drawtext[originalString] = translatedText # Apply deferred replacements longest-first to avoid substring collisions # (e.g., replacing 回数: before 使用回数: would corrupt the longer string) if setData and _pending_drawtext: for orig in sorted(_pending_drawtext, key=len, reverse=True): data[i] = data[i].replace(orig, _pending_drawtext[orig]) _pending_drawtext.clear() saveCheckLines(data, filename) # DrawTextEx - Stat labels (e.g., C[16]攻撃力\\...C[0]) if 'drawTextEx' in data[i]: regex = r'C\[\d+\]([^C\\,`\[\]]+?)[\\]+C\[0\]' matchList = re.findall(regex, data[i]) if len(matchList) > 0: for match in matchList: if match: jaString = match originalString = jaString # Make sure it contains Japanese if not re.search(LANGREGEX, jaString): continue if jaString.replace("\u3000", "").strip(): # Pass 1 (deduplicated) if setData == False: if jaString.strip() not in sceneMenuStatLabel: sceneMenuStatLabel.append(jaString.strip()) # Pass 2 (collect mapping, skip duplicates) else: if sceneMenuStatLabel and originalString not in _pending_statlabel: # Grab and Pop translatedText = sceneMenuStatLabel[0] sceneMenuStatLabel.pop(0) # Queue for deferred replacement _pending_statlabel[originalString] = translatedText # Apply deferred replacements longest-first to avoid substring collisions if setData and _pending_statlabel: for orig in sorted(_pending_statlabel, key=len, reverse=True): data[i] = data[i].replace(orig, _pending_statlabel[orig]) _pending_statlabel.clear() saveCheckLines(data, filename) # DrawTextEx - Game variable counter word (hardcoded: 回 → time(s)) if setData and 'drawTextEx' in data[i] and '$gameVariables' in data[i]: data[i] = re.sub( r'(\$\{\$gameVariables\.value\(\d+\)\}\s*)回', r'\1x', data[i] ) saveCheckLines(data, filename) # Next Line i += 1 # EOF translate = False questListTL = [[], [], [], [], [], []] customTL = [] # Compute combined total for progress bar across all categories combinedTotal = ( sum(len(quest) for quest in questList) + len(custom) + len(sceneMenuText) + len(sceneMenuCommonHelpText) + len(sceneMenuHelpText) + len(sceneMenuDrawText) + len(sceneMenuStatLabel) + len(saveParamName) ) if combinedTotal > 0: pbar.total = combinedTotal pbar.refresh() # Quest if len(questList) > 0: # Quest Name response = translateAI(questList[0], "Quest Name") tokens[0] += response[1][0] tokens[1] += response[1][1] questName = response[0] # Quest Client response = translateAI(questList[1], "Quest Client") tokens[0] += response[1][0] tokens[1] += response[1][1] questClient = response[0] # Quest Location response = translateAI(questList[2], "Quest Location") tokens[0] += response[1][0] tokens[1] += response[1][1] questLocation = response[0] # Quest Target response = translateAI(questList[3], "Quest Location") tokens[0] += response[1][0] tokens[1] += response[1][1] questTarget = response[0] # Quest Summary response = translateAI(questList[4], "Quest Summary") tokens[0] += response[1][0] tokens[1] += response[1][1] questSummary = response[0] # Quest Goal 1 response = translateAI(questList[5], "Quest Goal") tokens[0] += response[1][0] tokens[1] += response[1][1] questGoal1 = response[0] # Check Mismatch if ( len(questName) == len(questList[0]) or len(questClient) == len(questList[1]) or len(questLocation) == len(questList[2]) or len(questTarget) == len(questList[3]) or len(questSummary) == len(questList[4]) or len(questGoal1) == len(questList[5]) ): # Set Strings questListTL = [questName, questClient, questLocation, questTarget, questSummary, questGoal1] translate = True # Mismatch else: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) # Custom if custom: # TL response = translateAI(custom, "Relic Name") tokens[0] += response[1][0] tokens[1] += response[1][1] customResponse = response[0] # Check Mismatch if len(custom) == len(customResponse): customTL = customResponse translate = True # Mismatch else: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) # SceneCustomMenu Text sceneMenuTextTL = [] sceneMenuCommonHelpTextTL = [] sceneMenuHelpTextTL = [] sceneMenuDrawTextTL = [] sceneMenuStatLabelTL = [] saveParamNameTL = [] if sceneMenuText: # TL response = translateAI(sceneMenuText, "Menu Item") tokens[0] += response[1][0] tokens[1] += response[1][1] sceneMenuTextResponse = response[0] # Check Mismatch if len(sceneMenuText) == len(sceneMenuTextResponse): sceneMenuTextTL = sceneMenuTextResponse translate = True # Mismatch else: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) # SceneCustomMenu CommonHelpText if sceneMenuCommonHelpText: # TL response = translateAI(sceneMenuCommonHelpText, "Menu Help Text") tokens[0] += response[1][0] tokens[1] += response[1][1] sceneMenuCommonHelpTextResponse = response[0] # Check Mismatch if len(sceneMenuCommonHelpText) == len(sceneMenuCommonHelpTextResponse): sceneMenuCommonHelpTextTL = sceneMenuCommonHelpTextResponse translate = True # Mismatch else: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) # SceneCustomMenu HelpText if sceneMenuHelpText: # TL response = translateAI(sceneMenuHelpText, "Menu Help Text") tokens[0] += response[1][0] tokens[1] += response[1][1] sceneMenuHelpTextResponse = response[0] # Check Mismatch if len(sceneMenuHelpText) == len(sceneMenuHelpTextResponse): sceneMenuHelpTextTL = sceneMenuHelpTextResponse translate = True # Mismatch else: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) # DrawTextEx - Brace-delimited labels (tracker section headers and field labels) if sceneMenuDrawText: # TL response = translateAI(sceneMenuDrawText, "Status Tracker Label") tokens[0] += response[1][0] tokens[1] += response[1][1] sceneMenuDrawTextResponse = response[0] # Check Mismatch if len(sceneMenuDrawText) == len(sceneMenuDrawTextResponse): sceneMenuDrawTextTL = sceneMenuDrawTextResponse translate = True # Mismatch else: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) # DrawTextEx - Stat labels (e.g., 攻撃力, 魔力, etc.) if sceneMenuStatLabel: # TL response = translateAI(sceneMenuStatLabel, "Character Stat Name") tokens[0] += response[1][0] tokens[1] += response[1][1] sceneMenuStatLabelResponse = response[0] # Check Mismatch if len(sceneMenuStatLabel) == len(sceneMenuStatLabelResponse): sceneMenuStatLabelTL = sceneMenuStatLabelResponse translate = True # Mismatch else: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) # NUUN_SaveScreen ParamName if saveParamName: # TL response = translateAI(saveParamName, "Save Screen Label") tokens[0] += response[1][0] tokens[1] += response[1][1] saveParamNameResponse = response[0] # Check Mismatch if len(saveParamName) == len(saveParamNameResponse): saveParamNameTL = saveParamNameResponse translate = True # Mismatch else: with LOCK: if filename not in MISMATCH: MISMATCH.append(filename) # Pass 2 if translate and not setData: translatePlugin(data, pbar, filename, [questListTL, customTL, sceneMenuTextTL, sceneMenuCommonHelpTextTL, sceneMenuHelpTextTL, sceneMenuDrawTextTL, sceneMenuStatLabelTL, saveParamNameTL]) return tokens # Save some money and enter the character before translation def getSpeaker(speaker): match speaker: case "ファイン": return ["Fine", [0, 0]] case "": return ["", [0, 0]] case _: # Find Speaker for i in range(len(NAMESLIST)): if speaker == NAMESLIST[i][0]: return [NAMESLIST[i][1], [0, 0]] # Translate and Store Speaker response = translateAI( f"{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]) == None: response = translateAI( f"{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 return [speaker, [0, 0]] def translateAI(text, history, history_ctx=None): """ Legacy wrapper function for the new shared translation utility. This maintains compatibility with existing code while using the new shared implementation. """ 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, config=TRANSLATION_CONFIG, filename=FILENAME, pbar=PBAR, lock=LOCK, mismatchList=MISMATCH )