From 6f01e33df01ad56622b85639824b9a1b0161f967 Mon Sep 17 00:00:00 2001 From: cs Date: Mon, 13 Jul 2026 18:32:00 +0200 Subject: [PATCH 1/2] [BUGFIX] Keep non-ASCII characters literal in XLIFF output - Default to UTF-8 when the document has no encoding, so characters like umlauts stay literal instead of being written as numeric entities --- .../fractor-xliff/src/XliffFileProcessor.php | 4 + .../XliffEncoding/Fixtures/french-accents.xlf | 9 ++ .../tests/XliffEncoding/Fixtures/symbols.xlf | 9 ++ .../Fixtures/umlaut-and-rule-change.xlf | 13 +++ .../Fixtures/umlauts-no-declaration.xlf | 9 ++ .../Fixtures/umlauts-with-declaration.xlf | 10 ++ .../tests/XliffEncoding/XliffEncodingTest.php | 98 +++++++++++++++++++ .../tests/XliffEncoding/config/fractor.php | 15 +++ 8 files changed, 167 insertions(+) create mode 100644 packages/fractor-xliff/tests/XliffEncoding/Fixtures/french-accents.xlf create mode 100644 packages/fractor-xliff/tests/XliffEncoding/Fixtures/symbols.xlf create mode 100644 packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlaut-and-rule-change.xlf create mode 100644 packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-no-declaration.xlf create mode 100644 packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-with-declaration.xlf create mode 100644 packages/fractor-xliff/tests/XliffEncoding/XliffEncodingTest.php create mode 100644 packages/fractor-xliff/tests/XliffEncoding/config/fractor.php diff --git a/packages/fractor-xliff/src/XliffFileProcessor.php b/packages/fractor-xliff/src/XliffFileProcessor.php index d1144022..042d7993 100644 --- a/packages/fractor-xliff/src/XliffFileProcessor.php +++ b/packages/fractor-xliff/src/XliffFileProcessor.php @@ -93,6 +93,10 @@ public function getAllRules(): iterable private function saveXml(\DOMDocument $document): string { + // Ensure a proper encoding so non-ASCII characters stay literal + if ($document->encoding === null || $document->encoding === '') { + $document->encoding = 'UTF-8'; + } $xml = $document->saveXML(); if ($xml === false) { throw new ShouldNotHappenException('Could not save XLIFF document'); diff --git a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/french-accents.xlf b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/french-accents.xlf new file mode 100644 index 00000000..7cc1ac51 --- /dev/null +++ b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/french-accents.xlf @@ -0,0 +1,9 @@ + + + + + Résultats à côté, déjà vu + + + + diff --git a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/symbols.xlf b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/symbols.xlf new file mode 100644 index 00000000..24e83a25 --- /dev/null +++ b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/symbols.xlf @@ -0,0 +1,9 @@ + + + + + Preis: 5€ – naïve façade + + + + diff --git a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlaut-and-rule-change.xlf b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlaut-and-rule-change.xlf new file mode 100644 index 00000000..fb447777 --- /dev/null +++ b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlaut-and-rule-change.xlf @@ -0,0 +1,13 @@ + + + + + + Hello + + + Ergebnisse für "%s". + + + + diff --git a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-no-declaration.xlf b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-no-declaration.xlf new file mode 100644 index 00000000..1a53adbc --- /dev/null +++ b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-no-declaration.xlf @@ -0,0 +1,9 @@ + + + + + Ergebnisse für "%s". Ä Ö Ü ä ö ü ß + + + + diff --git a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-with-declaration.xlf b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-with-declaration.xlf new file mode 100644 index 00000000..9ebf2d7a --- /dev/null +++ b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-with-declaration.xlf @@ -0,0 +1,10 @@ + + + + + + Ergebnisse für "%s". Ä Ö Ü ä ö ü ß + + + + diff --git a/packages/fractor-xliff/tests/XliffEncoding/XliffEncodingTest.php b/packages/fractor-xliff/tests/XliffEncoding/XliffEncodingTest.php new file mode 100644 index 00000000..975c5167 --- /dev/null +++ b/packages/fractor-xliff/tests/XliffEncoding/XliffEncodingTest.php @@ -0,0 +1,98 @@ +inputFile)) { + FileSystem::delete($this->inputFile); + $this->inputFile = null; + } + + parent::tearDown(); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/fractor.php'; + } + + /** + * @param non-empty-string $fixture + * @param non-empty-string $expectedText + */ + #[DataProvider('provideNonAsciiFixtures')] + public function testNonAsciiCharactersAreKeptLiteral(string $fixture, string $expectedText): void + { + $result = $this->fixAndRead($fixture); + + self::assertStringContainsString($expectedText, $result); + self::assertStringNotContainsString('&#', $result); + } + + /** + * @return array + */ + public static function provideNonAsciiFixtures(): array + { + return [ + // A missing declaration leaves the encoding empty; that is what turned + // "für" into "für" during serialisation. + 'german umlauts, no declaration' => [ + 'umlauts-no-declaration.xlf', + 'Ergebnisse für "%s". Ä Ö Ü ä ö ü ß', + ], + 'german umlauts, with declaration' => [ + 'umlauts-with-declaration.xlf', + 'Ergebnisse für "%s". Ä Ö Ü ä ö ü ß', + ], + 'french accents, no declaration' => ['french-accents.xlf', 'Résultats à côté, déjà vu'], + 'symbols and dashes, no declaration' => ['symbols.xlf', 'Preis: 5€ – naïve façade'], + ]; + } + + public function testNonAsciiSurvivesAlongsideRuleChange(): void + { + // One unit triggers the rule, the other carries non-ASCII text: the rule + // change must apply and the umlauts must stay literal. + $result = $this->fixAndRead('umlaut-and-rule-change.xlf'); + + self::assertStringContainsString('Hello World', $result); + self::assertStringContainsString('Ergebnisse für "%s".', $result); + self::assertStringNotContainsString('&#', $result); + } + + private function fixAndRead(string $fixture): string + { + $this->inputFile = sys_get_temp_dir() . '/fractor-xliff-' . uniqid() . '.xlf'; + FileSystem::write($this->inputFile, FileSystem::read(__DIR__ . '/Fixtures/' . $fixture), null); + + $configuration = new Configuration( + dryRun: false, + showProgressBar: false, + fileExtensions: ['xlf', 'xliff'], + paths: [$this->inputFile], + ); + + $this->getService(ConfigurationRuleFilter::class)->setConfiguration($configuration); + $this->getService(FractorRunner::class)->run($configuration); + + return FileSystem::read($this->inputFile); + } +} diff --git a/packages/fractor-xliff/tests/XliffEncoding/config/fractor.php b/packages/fractor-xliff/tests/XliffEncoding/config/fractor.php new file mode 100644 index 00000000..439dd186 --- /dev/null +++ b/packages/fractor-xliff/tests/XliffEncoding/config/fractor.php @@ -0,0 +1,15 @@ +withOptions([ + XliffProcessorOption::INDENT_CHARACTER => Indent::STYLE_TAB, + XliffProcessorOption::INDENT_SIZE => 1, + ]) + ->withRules([DummyXliffFractorRule::class]); From 32b5795da3cd1146fa0d122fbf49fb7bf8146ac6 Mon Sep 17 00:00:00 2001 From: cs Date: Tue, 14 Jul 2026 13:15:28 +0200 Subject: [PATCH 2/2] [TASK] Align XLIFF encoding test with standard fixture pattern - Convert XliffEncodingTest to the shared doTestFile/yieldFilesFromDirectory approach - Migrate encoding fixtures to the input/expected split .xlf.fixture format - Assert exact serialized output so non-ASCII characters stay literal without numeric entities - Drop the bespoke runner wiring and manual temp-file handling in favor of AbstractFractorTestCase --- .../XliffEncoding/Fixtures/french-accents.xlf | 9 -- .../Fixtures/french-accents.xlf.fixture | 21 +++++ .../tests/XliffEncoding/Fixtures/symbols.xlf | 9 -- .../Fixtures/symbols.xlf.fixture | 21 +++++ .../Fixtures/umlaut-and-rule-change.xlf | 13 --- .../umlaut-and-rule-change.xlf.fixture | 28 +++++++ .../Fixtures/umlauts-no-declaration.xlf | 9 -- ...xlf => umlauts-no-declaration.xlf.fixture} | 13 ++- .../umlauts-with-declaration.xlf.fixture | 22 +++++ .../tests/XliffEncoding/XliffEncodingTest.php | 82 ++----------------- 10 files changed, 111 insertions(+), 116 deletions(-) delete mode 100644 packages/fractor-xliff/tests/XliffEncoding/Fixtures/french-accents.xlf create mode 100644 packages/fractor-xliff/tests/XliffEncoding/Fixtures/french-accents.xlf.fixture delete mode 100644 packages/fractor-xliff/tests/XliffEncoding/Fixtures/symbols.xlf create mode 100644 packages/fractor-xliff/tests/XliffEncoding/Fixtures/symbols.xlf.fixture delete mode 100644 packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlaut-and-rule-change.xlf create mode 100644 packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlaut-and-rule-change.xlf.fixture delete mode 100644 packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-no-declaration.xlf rename packages/fractor-xliff/tests/XliffEncoding/Fixtures/{umlauts-with-declaration.xlf => umlauts-no-declaration.xlf.fixture} (52%) create mode 100644 packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-with-declaration.xlf.fixture diff --git a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/french-accents.xlf b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/french-accents.xlf deleted file mode 100644 index 7cc1ac51..00000000 --- a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/french-accents.xlf +++ /dev/null @@ -1,9 +0,0 @@ - - - - - Résultats à côté, déjà vu - - - - diff --git a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/french-accents.xlf.fixture b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/french-accents.xlf.fixture new file mode 100644 index 00000000..5f50f5df --- /dev/null +++ b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/french-accents.xlf.fixture @@ -0,0 +1,21 @@ + + + + + Résultats à côté, déjà vu + + + + +----- + + + + + + Résultats à côté, déjà vu + + + + diff --git a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/symbols.xlf b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/symbols.xlf deleted file mode 100644 index 24e83a25..00000000 --- a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/symbols.xlf +++ /dev/null @@ -1,9 +0,0 @@ - - - - - Preis: 5€ – naïve façade - - - - diff --git a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/symbols.xlf.fixture b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/symbols.xlf.fixture new file mode 100644 index 00000000..86188be0 --- /dev/null +++ b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/symbols.xlf.fixture @@ -0,0 +1,21 @@ + + + + + Preis: 5€ – naïve façade + + + + +----- + + + + + + Preis: 5€ – naïve façade + + + + diff --git a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlaut-and-rule-change.xlf b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlaut-and-rule-change.xlf deleted file mode 100644 index fb447777..00000000 --- a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlaut-and-rule-change.xlf +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Hello - - - Ergebnisse für "%s". - - - - diff --git a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlaut-and-rule-change.xlf.fixture b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlaut-and-rule-change.xlf.fixture new file mode 100644 index 00000000..1e28f5dc --- /dev/null +++ b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlaut-and-rule-change.xlf.fixture @@ -0,0 +1,28 @@ + + + + + + Hello + + + Ergebnisse für "%s". + + + + +----- + + + + + + Hello World + + + Ergebnisse für "%s". + + + + diff --git a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-no-declaration.xlf b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-no-declaration.xlf deleted file mode 100644 index 1a53adbc..00000000 --- a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-no-declaration.xlf +++ /dev/null @@ -1,9 +0,0 @@ - - - - - Ergebnisse für "%s". Ä Ö Ü ä ö ü ß - - - - diff --git a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-with-declaration.xlf b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-no-declaration.xlf.fixture similarity index 52% rename from packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-with-declaration.xlf rename to packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-no-declaration.xlf.fixture index 9ebf2d7a..a9c89cb4 100644 --- a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-with-declaration.xlf +++ b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-no-declaration.xlf.fixture @@ -1,4 +1,3 @@ - @@ -8,3 +7,15 @@ +----- + + + + + + Ergebnisse für "%s". Ä Ö Ü ä ö ü ß + + + + diff --git a/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-with-declaration.xlf.fixture b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-with-declaration.xlf.fixture new file mode 100644 index 00000000..cbb59126 --- /dev/null +++ b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-with-declaration.xlf.fixture @@ -0,0 +1,22 @@ + + + + + + Ergebnisse für "%s". Ä Ö Ü ä ö ü ß + + + + +----- + + + + + + Ergebnisse für "%s". Ä Ö Ü ä ö ü ß + + + + diff --git a/packages/fractor-xliff/tests/XliffEncoding/XliffEncodingTest.php b/packages/fractor-xliff/tests/XliffEncoding/XliffEncodingTest.php index 975c5167..a9addb76 100644 --- a/packages/fractor-xliff/tests/XliffEncoding/XliffEncodingTest.php +++ b/packages/fractor-xliff/tests/XliffEncoding/XliffEncodingTest.php @@ -4,11 +4,7 @@ namespace a9f\FractorXliff\Tests\XliffEncoding; -use a9f\Fractor\Application\FractorRunner; -use a9f\Fractor\Configuration\ConfigurationRuleFilter; -use a9f\Fractor\Configuration\ValueObject\Configuration; use a9f\Fractor\Testing\PHPUnit\AbstractFractorTestCase; -use Nette\Utils\FileSystem; use PHPUnit\Framework\Attributes\DataProvider; /** @@ -16,83 +12,19 @@ */ final class XliffEncodingTest extends AbstractFractorTestCase { - private ?string $inputFile = null; - - protected function tearDown(): void - { - if (\is_string($this->inputFile)) { - FileSystem::delete($this->inputFile); - $this->inputFile = null; - } - - parent::tearDown(); - } - - public function provideConfigFilePath(): string - { - return __DIR__ . '/config/fractor.php'; - } - - /** - * @param non-empty-string $fixture - * @param non-empty-string $expectedText - */ - #[DataProvider('provideNonAsciiFixtures')] - public function testNonAsciiCharactersAreKeptLiteral(string $fixture, string $expectedText): void + #[DataProvider('provideData')] + public function test(string $filePath): void { - $result = $this->fixAndRead($fixture); - - self::assertStringContainsString($expectedText, $result); - self::assertStringNotContainsString('&#', $result); + $this->doTestFile($filePath); } - /** - * @return array - */ - public static function provideNonAsciiFixtures(): array + public static function provideData(): \Iterator { - return [ - // A missing declaration leaves the encoding empty; that is what turned - // "für" into "für" during serialisation. - 'german umlauts, no declaration' => [ - 'umlauts-no-declaration.xlf', - 'Ergebnisse für "%s". Ä Ö Ü ä ö ü ß', - ], - 'german umlauts, with declaration' => [ - 'umlauts-with-declaration.xlf', - 'Ergebnisse für "%s". Ä Ö Ü ä ö ü ß', - ], - 'french accents, no declaration' => ['french-accents.xlf', 'Résultats à côté, déjà vu'], - 'symbols and dashes, no declaration' => ['symbols.xlf', 'Preis: 5€ – naïve façade'], - ]; - } - - public function testNonAsciiSurvivesAlongsideRuleChange(): void - { - // One unit triggers the rule, the other carries non-ASCII text: the rule - // change must apply and the umlauts must stay literal. - $result = $this->fixAndRead('umlaut-and-rule-change.xlf'); - - self::assertStringContainsString('Hello World', $result); - self::assertStringContainsString('Ergebnisse für "%s".', $result); - self::assertStringNotContainsString('&#', $result); + return self::yieldFilesFromDirectory(__DIR__ . '/Fixtures', '*.xlf.fixture'); } - private function fixAndRead(string $fixture): string + public function provideConfigFilePath(): string { - $this->inputFile = sys_get_temp_dir() . '/fractor-xliff-' . uniqid() . '.xlf'; - FileSystem::write($this->inputFile, FileSystem::read(__DIR__ . '/Fixtures/' . $fixture), null); - - $configuration = new Configuration( - dryRun: false, - showProgressBar: false, - fileExtensions: ['xlf', 'xliff'], - paths: [$this->inputFile], - ); - - $this->getService(ConfigurationRuleFilter::class)->setConfiguration($configuration); - $this->getService(FractorRunner::class)->run($configuration); - - return FileSystem::read($this->inputFile); + return __DIR__ . '/config/fractor.php'; } }