migrate to gitea

This commit is contained in:
jonasgaudian
2026-02-14 18:12:28 +01:00
commit 7c17f0f0cf
21 changed files with 2037 additions and 0 deletions

310
translation_tool.py Normal file
View 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)