From c6249796826eec0618a39e8d9a09c0263b5ae598 Mon Sep 17 00:00:00 2001 From: Denny Lubitz Date: Fri, 26 Jan 2024 16:36:23 +0100 Subject: [PATCH] Xliff 1.2 reader and writer --- Classes/Domain/V12/Attributes.php | 22 +++++ Classes/Domain/V12/BodyItem.php | 8 ++ Classes/Domain/V12/Document.php | 22 +++++ Classes/Domain/V12/File.php | 30 +++++++ Classes/Domain/V12/Group.php | 29 +++++++ Classes/Domain/V12/Source.php | 23 ++++++ Classes/Domain/V12/Target.php | 23 ++++++ Classes/Domain/V12/TransUnit.php | 31 ++++++++ Classes/Reader/V12/XliffReader.php | 95 ++++++++++++++++++++++ Classes/Writer/V12/XliffWriter.php | 124 +++++++++++++++++++++++++++++ LICENSE | 21 +++++ Readme.md | 77 ++++++++++++++++++ composer.json | 16 ++++ 13 files changed, 521 insertions(+) create mode 100644 Classes/Domain/V12/Attributes.php create mode 100644 Classes/Domain/V12/BodyItem.php create mode 100644 Classes/Domain/V12/Document.php create mode 100644 Classes/Domain/V12/File.php create mode 100644 Classes/Domain/V12/Group.php create mode 100644 Classes/Domain/V12/Source.php create mode 100644 Classes/Domain/V12/Target.php create mode 100644 Classes/Domain/V12/TransUnit.php create mode 100644 Classes/Reader/V12/XliffReader.php create mode 100644 Classes/Writer/V12/XliffWriter.php create mode 100644 LICENSE create mode 100644 Readme.md create mode 100644 composer.json diff --git a/Classes/Domain/V12/Attributes.php b/Classes/Domain/V12/Attributes.php new file mode 100644 index 0000000..e65b932 --- /dev/null +++ b/Classes/Domain/V12/Attributes.php @@ -0,0 +1,22 @@ +attributes[$namespace] ?? []; + } + + public function get($key, $namespace = null): mixed + { + return $this->attributes[$namespace][$key] ?? null; + } +} \ No newline at end of file diff --git a/Classes/Domain/V12/BodyItem.php b/Classes/Domain/V12/BodyItem.php new file mode 100644 index 0000000..67d2093 --- /dev/null +++ b/Classes/Domain/V12/BodyItem.php @@ -0,0 +1,8 @@ + $files */ + protected array $files = [], + ) { + } + + public function addFile(File $file): void + { + $this->files[] = $file; + } + + public function getFiles(): array + { + return $this->files; + } +} diff --git a/Classes/Domain/V12/File.php b/Classes/Domain/V12/File.php new file mode 100644 index 0000000..2a8b973 --- /dev/null +++ b/Classes/Domain/V12/File.php @@ -0,0 +1,30 @@ + $items */ + protected array $items = [], + protected ?Attributes $attributes = new Attributes(), + ) { + + } + + public function addBodyItem(BodyItem $item): void + { + $this->items[] = $item; + } + + public function getBodyItems(): array + { + return $this->items; + } + + public function getAttributes(): Attributes + { + return $this->attributes; + } + +} diff --git a/Classes/Domain/V12/Group.php b/Classes/Domain/V12/Group.php new file mode 100644 index 0000000..2bdba22 --- /dev/null +++ b/Classes/Domain/V12/Group.php @@ -0,0 +1,29 @@ + $transUnits */ + protected array $transUnits = [], + protected ?Attributes $attributes = new Attributes(), + ) { + } + + public function addTransUnit(TransUnit $transUnit) + { + $this->transUnits[] = $transUnit; + } + + public function getTransUnits(): array + { + return $this->transUnits; + } + + public function getAttributes(): Attributes + { + return $this->attributes; + } + +} diff --git a/Classes/Domain/V12/Source.php b/Classes/Domain/V12/Source.php new file mode 100644 index 0000000..6662d94 --- /dev/null +++ b/Classes/Domain/V12/Source.php @@ -0,0 +1,23 @@ +content; + } + + public function getAttributes(): Attributes + { + return $this->attributes; + } +} diff --git a/Classes/Domain/V12/Target.php b/Classes/Domain/V12/Target.php new file mode 100644 index 0000000..602ecb6 --- /dev/null +++ b/Classes/Domain/V12/Target.php @@ -0,0 +1,23 @@ +content; + } + + public function getAttributes(): Attributes + { + return $this->attributes; + } +} diff --git a/Classes/Domain/V12/TransUnit.php b/Classes/Domain/V12/TransUnit.php new file mode 100644 index 0000000..72e6447 --- /dev/null +++ b/Classes/Domain/V12/TransUnit.php @@ -0,0 +1,31 @@ +source; + } + + public function getTarget(): ?Target + { + return $this->target; + } + + public function getAttributes(): Attributes + { + return $this->attributes; + } + + +} diff --git a/Classes/Reader/V12/XliffReader.php b/Classes/Reader/V12/XliffReader.php new file mode 100644 index 0000000..54008be --- /dev/null +++ b/Classes/Reader/V12/XliffReader.php @@ -0,0 +1,95 @@ +getName() === 'xliff' && $xml->version = '1.2') { + $document = $this->getDocumentForNode($xml); + } else { + throw new \RuntimeException("Not a valid Xliff file in Version 1.2"); + } + + return $document; + } + + private function xmlAttributesToAttributes(\SimpleXMLElement $bodyItemNode): Attributes + { + return new Attributes([ + null => ((array)$bodyItemNode->attributes())['@attributes'] ?? [], + 'xml' => ((array)$bodyItemNode->attributes('http://www.w3.org/XML/1998/namespace'))['@attributes'] ?? [], + ]); + } + + private function getDocumentForNode(\SimpleXMLElement $node): Document + { + $files = []; + foreach ($node->children() as $fileNode) { + if ($fileNode->getName() === 'file') { + if ($fileNode->body) { + $files[] = $this->getFileForNode($fileNode); + } else { + throw new \RuntimeException("Not a valid Xliff file in Version 1.2"); + } + } + } + + return new Document($files); + } + + private function getFileForNode(\SimpleXMLElement $node): File + { + $items = []; + foreach ($node->body->children() as $bodyItemNode) { + if ($bodyItemNode->getName() == 'trans-unit') { + $items[] = $this->getTransUnitForNode($bodyItemNode); + } elseif ($bodyItemNode->getName() == 'group') { + $items[] = $this->getGroupForNode($bodyItemNode); + } + } + return new File($items, $this->xmlAttributesToAttributes($node)); + } + + private function getGroupForNode(\SimpleXMLElement $node): Group + { + $groupItems = []; + foreach ($node->children() as $groupItemNode) { + $groupItems[] = $this->getTransUnitForNode($groupItemNode); + } + return new Group($groupItems, $this->xmlAttributesToAttributes($node)); + } + + private function getTransUnitForNode(\SimpleXMLElement $node): TransUnit + { + $source = new Source( + (string)$node->source, + $this->xmlAttributesToAttributes($node->source) + ); + + $target = null; + if ($node->target) { + $target = new Target( + (string)$node->target, + $this->xmlAttributesToAttributes($node->target) + ); + } + + return new TransUnit( + $source, + $target, + $this->xmlAttributesToAttributes($node) + ); + } +} diff --git a/Classes/Writer/V12/XliffWriter.php b/Classes/Writer/V12/XliffWriter.php new file mode 100644 index 0000000..de007ab --- /dev/null +++ b/Classes/Writer/V12/XliffWriter.php @@ -0,0 +1,124 @@ +generateXml($document); + + $dom = dom_import_simplexml($documentXml)->ownerDocument; + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + $dom->save($path); + } + + public function output(Document $document): string + { + $documentXml = $this->generateXml($document); + + $dom = dom_import_simplexml($documentXml)->ownerDocument; + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + return $dom->saveXML(); + } + + protected function generateXml(Document $document) + { + $documentXml = new \SimpleXMLElement('', LIBXML_COMPACT); + $documentXml->addAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:1.2'); + $documentXml->addAttribute('version', '1.2'); + + foreach ($document->getFiles() as $file) { + $fileXml = $this->addFileToXml($documentXml, $file); + $bodyXml = $fileXml->addChild('body'); + + foreach ($file->getBodyItems() as $bodyItem) { + if ($bodyItem instanceof TransUnit) { + $transUnitXml = $this->addTransUnitToXml($bodyXml, $bodyItem); + $this->addSourceToXml($transUnitXml, $bodyItem->getSource()); + $this->addTargetToXml($transUnitXml, $bodyItem->getTarget()); + } elseif ($bodyItem instanceof Group) { + $groupXml = $this->addGroupToXml($bodyXml, $bodyItem); + foreach ($bodyItem->getTransUnits() as $transUnit) { + $this->addTransUnitToXml($groupXml, $transUnit); + } + } + } + } + return $documentXml; + } + + private function addFileToXml(?\SimpleXMLElement $xml, File $file): \SimpleXMLElement + { + $fileXml = $xml->addChild('file'); + foreach ($file->getAttributes()->all() as $key => $value) { + $fileXml->addAttribute($key, $value); + } + foreach ($file->getAttributes()->all('xml') as $key => $value) { + $fileXml->addAttribute("xml:" . $key, $value, "xml"); + } + + return $fileXml; + } + + private function addTransUnitToXml(?\SimpleXMLElement $xml, TransUnit $transUnit): \SimpleXMLElement + { + $transUnitXml = $xml->addChild('trans-unit'); + foreach ($transUnit->getAttributes()->all() as $key => $value) { + $transUnitXml->addAttribute($key, $value); + } + foreach ($transUnit->getAttributes()->all('xml') as $key => $value) { + $transUnitXml->addAttribute("xml:" . $key, $value, "xml"); + } + + return $transUnitXml; + } + + private function addGroupToXml(?\SimpleXMLElement $xml, Group $group): \SimpleXMLElement + { + $groupXml = $xml->addChild('group'); + foreach ($group->getAttributes()->all() as $key => $value) { + $groupXml->addAttribute($key, $value); + } + foreach ($group->getAttributes()->all('xml') as $key => $value) { + $groupXml->addAttribute("xml:" . $key, $value, "xml"); + } + + return $groupXml; + } + + private function addSourceToXml(?\SimpleXMLElement $xml, Source $source): \SimpleXMLElement + { + $sourceXml = $xml->addChild('source', htmlspecialchars($source->getContent())); + foreach ($source->getAttributes()->all() as $key => $value) { + $sourceXml->addAttribute($key, $value); + } + foreach ($source->getAttributes()->all('xml') as $key => $value) { + $sourceXml->addAttribute("xml:" . $key, $value, "xml"); + } + + return $sourceXml; + } + + private function addTargetToXml(?\SimpleXMLElement $xml, Target $target): \SimpleXMLElement + { + $targetXml = $xml->addChild('target', htmlspecialchars($target->getContent())); + foreach ($target->getAttributes()->all() as $key => $value) { + $targetXml->addAttribute($key, $value); + } + foreach ($target->getAttributes()->all('xml') as $key => $value) { + $targetXml->addAttribute("xml:" . $key, $value, "xml"); + } + + return $targetXml; + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..97d51c2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 VIVOMEDIA + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..783a879 --- /dev/null +++ b/Readme.md @@ -0,0 +1,77 @@ +# VIVOMEDIA XliffParser +Simple Xliff Reader and Writer. Currently only Xliff Version 1.2. + +## Install +``` +composer require vivomedia/xliff-parser +``` + + +## How to use + +### Reader +```php +use VIVOMEDIA\XliffParser\Domain\V12\TransUnit; +use VIVOMEDIA\XliffParser\Reader\V12\XliffReader; + +$reader = new XliffReader(); +$document = $reader->read('/path/to/file.xlf'); + + +foreach ($read->getFiles() as $file) { + foreach ($file->getBodyItems() as $bodyItem) { + if ($bodyItem instanceof TransUnit) { + printf( + "'%s' | '%s' => '%s'\n", + $bodyItem->getAttributes()->get('id'), + $bodyItem->getSource()->getContent(), + $bodyItem->getTarget()?->getContent(), + ); + } + } +} + +``` +### Writer +```php +use VIVOMEDIA\XliffParser\Domain\V12\Attributes; +use VIVOMEDIA\XliffParser\Domain\V12\Document; +use VIVOMEDIA\XliffParser\Domain\V12\File; +use VIVOMEDIA\XliffParser\Domain\V12\Source;use VIVOMEDIA\XliffParser\Domain\V12\Target;use VIVOMEDIA\XliffParser\Domain\V12\TransUnit; +use VIVOMEDIA\XliffParser\Writer\V12\XliffWriter; + +$b = new XliffWriter(); +$b->write($read, $pathES); + +$sourceAttributes = new Attributes(["xml" => ['lang' => "de"]]); +$source = new Source("Bitte übersetzen", $sourceAttributes); + +$targetAttributes = new Attributes([null => ['state' => 'translated'], "xml" => ['lang' => "en"]]); +$target = new Target("Please translate", $targetAttributes); + +$transUnitAttributes = new Attributes([null => ['id' => "my.identifier", "approved" => "yes"]]); +$transUnitItem = new TransUnit($source, $target, $transUnitAttributes); + +$fileAttributes = new Attributes([null => ['product-name' => "MyProduct", "source-language" => "de", "target-language" => "en"]]); +$file = new File([$transUnitItem], $fileAttributes); + +$document = new Document([$file]); + +$writer = new XliffWriter(); +$writer->write($document, '/path/to/other.xlf'); +``` +Result +```xml + + + + + + Bitte übersetzen + Please translate + + + + + +``` \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..61ff0f7 --- /dev/null +++ b/composer.json @@ -0,0 +1,16 @@ +{ + "description": "Simple Xliff-File reader and writer", + "type": "library", + "name": "vivomedia/xliff", + "license": "MIT", + "require": { + "php": "^8.0", + "ext-xmlreader": "*", + "ext-dom": "*" + }, + "autoload": { + "psr-4": { + "VIVOMEDIA\\XliffParser\\": "Classes/" + } + } +} \ No newline at end of file