Pre-publication review draft
日常訳NICHIJOYAKU日本語 ↗

OPEN SOURCE

software/src/context/glossary.py

Read on this site and download the files you need.

Nichijoyaku was created using AI. A complete review by human biblical-language and biblical-studies specialists has not been established. Read alongside established translations and original-language resources.

← Back to the open-source guide

software/src/context/glossary.py302 linesDownload this file ↓

This is the same source included in the download. Each line number is a link you can save or share.

File verification detailsSHA-256 9285ed008cdf34fb98816a321eaebaa870479fc79554385625122874a833ddac
1"""Glossary management for consistent theological term translation."""23from __future__ import annotations45import re6from typing import TYPE_CHECKING, cast78from src.models.glossary import EnhancedGlossaryTerm, GlossaryTerm, TheologicalDomain910if TYPE_CHECKING:11    pass1213_HEBREW_MARKS = re.compile(r"[֑-ׇ/]")141516def _normalize_hebrew(text: str) -> str:17    """Strip Hebrew pointing/cantillation and morpheme separators."""18    return _HEBREW_MARKS.sub("", text)192021class GlossaryManager:22    """Manages theological term glossary for consistent translations.2324    Supports morphological matching through lemmas and inflected forms.25    """2627    def __init__(self, terms: list[GlossaryTerm] | None = None) -> None:28        """Initialize the glossary manager.2930        Args:31            terms: Initial list of glossary terms.32        """33        self._terms: list[GlossaryTerm] = terms or []34        self._by_original: dict[str, GlossaryTerm] = {}35        self._by_transliteration: dict[str, GlossaryTerm] = {}36        self._by_domain: dict[TheologicalDomain, list[GlossaryTerm]] = {37            domain: [] for domain in TheologicalDomain38        }39        # Morphological indexes40        self._by_lemma: dict[str, GlossaryTerm] = {}41        self._by_strongs: dict[str, GlossaryTerm] = {}42        self._all_forms: dict[str, GlossaryTerm] = {}  # All matchable forms43        self._match_pattern: re.Pattern | None = None  # Compiled regex for fast matching44        self._rebuild_indexes()4546    def _rebuild_indexes(self) -> None:47        """Rebuild lookup indexes including morphological indexes."""48        self._by_original = {term.original: term for term in self._terms}49        self._by_transliteration = {50            term.transliteration.lower(): term for term in self._terms51        }52        # Rebuild domain index53        self._by_domain = {domain: [] for domain in TheologicalDomain}54        for term in self._terms:55            self._by_domain[term.domain].append(term)5657        # Rebuild morphological indexes58        self._by_lemma = {}59        self._by_strongs = {}60        self._all_forms = {}6162        for term in self._terms:63            # Index by lemma64            if term.lemma:65                self._by_lemma[term.lemma] = term6667            # Index by Strong's number68            if term.strongs_number:69                self._by_strongs[term.strongs_number.upper()] = term7071            # Index all matchable forms72            for form in term.get_all_matchable_forms():73                self._all_forms[form.lower()] = term7475        # Build compiled regex pattern for fast matching76        self._build_match_pattern()7778    def _build_match_pattern(self) -> None:79        """Build compiled regex pattern for matching all glossary forms."""80        if not self._all_forms:81            self._match_pattern = None82            return8384        # Sort forms by length (longest first) to match longest forms first85        sorted_forms = cast(list[str], sorted(self._all_forms.keys(), key=len, reverse=True))8687        # Escape special regex characters and join with |88        escaped_forms = [re.escape(form) for form in sorted_forms]89        pattern_str = "|".join(escaped_forms)9091        # Compile with Unicode support; IGNORECASE so verse-initial92        # capitalized Greek forms (Μακάριοι) still match.93        self._match_pattern = re.compile(pattern_str, re.UNICODE | re.IGNORECASE)9495    def add_term(self, term: GlossaryTerm) -> None:96        """Add a term to the glossary."""97        self._terms.append(term)98        self._by_original[term.original] = term99        self._by_transliteration[term.transliteration.lower()] = term100        self._by_domain[term.domain].append(term)101102        # Update morphological indexes103        if term.lemma:104            self._by_lemma[term.lemma] = term105        if term.strongs_number:106            self._by_strongs[term.strongs_number.upper()] = term107        for form in term.get_all_matchable_forms():108            self._all_forms[form.lower()] = term109110        # Rebuild match pattern (could be optimized for single additions)111        self._build_match_pattern()112113    def get_by_original(self, original: str) -> GlossaryTerm | None:114        """Look up a term by its original Greek/Hebrew form."""115        return self._by_original.get(original)116117    def get_by_transliteration(self, translit: str) -> GlossaryTerm | None:118        """Look up a term by its transliteration."""119        return self._by_transliteration.get(translit.lower())120121    def get_by_lemma(self, lemma: str) -> GlossaryTerm | None:122        """Look up a term by its lemma (dictionary form).123124        Args:125            lemma: The lemma to look up.126127        Returns:128            The GlossaryTerm if found, None otherwise.129        """130        return self._by_lemma.get(lemma)131132    def get_by_strongs(self, strongs: str) -> GlossaryTerm | None:133        """Look up a term by Strong's Concordance number.134135        Args:136            strongs: Strong's number (e.g., 'G3056').137138        Returns:139            The GlossaryTerm if found, None otherwise.140        """141        return self._by_strongs.get(strongs.upper())142143    def get_by_any_form(self, form: str) -> GlossaryTerm | None:144        """Look up a term by any of its matchable forms.145146        Searches original, lemma, and all inflected forms.147148        Args:149            form: The form to look up.150151        Returns:152            The GlossaryTerm if found, None otherwise.153        """154        return self._all_forms.get(form.lower())155156    def get_by_domain(self, domain: TheologicalDomain) -> list[GlossaryTerm]:157        """Get all terms in a specific theological domain.158159        Args:160            domain: The theological domain to filter by.161162        Returns:163            List of terms in that domain.164        """165        return list(self._by_domain.get(domain, []))166167    def get_related_terms(self, term: GlossaryTerm) -> list[GlossaryTerm]:168        """Get all related terms for a given term.169170        Args:171            term: The term to find related terms for.172173        Returns:174            List of related GlossaryTerm objects.175        """176        related = []177        for original in term.related_terms:178            related_term = self.get_by_original(original)179            if related_term:180                related.append(related_term)181        return related182183    def find_terms_in_text(self, text: str) -> list[GlossaryTerm]:184        """Find all glossary terms present in a text.185186        Uses efficient compiled regex matching with morphological awareness.187        Matches original forms, lemmas, and all known inflected forms.188189        Args:190            text: Source text (Greek, Hebrew, or transliteration).191192        Returns:193            List of unique matching glossary terms.194        """195        if not self._match_pattern or not text:196            return []197198        found_terms: dict[str, GlossaryTerm] = {}  # Use dict to deduplicate199200        # Match against the raw text and, for Hebrew, against a normalized201        # copy with vowel points/cantillation (U+0591-U+05C7) and MorphHB202        # "/" morpheme separators removed — glossary entries store unpointed203        # consonantal forms, which never match pointed text directly.204        candidates = [text]205        normalized = _normalize_hebrew(text)206        if normalized != text:207            candidates.append(normalized)208209        for candidate in candidates:210            for match in self._match_pattern.finditer(candidate):211                matched_form = match.group(0)212                term = self._all_forms.get(matched_form.lower())213                if term and term.original not in found_terms:214                    found_terms[term.original] = term215216        # Also check transliterations (case-insensitive)217        text_lower = text.lower()218        for term in self._terms:219            if term.original not in found_terms:220                if term.transliteration.lower() in text_lower:221                    found_terms[term.original] = term222223        return list(found_terms.values())224225    def find_terms_in_text_with_positions(226        self, text: str227    ) -> list[tuple[GlossaryTerm, int, int]]:228        """Find all glossary terms in text with their positions.229230        Useful for highlighting or annotating text with glossary matches.231232        Args:233            text: Source text (Greek, Hebrew, or transliteration).234235        Returns:236            List of tuples (term, start_pos, end_pos) for each match.237        """238        if not self._match_pattern or not text:239            return []240241        matches = []242        for match in self._match_pattern.finditer(text):243            matched_form = match.group(0)244            term = self._all_forms.get(matched_form.lower())245            if term:246                matches.append((term, match.start(), match.end()))247248        return matches249250    def get_all_terms(self) -> list[GlossaryTerm]:251        """Get all glossary terms."""252        return list(self._terms)253254    def format_for_prompt(255        self, terms: list[GlossaryTerm], include_enhanced: bool = True256    ) -> str:257        """Format glossary terms for inclusion in a translation prompt.258259        Args:260            terms: List of terms to format.261            include_enhanced: Whether to include enhanced metadata.262263        Returns:264            Formatted string for prompt inclusion.265        """266        if not terms:267            return "No specific glossary terms apply to this verse."268269        lines = []270        for term in terms:271            # Core information272            line = f"- {term.original} ({term.transliteration}) → {term.japanese}"273            if term.notes:274                line += f" [{term.notes}]"275            lines.append(line)276277            # Enhanced metadata (if available and requested)278            if include_enhanced and isinstance(term, EnhancedGlossaryTerm):279                if term.semantic_range:280                    semantic_str = ", ".join(term.semantic_range)281                    lines.append(f"  Semantic range: {semantic_str}")282                if term.avoid_translations:283                    avoid_str = ", ".join(term.avoid_translations)284                    lines.append(f"  Avoid: {avoid_str}")285                if term.definition:286                    lines.append(f"  Definition: {term.definition}")287288        return "\n".join(lines)289290    def get_domain_summary(self) -> dict[str, int]:291        """Get a summary of term counts by domain.292293        Returns:294            Dictionary mapping domain names to term counts.295        """296        return {297            domain.value: len(terms) for domain, terms in self._by_domain.items()298        }299300    def __len__(self) -> int:301        return len(self._terms)302