Files
ResourceTranslate/translation_tool.py
2026-02-14 18:12:28 +01:00

311 lines
12 KiB
Python

"""
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)