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.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.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.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.fixture b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-no-declaration.xlf.fixture
new file mode 100644
index 00000000..a9c89cb4
--- /dev/null
+++ b/packages/fractor-xliff/tests/XliffEncoding/Fixtures/umlauts-no-declaration.xlf.fixture
@@ -0,0 +1,21 @@
+
+
+
+
+ Ergebnisse für "%s". Ä Ö Ü ä ö ü ß
+
+
+
+
+-----
+
+
+
+
+
+ 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
new file mode 100644
index 00000000..a9addb76
--- /dev/null
+++ b/packages/fractor-xliff/tests/XliffEncoding/XliffEncodingTest.php
@@ -0,0 +1,30 @@
+doTestFile($filePath);
+ }
+
+ public static function provideData(): \Iterator
+ {
+ return self::yieldFilesFromDirectory(__DIR__ . '/Fixtures', '*.xlf.fixture');
+ }
+
+ public function provideConfigFilePath(): string
+ {
+ return __DIR__ . '/config/fractor.php';
+ }
+}
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]);