Update Translation.py

This commit is contained in:
Yusarina
2025-04-16 16:17:57 +01:00
parent bb5a314796
commit 19c2ede791
+92 -37
View File
@@ -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)