Source code for dsw_document_template_tool._translation_tree.tree_audit

"""Audit translator-facing tree files before syncing them."""

from __future__ import annotations

from pathlib import Path

from .._template_transform.markers import GENERATED_BLOCK_PATTERN, generated_block_body
from .document import parse_translation_document
from .manifest import TREE_MANIFEST_PATH, load_tree_manifest
from .models import TranslationTreeAuditIssue, TranslationTreeError
from .placeholders import (
    build_source_placeholder_map,
    contains_raw_jinja_in_translation,
    source_placeholder_counts,
    validate_translation_placeholders,
)
from .source_text import (
    contains_jinja_block_or_comment,
    hard_fragment_sentence_message,
)
from .workspace import validate_expanded_workspace


[docs] def audit_translation_tree( *, tree_dir: Path, source_dir: Path, source_lang: str = "en", target_lang: str = "zh_Hant", ) -> list[TranslationTreeAuditIssue]: """Return structural issues that make translation blocks unsafe to edit.""" tree_dir = Path(tree_dir).resolve() source_dir = Path(source_dir).resolve() issues: list[TranslationTreeAuditIssue] = [] try: validate_expanded_workspace(source_dir) except TranslationTreeError as exc: return [ TranslationTreeAuditIssue( code="invalid-expanded-workspace", location=str(source_dir), message=str(exc), ) ] try: manifest = load_tree_manifest(tree_dir) except TranslationTreeError as exc: return [ TranslationTreeAuditIssue( code="invalid-translation-tree", location=str(tree_dir), message=str(exc), ) ] units = manifest.get("units") if not isinstance(units, list): return [ TranslationTreeAuditIssue( code="invalid-translation-tree", location=str(tree_dir / TREE_MANIFEST_PATH), message="Manifest `units` must be a list.", ) ] for unit in units: issues.extend( _audit_manifest_unit( tree_dir=tree_dir, source_dir=source_dir, unit=unit, source_lang=source_lang, target_lang=target_lang, ) ) return issues
def _audit_manifest_unit( *, tree_dir: Path, source_dir: Path, unit: object, source_lang: str, target_lang: str, ) -> list[TranslationTreeAuditIssue]: issues: list[TranslationTreeAuditIssue] = [] if not isinstance(unit, dict): return [ TranslationTreeAuditIssue( code="invalid-manifest-unit", location=str(tree_dir / TREE_MANIFEST_PATH), message=f"Manifest unit must be an object, got {type(unit).__name__}.", ) ] source_file = unit.get("source_file") unit_key = unit.get("unit_key") document_path_raw = unit.get("document_path") if not isinstance(source_file, str) or not isinstance(unit_key, str): return [ TranslationTreeAuditIssue( code="invalid-manifest-unit", location=str(tree_dir / TREE_MANIFEST_PATH), message="Manifest unit is missing a valid source_file or unit_key.", ) ] location = source_file if not isinstance(unit_key, str) else f"{source_file} ({unit_key})" try: source_unit_text = _source_unit_text_from_manifest_unit( source_dir=source_dir, unit=unit, ) except TranslationTreeError as exc: issues.append( TranslationTreeAuditIssue( code="invalid-source-span", location=location, message=str(exc), ) ) source_unit_text = None if source_unit_text is not None: issues.extend(_audit_source_unit_text(source_unit_text, location=location)) document_path_issue = _audit_document_path( tree_dir=tree_dir, document_path_raw=document_path_raw, location=location, ) if document_path_issue is not None: issues.append(document_path_issue) return issues assert isinstance(document_path_raw, str) try: translation_text = parse_translation_document( document_path=tree_dir / document_path_raw, source_lang=source_lang, target_lang=target_lang, ) except TranslationTreeError as exc: issues.append( TranslationTreeAuditIssue( code="invalid-translation-document", location=document_path_raw, message=str(exc), ) ) return issues issues.extend( _audit_translation_text( source_file=source_file, unit_key=unit_key, document_path_raw=document_path_raw, source_unit_text=source_unit_text, translation_text=translation_text, ) ) return issues def _audit_source_unit_text( source_unit_text: str, *, location: str, ) -> list[TranslationTreeAuditIssue]: issues: list[TranslationTreeAuditIssue] = [] if contains_jinja_block_or_comment(source_unit_text): issues.append( TranslationTreeAuditIssue( code="unsafe-source-jinja-block", location=location, message=( "Source unit contains raw Jinja block/comment syntax. " "Expand/export must split this before translators edit it." ), ) ) hard_fragment_message = hard_fragment_sentence_message(source_unit_text) if hard_fragment_message is not None: issues.append( TranslationTreeAuditIssue( code="hard-to-translate-source-fragment", location=location, message=hard_fragment_message, ) ) ambiguous_names = _ambiguous_source_placeholder_names(source_unit_text) if ambiguous_names: issues.append( TranslationTreeAuditIssue( code="ambiguous-source-placeholder", location=location, message=( "Source unit has placeholder names that cannot be mapped back " f"unambiguously: {', '.join('{' + name + '}' for name in ambiguous_names)}" ), ) ) return issues def _ambiguous_source_placeholder_names(source_unit_text: str) -> list[str]: placeholder_map = build_source_placeholder_map(source_unit_text) source_placeholder_names = set(source_placeholder_counts(source_unit_text)) return sorted(source_placeholder_names - set(placeholder_map)) def _audit_document_path( *, tree_dir: Path, document_path_raw: object, location: str, ) -> TranslationTreeAuditIssue | None: if not isinstance(document_path_raw, str): return TranslationTreeAuditIssue( code="invalid-manifest-unit", location=location, message="Manifest unit is missing a valid document_path.", ) document_path = tree_dir / document_path_raw if not document_path.is_file(): return TranslationTreeAuditIssue( code="missing-translation-document", location=document_path_raw, message="Translation document is missing. Re-run export to restore it.", ) return None def _audit_translation_text( *, source_file: str, unit_key: str, document_path_raw: str, source_unit_text: str | None, translation_text: str, ) -> list[TranslationTreeAuditIssue]: issues: list[TranslationTreeAuditIssue] = [] has_raw_jinja_translation = contains_raw_jinja_in_translation(translation_text) if has_raw_jinja_translation: issues.append( TranslationTreeAuditIssue( code="raw-jinja-in-translation", location=document_path_raw, message=( "Translation block contains raw Jinja syntax. Use translator " "placeholders such as `{name}` instead of `{% ... %}` or `{{ ... }}`." ), ) ) if source_unit_text is None or not translation_text.strip() or has_raw_jinja_translation: return issues try: validate_translation_placeholders( source_file=source_file, unit_key=unit_key, translation_document_path=document_path_raw, source_text=source_unit_text, translation_text=translation_text, ) except TranslationTreeError as exc: issues.append( TranslationTreeAuditIssue( code="invalid-translation-placeholders", location=document_path_raw, message=str(exc), ) ) return issues def _source_unit_text_from_manifest_unit(*, source_dir: Path, unit: dict) -> str: source_file = unit.get("source_file") wrapper_order = unit.get("wrapper_order") unit_start = unit.get("unit_start") unit_end = unit.get("unit_end") if ( not isinstance(source_file, str) or not isinstance(wrapper_order, int) or not isinstance(unit_start, int) or not isinstance(unit_end, int) ): raise TranslationTreeError("Manifest unit has invalid source span metadata.") source_path = source_dir / source_file if not source_path.is_file(): raise TranslationTreeError(f"Source file is missing: {source_path}") source_text = source_path.read_text(encoding="utf-8") wrappers = list(GENERATED_BLOCK_PATTERN.finditer(source_text)) if wrapper_order < 1 or wrapper_order > len(wrappers): raise TranslationTreeError( f"Wrapper order {wrapper_order} does not exist in {source_file}." ) wrapper_body = generated_block_body(wrappers[wrapper_order - 1]) if unit_start < 0 or unit_end > len(wrapper_body) or unit_start >= unit_end: raise TranslationTreeError( f"Unit span {unit_start}:{unit_end} is invalid for {source_file}." ) return wrapper_body[unit_start:unit_end]