migrate to gitea
This commit is contained in:
310
translation_tool.py
Normal file
310
translation_tool.py
Normal file
@@ -0,0 +1,310 @@
|
||||
"""
|
||||
Main translation tool logic
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from lxml import etree
|
||||
from typing import List
|
||||
from config import Config
|
||||
from models import TranslationItem, TranslationBatch
|
||||
from llm_client import LLMClient
|
||||
from xml_processor import XMLProcessor
|
||||
from md_processor import MDProcessor
|
||||
from ui import UI
|
||||
|
||||
|
||||
class TranslationTool:
|
||||
"""Main translation tool class"""
|
||||
|
||||
def __init__(self, config_path: str = "config.yaml"):
|
||||
self.config = Config(config_path)
|
||||
self.llm_client = LLMClient(self.config)
|
||||
self.xml_processor = XMLProcessor(self.config)
|
||||
self.md_processor = MDProcessor()
|
||||
self.ui = UI()
|
||||
|
||||
# Setup logging
|
||||
self._setup_logging()
|
||||
|
||||
def _setup_logging(self):
|
||||
"""Setup logging configuration"""
|
||||
log_level = self.config.output_config.get('log_level', 'INFO')
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, log_level),
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def run(self):
|
||||
"""Main execution method"""
|
||||
self.ui.show_header()
|
||||
|
||||
# Process Android XML files
|
||||
self._process_android_files()
|
||||
|
||||
# Process Markdown files if configured
|
||||
if self.config.has_examples_config():
|
||||
self._process_md_files()
|
||||
|
||||
self.ui.show_success("Translation process completed!")
|
||||
|
||||
def _process_android_files(self):
|
||||
"""Process Android XML files"""
|
||||
android_config = self.config.android_config
|
||||
input_folder = android_config['input_folder']
|
||||
base_folder = android_config['base_values_folder']
|
||||
target_folders = android_config['target_folders']
|
||||
files_to_translate = android_config['files_to_translate']
|
||||
|
||||
# Check if input folder exists
|
||||
if not os.path.exists(input_folder):
|
||||
self.ui.show_error(f"Input folder {input_folder} not found!")
|
||||
return
|
||||
|
||||
base_values_path = os.path.join(input_folder, base_folder)
|
||||
if not os.path.exists(base_values_path):
|
||||
self.ui.show_error(f"Base values folder {base_values_path} not found!")
|
||||
return
|
||||
|
||||
# Process each target language
|
||||
for target_folder in target_folders:
|
||||
self.ui.show_processing_language(target_folder)
|
||||
self._process_language(input_folder, base_folder, target_folder, files_to_translate)
|
||||
|
||||
def _process_md_files(self):
|
||||
"""Process Markdown files in examples folder"""
|
||||
examples_config = self.config.examples_config
|
||||
input_folder = examples_config['input_folder']
|
||||
base_folder = examples_config['base_folder']
|
||||
target_folders = examples_config['target_folders']
|
||||
file_extension = examples_config.get('file_extension', '.md')
|
||||
|
||||
# Update MD processor with configured extension
|
||||
self.md_processor = MDProcessor(file_extension)
|
||||
|
||||
# Check if input folder exists
|
||||
if not os.path.exists(input_folder):
|
||||
self.ui.show_warning(f"Examples input folder {input_folder} not found, skipping...")
|
||||
return
|
||||
|
||||
base_path = os.path.join(input_folder, base_folder)
|
||||
if not os.path.exists(base_path):
|
||||
self.ui.show_warning(f"Examples base folder {base_path} not found, skipping...")
|
||||
return
|
||||
|
||||
# Check asset counts across all locales
|
||||
target_paths = [os.path.join(input_folder, tf) for tf in target_folders]
|
||||
is_valid, errors = self.md_processor.check_asset_counts(base_path, target_paths)
|
||||
|
||||
if not is_valid:
|
||||
self.ui.show_warning("Asset count check failed:")
|
||||
for error in errors:
|
||||
self.ui.show_warning(f" - {error}")
|
||||
else:
|
||||
self.ui.show_asset_count_check(len(self.md_processor.get_md_files(base_path)))
|
||||
|
||||
# Process each target language
|
||||
for target_folder in target_folders:
|
||||
self.ui.show_processing_language(f"examples/{target_folder}")
|
||||
self._process_md_language(input_folder, base_folder, target_folder)
|
||||
|
||||
def _process_md_language(self, input_folder: str, base_folder: str, target_folder: str):
|
||||
"""Process Markdown translation for a specific language - files are processed one by one"""
|
||||
base_path = os.path.join(input_folder, base_folder)
|
||||
target_path = os.path.join(input_folder, target_folder)
|
||||
|
||||
# Create target folder if it doesn't exist
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
|
||||
# Find missing files
|
||||
missing_files = self.md_processor.find_missing_files(base_path, target_path)
|
||||
|
||||
if not missing_files:
|
||||
self.ui.show_all_translated(target_path)
|
||||
return
|
||||
|
||||
self.ui.show_missing_strings(len(missing_files), target_path)
|
||||
|
||||
# Get language instruction
|
||||
instruction = self._get_instruction(target_folder)
|
||||
|
||||
interactive = self.config.translation_config['interactive_approval']
|
||||
|
||||
# Process each missing file one by one (no batching for .md files)
|
||||
for i, filename in enumerate(missing_files, 1):
|
||||
file_path = os.path.join(base_path, filename)
|
||||
content = self.md_processor.load_md_file(file_path)
|
||||
|
||||
if not content:
|
||||
self.ui.show_warning(f"Could not load {filename}, skipping...")
|
||||
continue
|
||||
|
||||
# Create a single-item batch for this file
|
||||
item = TranslationItem(
|
||||
name=filename,
|
||||
value=content,
|
||||
item_type='string'
|
||||
)
|
||||
|
||||
batch = TranslationBatch(
|
||||
items=[item],
|
||||
target_language=target_folder,
|
||||
target_file=target_path
|
||||
)
|
||||
|
||||
self.ui.show_processing_file(filename)
|
||||
|
||||
# Translate single file
|
||||
translations = self.llm_client.translate_batch(batch, instruction)
|
||||
|
||||
if not translations or len(translations) != 1:
|
||||
self.ui.show_batch_failed(i)
|
||||
continue
|
||||
|
||||
translation = translations[0]
|
||||
|
||||
# Display translation for approval (truncated for display)
|
||||
if interactive:
|
||||
display_batch = TranslationBatch(
|
||||
items=[TranslationItem(
|
||||
name=filename,
|
||||
value=content[:500] + "..." if len(content) > 500 else content,
|
||||
item_type='string'
|
||||
)],
|
||||
target_language=target_folder,
|
||||
target_file=target_path
|
||||
)
|
||||
approved = self.ui.show_batch_approval(display_batch, [translation[:500] + "..." if len(translation) > 500 else translation])
|
||||
if not approved:
|
||||
self.ui.show_batch_skipped(i)
|
||||
continue
|
||||
|
||||
# Save translated file
|
||||
target_file = os.path.join(target_path, filename)
|
||||
self.md_processor.save_md_file(translation, target_file)
|
||||
|
||||
self.ui.show_batch_added(i, filename)
|
||||
|
||||
def _get_instruction(self, target_folder: str) -> str:
|
||||
"""Get language instruction for a target folder"""
|
||||
language_instructions = self.config.translation_config.get('language_instructions', {})
|
||||
|
||||
# Try direct lookup first
|
||||
if target_folder in language_instructions:
|
||||
return language_instructions[target_folder]
|
||||
|
||||
# Extract folder name (e.g., "res/values-de-rDE" -> "values-de-rDE")
|
||||
folder_name = os.path.basename(target_folder)
|
||||
if folder_name in language_instructions:
|
||||
return language_instructions[folder_name]
|
||||
|
||||
# Try hints folder mapping (e.g., "assets/hints-de-rDE" -> "values-de-rDE")
|
||||
if 'hints-' in folder_name:
|
||||
values_key = folder_name.replace('hints-', 'values-')
|
||||
if values_key in language_instructions:
|
||||
return language_instructions[values_key]
|
||||
|
||||
# Default fallback
|
||||
return f"Translate to {target_folder}"
|
||||
|
||||
def _process_language(self, input_folder: str, base_folder: str, target_folder: str, files_to_translate: List[str]):
|
||||
"""Process translation for a specific language"""
|
||||
base_values_path = os.path.join(input_folder, base_folder)
|
||||
target_values_path = os.path.join(input_folder, target_folder)
|
||||
|
||||
# Create target folder if it doesn't exist
|
||||
os.makedirs(target_values_path, exist_ok=True)
|
||||
|
||||
# Get language instruction
|
||||
instruction = self._get_instruction(target_folder)
|
||||
|
||||
# Process each file
|
||||
for filename in files_to_translate:
|
||||
base_file = os.path.join(base_values_path, filename)
|
||||
target_file = os.path.join(target_values_path, filename)
|
||||
|
||||
if not os.path.exists(base_file):
|
||||
self.ui.show_warning(f"Base file {base_file} not found, skipping...")
|
||||
continue
|
||||
|
||||
self.ui.show_processing_file(filename)
|
||||
self._process_file(base_file, target_file, target_folder, instruction)
|
||||
|
||||
def _process_file(self, base_file: str, target_file: str, target_folder: str, instruction: str):
|
||||
"""Process translation for a specific file"""
|
||||
# Load base XML
|
||||
base_root = self.xml_processor.load_xml_file(base_file)
|
||||
if base_root is None:
|
||||
return
|
||||
|
||||
# Load or create target XML
|
||||
if os.path.exists(target_file):
|
||||
target_root = self.xml_processor.load_xml_file(target_file)
|
||||
else:
|
||||
# Create new XML structure
|
||||
target_root = etree.Element('resources')
|
||||
|
||||
if target_root is None:
|
||||
return
|
||||
|
||||
# Extract strings
|
||||
base_strings = self.xml_processor.extract_strings(base_root)
|
||||
target_strings = self.xml_processor.extract_strings(target_root)
|
||||
|
||||
# Find missing strings
|
||||
missing_strings = []
|
||||
for name, item in base_strings.items():
|
||||
if name not in target_strings:
|
||||
missing_strings.append(item)
|
||||
|
||||
if not missing_strings:
|
||||
self.ui.show_all_translated(target_file)
|
||||
return
|
||||
|
||||
self.ui.show_missing_strings(len(missing_strings), target_file)
|
||||
|
||||
# Process in batches
|
||||
batch_size = self.config.translation_config['batch_size']
|
||||
interactive = self.config.translation_config['interactive_approval']
|
||||
|
||||
for i in range(0, len(missing_strings), batch_size):
|
||||
batch_items = missing_strings[i:i + batch_size]
|
||||
batch = TranslationBatch(
|
||||
items=batch_items,
|
||||
target_language=target_folder,
|
||||
target_file=target_file
|
||||
)
|
||||
|
||||
# Translate batch
|
||||
translations = self.llm_client.translate_batch(batch, instruction)
|
||||
|
||||
if not translations or len(translations) != len(batch_items):
|
||||
self.ui.show_batch_failed(i // batch_size + 1)
|
||||
continue
|
||||
|
||||
# Display translations for approval
|
||||
if interactive:
|
||||
approved = self.ui.show_batch_approval(batch, translations)
|
||||
if not approved:
|
||||
self.ui.show_batch_skipped(i // batch_size + 1)
|
||||
continue
|
||||
|
||||
# Add translations to target XML
|
||||
new_translations = []
|
||||
for item, translation in zip(batch_items, translations):
|
||||
if item.item_type == 'string-array':
|
||||
# For string arrays, parse the translation into individual items
|
||||
# Expected format: item1 | item2 | item3 (same separator as input)
|
||||
translated_items = [t.strip() for t in translation.split('|')]
|
||||
new_translations.append((item.name, translation, 'string-array', translated_items))
|
||||
else:
|
||||
# Regular string
|
||||
new_translations.append((item.name, translation, 'string', []))
|
||||
|
||||
self.xml_processor.add_missing_strings(target_root, new_translations)
|
||||
|
||||
# Save the file
|
||||
self.xml_processor.save_xml_file(target_root, target_file)
|
||||
self.ui.show_batch_added(i // batch_size + 1, target_file)
|
||||
Reference in New Issue
Block a user