diff --git a/core/mmd_converter.py b/core/mmd_converter.py index d260097..dd4c44e 100644 --- a/core/mmd_converter.py +++ b/core/mmd_converter.py @@ -4,12 +4,13 @@ Handles armature hierarchy and naming conventions """ import bpy from typing import Dict, List, Optional, Tuple, Set -from bpy.types import Object, Bone, Collection +from bpy.types import Object, Bone, Collection, Material, ShapeKey from .common import get_active_armature from .dictionaries import simplify_bonename from .enhanced_dictionaries import mmd_bone_patterns from .logging_setup import logger from .translations import t +from .mmd.translations import jp_to_en_tuples, translateFromJp def detect_mmd_armature(armature: Object) -> bool: @@ -164,3 +165,279 @@ def convert_mmd_armature(armature: Object, logger.warning("MMD armature conversion completed with errors") return overall_success, messages + + +def translate_mmd_name(name: str, category: str = "auto") -> Tuple[str, str]: + """Translate MMD name using MMD dictionary first, then translation services""" + if not name or not name.strip(): + return name, "unchanged" + + original_name = name.strip() + + # Step 1: Try MMD built-in dictionary translation + mmd_translated = translateFromJp(original_name) + + # Check if MMD dictionary actually translated something + if mmd_translated != original_name and mmd_translated: + logger.debug(f"MMD dictionary translated: '{original_name}' -> '{mmd_translated}'") + return mmd_translated, "mmd_dictionary" + + # Step 2: If MMD dictionary didn't translate or only partially translated, + # use Avatar Toolkit translation services + try: + from .translation_manager import get_avatar_translation_manager + + manager = get_avatar_translation_manager() + result = manager.translate_single(original_name, category=category, source_lang="ja", target_lang="en") + + if result.translated != original_name: + logger.debug(f"API translated: '{original_name}' -> '{result.translated}' (method: {result.method})") + return result.translated, "api_translation" + except Exception as e: + logger.warning(f"Translation service failed for '{original_name}': {e}") + + # Step 3: No translation available + logger.debug(f"No translation available for: '{original_name}'") + return original_name, "unchanged" + + +def translate_mmd_armature_bones(armature: Object, apply_translation: bool = True) -> Tuple[int, int, List[str]]: + """Translate all bone names in an MMD armature""" + if not armature or armature.type != 'ARMATURE': + return 0, 0, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting bone translation for armature: {armature.name}") + + successful = 0 + failed = 0 + messages = [] + bone_translations = {} + + # Store the current mode + current_mode = bpy.context.mode + if current_mode != 'EDIT': + bpy.context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='EDIT') + + try: + for bone in armature.data.edit_bones: + original_name = bone.name + translated_name, method = translate_mmd_name(original_name, category="bones") + + if translated_name != original_name: + bone_translations[original_name] = (translated_name, method) + + if apply_translation: + try: + bone.name = translated_name + logger.info(f"Translated bone: '{original_name}' -> '{translated_name}' ({method})") + successful += 1 + except Exception as e: + logger.error(f"Failed to rename bone '{original_name}': {e}") + failed += 1 + else: + successful += 1 + else: + logger.debug(f"Bone '{original_name}' not translated") + + finally: + # Restore original mode + if current_mode != 'EDIT': + bpy.ops.object.mode_set(mode='OBJECT') + + # Generate summary messages + if successful > 0: + messages.append(t("MMD.bones_translated", count=successful)) + if failed > 0: + messages.append(t("MMD.bones_failed", count=failed)) + + mmd_dict_count = sum(1 for _, (_, method) in bone_translations.items() if method == "mmd_dictionary") + api_count = sum(1 for _, (_, method) in bone_translations.items() if method == "api_translation") + + logger.info(f"Bone translation complete: {successful} successful, {failed} failed") + logger.info(f"Translation methods: MMD Dictionary: {mmd_dict_count}, API: {api_count}") + + return successful, failed, messages + + +def translate_mmd_materials(armature: Object, apply_translation: bool = True) -> Tuple[int, int, List[str]]: + """Translate all material names for meshes parented to the armature""" + if not armature or armature.type != 'ARMATURE': + return 0, 0, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting material translation for armature: {armature.name}") + + successful = 0 + failed = 0 + messages = [] + processed_materials = set() + + # Get all mesh objects parented to this armature + for obj in bpy.data.objects: + if obj.type == 'MESH' and obj.parent == armature and obj.data.materials: + for mat in obj.data.materials: + if mat and mat.name not in processed_materials: + processed_materials.add(mat.name) + original_name = mat.name + translated_name, method = translate_mmd_name(original_name, category="materials") + + if translated_name != original_name and apply_translation: + try: + mat.name = translated_name + logger.info(f"Translated material: '{original_name}' -> '{translated_name}' ({method})") + successful += 1 + except Exception as e: + logger.error(f"Failed to rename material '{original_name}': {e}") + failed += 1 + elif translated_name != original_name: + successful += 1 + + if successful > 0: + messages.append(t("MMD.materials_translated", count=successful)) + if failed > 0: + messages.append(t("MMD.materials_failed", count=failed)) + + logger.info(f"Material translation complete: {successful} successful, {failed} failed") + + return successful, failed, messages + + +def translate_mmd_shapekeys(armature: Object, apply_translation: bool = True) -> Tuple[int, int, List[str]]: + """Translate all shape key names for meshes parented to the armature""" + if not armature or armature.type != 'ARMATURE': + return 0, 0, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting shape key translation for armature: {armature.name}") + + successful = 0 + failed = 0 + messages = [] + + # Get all mesh objects parented to this armature + for obj in bpy.data.objects: + if obj.type == 'MESH' and obj.parent == armature and obj.data.shape_keys: + for shape_key in obj.data.shape_keys.key_blocks: + original_name = shape_key.name + translated_name, method = translate_mmd_name(original_name, category="shapekeys") + + if translated_name != original_name and apply_translation: + try: + shape_key.name = translated_name + logger.info(f"Translated shape key: '{original_name}' -> '{translated_name}' ({method})") + successful += 1 + except Exception as e: + logger.error(f"Failed to rename shape key '{original_name}': {e}") + failed += 1 + elif translated_name != original_name: + successful += 1 + + if successful > 0: + messages.append(t("MMD.shapekeys_translated", count=successful)) + if failed > 0: + messages.append(t("MMD.shapekeys_failed", count=failed)) + + logger.info(f"Shape key translation complete: {successful} successful, {failed} failed") + + return successful, failed, messages + + +def translate_mmd_objects(armature: Object, apply_translation: bool = True) -> Tuple[int, int, List[str]]: + """Translate object names parented to the armature""" + if not armature or armature.type != 'ARMATURE': + return 0, 0, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting object name translation for armature: {armature.name}") + + successful = 0 + failed = 0 + messages = [] + + # Get all objects parented to this armature + for obj in bpy.data.objects: + if obj.parent == armature: + original_name = obj.name + translated_name, method = translate_mmd_name(original_name, category="objects") + + if translated_name != original_name and apply_translation: + try: + obj.name = translated_name + logger.info(f"Translated object: '{original_name}' -> '{translated_name}' ({method})") + successful += 1 + except Exception as e: + logger.error(f"Failed to rename object '{original_name}': {e}") + failed += 1 + elif translated_name != original_name: + successful += 1 + + if successful > 0: + messages.append(t("MMD.objects_translated", count=successful)) + if failed > 0: + messages.append(t("MMD.objects_failed", count=failed)) + + logger.info(f"Object translation complete: {successful} successful, {failed} failed") + + return successful, failed, messages + + +def translate_mmd_everything(armature: Object, + translate_bones: bool = True, + translate_materials: bool = True, + translate_shapekeys: bool = True, + translate_objects: bool = True) -> Tuple[bool, List[str]]: + """ + Translate all MMD names (bones, materials, shape keys, objects) + + Args: + armature: The armature object + translate_bones: Whether to translate bone names + translate_materials: Whether to translate material names + translate_shapekeys: Whether to translate shape key names + translate_objects: Whether to translate object names + + Returns: + Tuple of (success, messages) + """ + if not armature or armature.type != 'ARMATURE': + return False, [t("MMD.error.invalid_armature")] + + logger.info(f"Starting comprehensive MMD translation for: {armature.name}") + + all_messages = [] + total_successful = 0 + total_failed = 0 + + # Translate bones + if translate_bones: + success, failed, messages = translate_mmd_armature_bones(armature, apply_translation=True) + total_successful += success + total_failed += failed + all_messages.extend(messages) + + # Translate materials + if translate_materials: + success, failed, messages = translate_mmd_materials(armature, apply_translation=True) + total_successful += success + total_failed += failed + all_messages.extend(messages) + + # Translate shape keys + if translate_shapekeys: + success, failed, messages = translate_mmd_shapekeys(armature, apply_translation=True) + total_successful += success + total_failed += failed + all_messages.extend(messages) + + # Translate objects + if translate_objects: + success, failed, messages = translate_mmd_objects(armature, apply_translation=True) + total_successful += success + total_failed += failed + all_messages.extend(messages) + + # Summary + if total_successful > 0: + all_messages.append(t("MMD.translation_complete", total=total_successful)) + + logger.info(f"Comprehensive MMD translation complete: {total_successful} successful, {total_failed} failed") + + return total_failed == 0, all_messages diff --git a/core/properties.py b/core/properties.py index 5c9dbab..10ef5d5 100644 --- a/core/properties.py +++ b/core/properties.py @@ -715,6 +715,36 @@ class AvatarToolkitSceneProperties(PropertyGroup): description="Rename the armature object to 'Armature'", default=True ) + + mmd_translate_names: BoolProperty( + name=t("MMD.translate_names"), + description="Translate Japanese names to English using MMD dictionary and translation services", + default=True + ) + + mmd_translate_bones: BoolProperty( + name=t("MMD.translate_bones"), + description="Translate bone names", + default=True + ) + + mmd_translate_materials: BoolProperty( + name=t("MMD.translate_materials"), + description="Translate material names", + default=True + ) + + mmd_translate_shapekeys: BoolProperty( + name=t("MMD.translate_shapekeys"), + description="Translate shape key names", + default=True + ) + + mmd_translate_objects: BoolProperty( + name=t("MMD.translate_objects"), + description="Translate object names", + default=True + ) # Translation System Properties translation_service: EnumProperty( diff --git a/functions/tools/mmd_conversion.py b/functions/tools/mmd_conversion.py index 0f238d6..dd81254 100644 --- a/functions/tools/mmd_conversion.py +++ b/functions/tools/mmd_conversion.py @@ -6,7 +6,8 @@ import bpy from bpy.types import Operator from ...core.common import get_active_armature from ...core.translations import t -from ...core.mmd_converter import convert_mmd_armature, detect_mmd_armature +from ...core.mmd_converter import (convert_mmd_armature, detect_mmd_armature, + translate_mmd_everything) from ...core.logging_setup import logger @@ -37,12 +38,21 @@ class AvatarToolkit_OT_ConvertMMDArmature(Operator): self.report({'WARNING'}, t("MMD.not_mmd_armature")) return {'CANCELLED'} - # conversion settings + # Get conversion settings toolkit = context.scene.avatar_toolkit make_parent = toolkit.mmd_make_parent rename_armature = toolkit.mmd_rename_armature + translate_names = toolkit.mmd_translate_names + translate_bones = toolkit.mmd_translate_bones + translate_materials = toolkit.mmd_translate_materials + translate_shapekeys = toolkit.mmd_translate_shapekeys + translate_objects = toolkit.mmd_translate_objects logger.info(f"Conversion settings - Make parent: {make_parent}, Rename: {rename_armature}") + logger.info(f"Translation settings - Enabled: {translate_names}, Bones: {translate_bones}, " + + f"Materials: {translate_materials}, Shapekeys: {translate_shapekeys}, Objects: {translate_objects}") + + # Step 1: Basic conversion (parent and rename) success, messages = convert_mmd_armature(armature, make_parent, rename_armature) if not success: @@ -51,8 +61,29 @@ class AvatarToolkit_OT_ConvertMMDArmature(Operator): self.report({'WARNING'}, msg) return {'CANCELLED'} - logger.info(f"MMD conversion completed successfully") + logger.info(f"MMD basic conversion completed successfully") for msg in messages: self.report({'INFO'}, msg) + # Step 2: Translation (if enabled) + if translate_names: + logger.info("Starting MMD name translation") + self.report({'INFO'}, t("MMD.translation_starting")) + + trans_success, trans_messages = translate_mmd_everything( + armature, + translate_bones=translate_bones, + translate_materials=translate_materials, + translate_shapekeys=translate_shapekeys, + translate_objects=translate_objects + ) + + if trans_success: + logger.info("MMD name translation completed successfully") + else: + logger.warning("MMD name translation completed with some failures") + + for msg in trans_messages: + self.report({'INFO'}, msg) + return {'FINISHED'} diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 80a1f3b..c8fb9a2 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -611,6 +611,12 @@ "MMD.not_mmd_armature": "Selected armature does not appear to be MMD format", "MMD.make_armature_parent": "Make Armature Main Parent", "MMD.rename_to_armature": "Rename to 'Armature'", + "MMD.translate_names": "Translate Names to English", + "MMD.translate_bones": "Bones", + "MMD.translate_materials": "Materials", + "MMD.translate_shapekeys": "Shape Keys", + "MMD.translate_objects": "Objects", + "MMD.translation_options": "Translation Options:", "MMD.convert_armature_button": "Convert MMD Armature", "MMD.convert_armature.label": "Convert MMD Armature", "MMD.convert_armature.desc": "Convert MMD armature to standard Blender format", @@ -618,6 +624,7 @@ "MMD.conversion_info.removes_parent": "• Removes parent Empty object", "MMD.conversion_info.renames_armature": "• Renames armature to 'Armature'", "MMD.conversion_info.maintains_hierarchy": "• Maintains object hierarchy", + "MMD.conversion_info.translates_names": "• Translates Japanese names to English", "MMD.detection_failed.title": "MMD Detection Failed:", "MMD.detection_failed.not_mmd_format": "• Selected armature is not MMD format", "MMD.detection_failed.need_mmd_bones": "• Need at least 5 MMD bones detected", @@ -633,6 +640,16 @@ "MMD.armature_renamed": "Renamed armature from '{old_name}' to '{new_name}'", "MMD.armature_renamed_with_suffix": "Renamed armature from '{old_name}' to '{new_name}' (name collision)", "MMD.conversion_complete": "MMD armature conversion completed successfully", + "MMD.translation_starting": "Starting name translation...", + "MMD.bones_translated": "Translated {count} bones", + "MMD.bones_failed": "Failed to translate {count} bones", + "MMD.materials_translated": "Translated {count} materials", + "MMD.materials_failed": "Failed to translate {count} materials", + "MMD.shapekeys_translated": "Translated {count} shape keys", + "MMD.shapekeys_failed": "Failed to translate {count} shape keys", + "MMD.objects_translated": "Translated {count} objects", + "MMD.objects_failed": "Failed to translate {count} objects", + "MMD.translation_complete": "Translation complete: {total} items translated", "Translation.label": "Translation", "Translation.service": "Translation Service", diff --git a/ui/mmd_panel.py b/ui/mmd_panel.py index 39baf4f..43a9100 100644 --- a/ui/mmd_panel.py +++ b/ui/mmd_panel.py @@ -49,10 +49,27 @@ class AvatarToolKit_PT_MMDPanel(Panel): col.separator(factor=0.3) toolkit = context.scene.avatar_toolkit + + # Basic conversion settings col.prop(toolkit, 'mmd_make_parent', text=t("MMD.make_armature_parent")) col.prop(toolkit, 'mmd_rename_armature', text=t("MMD.rename_to_armature")) col.separator(factor=0.2) + # Translation settings + col.prop(toolkit, 'mmd_translate_names', text=t("MMD.translate_names")) + + # Translation sub-options (only show if translation is enabled) + if toolkit.mmd_translate_names: + trans_box = col.box() + trans_col = trans_box.column(align=True) + trans_col.label(text=t("MMD.translation_options"), icon='WORLD') + trans_col.prop(toolkit, 'mmd_translate_bones', text=t("MMD.translate_bones")) + trans_col.prop(toolkit, 'mmd_translate_materials', text=t("MMD.translate_materials")) + trans_col.prop(toolkit, 'mmd_translate_shapekeys', text=t("MMD.translate_shapekeys")) + trans_col.prop(toolkit, 'mmd_translate_objects', text=t("MMD.translate_objects")) + + col.separator(factor=0.2) + col.operator( AvatarToolkit_OT_ConvertMMDArmature.bl_idname, text=t("MMD.convert_armature_button"), @@ -65,6 +82,8 @@ class AvatarToolKit_PT_MMDPanel(Panel): info_col.label(text=t("MMD.conversion_info.removes_parent")) info_col.label(text=t("MMD.conversion_info.renames_armature")) info_col.label(text=t("MMD.conversion_info.maintains_hierarchy")) + if toolkit.mmd_translate_names: + info_col.label(text=t("MMD.conversion_info.translates_names")) else: col.label(text=t("MMD.armature_name", name=armature.name), icon='ERROR')