Bug 1 (secondary): _persist_pre() created TranslationLanguage with
empty final_value for non-source target languages when same_language
flag was set but no cache/approved value existed. This produced
is_original=0 rows with empty text in target table.
Fix: skip TranslationLanguage creation when fv is empty (continue
in the loop instead of falling through to else with empty string).
Bug 2 (diagnostic): No visibility into _classify() routing decisions.
Add logger.reason with pre/llm/total counts per batch.
Scenario trace: ru source + targets [ru,en] + no cache → llm_rows ✓
If same-language partial match hits cache for all targets → pre_rows ✓
If same-language partial match misses cache → llm_rows ✓
If same-language all-match (targets=[ru]) → short-circuit pre_rows ✓
Tests: 69/69 translate tests pass.
Root cause: _classify() immediately skipped (continue) when detected
language matched ANY target language, preventing cache lookup and LLM
translation for remaining targets. E.g., ru source + targets [ru, en]
→ only ru row created, en never processed (8978 ru vs 18 en in DB).
Fix:
1. _classify(): Only short-circuit when ALL targets match detected
language. Partial match → mark _same_language but continue to
cache check and LLM for non-matching targets.
2. _persist_pre(): Unify two code paths into single loop over all
target languages. Same-language targets use source_text as-is;
cached targets use cache value; fallback to approved_translation.
3. SIM102: Merge nested 'if cl: if all(...)' → 'if cl and all(...)'
to keep cyclomatic complexity ≤ 10 (INV_7).
QA: All 69 translate tests pass. 7 scenario traces (A-G) verified.
No regressions in approved_translation, preview edits, or single-
target same-language flows.