parser = new IcuParser(); $this->serializer = new IcuSerializer(); } #[DataProvider('corpus')] public function testRoundTripPreservesStructure(string $source): void { $ast = $this->parser->parse($source); $rendered = $this->serializer->serialize($ast); $reparsed = $this->parser->parse($rendered); self::assertSame( $ast->toComparable(), $reparsed->toComparable(), sprintf("Aller-retour non idempotent.\n source : %s\n rendu : %s", $source, $rendered), ); } /** * La sérialisation est un point fixe : re-sérialiser une forme canonique ne * doit plus rien changer. C'est ce qui garantit la stabilité de * source_checksum, donc l'absence de bascules en needs_review fantômes. */ #[DataProvider('corpus')] public function testSerializationIsIdempotent(string $source): void { $once = $this->serializer->serialize($this->parser->parse($source)); $twice = $this->serializer->serialize($this->parser->parse($once)); self::assertSame($once, $twice, 'La forme canonique n\'est pas stable.'); } /** * @return iterable */ public static function corpus(): iterable { yield 'texte simple' => ['Enregistrer']; yield 'texte vide' => ['']; yield 'argument simple' => ['Bonjour {prenom}']; yield 'arguments multiples' => ['{prenom} {nom} vous a invité']; // Le piège français : sans la règle « apostrophe-friendly » d'ICU, ces // apostrophes ouvriraient une citation et avaleraient la suite. yield 'apostrophe française' => ["Votre séance d'aujourd'hui"]; yield 'apostrophe avant argument' => ["L'équipe de {societe} vous souhaite la bienvenue"]; yield 'apostrophes consécutives' => ["Deux apostrophes '' ici"]; yield 'accolade échappée' => ["Utilisez '{' pour ouvrir un bloc"]; yield 'accolades échappées appariées' => ["Syntaxe : '{'clé'}'"]; yield 'dièse hors pluriel' => ['Commande #42 confirmée']; yield 'argument typé' => ['Réglé le {date, date, long}']; yield 'argument typé avec motif' => ['Total : {prix, number, currency}']; yield 'argument nombre' => ['{count, number} participants']; yield 'pluriel simple' => ['{count, plural, one {# séance} other {# séances}}']; yield 'pluriel avec sélecteur exact' => ['{count, plural, =0 {aucune séance} one {# séance} other {# séances}}']; yield 'pluriel avec offset' => ['{count, plural, offset:1 =0 {personne} one {vous et # autre} other {vous et # autres}}']; yield 'ordinal' => ['{rang, selectordinal, one {#er} other {#e}} de la liste']; yield 'select genre' => ['{genre, select, homme {Bienvenu} femme {Bienvenue} other {Bienvenue}}']; yield 'pluriel imbriqué dans select' => [ '{genre, select, femme {{count, plural, one {# inscrite} other {# inscrites}}} other {{count, plural, one {# inscrit} other {# inscrits}}}}', ]; yield 'select imbriqué dans pluriel' => [ '{count, plural, one {{genre, select, femme {une inscrite} other {un inscrit}}} other {# inscrits}}', ]; // Six catégories CLDR : c'est la langue qui casse les moteurs qui // supposent « singulier / pluriel ». yield 'arabe six formes' => [ '{count, plural, =0 {لا جلسات} zero {لا جلسات} one {جلسة واحدة} two {جلستان} few {# جلسات} many {# جلسة} other {# جلسة}}', ]; yield 'dièse échappé dans pluriel' => ["{count, plural, one {le '#' un} other {# éléments}}"]; yield 'accolade dans branche plurielle' => ["{count, plural, one {'{'# élément'}'} other {'{'# éléments'}'}}"]; yield 'espaces superflus' => ['{ count , plural , one {# séance} other {# séances} }']; } public function testMissingOtherBranchIsRejected(): void { $this->expectException(MessageParseException::class); $this->expectExceptionMessageMatches('/exige une branche « other »/'); $this->parser->parse('{count, plural, one {# séance}}'); } public function testUnbalancedBraceIsRejected(): void { $this->expectException(MessageParseException::class); $this->parser->parse('Bonjour {prenom'); } public function testStrayClosingBraceIsRejected(): void { $this->expectException(MessageParseException::class); $this->expectExceptionMessageMatches('/sans ouvrante/'); $this->parser->parse('Bonjour} tout le monde'); } public function testDuplicateSelectorIsRejected(): void { $this->expectException(MessageParseException::class); $this->expectExceptionMessageMatches('/défini deux fois/'); $this->parser->parse('{count, plural, one {a} one {b} other {c}}'); } /** * L'ordre d'écriture des branches n'a aucune signification métier. Deux * traducteurs qui saisissent les mêmes formes dans un ordre différent * doivent produire exactement la même chaîne canonique — sinon le checksum * change et toutes les langues cibles basculent en needs_review pour rien. */ public function testBranchOrderIsNormalised(): void { $a = $this->serializer->serialize($this->parser->parse('{n, plural, other {# éléments} one {# élément} =0 {vide}}')); $b = $this->serializer->serialize($this->parser->parse('{n, plural, =0 {vide} one {# élément} other {# éléments}}')); self::assertSame($b, $a); self::assertSame('{n, plural, =0 {vide} one {# élément} other {# éléments}}', $a); } public function testApostropheIsNotDoubledUnnecessarily(): void { $canonical = $this->serializer->serialize($this->parser->parse("Séance d'aujourd'hui")); self::assertSame("Séance d'aujourd'hui", $canonical, 'Une apostrophe ordinaire ne doit pas être doublée en base.'); } }