From e991f27cbf0facaf34caa00081dd2a16a83f324b Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Sat, 11 Jul 2026 09:41:05 -0500 Subject: [PATCH] Make convert quotes optional. --- gui/config_tab.py | 35 +++++++++++++++++++++++++++++++++++ modules/rpgmakermvmz.py | 7 ++++--- util/translation.py | 34 +++++++++++++++++++++++++++------- 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/gui/config_tab.py b/gui/config_tab.py index a4bbbdf..486d5e9 100644 --- a/gui/config_tab.py +++ b/gui/config_tab.py @@ -511,6 +511,20 @@ class ConfigTab(QWidget): self.note_width_spin.setSuffix(" chars") self.note_width_spin.setFixedWidth(120) # Small format_form.addRow(note_label, self.note_width_spin) + + quotes_label = QLabel("Convert Quotes:") + quotes_label.setFixedWidth(150) + quotes_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + self.convert_quotes_cb = QCheckBox('Convert 「」 / 『』 to ""') + self.convert_quotes_cb.setChecked(True) + self.convert_quotes_cb.setToolTip( + "When enabled, Japanese corner brackets 「」 and 『』 are replaced " + 'with ASCII double quotes "" in translated output (and on RPG Maker ' + "source text before translation).\n\n" + "Leaving this on is recommended - the AI often fails to keep 「」 " + "consistent across lines." + ) + format_form.addRow(quotes_label, self.convert_quotes_cb) right_column.addLayout(format_form) right_column.addWidget(create_horizontal_line()) @@ -774,6 +788,9 @@ class ConfigTab(QWidget): self.width_spin.setValue(int(_get("width", "60"))) self.list_width_spin.setValue(int(_get("listWidth", "100"))) self.note_width_spin.setValue(int(_get("noteWidth", "75"))) + self.convert_quotes_cb.setChecked( + _get("convertQuotes", "true").strip().lower() in ("true", "1", "yes") + ) # Custom API pricing self.input_cost_spin.setValue(float(_get("input_cost", "2.0"))) @@ -835,6 +852,7 @@ class ConfigTab(QWidget): self.width_spin.editingFinished.connect(self.auto_save) self.list_width_spin.editingFinished.connect(self.auto_save) self.note_width_spin.editingFinished.connect(self.auto_save) + self.convert_quotes_cb.stateChanged.connect(self._on_convert_quotes_changed) self.input_cost_spin.editingFinished.connect(self.auto_save) self.output_cost_spin.editingFinished.connect(self.auto_save) self.font_scale_spin.editingFinished.connect(self.auto_save) @@ -859,6 +877,7 @@ class ConfigTab(QWidget): self.width_spin.editingFinished.disconnect(self.auto_save) self.list_width_spin.editingFinished.disconnect(self.auto_save) self.note_width_spin.editingFinished.disconnect(self.auto_save) + self.convert_quotes_cb.stateChanged.disconnect(self._on_convert_quotes_changed) self.input_cost_spin.editingFinished.disconnect(self.auto_save) self.output_cost_spin.editingFinished.disconnect(self.auto_save) self.font_scale_spin.editingFinished.disconnect(self.auto_save) @@ -869,6 +888,19 @@ class ConfigTab(QWidget): except (TypeError, RuntimeError): pass + def _on_convert_quotes_changed(self, state): + """Warn when quote conversion is disabled, then auto-save.""" + if state == Qt.Unchecked: + QMessageBox.warning( + self, + "Convert Quotes Disabled", + "The AI often struggles to keep 「」 and 『』 consistent " + "across translated lines.\n\n" + "Leaving conversion enabled is recommended unless you " + "specifically need the Japanese brackets in the output.", + ) + self.auto_save() + def auto_save(self): """Auto-save configuration without showing message.""" self.save_to_env(show_message=False) @@ -901,6 +933,7 @@ class ConfigTab(QWidget): "width": str(self.width_spin.value()), "listWidth": str(self.list_width_spin.value()), "noteWidth": str(self.note_width_spin.value()), + "convertQuotes": "true" if self.convert_quotes_cb.isChecked() else "false", "input_cost": str(self.input_cost_spin.value()), "output_cost": str(self.output_cost_spin.value()), "font_scale": str(self.font_scale_spin.value()), @@ -964,6 +997,7 @@ class ConfigTab(QWidget): self.width_spin.setValue(60) self.list_width_spin.setValue(100) self.note_width_spin.setValue(75) + self.convert_quotes_cb.setChecked(True) # UI settings self.font_scale_spin.setValue(1.0) @@ -1008,6 +1042,7 @@ class ConfigTab(QWidget): "width": self.width_spin.value(), "listWidth": self.list_width_spin.value(), "noteWidth": self.note_width_spin.value(), + "convertQuotes": self.convert_quotes_cb.isChecked(), "input_cost": self.input_cost_spin.value(), "output_cost": self.output_cost_spin.value(), "font_scale": self.font_scale_spin.value(), diff --git a/modules/rpgmakermvmz.py b/modules/rpgmakermvmz.py index db8f912..bcaf162 100644 --- a/modules/rpgmakermvmz.py +++ b/modules/rpgmakermvmz.py @@ -13,7 +13,7 @@ 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, getPricingConfig, calculateCost, get_var_translation, set_var_translations_batch +from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost, get_var_translation, set_var_translations_batch, convert_corner_brackets from util.speakers import SPEAKER_BRACKET_INNER, strip_speaker_prefix # Globals @@ -2470,8 +2470,9 @@ def searchCodes(page, pbar, jobList, filename): # Remove Extra Stuff bad for translation. finalJAString = finalJAString.replace("゙", "") finalJAString = finalJAString.replace(" ", "") - finalJAString = finalJAString.replace("「", '"') - finalJAString = finalJAString.replace("」", '"') + finalJAString = convert_corner_brackets( + finalJAString, TRANSLATION_CONFIG.convertQuotes + ) finalJAString = finalJAString.replace("\\,", ',') ### Remove format codes diff --git a/util/translation.py b/util/translation.py index 5ccd1fa..5cc8834 100644 --- a/util/translation.py +++ b/util/translation.py @@ -1467,7 +1467,8 @@ class TranslationConfig: maxHistory=10, estimateMode=False, logFilePath="log/translationHistory.txt", - mismatchLogPath="log/mismatchHistory.txt"): + mismatchLogPath="log/mismatchHistory.txt", + convertQuotes=None): # Load from environment if not provided self.model = model or os.getenv("model") @@ -1504,6 +1505,24 @@ class TranslationConfig: self.estimateMode = estimateMode self.logFilePath = logFilePath self.mismatchLogPath = mismatchLogPath + if convertQuotes is None: + self.convertQuotes = os.getenv("convertQuotes", "true").strip().lower() in ( + "true", "1", "yes", + ) + else: + self.convertQuotes = bool(convertQuotes) + + +def convert_corner_brackets(text, enabled=True): + """Replace Japanese corner brackets 「」『』 with ASCII double quotes when enabled.""" + if not enabled or not isinstance(text, str): + return text + return ( + text.replace("「", '"') + .replace("」", '"') + .replace("『", '"') + .replace("』", '"') + ) _LITELLM_PRICING_URL = ( @@ -2295,7 +2314,7 @@ def cleanTranslatedText(translatedText, language): "。": ".", # Note: 「 and 」 are NOT replaced here — replacing them with ASCII " would # corrupt raw JSON strings before extraction. They are handled per-line - # in _clean_extracted_line() after JSON parsing. + # in _clean_extracted_line() after JSON parsing (when convertQuotes is on). "—": "―", "】": "]", "【": "[", @@ -2640,7 +2659,7 @@ def translateAI(text, history, config, filename=None, pbar=None, lock=None, mism return bool(inner) and all(c in '\u2026\u30FC' for c in inner) def _convert_ellipsis(s): - return str(s).replace('「', '"').replace('」', '"').replace('『', '"').replace('』', '"') + return convert_corner_brackets(str(s), config.convertQuotes) if isinstance(tItem, list): if all(_is_ellipsis_only(s) for s in tItem): @@ -2700,7 +2719,7 @@ def translateAI(text, history, config, filename=None, pbar=None, lock=None, mism item_str = str(tItem[j]).strip() if tItem[j] else "" if item_str and item_str != "Placeholder Text" and not re.search(config.langRegex, item_str): cleaned = cleanTranslatedText(tItem[j], config.language) - cleaned = cleaned.replace("「", '"').replace("」", '"').strip() + cleaned = convert_corner_brackets(cleaned, config.convertQuotes).strip() no_japanese_map[j] = cleaned # Combine skip sets and rebuild protected_items / all_replacements @@ -3047,9 +3066,7 @@ def translateAI(text, history, config, filename=None, pbar=None, lock=None, mism if not isinstance(line, str): return line line = line.replace("Placeholder Text", "").strip() - line = line.replace("「", '"').replace("」", '"') - line = line.replace("『", '"').replace("』", '"') - return line + return convert_corner_brackets(line, config.convertQuotes) final_translations = [_clean_extracted_line(line) for line in extracted] else: # Single string: extract from JSON schema response @@ -3084,6 +3101,9 @@ def translateAI(text, history, config, filename=None, pbar=None, lock=None, mism pbar.write(f" - {reason}") else: # Accept output - all validations passed + final_cleaned = convert_corner_brackets( + final_cleaned, config.convertQuotes + ) final_translations = final_cleaned else: is_valid = False