diff --git a/.github/scripts/check-structure.php b/.github/scripts/check-structure.php deleted file mode 100644 index 168e03cfce..0000000000 --- a/.github/scripts/check-structure.php +++ /dev/null @@ -1,246 +0,0 @@ - en trop, une liste devenue paragraphe, etc.). - * - * Ce qu'on NE compare PAS : la prose inline (le texte d'un , d'un …), - * qui diverge légitimement d'une langue à l'autre. On s'arrête donc au bord des - * conteneurs de texte. - * - * Point clé : on compare au fichier doc-en *à la révision que la traduction dit - * mirrorer* (commentaire « EN-Revision: <hash> »), récupérée via `git show`. - * Conséquence : un fichier en retard de synchro ne déclenche pas de faux - * positif, puisqu'on le compare à l'anglais qu'il a réellement traduit. - * - * Layout attendu : le dépôt de traduction est à la racine (répertoire courant), - * doc-en est dans le sous-répertoire `en/`. - * - * Usage (depuis la CI) : - * git diff --name-only … | php .github/scripts/check-structure.php - * STDIN un chemin .xml par ligne (relatif à la racine du dépôt) - * La sortie est en annotations GitHub Actions (::error) ; le script sort en code - * non nul si au moins une divergence est trouvée. - */ - -// --- Vocabulaire DocBook -------------------------------------------------- - -// Attributs qui font partie de la « structure » : on les inclut dans la -// signature d'un élément (un role= ou un xml:id qui change = dérive). -// href et xpointer viennent de XInclude : une cible traduite ne sélectionne -// plus rien et casse le build, ils doivent donc être identiques à doc-en. -const STRUCTURAL_ATTRIBUTES = ['role', 'choice', 'class', 'xml:id', 'rep', 'href', 'xpointer']; - -// Éléments dont le contenu est du texte (inline). On enregistre l'élément -// lui-même mais on NE descend PAS dedans : sa prose est l'affaire du traducteur. -const TEXT_CONTAINERS = [ - 'para', 'simpara', 'term', 'title', 'titleabbrev', 'refpurpose', 'refname', - 'member', 'entry', 'literallayout', 'programlisting', 'screen', 'seg', - 'segtitle', 'synopsis', -]; - -// --- Construction du squelette -------------------------------------------- - -/** - * Remplace toute entité nommée (&reftitle.intro;, &warn.foo;, …) par un simple - * texte « E ». Deux raisons : - * 1. sans ça, le DOM refuse de parser (entités non déclarées hors DTD) ; - * 2. c'est symétrique — EN et traduction ont les mêmes entités aux mêmes - * endroits, donc les remplacer pareil des deux côtés ne crée aucune - * différence artificielle. - * On laisse intactes les entités prédéfinies XML (& < …) et numériques. - */ -function entitiesToPlaceholder(string $xml): string -{ - $namedEntity = '/&(?!(?:amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);)[\w.:-]+;/'; - return preg_replace($namedEntity, 'E', $xml); -} - -/** - * Signature d'un élément : son nom suivi de ses attributs structurels. - * Ex. : <refsect1 role="description"> donne "refsect1(role=description)". - */ -function elementSignature(DOMElement $element): string -{ - $attributes = []; - foreach (STRUCTURAL_ATTRIBUTES as $name) { - $value = $element->getAttribute($name); - if ($value !== '') { - $attributes[] = "$name=$value"; - } - } - - if ($attributes === []) { - return $element->nodeName; - } - return $element->nodeName . '(' . implode(',', $attributes) . ')'; -} - -/** - * Parcourt l'arbre en profondeur et ajoute la signature de chaque élément - * (préfixée par sa profondeur, en espaces) dans $skeleton. On s'arrête au bord - * des conteneurs de texte : on enregistre le conteneur mais pas son contenu. - */ -function collectSkeleton(DOMElement $element, string $depth, array &$skeleton): void -{ - // Les crédits traducteurs sont propres à la traduction (absents de doc-en) : - // on les ignore pour ne pas signaler une fausse dérive. - $isTranslatorCredits = $element->nodeName === 'authorgroup' - && str_starts_with($element->getAttribute('xml:id'), 'translators'); - if ($isTranslatorCredits) { - return; - } - - $skeleton[] = $depth . elementSignature($element); - - // Conteneur de texte : on n'entre pas dedans (sa prose peut diverger). - if (in_array($element->nodeName, TEXT_CONTAINERS, true)) { - return; - } - - foreach ($element->childNodes as $child) { - if ($child->nodeType === XML_ELEMENT_NODE) { - collectSkeleton($child, $depth . ' ', $skeleton); - } - } -} - -/** - * Squelette d'un document XML donné en chaîne : la liste à plat des signatures - * de ses éléments bloc. Renvoie ['<<INVALIDE>>'] si le XML est illisible. - */ -function buildSkeleton(string $xml): array -{ - $document = new DOMDocument(); - libxml_use_internal_errors(true); // on gère nous-mêmes les erreurs de parse - $parsed = $document->loadXML(entitiesToPlaceholder($xml), LIBXML_NONET); - libxml_clear_errors(); - - if (!$parsed || !$document->documentElement) { - return ['<<INVALIDE>>']; - } - - $skeleton = []; - collectSkeleton($document->documentElement, '', $skeleton); - return $skeleton; -} - -// --- Accès à doc-en et comparaison ---------------------------------------- - -/** - * Contenu d'un fichier doc-en à une révision donnée (`git show <hash>:<chemin>`), - * ou null si le fichier n'existait pas à cette révision. - */ -function docEnFileAtRevision(string $enRepo, string $hash, string $relativePath): ?string -{ - $command = sprintf( - 'git -C %s show %s:%s 2>/dev/null', - escapeshellarg($enRepo), - escapeshellarg($hash), - escapeshellarg($relativePath) - ); - $content = shell_exec($command); - return ($content === null || $content === '') ? null : $content; -} - -/** - * Première position où deux squelettes diffèrent, sous la forme - * [ligneEN, ligneTraduction], ou null s'ils sont identiques. Une ligne absente - * (squelette plus court) est représentée par « (rien) ». - */ -function firstDivergence(array $enSkeleton, array $translationSkeleton): ?array -{ - $length = max(count($enSkeleton), count($translationSkeleton)); - for ($i = 0; $i < $length; $i++) { - $enLine = $enSkeleton[$i] ?? ''; - $translationLine = $translationSkeleton[$i] ?? ''; - if ($enLine !== $translationLine) { - return [ - trim($enSkeleton[$i] ?? '(rien)'), - trim($translationSkeleton[$i] ?? '(rien)'), - ]; - } - } - return null; -} - -// --- Entrée / sortie ------------------------------------------------------ - -/** Lit les chemins (.xml) fournis sur l'entrée standard, un par ligne. */ -function readPathsFromStdin(): array -{ - $paths = []; - foreach (explode("\n", stream_get_contents(STDIN)) as $line) { - $path = trim($line); - if ($path !== '' && str_ends_with($path, '.xml')) { - $paths[] = $path; - } - } - return $paths; -} - -/** - * Émet chaque divergence en annotation GitHub Actions (::error). Le chemin est - * relatif à la racine du dépôt de traduction, pour que l'annotation tombe sur - * le bon fichier dans la PR. - */ -function printAnnotations(array $violations): void -{ - foreach ($violations as [$file, $enLine, $translationLine, $enCount, $translationCount]) { - $message = sprintf( - 'structure diffère de doc-en (EN: %s | trad: %s) [blocs EN=%d trad=%d]', - $enLine, $translationLine, $enCount, $translationCount - ); - printf("::error file=%s::%s\n", $file, $message); - } -} - -// --- Programme principal -------------------------------------------------- -// -// Layout attendu : le dépôt de traduction est à la racine (répertoire courant), -// doc-en est dans le sous-répertoire `en/`. - -$workspace = getcwd(); -$enRepo = "$workspace/en"; - -$violations = []; // chaque entrée : [chemin, ligneEN, ligneTrad, nbBlocsEN, nbBlocsTrad] -$checkedCount = 0; - -foreach (readPathsFromStdin() as $relativePath) { - $translationPath = "$workspace/$relativePath"; - if (!is_file($translationPath)) { - continue; - } - $translationXml = file_get_contents($translationPath); - - // De quelle révision EN cette traduction se réclame-t-elle ? - if (!preg_match('/EN-Revision:\s*([0-9a-f]{40})/', $translationXml, $match)) { - continue; // pas de hash : on ne sait pas à quoi comparer - } - $enRevision = $match[1]; - - $enXml = docEnFileAtRevision($enRepo, $enRevision, $relativePath); - if ($enXml === null) { - continue; // fichier absent côté EN à ce hash (nouveau, renommé…) - } - - $checkedCount++; - $enSkeleton = buildSkeleton($enXml); - $translationSkeleton = buildSkeleton($translationXml); - - $divergence = firstDivergence($enSkeleton, $translationSkeleton); - if ($divergence === null) { - continue; // structures identiques : rien à signaler - } - - [$enLine, $translationLine] = $divergence; - $violations[] = [ - $relativePath, $enLine, $translationLine, - count($enSkeleton), count($translationSkeleton), - ]; -} - -printAnnotations($violations); -fprintf(STDERR, "vérifiés=%d divergents=%d\n", $checkedCount, count($violations)); - -exit($violations ? 1 : 0); diff --git a/.github/workflows/check-xml.yml b/.github/workflows/check-xml.yml index 560da3de41..7c5f6bfc5b 100644 --- a/.github/workflows/check-xml.yml +++ b/.github/workflows/check-xml.yml @@ -3,6 +3,9 @@ # miroir de doc-en. Chaque fichier est comparé au doc-en à la révision qu'il dit # mirrorer (EN-Revision), donc un fichier en retard ne produit pas de faux # positif. Le script émet ses annotations ::error lui-même. +# +# Le script est celui de doc-base, partagé par toutes les traductions, et non +# plus une copie propre à ce dépôt. name: "Structure" @@ -19,9 +22,9 @@ jobs: name: "Check XML" runs-on: ubuntu-22.04 steps: - # doc-fr à la racine, doc-en dans le sous-répertoire en/ : c'est - # l'arborescence attendue par check-structure.php. Le ref explicite - # prend la tête réelle de la PR, et non le commit de fusion que + # fr/, en/ et doc-base/ côte à côte : c'est l'arborescence attendue par + # les scripts de doc-base, la même que celle du workflow de build. Le ref + # explicite prend la tête réelle de la PR, et non le commit de fusion que # actions/checkout construit par défaut : ce dernier a master pour # deuxième parent, donc le diff plus bas y verrait aussi tous les # fichiers arrivés sur master depuis le dernier push de la PR. @@ -29,6 +32,7 @@ jobs: - name: "Checkout php/doc-fr" uses: actions/checkout@v4 with: + path: fr ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 @@ -39,9 +43,16 @@ jobs: repository: php/doc-en fetch-depth: 0 + - name: "Checkout php/doc-base" + uses: actions/checkout@v4 + with: + path: doc-base + repository: php/doc-base + - name: "Check structure" run: | BASE="${{ github.event.pull_request.base.sha }}" - git fetch --no-tags --depth=1 origin "$BASE" - git diff --name-only "$BASE"...HEAD -- '*.xml' \ - | php8.1 .github/scripts/check-structure.php + git -C fr fetch --no-tags --depth=1 origin "$BASE" + git -C fr diff --name-only "$BASE"...HEAD -- '*.xml' \ + | php8.1 doc-base/scripts/translation/check-structure.php \ + --lang=fr --github diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 07f936d13e..c8a964e372 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -203,10 +203,13 @@ Ne créez et pushez pas des branches différentes de ``master`` sur le repo git Quatre vérifications automatiques tournent sur chaque pull request. Elles se rejouent en local, ce qui évite un aller-retour avec la CI. -Les deux premières comparent les fichiers à la documentation anglaise et -attendent de la trouver dans un dossier `en` **à la racine du dépôt** `fr` -(`.gitignore` le prévoit). Si la documentation anglaise est ailleurs, un lien -symbolique suffit : +Deux d'entre elles comparent les fichiers à la documentation anglaise, mais +pas avec la même disposition. La vérification de structure se lance depuis le +répertoire qui contient `fr`, `en` et `doc-base` côte à côte, comme la +construction de la documentation. La vérification de l'`EN-Revision`, elle, +attend la documentation anglaise dans un dossier `en` **à la racine du dépôt** +`fr` (`.gitignore` le prévoit) ; si elle est ailleurs, un lien symbolique +suffit : ```shell ln -s ../en en @@ -230,8 +233,13 @@ Chaque fichier est comparé à la version anglaise **à la révision qu'il décl mirrorer** (`EN-Revision`), donc un fichier en retard ne produit pas de faux positif. +Le script est celui de `doc-base`, partagé par toutes les traductions. Il se +lance depuis le répertoire qui contient `fr`, `en` et `doc-base` côte à côte, +comme la construction de la documentation : + ```shell -git diff --name-only master...HEAD -- '*.xml' | php .github/scripts/check-structure.php +git -C fr diff --name-only master...HEAD -- '*.xml' \ + | php doc-base/scripts/translation/check-structure.php --lang=fr ``` ### EN-Revision : la clé de hachage