17fb0fcadd
Replaced hardcoded panel order and default open/closed options with dynamic values using get_panel_order and should_open_by_default from panel_layout.
733 lines
34 KiB
Python
733 lines
34 KiB
Python
# GPL License
|
|
|
|
import bpy
|
|
from typing import Set, Dict, List, Optional, Any
|
|
from bpy.types import (
|
|
Operator,
|
|
Panel,
|
|
Context,
|
|
UILayout,
|
|
WindowManager,
|
|
Event,
|
|
Object
|
|
)
|
|
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
|
from .panel_layout import get_panel_order, should_open_by_default
|
|
from ..core.translations import t
|
|
from ..core.logging_setup import logger
|
|
from ..core.common import get_active_armature, ProgressTracker
|
|
|
|
# Module-level cache for UI performance (avoids Blender scene property write restrictions)
|
|
_ui_cache = {
|
|
'translation_status': {},
|
|
'deepl_config': {},
|
|
'libretranslate_config': {},
|
|
'last_refresh_frame': 0,
|
|
'cache_refresh_interval': 30
|
|
}
|
|
|
|
|
|
class AvatarToolkit_OT_TranslateNames(Operator):
|
|
"""Translate names using the translation system"""
|
|
bl_idname: str = "avatar_toolkit.translate_names"
|
|
bl_label: str = t("Translation.translate_names")
|
|
bl_description: str = t("Translation.translate_names_desc")
|
|
|
|
translation_type: bpy.props.EnumProperty(
|
|
items=[
|
|
('bones', t("Translation.type.bones"), t("Translation.type.bones_desc")),
|
|
('shapekeys', t("Translation.type.shapekeys"), t("Translation.type.shapekeys_desc")),
|
|
('materials', t("Translation.type.materials"), t("Translation.type.materials_desc")),
|
|
('objects', t("Translation.type.objects"), t("Translation.type.objects_desc")),
|
|
('all', t("Translation.type.all"), t("Translation.type.all_desc"))
|
|
],
|
|
default='bones'
|
|
)
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
logger.info(f"Starting translation operation: {self.translation_type}")
|
|
|
|
try:
|
|
from ..core.translation_manager import get_avatar_translation_manager
|
|
manager = get_avatar_translation_manager()
|
|
|
|
# Set up progress callback for detailed feedback
|
|
def progress_callback(current: int, total: int, message: str):
|
|
progress_percent = (current / max(total, 1)) * 100
|
|
logger.info(f"Translation progress: {current}/{total} ({progress_percent:.1f}%) - {message}")
|
|
context.area.header_text_set(f"Translating: {current}/{total} - {message}")
|
|
|
|
manager.set_progress_callback(progress_callback)
|
|
|
|
results = []
|
|
armature = get_active_armature(context)
|
|
|
|
total_steps = 0
|
|
if self.translation_type == 'bones' or self.translation_type == 'all':
|
|
if armature:
|
|
total_steps += len(armature.data.bones)
|
|
if self.translation_type == 'shapekeys' or self.translation_type == 'all':
|
|
meshes = [obj for obj in context.scene.objects if obj.type == 'MESH']
|
|
for mesh in meshes:
|
|
if mesh.data.shape_keys:
|
|
total_steps += len(mesh.data.shape_keys.key_blocks)
|
|
if self.translation_type == 'materials' or self.translation_type == 'all':
|
|
materials = set()
|
|
for obj in context.scene.objects:
|
|
if obj.type == 'MESH' and obj.data.materials:
|
|
for mat in obj.data.materials:
|
|
if mat:
|
|
materials.add(mat)
|
|
total_steps += len(materials)
|
|
if self.translation_type == 'objects' or self.translation_type == 'all':
|
|
objects = [obj for obj in context.scene.objects if obj.type in {'MESH', 'ARMATURE', 'EMPTY'}]
|
|
total_steps += len(objects)
|
|
|
|
logger.info(f"Translation operation will process approximately {total_steps} items")
|
|
|
|
with ProgressTracker(context, total_steps, "Translation") as progress:
|
|
if self.translation_type == 'bones' or self.translation_type == 'all':
|
|
if armature:
|
|
logger.info(f"Starting bone translation for armature: {armature.name}")
|
|
self.report({'INFO'}, f"Translating {len(armature.data.bones)} bones...")
|
|
|
|
bone_results = manager.translate_armature_bones(armature, apply_results=True)
|
|
results.extend(bone_results)
|
|
|
|
successful_bones = sum(1 for r in bone_results if r.method not in ['failed', 'skipped'])
|
|
progress.step(f"Bones: {successful_bones}/{len(bone_results)} translated")
|
|
logger.info(f"Bone translation complete: {successful_bones}/{len(bone_results)} successful")
|
|
else:
|
|
self.report({'WARNING'}, t("Translation.no_armature"))
|
|
logger.warning("No armature selected for bone translation")
|
|
|
|
if self.translation_type == 'shapekeys' or self.translation_type == 'all':
|
|
meshes = [obj for obj in context.scene.objects if obj.type == 'MESH']
|
|
logger.info(f"Starting shape key translation for {len(meshes)} mesh objects")
|
|
|
|
total_shapekeys = 0
|
|
for mesh in meshes:
|
|
if mesh.data.shape_keys:
|
|
shapekey_count = len(mesh.data.shape_keys.key_blocks)
|
|
self.report({'INFO'}, f"Translating {shapekey_count} shape keys in {mesh.name}...")
|
|
|
|
shapekey_results = manager.translate_object_shapekeys(mesh, apply_results=True)
|
|
results.extend(shapekey_results)
|
|
total_shapekeys += len(shapekey_results)
|
|
|
|
successful_shapekeys = sum(1 for r in results[-total_shapekeys:] if r.method not in ['failed', 'skipped'])
|
|
progress.step(f"Shape keys: {successful_shapekeys}/{total_shapekeys} translated")
|
|
logger.info(f"Shape key translation complete: {successful_shapekeys}/{total_shapekeys} successful")
|
|
|
|
if self.translation_type == 'materials' or self.translation_type == 'all':
|
|
logger.info("Starting material translation")
|
|
self.report({'INFO'}, "Translating materials...")
|
|
|
|
material_results = manager.translate_scene_materials(apply_results=True)
|
|
results.extend(material_results)
|
|
|
|
successful_materials = sum(1 for r in material_results if r.method not in ['failed', 'skipped'])
|
|
progress.step(f"Materials: {successful_materials}/{len(material_results)} translated")
|
|
logger.info(f"Material translation complete: {successful_materials}/{len(material_results)} successful")
|
|
|
|
if self.translation_type == 'objects' or self.translation_type == 'all':
|
|
logger.info("Starting object translation")
|
|
self.report({'INFO'}, "Translating objects...")
|
|
|
|
object_results = manager.translate_scene_objects(apply_results=True)
|
|
results.extend(object_results)
|
|
|
|
successful_objects = sum(1 for r in object_results if r.method not in ['failed', 'skipped'])
|
|
progress.step(f"Objects: {successful_objects}/{len(object_results)} translated")
|
|
logger.info(f"Object translation complete: {successful_objects}/{len(object_results)} successful")
|
|
|
|
manager.set_progress_callback(None)
|
|
context.area.header_text_set(None)
|
|
|
|
# Final results summary
|
|
successful = sum(1 for r in results if r.method not in ['failed', 'skipped'])
|
|
total = len(results)
|
|
|
|
dictionary_count = sum(1 for r in results if r.method == 'dictionary')
|
|
api_count = sum(1 for r in results if r.method == 'api')
|
|
cache_count = sum(1 for r in results if r.method == 'cache')
|
|
failed_count = sum(1 for r in results if r.method == 'failed')
|
|
|
|
logger.info(f"Translation summary: {successful}/{total} successful (Dictionary: {dictionary_count}, API: {api_count}, Cache: {cache_count}, Failed: {failed_count})")
|
|
|
|
if successful > 0:
|
|
success_msg = f"Successfully translated {successful}/{total} items"
|
|
if dictionary_count > 0:
|
|
success_msg += f" (Dictionary: {dictionary_count}"
|
|
if api_count > 0:
|
|
success_msg += f", API: {api_count}"
|
|
if cache_count > 0:
|
|
success_msg += f", Cache: {cache_count}"
|
|
if dictionary_count > 0 or api_count > 0 or cache_count > 0:
|
|
success_msg += ")"
|
|
|
|
self.report({'INFO'}, success_msg)
|
|
else:
|
|
if total > 0:
|
|
self.report({'WARNING'}, f"No translations were applied ({total} items checked)")
|
|
else:
|
|
self.report({'WARNING'}, "No items found to translate")
|
|
|
|
return {'FINISHED'}
|
|
|
|
except Exception as e:
|
|
try:
|
|
manager.set_progress_callback(None)
|
|
context.area.header_text_set(None)
|
|
except:
|
|
pass
|
|
|
|
logger.error(f"Translation operation failed: {e}", exc_info=True)
|
|
self.report({'ERROR'}, f"Translation failed: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
|
|
class AvatarToolkit_OT_TestTranslationService(Operator):
|
|
"""Test the currently selected translation service"""
|
|
bl_idname: str = "avatar_toolkit.test_translation_service"
|
|
bl_label: str = t("Translation.test_service")
|
|
bl_description: str = t("Translation.test_service_desc")
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
logger.info("Starting translation service test")
|
|
|
|
try:
|
|
from ..core.translation_manager import get_avatar_translation_manager
|
|
manager = get_avatar_translation_manager()
|
|
|
|
self.report({'INFO'}, "Testing translation service...")
|
|
context.area.header_text_set("Testing translation service...")
|
|
|
|
# Test translation with a simple word
|
|
test_word = "テスト" # "Test" in Japanese
|
|
logger.info(f"Testing translation of '{test_word}'")
|
|
|
|
result = manager.translate_single(test_word, "auto")
|
|
|
|
# Clear status
|
|
context.area.header_text_set(None)
|
|
|
|
if result.method == "failed":
|
|
logger.error(f"Translation test failed: {result}")
|
|
self.report({'ERROR'}, t("Translation.test_failed"))
|
|
else:
|
|
service_info = f" ({result.service})" if result.service else ""
|
|
success_msg = f"Translation test successful: '{test_word}' → '{result.translated}' via {result.method}{service_info}"
|
|
logger.info(f"Translation test successful: {result}")
|
|
self.report({'INFO'}, success_msg)
|
|
|
|
return {'FINISHED'}
|
|
|
|
except Exception as e:
|
|
try:
|
|
context.area.header_text_set(None)
|
|
except:
|
|
pass
|
|
|
|
logger.error(f"Translation service test failed: {e}", exc_info=True)
|
|
self.report({'ERROR'}, f"Service test failed: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
|
|
class AvatarToolkit_OT_ClearTranslationCache(Operator):
|
|
"""Clear all translation caches"""
|
|
bl_idname: str = "avatar_toolkit.clear_translation_cache"
|
|
bl_label: str = t("Translation.clear_cache")
|
|
bl_description: str = t("Translation.clear_cache_desc")
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
try:
|
|
from ..core.translation_manager import get_avatar_translation_manager
|
|
manager = get_avatar_translation_manager()
|
|
manager.clear_all_caches()
|
|
|
|
self.report({'INFO'}, t("Translation.cache_cleared"))
|
|
return {'FINISHED'}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to clear translation cache: {e}")
|
|
self.report({'ERROR'}, f"Failed to clear cache: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
|
|
class AvatarToolkit_OT_ConfigureDeepL(Operator):
|
|
"""Configure DeepL API settings"""
|
|
bl_idname: str = "avatar_toolkit.configure_deepl"
|
|
bl_label: str = t("Translation.configure_deepl")
|
|
bl_description: str = t("Translation.configure_deepl_desc")
|
|
|
|
api_key: bpy.props.StringProperty(
|
|
name=t("Translation.deepl_api_key"),
|
|
description=t("Translation.deepl_api_key_desc"),
|
|
default="",
|
|
subtype='PASSWORD'
|
|
)
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
try:
|
|
if not self.api_key.strip():
|
|
self.report({'ERROR'}, "API key cannot be empty")
|
|
return {'CANCELLED'}
|
|
|
|
from ..core.translation_manager import configure_translation_service
|
|
success = configure_translation_service("deepl", api_key=self.api_key.strip())
|
|
|
|
if success:
|
|
_ui_cache['deepl_config'].clear()
|
|
_ui_cache['translation_status'].clear()
|
|
if 'batch_info' in _ui_cache:
|
|
del _ui_cache['batch_info']
|
|
self.report({'INFO'}, "DeepL API configured successfully")
|
|
return {'FINISHED'}
|
|
else:
|
|
self.report({'ERROR'}, "Failed to configure DeepL API - check your API key")
|
|
return {'CANCELLED'}
|
|
|
|
except Exception as e:
|
|
logger.error(f"DeepL configuration failed: {e}")
|
|
self.report({'ERROR'}, f"Configuration failed: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
def invoke(self, context: Context, event: Event) -> Set[str]:
|
|
# Load existing API key if available
|
|
try:
|
|
from ..core.addon_preferences import get_preference
|
|
existing_key = get_preference("deepl_api_key", "")
|
|
if existing_key:
|
|
# Show only first/last few characters for security
|
|
if len(existing_key) > 8:
|
|
display_key = existing_key[:4] + "..." + existing_key[-4:]
|
|
self.api_key = existing_key # Keep full key for editing
|
|
else:
|
|
self.api_key = existing_key
|
|
except:
|
|
pass
|
|
|
|
wm: WindowManager = context.window_manager
|
|
return wm.invoke_props_dialog(self, width=400)
|
|
|
|
def draw(self, context: Context) -> None:
|
|
layout: UILayout = self.layout
|
|
|
|
info_box = layout.box()
|
|
info_col = info_box.column()
|
|
info_col.label(text="DeepL API Configuration", icon='SETTINGS')
|
|
info_col.separator()
|
|
info_col.label(text="1. Visit deepl.com/pro to get your free API key")
|
|
info_col.label(text="2. Free tier: 500,000 characters/month")
|
|
info_col.label(text="3. Higher quality than other services")
|
|
info_col.label(text="4. The Fastest Option due to native batching support")
|
|
|
|
layout.separator()
|
|
layout.prop(self, "api_key")
|
|
|
|
|
|
class AvatarToolkit_OT_ConfigureLibreTranslate(Operator):
|
|
"""Configure LibreTranslate server settings"""
|
|
bl_idname: str = "avatar_toolkit.configure_libretranslate"
|
|
bl_label: str = t("Translation.configure_libretranslate")
|
|
bl_description: str = t("Translation.configure_libretranslate_desc")
|
|
|
|
server_url: bpy.props.StringProperty(
|
|
name=t("Translation.server_url"),
|
|
description=t("Translation.server_url_desc"),
|
|
default="https://libretranslate.com"
|
|
)
|
|
|
|
api_key: bpy.props.StringProperty(
|
|
name=t("Translation.api_key"),
|
|
description=t("Translation.api_key_desc"),
|
|
default="",
|
|
subtype='PASSWORD'
|
|
)
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
try:
|
|
if not self.server_url.strip():
|
|
self.report({'ERROR'}, "Server URL cannot be empty")
|
|
return {'CANCELLED'}
|
|
|
|
from ..core.translation_manager import configure_translation_service
|
|
success = configure_translation_service("libretranslate",
|
|
server_url=self.server_url.strip(),
|
|
api_key=self.api_key.strip() if self.api_key.strip() else None)
|
|
|
|
if success:
|
|
_ui_cache['libretranslate_config'].clear()
|
|
_ui_cache['translation_status'].clear()
|
|
if 'batch_info' in _ui_cache:
|
|
del _ui_cache['batch_info']
|
|
self.report({'INFO'}, f"LibreTranslate server configured: {self.server_url}")
|
|
return {'FINISHED'}
|
|
else:
|
|
self.report({'ERROR'}, "Failed to connect to LibreTranslate server")
|
|
return {'CANCELLED'}
|
|
|
|
except Exception as e:
|
|
logger.error(f"LibreTranslate configuration failed: {e}")
|
|
self.report({'ERROR'}, f"Configuration failed: {str(e)}")
|
|
return {'CANCELLED'}
|
|
|
|
def invoke(self, context: Context, event: Event) -> Set[str]:
|
|
# Load existing server URL and API key if available
|
|
try:
|
|
from ..core.addon_preferences import get_preference
|
|
existing_url = get_preference("libretranslate_url", "https://libretranslate.com")
|
|
existing_api_key = get_preference("libretranslate_api_key", "")
|
|
self.server_url = existing_url
|
|
self.api_key = existing_api_key
|
|
except:
|
|
pass
|
|
|
|
wm: WindowManager = context.window_manager
|
|
return wm.invoke_props_dialog(self, width=500)
|
|
|
|
def draw(self, context: Context) -> None:
|
|
layout: UILayout = self.layout
|
|
|
|
info_box = layout.box()
|
|
info_col = info_box.column()
|
|
info_col.label(text="LibreTranslate Server Configuration", icon='SETTINGS')
|
|
info_col.separator()
|
|
info_col.label(text="⚠ libretranslate.com requires payment for API access")
|
|
info_col.label(text="✓ You can run your own LibreTranslate server")
|
|
info_col.label(text="✓ Or find community-hosted instances")
|
|
info_col.separator()
|
|
info_col.label(text="Examples:")
|
|
info_col.label(text=" • Your server: https://translate.yoursite.com")
|
|
info_col.label(text=" • Docker local: http://localhost:5000")
|
|
|
|
layout.separator()
|
|
layout.prop(self, "server_url")
|
|
layout.prop(self, "api_key")
|
|
|
|
|
|
class AvatarToolkit_OT_TranslationStats(Operator):
|
|
"""Show translation statistics"""
|
|
bl_idname: str = "avatar_toolkit.translation_stats"
|
|
bl_label: str = t("Translation.show_stats")
|
|
bl_description: str = t("Translation.show_stats_desc")
|
|
|
|
def execute(self, context: Context) -> Set[str]:
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context: Context, event: Event) -> Set[str]:
|
|
wm: WindowManager = context.window_manager
|
|
return wm.invoke_props_dialog(self, width=400)
|
|
|
|
def draw(self, context: Context) -> None:
|
|
layout: UILayout = self.layout
|
|
|
|
try:
|
|
from ..core.translation_manager import get_avatar_translation_manager
|
|
manager = get_avatar_translation_manager()
|
|
stats = manager.get_translation_stats()
|
|
|
|
dict_box = layout.box()
|
|
dict_box.label(text="Dictionary Translations", icon='BOOKMARKS')
|
|
dict_stats = stats['dictionary_translations']
|
|
for category, count in dict_stats.items():
|
|
if count > 0:
|
|
dict_box.label(text=f"{category.title()}: {count}")
|
|
|
|
cache_box = layout.box()
|
|
cache_box.label(text="Translation Cache", icon='FILE_CACHE')
|
|
cache_stats = stats['cache_stats']
|
|
cache_box.label(text=f"Language pairs: {cache_stats['language_pairs']}")
|
|
cache_box.label(text=f"Total cached: {cache_stats['total_entries']}")
|
|
|
|
service_box = layout.box()
|
|
service_box.label(text="Translation Services", icon='WORLD')
|
|
service_box.label(text=f"Current mode: {stats['current_mode']}")
|
|
service_box.label(text=f"Primary service: {stats['primary_service']}")
|
|
|
|
available_services = stats['available_services']
|
|
if available_services:
|
|
service_box.label(text="Available services:")
|
|
for service_id, service_name in available_services:
|
|
service_box.label(text=f" • {service_name}")
|
|
else:
|
|
service_box.label(text="No services available", icon='ERROR')
|
|
|
|
except Exception as e:
|
|
layout.label(text=f"Error loading stats: {str(e)}", icon='ERROR')
|
|
|
|
|
|
class AvatarToolKit_PT_TranslationPanel(Panel):
|
|
"""Translation panel for Avatar Toolkit"""
|
|
bl_label: str = t("Translation.label")
|
|
bl_idname: str = "OBJECT_PT_avatar_toolkit_translation"
|
|
bl_space_type: str = 'VIEW_3D'
|
|
bl_region_type: str = 'UI'
|
|
bl_category: str = CATEGORY_NAME
|
|
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
|
bl_order: int = get_panel_order('translation')
|
|
bl_options = set() if not should_open_by_default('TRANSLATION') else {'DEFAULT_CLOSED'}
|
|
|
|
def draw(self, context: Context) -> None:
|
|
"""Draw the translation panel layout"""
|
|
layout: UILayout = self.layout
|
|
props = context.scene.avatar_toolkit
|
|
|
|
# Translation Service Settings
|
|
service_box: UILayout = layout.box()
|
|
col: UILayout = service_box.column(align=True)
|
|
row: UILayout = col.row()
|
|
row.scale_y = 1.2
|
|
row.label(text=t("Translation.service_settings"), icon='WORLD')
|
|
col.separator()
|
|
|
|
col.prop(props, "translation_service", text="")
|
|
|
|
col.prop(props, "translation_mode", text="")
|
|
|
|
row = col.row(align=True)
|
|
row.prop(props, "translation_expand",
|
|
icon="TRIA_DOWN" if props.translation_expand else "TRIA_RIGHT",
|
|
icon_only=True, emboss=False)
|
|
row.label(text=t("Translation.advanced_settings"))
|
|
|
|
if props.translation_expand:
|
|
config_col = service_box.column(align=True)
|
|
|
|
# MyMemory settings (no configuration needed)
|
|
if props.translation_service == 'mymemory':
|
|
config_col.separator()
|
|
config_col.label(text="MyMemory Configuration:", icon='CHECKMARK')
|
|
success_col = config_col.column()
|
|
success_col.alert = False
|
|
success_col.label(text="✓ No API key required!", icon='CHECKMARK')
|
|
success_col.label(text="✓ Completely free service")
|
|
success_col.label(text="✓ 1000 translations per day")
|
|
success_col.label(text="✓ Slowest Option due to no native batching")
|
|
success_col.label(text="✓ Ready to use!")
|
|
|
|
elif props.translation_service == 'libretranslate':
|
|
config_col.separator()
|
|
config_col.label(text="LibreTranslate Configuration:", icon='SETTINGS')
|
|
|
|
# Check current server configuration (cached to avoid performance issues)
|
|
try:
|
|
if 'libretranslate_url' not in _ui_cache['libretranslate_config']:
|
|
from ..core.addon_preferences import get_preference
|
|
_ui_cache['libretranslate_config']['libretranslate_url'] = get_preference("libretranslate_url", "https://libretranslate.com")
|
|
|
|
server_url = _ui_cache['libretranslate_config']['libretranslate_url']
|
|
|
|
info_col = config_col.column()
|
|
info_col.alert = False
|
|
info_col.label(text=f"Server: {server_url}", icon='URL')
|
|
|
|
if "libretranslate.com" in server_url.lower():
|
|
warning_col = config_col.column()
|
|
warning_col.alert = True
|
|
warning_col.label(text="⚠ Default server requires payment", icon='ERROR')
|
|
warning_col.label(text="Configure your own LibreTranslate server")
|
|
else:
|
|
success_col = config_col.column()
|
|
success_col.alert = False
|
|
success_col.label(text="✓ Custom server configured", icon='CHECKMARK')
|
|
|
|
config_row = config_col.row()
|
|
config_row.operator("avatar_toolkit.configure_libretranslate", text="Configure Server", icon='SETTINGS')
|
|
except Exception as e:
|
|
config_col.label(text="LibreTranslate configuration error", icon='ERROR')
|
|
|
|
elif props.translation_service == 'deepl':
|
|
config_col.separator()
|
|
config_col.label(text="DeepL Configuration:", icon='SETTINGS')
|
|
|
|
# Check if API key is configured (cached to avoid performance issues)
|
|
try:
|
|
if 'deepl_api_key' not in _ui_cache['deepl_config']:
|
|
from ..core.addon_preferences import get_preference
|
|
_ui_cache['deepl_config']['deepl_api_key'] = get_preference("deepl_api_key", "")
|
|
|
|
deepl_api_key = _ui_cache['deepl_config']['deepl_api_key']
|
|
|
|
if deepl_api_key and deepl_api_key.strip():
|
|
success_col = config_col.column()
|
|
success_col.alert = False
|
|
success_col.label(text="✓ API key configured", icon='CHECKMARK')
|
|
success_col.label(text="✓ High quality translations")
|
|
success_col.label(text="✓ 500,000 chars/month free")
|
|
success_col.label(text="✓ Ready to use!")
|
|
|
|
reconfig_row = config_col.row()
|
|
reconfig_row.operator("avatar_toolkit.configure_deepl", text="Reconfigure API Key", icon='SETTINGS')
|
|
else:
|
|
warning_col = config_col.column()
|
|
warning_col.alert = True
|
|
warning_col.label(text="⚠ API key required!", icon='ERROR')
|
|
warning_col.label(text="Get free key at deepl.com/pro")
|
|
warning_col.label(text="500,000 characters/month free")
|
|
|
|
config_row = config_col.row()
|
|
config_row.operator("avatar_toolkit.configure_deepl", text="Configure API Key", icon='PLUS')
|
|
except Exception as e:
|
|
config_col.label(text="DeepL configuration error", icon='ERROR')
|
|
|
|
|
|
|
|
# Language Settings
|
|
lang_box: UILayout = layout.box()
|
|
col = lang_box.column(align=True)
|
|
row = col.row()
|
|
row.scale_y = 1.2
|
|
row.label(text=t("Translation.language_settings"), icon='SYNTAX_ON')
|
|
col.separator()
|
|
col.prop(props, "translation_source_language", text="From")
|
|
col.prop(props, "translation_target_language", text="To")
|
|
|
|
# Quick Actions
|
|
action_box: UILayout = layout.box()
|
|
col = action_box.column(align=True)
|
|
row = col.row()
|
|
row.scale_y = 1.2
|
|
row.label(text=t("Translation.quick_actions"), icon='PLAY')
|
|
col.separator()
|
|
|
|
# Translate buttons
|
|
row = col.row(align=True)
|
|
op_bones = row.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Bones", icon='BONE_DATA')
|
|
op_bones.translation_type = 'bones'
|
|
|
|
op_shapes = row.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Shape Keys", icon='SHAPEKEY_DATA')
|
|
op_shapes.translation_type = 'shapekeys'
|
|
|
|
row = col.row(align=True)
|
|
op_mats = row.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Materials", icon='MATERIAL_DATA')
|
|
op_mats.translation_type = 'materials'
|
|
|
|
op_objs = row.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Objects", icon='OBJECT_DATA')
|
|
op_objs.translation_type = 'objects'
|
|
|
|
col.separator()
|
|
op_all = col.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Translate All", icon='WORLD')
|
|
op_all.translation_type = 'all'
|
|
|
|
# Utility buttons
|
|
util_box: UILayout = layout.box()
|
|
col = util_box.column(align=True)
|
|
row = col.row()
|
|
row.scale_y = 1.2
|
|
row.label(text=t("Translation.utilities"), icon='TOOL_SETTINGS')
|
|
col.separator()
|
|
|
|
row = col.row(align=True)
|
|
row.operator(AvatarToolkit_OT_TestTranslationService.bl_idname, icon='PLAY')
|
|
row.operator(AvatarToolkit_OT_TranslationStats.bl_idname, icon='INFO')
|
|
|
|
col.operator(AvatarToolkit_OT_ClearTranslationCache.bl_idname, icon='TRASH')
|
|
|
|
status_box = layout.box()
|
|
status_col = status_box.column()
|
|
|
|
try:
|
|
status_cache_key = f"translation_status_{props.translation_service}_{props.translation_mode}"
|
|
|
|
# Refresh cache periodically
|
|
frame = context.scene.frame_current
|
|
cache_expired = (frame - _ui_cache['last_refresh_frame'] >= _ui_cache['cache_refresh_interval']) or status_cache_key not in _ui_cache['translation_status']
|
|
|
|
if cache_expired:
|
|
from ..core.translation_manager import get_available_translation_services, get_avatar_translation_manager
|
|
|
|
manager = get_avatar_translation_manager()
|
|
available_services = get_available_translation_services()
|
|
|
|
_ui_cache['translation_status'][status_cache_key] = {
|
|
'available_services': available_services,
|
|
'manager': manager,
|
|
'cache_stats': None
|
|
}
|
|
_ui_cache['last_refresh_frame'] = frame
|
|
|
|
try:
|
|
stats = manager.get_translation_stats()
|
|
_ui_cache['translation_status'][status_cache_key]['cache_stats'] = stats['cache_stats']
|
|
except:
|
|
pass
|
|
|
|
# Use cached data
|
|
cached_data = _ui_cache['translation_status'].get(status_cache_key, {})
|
|
available_services = cached_data.get('available_services', [])
|
|
cache_stats = cached_data.get('cache_stats')
|
|
|
|
if available_services:
|
|
status_col.label(text="Translation services ready", icon='CHECKMARK')
|
|
|
|
# Show current service status
|
|
current_service = props.translation_service
|
|
service_available = any(service_id == current_service for service_id, _ in available_services)
|
|
|
|
if service_available:
|
|
service_name = next((name for sid, name in available_services if sid == current_service), current_service)
|
|
status_col.label(text=f"Active: {service_name}", icon='WORLD')
|
|
|
|
# Show translation mode
|
|
mode_display = {
|
|
'hybrid': 'Dictionary + API',
|
|
'dictionary_only': 'Dictionary Only',
|
|
'api_only': 'API Only'
|
|
}.get(props.translation_mode, props.translation_mode)
|
|
status_col.label(text=f"Mode: {mode_display}", icon='SETTINGS')
|
|
|
|
# Show cache status
|
|
if cache_stats and cache_stats['total_entries'] > 0:
|
|
status_col.label(text=f"Cache: {cache_stats['total_entries']} translations", icon='FILE_CACHE')
|
|
|
|
# Show batch translation capability
|
|
try:
|
|
if 'batch_info' not in _ui_cache:
|
|
from ..core.translation_manager import get_batch_translation_info
|
|
_ui_cache['batch_info'] = get_batch_translation_info()
|
|
|
|
batch_info = _ui_cache['batch_info'].get(current_service, {})
|
|
if batch_info.get('supports_batch', False):
|
|
batch_type = batch_info.get('batch_type', 'individual')
|
|
if batch_type == 'native':
|
|
status_col.label(text="⚡ DeepL Native batch translation (up to 50x faster)", icon='LIGHT')
|
|
elif batch_type == 'concurrent':
|
|
if current_service == 'mymemory':
|
|
status_col.label(text="⚡ Slowest Option, no native Batching", icon='LIGHT')
|
|
else:
|
|
status_col.label(text="⚡ Slightly Faster then MyMemory processing (3x faster)", icon='LIGHT')
|
|
except:
|
|
pass
|
|
|
|
else:
|
|
warning_col = status_col.column()
|
|
warning_col.alert = True
|
|
warning_col.label(text=f"Service unavailable: {props.translation_service}", icon='ERROR')
|
|
|
|
|
|
else:
|
|
warning_col = status_col.column()
|
|
warning_col.alert = True
|
|
warning_col.label(text="No translation services available", icon='ERROR')
|
|
|
|
if props.translation_service == 'mymemory':
|
|
warning_col.label(text="Internet connection required")
|
|
|
|
except Exception as e:
|
|
error_col = status_col.column()
|
|
error_col.alert = True
|
|
error_col.label(text="Translation system error", icon='ERROR')
|
|
logger.error(f"Status display error: {e}")
|
|
|
|
try:
|
|
if hasattr(context.area, 'header_text') and context.area.header_text:
|
|
progress_col = status_col.column()
|
|
progress_col.alert = False
|
|
progress_col.label(text=context.area.header_text, icon='TIME')
|
|
except:
|
|
pass
|
|
|
|
|