diff --git a/src/Symfony/Component/Translation/Loader/XliffFileLoader.php b/src/Symfony/Component/Translation/Loader/XliffFileLoader.php index aa60c438d779..36019a6326fb 100644 --- a/src/Symfony/Component/Translation/Loader/XliffFileLoader.php +++ b/src/Symfony/Component/Translation/Loader/XliffFileLoader.php @@ -41,110 +41,36 @@ public function load($resource, $locale, $domain = 'messages') 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); - foreach ($xml->xpath('//xliff:trans-unit') as $translation) { - $attributes = $translation->attributes(); - - if (!(isset($attributes['resname']) || isset($translation->source)) || !isset($translation->target)) { - continue; - } - - $source = isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->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) $translation->target, $encoding); - - $catalogue->set((string) $source, $target, $domain); + try { + $dom = XmlUtils::loadFile($resource); + $version = $this->getVersion($dom); + } catch (\InvalidArgumentException $e) { + $message = sprintf('Unable to load "%s": %s', $resource, $e->getMessage()); - if (isset($translation->note)) { - $notes = array(); - foreach ($translation->note as $xmlNote) { - $noteAttributes = $xmlNote->attributes(); - $note = array('content' => $this->utf8ToCharset((string) $xmlNote, $encoding)); - if (isset($noteAttributes['priority'])) { - $note['priority'] = (int) $noteAttributes['priority']; - } + throw new InvalidResourceException($message, $e->getCode(), $e); + } - if (isset($noteAttributes['from'])) { - $note['from'] = (string) $noteAttributes['from']; - } + $this->validateSchema($dom, $version->getSchema()); - $notes[] = $note; - } + $catalogue = new MessageCatalogue($locale); + $version->extract($dom, $catalogue, $domain); - $catalogue->setMetadata((string) $source, array('notes' => $notes), $domain); - } - } $catalogue->addResource(new FileResource($resource)); return $catalogue; } /** - * Convert a UTF8 string to the specified encoding. - * - * @param string $content String to decode - * @param string $encoding Target encoding - * - * @return string - */ - private function utf8ToCharset($content, $encoding = null) - { - if ('UTF-8' !== $encoding && !empty($encoding)) { - if (function_exists('mb_convert_encoding')) { - return mb_convert_encoding($content, $encoding, 'UTF-8'); - } - - if (function_exists('iconv')) { - return iconv('UTF-8', $encoding, $content); - } - - throw new \RuntimeException('No suitable convert encoding function (use UTF-8 as your encoding or install the iconv or mbstring extension).'); - } - - return $content; - } - - /** - * 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(\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(implode("\n", $this->getXmlErrors($internalErrors))); } @@ -152,8 +78,6 @@ private function parseFile($file) libxml_clear_errors(); libxml_use_internal_errors($internalErrors); - - return array(simplexml_import_dom($dom), strtoupper($dom->encoding)); } /** @@ -182,4 +106,67 @@ private function getXmlErrors($internalErrors) return $errors; } + + /** + * Detects xliff version from file. + * + * @param \DOMDocument $dom + * + * @throws \InvalidArgumentException + * + * @return XliffVersion\AbstractXliffVersion + */ + private function getVersion(\DOMDocument $dom) + { + $versionNumber = $this->getVersionNumber($dom); + + switch ($versionNumber) { + case '1.2': + return new XliffVersion\XliffVersion12(); + + case '2.0': + return new XliffVersion\XliffVersion20(); + } + + throw new \InvalidArgumentException(sprintf( + 'No support implemented for loading XLIFF version "%s".', + $versionNumber + )); + } + + /** + * 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'; + } } diff --git a/src/Symfony/Component/Translation/Loader/XliffVersion/AbstractXliffVersion.php b/src/Symfony/Component/Translation/Loader/XliffVersion/AbstractXliffVersion.php new file mode 100644 index 000000000000..8ec33ec3fd3d --- /dev/null +++ b/src/Symfony/Component/Translation/Loader/XliffVersion/AbstractXliffVersion.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader\XliffVersion; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * Base Xliff version loader class. + * + * @author Berny Cantos + */ +abstract class AbstractXliffVersion +{ + /** + * Get validation schema source for this version + * + * @return string + */ + abstract public function getSchema(); + + /** + * 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 + */ + abstract public function extract(\DOMDocument $dom, MessageCatalogue $catalogue, $domain); + + /** + * 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 + */ + protected 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); + } + + /** + * Convert a UTF8 string to the specified encoding + * + * @param string $content String to decode + * @param string $encoding Target encoding + * + * @throws \RuntimeException + * @return string + */ + protected function utf8ToCharset($content, $encoding = null) + { + if (empty($encoding) || 'UTF-8' === $encoding) { + return $content; + } + + if (function_exists('mb_convert_encoding')) { + return mb_convert_encoding($content, $encoding, 'UTF-8'); + } + + if (function_exists('iconv')) { + return iconv('UTF-8', $encoding, $content); + } + + throw new \RuntimeException( + 'No suitable convert encoding function (use UTF-8 as your encoding or install the iconv or mbstring extension).' + ); + } +} diff --git a/src/Symfony/Component/Translation/Loader/XliffVersion/XliffVersion12.php b/src/Symfony/Component/Translation/Loader/XliffVersion/XliffVersion12.php new file mode 100644 index 000000000000..78e54469a54e --- /dev/null +++ b/src/Symfony/Component/Translation/Loader/XliffVersion/XliffVersion12.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader\XliffVersion; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * XliffVersion12 loads XLIFF files identified with version 1.2 + * + * @author Berny Cantos + */ +class XliffVersion12 extends AbstractXliffVersion +{ + /** + * Get validation schema source for this version + * + * @return string + */ + public function getSchema() + { + $source = file_get_contents(__DIR__.'/../schema/dic/xliff-core/xliff-core-1.2-strict.xsd'); + + return $this->fixXmlLocation($source, 'http://www.w3.org/2001/xml.xsd'); + } + + /** + * 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 + */ + public function extract(\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(); + + if (!(isset($attributes['resname']) || isset($translation->source)) || !isset($translation->target)) { + continue; + } + + $source = isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->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) $translation->target, $encoding); + + $catalogue->set((string) $source, $target, $domain); + + if (isset($translation->note)) { + $notes = array(); + foreach ($translation->note as $xmlNote) { + $noteAttributes = $xmlNote->attributes(); + $note = array('content' => $this->utf8ToCharset((string) $xmlNote, $encoding)); + if (isset($noteAttributes['priority'])) { + $note['priority'] = (int) $noteAttributes['priority']; + } + + if (isset($noteAttributes['from'])) { + $note['from'] = (string) $noteAttributes['from']; + } + + $notes[] = $note; + } + + $catalogue->setMetadata((string) $source, array('notes' => $notes), $domain); + } + } + } +} diff --git a/src/Symfony/Component/Translation/Loader/XliffVersion/XliffVersion20.php b/src/Symfony/Component/Translation/Loader/XliffVersion/XliffVersion20.php new file mode 100644 index 000000000000..a3d942925770 --- /dev/null +++ b/src/Symfony/Component/Translation/Loader/XliffVersion/XliffVersion20.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader\XliffVersion; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * XliffVersion20 loads XLIFF files identified with version 2.0 + * + * @author Berny Cantos + */ +class XliffVersion20 extends AbstractXliffVersion +{ + /** + * @return string + */ + public function getSchema() + { + $source = file_get_contents(__DIR__.'/../schema/dic/xliff-core/xliff-core-2.0.xsd'); + + return $this->fixXmlLocation($source, 'informativeCopiesOf3rdPartySchemas/w3c/xml.xsd'); + } + + /** + * @param \DOMDocument $dom + * @param MessageCatalogue $catalogue + * @param string $domain + */ + public function extract(\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) $segment->target, $encoding); + + $catalogue->set((string) $source, $target, $domain); + } + } +} 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 000000000000..f429bb0f376d --- /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 c3d65b493202..a8a5a228f947 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 { @@ -139,4 +139,21 @@ public function testLoadNotes() $this->assertNull($catalogue->getMetadata('extra', 'domain1')); $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 000000000000..081ea69a9388 --- /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 データ・マネージャ + + + + +