From ff5d6a3a9894d07afeea2e31846e18413e602157 Mon Sep 17 00:00:00 2001 From: Berny Cantos Date: Mon, 7 Sep 2015 17:28:28 +0000 Subject: [PATCH] [Translation][Loader] added XLIFF 2.0 support. --- .../Translation/Loader/XliffFileLoader.php | 180 ++++++-- .../schema/dic/xliff-core/xliff-core-2.0.xsd | 411 ++++++++++++++++++ .../Tests/Loader/XliffFileLoaderTest.php | 19 +- .../Tests/fixtures/resources-2.0.xlf | 25 ++ 4 files changed, 596 insertions(+), 39 deletions(-) create mode 100644 src/Symfony/Component/Translation/Loader/schema/dic/xliff-core/xliff-core-2.0.xsd create mode 100644 src/Symfony/Component/Translation/Tests/fixtures/resources-2.0.xlf diff --git a/src/Symfony/Component/Translation/Loader/XliffFileLoader.php b/src/Symfony/Component/Translation/Loader/XliffFileLoader.php index 67c96eb168..1e7617a3c7 100644 --- a/src/Symfony/Component/Translation/Loader/XliffFileLoader.php +++ b/src/Symfony/Component/Translation/Loader/XliffFileLoader.php @@ -41,10 +41,49 @@ class XliffFileLoader implements LoaderInterface throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); } - list($xml, $encoding) = $this->parseFile($resource); - $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:1.2'); - $catalogue = new MessageCatalogue($locale); + $this->extract($resource, $catalogue, $domain); + + if (class_exists('Symfony\Component\Config\Resource\FileResource')) { + $catalogue->addResource(new FileResource($resource)); + } + + return $catalogue; + } + + private function extract($resource, MessageCatalogue $catalogue, $domain) + { + try { + $dom = XmlUtils::loadFile($resource); + } catch (\InvalidArgumentException $e) { + throw new InvalidResourceException(sprintf('Unable to load "%s": %s', $resource, $e->getMessage()), $e->getCode(), $e); + } + + $xliffVersion = $this->getVersionNumber($dom); + $this->validateSchema($xliffVersion, $dom, $this->getSchema($xliffVersion, $dom)); + + if ('1.2' === $xliffVersion) { + $this->extractXliff1($dom, $catalogue, $domain); + } + + if ('2.0' === $xliffVersion) { + $this->extractXliff2($dom, $catalogue, $domain); + } + } + + /** + * Extract messages and metadata from DOMDocument into a MessageCatalogue. + * + * @param \DOMDocument $dom Source to extract messages and metadata + * @param MessageCatalogue $catalogue Catalogue where we'll collect messages and metadata + * @param string $domain The domain + */ + private function extractXliff1(\DOMDocument $dom, MessageCatalogue $catalogue, $domain) + { + $xml = simplexml_import_dom($dom); + $encoding = strtoupper($dom->encoding); + + $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:1.2'); foreach ($xml->xpath('//xliff:trans-unit') as $translation) { $attributes = $translation->attributes(); @@ -69,12 +108,29 @@ class XliffFileLoader implements LoaderInterface $catalogue->setMetadata((string) $source, $metadata, $domain); } + } - if (class_exists('Symfony\Component\Config\Resource\FileResource')) { - $catalogue->addResource(new FileResource($resource)); + /** + * @param \DOMDocument $dom + * @param MessageCatalogue $catalogue + * @param string $domain + */ + private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, $domain) + { + $xml = simplexml_import_dom($dom); + $encoding = strtoupper($dom->encoding); + + $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:2.0'); + + foreach ($xml->xpath('//xliff:unit/xliff:segment') as $segment) { + $source = $segment->source; + + // If the xlf file has another encoding specified, try to convert it because + // simple_xml will always return utf-8 encoded values + $target = $this->utf8ToCharset((string) (isset($translation->target) ? $translation->target : $source), $encoding); + + $catalogue->set((string) $source, $target, $domain); } - - return $catalogue; } /** @@ -103,42 +159,16 @@ class XliffFileLoader implements LoaderInterface } /** - * Validates and parses the given file into a SimpleXMLElement. - * - * @param string $file - * - * @throws \RuntimeException - * - * @return \SimpleXMLElement + * @param \DOMDocument $dom + * @param string $schema source of the schema * * @throws InvalidResourceException */ - private function parseFile($file) + private function validateSchema($file, \DOMDocument $dom, $schema) { - try { - $dom = XmlUtils::loadFile($file); - } catch (\InvalidArgumentException $e) { - throw new InvalidResourceException(sprintf('Unable to load "%s": %s', $file, $e->getMessage()), $e->getCode(), $e); - } - $internalErrors = libxml_use_internal_errors(true); - $location = str_replace('\\', '/', __DIR__).'/schema/dic/xliff-core/xml.xsd'; - $parts = explode('/', $location); - if (0 === stripos($location, 'phar://')) { - $tmpfile = tempnam(sys_get_temp_dir(), 'sf2'); - if ($tmpfile) { - copy($location, $tmpfile); - $parts = explode('/', str_replace('\\', '/', $tmpfile)); - } - } - $drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : ''; - $location = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts)); - - $source = file_get_contents(__DIR__.'/schema/dic/xliff-core/xliff-core-1.2-strict.xsd'); - $source = str_replace('http://www.w3.org/2001/xml.xsd', $location, $source); - - if (!@$dom->schemaValidateSource($source)) { + if (!@$dom->schemaValidateSource($schema)) { throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: %s', $file, implode("\n", $this->getXmlErrors($internalErrors)))); } @@ -146,8 +176,49 @@ class XliffFileLoader implements LoaderInterface libxml_clear_errors(); libxml_use_internal_errors($internalErrors); + } - return array(simplexml_import_dom($dom), strtoupper($dom->encoding)); + /** + * @return string + */ + private function getSchema($xliffVersion, $dom) + { + if ('1.2' === $xliffVersion) { + $schemaSource = file_get_contents(__DIR__.'/schema/dic/xliff-core/xliff-core-1.2-strict.xsd'); + $xmlUri = 'http://www.w3.org/2001/xml.xsd'; + } elseif ('2.0' === $xliffVersion) { + $schemaSource = file_get_contents(__DIR__.'/schema/dic/xliff-core/xliff-core-2.0.xsd'); + $xmlUri = 'informativeCopiesOf3rdPartySchemas/w3c/xml.xsd'; + } else { + throw new \InvalidArgumentException(sprintf('No support implemented for loading XLIFF version "%s".', $xliffVersion)); + } + + return $this->fixXmlLocation($schemaSource, $xmlUri); + } + + /** + * Internally changes the URI of a dependent xsd to be loaded locally. + * + * @param string $schemaSource Current content of schema file + * @param string $xmlUri External URI of XML to convert to local + * + * @return string + */ + private function fixXmlLocation($schemaSource, $xmlUri) + { + $newPath = str_replace('\\', '/', __DIR__).'/schema/dic/xliff-core/xml.xsd'; + $parts = explode('/', $newPath); + if (0 === stripos($newPath, 'phar://')) { + $tmpfile = tempnam(sys_get_temp_dir(), 'sf2'); + if ($tmpfile) { + copy($newPath, $tmpfile); + $parts = explode('/', str_replace('\\', '/', $tmpfile)); + } + } + $drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : ''; + $newPath = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts)); + + return str_replace($xmlUri, $newPath, $schemaSource); } /** @@ -178,6 +249,39 @@ class XliffFileLoader implements LoaderInterface } /** + * Gets xliff file version based on the root "version" attribute. + * Defaults to 1.2 for backwards compatibility. + * + * @param \DOMDocument $dom + * + * @throws \InvalidArgumentException + * + * @return string + */ + private function getVersionNumber(\DOMDocument $dom) + { + /** @var \DOMNode $xliff */ + foreach ($dom->getElementsByTagName('xliff') as $xliff) { + $version = $xliff->attributes->getNamedItem('version'); + if ($version) { + return $version->nodeValue; + } + + $namespace = $xliff->attributes->getNamedItem('xmlns'); + if ($namespace) { + if (substr_compare('urn:oasis:names:tc:xliff:document:', $namespace->nodeValue, 0, 34) !== 0) { + throw new \InvalidArgumentException(sprintf('Not a valid XLIFF namespace "%s"', $namespace)); + } + + return substr($namespace, 34); + } + } + + // Falls back to v1.2 + return '1.2'; + } + + /* * @param \SimpleXMLElement|null $noteElement * @param string|null $encoding * diff --git a/src/Symfony/Component/Translation/Loader/schema/dic/xliff-core/xliff-core-2.0.xsd b/src/Symfony/Component/Translation/Loader/schema/dic/xliff-core/xliff-core-2.0.xsd new file mode 100644 index 0000000000..f429bb0f37 --- /dev/null +++ b/src/Symfony/Component/Translation/Loader/schema/dic/xliff-core/xliff-core-2.0.xsd @@ -0,0 +1,411 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php index a67af1a340..c7632d597f 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Translation\Tests\Loader; -use Symfony\Component\Translation\Loader\XliffFileLoader; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Translation\Loader\XliffFileLoader; class XliffFileLoaderTest extends \PHPUnit_Framework_TestCase { @@ -149,4 +149,21 @@ class XliffFileLoaderTest extends \PHPUnit_Framework_TestCase // message with empty target $this->assertEquals(array('notes' => array(array('content' => 'baz'), array('priority' => 2, 'from' => 'bar', 'content' => 'qux'))), $catalogue->getMetadata('key', 'domain1')); } + + public function testLoadVersion2() + { + $loader = new XliffFileLoader(); + $resource = __DIR__.'/../fixtures/resources-2.0.xlf'; + $catalogue = $loader->load($resource, 'en', 'domain1'); + + $this->assertEquals('en', $catalogue->getLocale()); + $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources()); + $this->assertSame(array(), libxml_get_errors()); + + $domains = $catalogue->all(); + $this->assertCount(3, $domains['domain1']); + + // Notes aren't assigned to specific segments, but to whole units, so there's no way to do a mapping + $this->assertEmpty($catalogue->getMetadata()); + } } diff --git a/src/Symfony/Component/Translation/Tests/fixtures/resources-2.0.xlf b/src/Symfony/Component/Translation/Tests/fixtures/resources-2.0.xlf new file mode 100644 index 0000000000..081ea69a93 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/fixtures/resources-2.0.xlf @@ -0,0 +1,25 @@ + + + + + + Quetzal + Quetzal + + + + + + An application to manipulate and process XLIFF documents + XLIFF 文書を編集、または処理 するアプリケーションです。 + + + + + XLIFF Data Manager + XLIFF データ・マネージャ + + + + +