Spaces:
Sleeping
Sleeping
File size: 5,236 Bytes
95cbd83 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | """Tests Sprint A11 — parité de traduction FR/EN (item M-17).
Garde-fou : pour chaque fichier ``X.md`` listé comme prioritaire,
la version EN ``X.en.md`` doit exister et avoir des sections de
premier niveau (``##``) qui se correspondent. Empêche une
divergence éditoriale silencieuse entre les deux versions.
"""
from __future__ import annotations
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
# Couples (FR, EN) à valider. Sprint A11 livre les 5 prioritaires ;
# d'autres viendront dans des sprints ultérieurs.
TRANSLATION_PAIRS: list[tuple[str, str]] = [
("docs/user/reading-a-report.md", "docs/user/reading-a-report.en.md"),
("docs/developer/index.md", "docs/developer/index.en.md"),
("docs/developer/narrative-engine.md", "docs/developer/narrative-engine.en.md"),
("docs/developer/extending-glossary.md", "docs/developer/extending-glossary.en.md"),
("docs/developer/extending-i18n.md", "docs/developer/extending-i18n.en.md"),
("CONTRIBUTING.md", "CONTRIBUTING.en.md"),
]
def _h2_titles(path: Path) -> list[str]:
"""Retourne la liste ordonnée des titres ``##`` d'un fichier markdown."""
text = path.read_text(encoding="utf-8")
return [
line[3:].strip()
for line in text.splitlines()
if line.startswith("## ") and not line.startswith("###")
]
@pytest.mark.parametrize("fr_path,en_path", TRANSLATION_PAIRS)
def test_en_version_exists(fr_path: str, en_path: str) -> None:
"""Pour chaque paire, la version EN doit exister."""
fr = REPO_ROOT / fr_path
en = REPO_ROOT / en_path
if not fr.exists():
pytest.skip(f"Source FR absente : {fr_path}")
assert en.exists(), f"Traduction EN manquante : {en_path}"
@pytest.mark.parametrize("fr_path,en_path", TRANSLATION_PAIRS)
def test_en_version_marks_translation_status(
fr_path: str, en_path: str
) -> None:
"""La version EN doit marquer son statut (translation: ...) en
tête, pour qu'un lecteur sache que la version FR fait foi en
cas de divergence."""
en = REPO_ROOT / en_path
if not en.exists():
pytest.skip(f"EN manquant : {en_path}")
text = en.read_text(encoding="utf-8")
assert "translation:" in text, (
f"{en_path} doit contenir un marqueur HTML "
"<!-- translation: ... --> en tête."
)
# Doit aussi linker vers la version FR canonique
fr_basename = Path(fr_path).name
assert fr_basename in text, (
f"{en_path} doit linker vers la version FR canonique "
f"({fr_basename})."
)
@pytest.mark.parametrize("fr_path,en_path", TRANSLATION_PAIRS)
def test_section_count_consistent(fr_path: str, en_path: str) -> None:
"""Les versions FR et EN doivent avoir un nombre cohérent de
sections de premier niveau (``##``).
Tolérances :
- **±1 section** par défaut (légère adaptation rédactionnelle).
- **±5 sections** si la version EN porte le marqueur
``<!-- translation: machine + human review pending -->``
(Sprint A11 livre des traductions synthétiques que le sprint
A15 ou un sprint dédié de revue humaine alignera ensuite).
Au-delà, c'est une divergence sérieuse à corriger.
"""
fr = REPO_ROOT / fr_path
en = REPO_ROOT / en_path
if not (fr.exists() and en.exists()):
pytest.skip(f"Une des deux versions manque : {fr_path} / {en_path}")
fr_sections = _h2_titles(fr)
en_sections = _h2_titles(en)
diff = abs(len(fr_sections) - len(en_sections))
en_text = en.read_text(encoding="utf-8")
is_pending_review = "machine + human review pending" in en_text
threshold = 5 if is_pending_review else 1
assert diff <= threshold, (
f"Divergence de structure trop forte : FR a {len(fr_sections)} sections, "
f"EN a {len(en_sections)} (diff {diff} > {threshold}). "
f"Pending-review : {is_pending_review}.\n"
f"FR: {fr_sections}\nEN: {en_sections}"
)
def test_no_orphan_en_translations() -> None:
"""Aucun fichier ``*.en.md`` ne doit exister sans son pendant FR
canonique (sinon dérive éditoriale)."""
en_files = list(REPO_ROOT.glob("**/*.en.md"))
# Exclure les __pycache__ et venv
en_files = [
f for f in en_files
if not any(part.startswith(".") or part == "__pycache__"
for part in f.parts)
]
orphans: list[str] = []
for en in en_files:
# Pendant FR : remplacer .en.md → .md
fr_name = en.name.replace(".en.md", ".md")
fr = en.with_name(fr_name)
if not fr.exists():
orphans.append(str(en.relative_to(REPO_ROOT)))
assert not orphans, (
f"Traductions EN orphelines (sans version FR) : {orphans}"
)
def test_translation_pairs_listed_actually_exist() -> None:
"""Méta-test : tous les couples listés ci-dessus doivent référencer
des fichiers existants (au moins le FR)."""
missing: list[str] = []
for fr, _ in TRANSLATION_PAIRS:
if not (REPO_ROOT / fr).exists():
missing.append(fr)
assert not missing, (
f"Fichiers FR listés mais absents : {missing}. "
f"Mettre à jour TRANSLATION_PAIRS si la doc canonique a bougé."
)
|