311 lines
12 KiB
Python
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)
|