"""Validation checks for translations.""" from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING if TYPE_CHECKING: from src.context.glossary import GlossaryManager from src.models.verse import TranslationOutput @dataclass class ValidationResult: """Result of a validation check.""" passed: bool check_name: str message: str details: dict = field(default_factory=dict) @dataclass class ValidationReport: """Full validation report for a translation.""" results: list[ValidationResult] = field(default_factory=list) @property def all_passed(self) -> bool: """Check if all validations passed.""" return all(r.passed for r in self.results) @property def failed_checks(self) -> list[ValidationResult]: """Get list of failed checks.""" return [r for r in self.results if not r.passed] def add_result(self, result: ValidationResult) -> None: """Add a validation result.""" self.results.append(result) class TranslationValidator: """Validates translation outputs.""" def __init__( self, glossary: GlossaryManager | None = None, min_length: int = 5, max_length: int = 2000, ) -> None: """Initialize the validator. Args: glossary: Glossary manager for term consistency checks. min_length: Minimum translation length. max_length: Maximum translation length. """ self.glossary = glossary self.min_length = min_length self.max_length = max_length def validate(self, output: TranslationOutput) -> ValidationReport: """Run all validation checks on a translation. Args: output: Translation output to validate. Returns: ValidationReport with all check results. """ report = ValidationReport() report.add_result(self._check_not_empty(output)) report.add_result(self._check_length(output)) report.add_result(self._check_no_english_only(output)) report.add_result(self._check_has_japanese(output)) if self.glossary: report.add_result(self._check_glossary_consistency(output)) return report def _check_not_empty(self, output: TranslationOutput) -> ValidationResult: """Check that translation is not empty.""" if not output.text or not output.text.strip(): return ValidationResult( passed=False, check_name="not_empty", message="Translation is empty", ) return ValidationResult( passed=True, check_name="not_empty", message="Translation is not empty", ) def _check_length(self, output: TranslationOutput) -> ValidationResult: """Check translation length is within bounds.""" length = len(output.text) if length < self.min_length: return ValidationResult( passed=False, check_name="length", message=f"Translation too short ({length} < {self.min_length})", details={"length": length, "min": self.min_length}, ) if length > self.max_length: return ValidationResult( passed=False, check_name="length", message=f"Translation too long ({length} > {self.max_length})", details={"length": length, "max": self.max_length}, ) return ValidationResult( passed=True, check_name="length", message=f"Translation length OK ({length})", details={"length": length}, ) def _check_no_english_only(self, output: TranslationOutput) -> ValidationResult: """Check that translation is not English-only.""" text = output.text.strip() # Check if text contains any Japanese characters has_japanese = any( "\u3040" <= char <= "\u309f" # Hiragana or "\u30a0" <= char <= "\u30ff" # Katakana or "\u4e00" <= char <= "\u9fff" # CJK for char in text ) if not has_japanese: return ValidationResult( passed=False, check_name="no_english_only", message="Translation appears to be English only", ) return ValidationResult( passed=True, check_name="no_english_only", message="Translation contains Japanese characters", ) def _check_has_japanese(self, output: TranslationOutput) -> ValidationResult: """Check that translation has sufficient Japanese content.""" text = output.text.strip() # Count Japanese characters japanese_chars = sum( 1 for char in text if ( "\u3040" <= char <= "\u309f" # Hiragana or "\u30a0" <= char <= "\u30ff" # Katakana or "\u4e00" <= char <= "\u9fff" # CJK ) ) total_chars = len(text) ratio = japanese_chars / total_chars if total_chars > 0 else 0 if ratio < 0.3: return ValidationResult( passed=False, check_name="has_japanese", message=f"Japanese character ratio too low ({ratio:.1%})", details={"ratio": ratio, "japanese_chars": japanese_chars}, ) return ValidationResult( passed=True, check_name="has_japanese", message=f"Japanese character ratio OK ({ratio:.1%})", details={"ratio": ratio, "japanese_chars": japanese_chars}, ) def _check_glossary_consistency( self, output: TranslationOutput ) -> ValidationResult: """Check that glossary terms are used consistently. Note: This method should only be called when self.glossary is set. The validate() method handles this check. """ assert self.glossary is not None # Get terms that were supposed to be applied applied_terms = output.glossary_terms_applied if not applied_terms: return ValidationResult( passed=True, check_name="glossary_consistency", message="No glossary terms expected", ) # Check if Japanese translations are present missing = [] for term_original in applied_terms: term = self.glossary.get_by_original(term_original) if term and term.japanese not in output.text: missing.append(term_original) if missing: return ValidationResult( passed=False, check_name="glossary_consistency", message=f"Missing expected glossary terms: {', '.join(missing)}", details={"missing": missing}, ) return ValidationResult( passed=True, check_name="glossary_consistency", message="All expected glossary terms present", )