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