From aec53bd1dd1d65622c28735a7ea0ee94f75386f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 7 Feb 2026 17:33:32 +0100 Subject: [PATCH] Do not output HTML chars in translations escaped in CDATA to ensure consistentcy with crowdin XMLs This should avoid some unnecessary diffs in the future --- src/Translation/NoCDATAXliffFileDumper.php | 88 ++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/Translation/NoCDATAXliffFileDumper.php diff --git a/src/Translation/NoCDATAXliffFileDumper.php b/src/Translation/NoCDATAXliffFileDumper.php new file mode 100644 index 00000000..a18e4e3b --- /dev/null +++ b/src/Translation/NoCDATAXliffFileDumper.php @@ -0,0 +1,88 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Translation; + +use DOMDocument; +use DOMXPath; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\Translation\Dumper\FileDumper; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * The goal of this class, is to ensure that the XLIFF dumper does not output CDATA, but instead outputs the text + * using the normal XML escaping. Crowdin outputs the translations without CDATA, we want to be consistent with that, to + * prevent unnecessary diffs in the translation files when we update them with translations from Crowdin. + */ +#[AsDecorator("translation.dumper.xliff")] +class NoCDATAXliffFileDumper extends FileDumper +{ + + public function __construct(private readonly FileDumper $decorated) + { + + } + + private function convertCDataToEscapedText(string $xmlContent): string + { + $dom = new DOMDocument(); + // Preserve whitespace to keep Symfony's formatting intact + $dom->preserveWhiteSpace = true; + $dom->formatOutput = true; + + // Load the XML (handle internal errors if necessary) + $dom->loadXML($xmlContent); + + $xpath = new DOMXPath($dom); + // Find all CDATA sections + $cdataNodes = $xpath->query('//node()/comment()|//node()/text()|//node()') ; + + // We specifically want CDATA sections. XPath 1.0 doesn't have a direct + // "cdata-section()" selector easily, so we iterate through all nodes + // and check their type. + + $nodesToRemove = []; + foreach ($xpath->query('//text() | //*') as $node) { + foreach ($node->childNodes as $child) { + if ($child->nodeType === XML_CDATA_SECTION_NODE) { + // Create a new text node with the content of the CDATA + // DOMDocument will automatically escape special chars on save + $newTextNode = $dom->createTextNode($child->textContent); + $node->replaceChild($newTextNode, $child); + } + } + } + + return $dom->saveXML(); + } + + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string + { + return $this->convertCDataToEscapedText($this->decorated->formatCatalogue($messages, $domain, $options)); + } + + protected function getExtension(): string + { + return $this->decorated->getExtension(); + } +}