From 19c2ede791f9d3f09259001dacd2634f2b3b1ea5 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Wed, 16 Apr 2025 16:17:57 +0100 Subject: [PATCH] Update Translation.py --- core/mmd/translations.py | 129 ++++++++++++++++++++++++++++----------- 1 file changed, 92 insertions(+), 37 deletions(-) diff --git a/core/mmd/translations.py b/core/mmd/translations.py index b7f5e3c..267891a 100644 --- a/core/mmd/translations.py +++ b/core/mmd/translations.py @@ -6,14 +6,20 @@ # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. import csv -import logging import time +from typing import List, Tuple, Dict, Optional, Any, Generator, Union, TextIO, Iterator, Set import bpy +from bpy.types import Text, Context from .bpyutils import FnContext +from ..logging_setup import logger -jp_half_to_full_tuples = ( +# Type definitions for translation tuples +TranslationTuple = Tuple[str, str] +TranslationList = List[TranslationTuple] + +jp_half_to_full_tuples: TranslationList = ( ("ヴ", "ヴ"), ("ガ", "ガ"), ("ギ", "ギ"), @@ -103,7 +109,7 @@ jp_half_to_full_tuples = ( ("ン", "ン"), ) -jp_to_en_tuples = [ +jp_to_en_tuples: TranslationList = [ ("全ての親", "ParentNode"), ("操作中心", "ControlNode"), ("センター", "Center"), @@ -293,22 +299,30 @@ jp_to_en_tuples = [ ] -def translateFromJp(name): +def translateFromJp(name: str) -> str: + """Translate a Japanese name to English using the translation tuples.""" + logger.debug(f"Translating from Japanese: {name}") for tuple in jp_to_en_tuples: if tuple[0] in name: name = name.replace(tuple[0], tuple[1]) + logger.debug(f"Translation result: {name}") return name -def getTranslator(csvfile="", keep_order=False): +def getTranslator(csvfile: Union[str, Dict[str, str], Text] = "", keep_order: bool = False) -> 'MMDTranslator': + """Get a translator instance with the specified CSV file.""" translator = MMDTranslator() if isinstance(csvfile, bpy.types.Text): + logger.debug(f"Loading translator from Text object: {csvfile.name}") translator.load_from_stream(csvfile) elif isinstance(csvfile, dict): + logger.debug(f"Loading translator from dictionary with {len(csvfile)} entries") translator.csv_tuples.extend(csvfile.items()) elif csvfile in bpy.data.texts.keys(): + logger.debug(f"Loading translator from text data: {csvfile}") translator.load_from_stream(bpy.data.texts[csvfile]) else: + logger.debug(f"Loading translator from file: {csvfile}") translator.load(csvfile) if not keep_order: @@ -318,16 +332,20 @@ def getTranslator(csvfile="", keep_order=False): class MMDTranslator: - def __init__(self): - self.__csv_tuples = [] - self.__fails = {} + """Handles translation of Japanese text to English for MMD models.""" + + def __init__(self) -> None: + self.__csv_tuples: List[Tuple[str, str]] = [] + self.__fails: Dict[str, str] = {} @staticmethod - def default_csv_filepath(): + def default_csv_filepath() -> str: + """Get the default CSV filepath for translations.""" return __file__[:-3] + ".csv" @staticmethod - def get_csv_text(text_name=None): + def get_csv_text(text_name: Optional[str] = None) -> Text: + """Get or create a Text object for CSV data.""" text_name = text_name or bpy.path.basename(MMDTranslator.default_csv_filepath()) csv_text = bpy.data.texts.get(text_name, None) if csv_text is None: @@ -335,69 +353,88 @@ class MMDTranslator: return csv_text @staticmethod - def replace_from_tuples(name, tuples): + def replace_from_tuples(name: str, tuples: List[Tuple[str, str]]) -> str: + """Replace parts of a string based on translation tuples.""" for pair in tuples: if pair[0] in name: name = name.replace(pair[0], pair[1]) return name @property - def csv_tuples(self): + def csv_tuples(self) -> List[Tuple[str, str]]: + """Get the CSV tuples.""" return self.__csv_tuples @property - def fails(self): + def fails(self) -> Dict[str, str]: + """Get the failed translations.""" return self.__fails - def sort(self): + def sort(self) -> None: + """Sort the CSV tuples by length (longest first) and then alphabetically.""" + logger.debug("Sorting translation tuples") self.__csv_tuples.sort(key=lambda row: (-len(row[0]), row)) - def update(self): + def update(self) -> None: + """Update the CSV tuples, removing duplicates.""" from collections import OrderedDict count_old = len(self.__csv_tuples) tuples_dict = OrderedDict((row[0], row) for row in self.__csv_tuples if len(row) >= 2 and row[0]) self.__csv_tuples.clear() self.__csv_tuples.extend(tuples_dict.values()) - logging.info(" - removed items:\t%d\t(of %d)", count_old - len(self.__csv_tuples), count_old) + logger.info("Translation update - removed items: %d (of %d)", count_old - len(self.__csv_tuples), count_old) - def half_to_full(self, name): + def half_to_full(self, name: str) -> str: + """Convert half-width Japanese characters to full-width.""" return self.replace_from_tuples(name, jp_half_to_full_tuples) - def is_translated(self, name): + def is_translated(self, name: str) -> bool: + """Check if a string is already translated (contains only ASCII characters).""" try: name.encode("ascii", errors="strict") except UnicodeEncodeError: return False return True - def translate(self, name, default=None, from_full_width=True): + def translate(self, name: str, default: Optional[str] = None, from_full_width: bool = True) -> str: + """Translate a string from Japanese to English.""" + logger.debug(f"Translating: {name}") if from_full_width: name = self.half_to_full(name) name_new = self.replace_from_tuples(name, self.__csv_tuples) if default is not None and not self.is_translated(name_new): + logger.warning(f"Translation failed for: {name}") self.__fails[name] = name_new return default return name_new - def save_fails(self, text_name=None): + def save_fails(self, text_name: Optional[str] = None) -> Text: + """Save failed translations to a Text object.""" text_name = text_name or (__name__ + ".fails") txt = self.get_csv_text(text_name) fmt = '"%s","%s"' items = sorted(self.__fails.items(), key=lambda row: (-len(row[0]), row)) txt.from_string("\n".join(fmt % (k, v) for k, v in items)) + logger.info(f"Saved {len(items)} failed translations to {text_name}") return txt - def load_from_stream(self, csvfile=None): + def load_from_stream(self, csvfile: Union[Text, Iterator[str]] = None) -> None: + """Load translations from a stream.""" csvfile = csvfile or self.get_csv_text() if isinstance(csvfile, bpy.types.Text): csvfile = (l.body + "\n" for l in csvfile.lines) spamreader = csv.reader(csvfile, delimiter=",", skipinitialspace=True) csv_tuples = [tuple(row) for row in spamreader if len(row) >= 2] self.__csv_tuples = csv_tuples - logging.info(" - load items:\t%d", len(self.__csv_tuples)) + logger.info("Loaded %d translation items", len(self.__csv_tuples)) - def save_to_stream(self, csvfile=None): + def save_to_stream(self, csvfile: Union[Text, TextIO] = None) -> None: + """Save translations to a stream. + + Args: + csvfile: The CSV file or stream to save to + """ csvfile = csvfile or self.get_csv_text() lineterminator = "\r\n" if isinstance(csvfile, bpy.types.Text): @@ -405,27 +442,38 @@ class MMDTranslator: lineterminator = "\n" spamwriter = csv.writer(csvfile, delimiter=",", lineterminator=lineterminator, quoting=csv.QUOTE_ALL) spamwriter.writerows(self.__csv_tuples) - logging.info(" - save items:\t%d", len(self.__csv_tuples)) + logger.info("Saved %d translation items", len(self.__csv_tuples)) - def load(self, filepath=None): + def load(self, filepath: Optional[str] = None) -> None: + """Load translations from a file.""" filepath = filepath or self.default_csv_filepath() - logging.info("Loading csv file:\t%s", filepath) - with open(filepath, "rt", encoding="utf-8", newline="") as csvfile: - self.load_from_stream(csvfile) + logger.info("Loading CSV file: %s", filepath) + try: + with open(filepath, "rt", encoding="utf-8", newline="") as csvfile: + self.load_from_stream(csvfile) + except Exception as e: + logger.error(f"Failed to load CSV file: {e}") - def save(self, filepath=None): + def save(self, filepath: Optional[str] = None) -> None: + """Save translations to a file.""" filepath = filepath or self.default_csv_filepath() - logging.info("Saving csv file:\t%s", filepath) - with open(filepath, "wt", encoding="utf-8", newline="") as csvfile: - self.save_to_stream(csvfile) + logger.info("Saving CSV file: %s", filepath) + try: + with open(filepath, "wt", encoding="utf-8", newline="") as csvfile: + self.save_to_stream(csvfile) + except Exception as e: + logger.error(f"Failed to save CSV file: {e}") class DictionaryEnum: - __items_ttl = 0.0 - __items_cache = None + """Handles dictionary enumeration for UI.""" + + __items_ttl: float = 0.0 + __items_cache: Optional[List[Tuple[str, str, str, int]]] = None @staticmethod - def get_dictionary_items(prop, context): + def get_dictionary_items(prop: Any, context: Context) -> List[Tuple[str, str, str, Union[int, str], int]]: + """Get dictionary items for UI enumeration.""" if DictionaryEnum.__items_ttl > time.time(): return DictionaryEnum.__items_cache @@ -437,7 +485,7 @@ class DictionaryEnum: items.append(("INTERNAL", "Internal Dictionary", "The dictionary defined in " + __name__, len(items))) for txt_name in sorted(x.name for x in bpy.data.texts if x.name.lower().endswith(".csv")): - items.append((txt_name, txt_name, "bpy.data.texts['%s']" % txt_name, "TEXT", len(items))) + items.append((txt_name, txt_name, f"bpy.data.texts['{txt_name}']", "TEXT", len(items))) import os @@ -450,12 +498,19 @@ class DictionaryEnum: if "dictionary" in prop: prop["dictionary"] = min(prop["dictionary"], len(items) - 1) + + logger.debug(f"Found {len(items)} dictionary items") return items @staticmethod - def get_translator(dictionary): + def get_translator(dictionary: str) -> Optional[MMDTranslator]: + """Get a translator for the specified dictionary.""" if dictionary == "DISABLED": + logger.debug("Translation disabled") return None if dictionary == "INTERNAL": + logger.debug("Using internal dictionary") return getTranslator(dict(jp_to_en_tuples)) + + logger.debug(f"Using dictionary: {dictionary}") return getTranslator(dictionary)