XliffFileLoader.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Translation\Loader;
  11. use Symfony\Component\Translation\MessageCatalogue;
  12. use Symfony\Component\Translation\Exception\InvalidResourceException;
  13. use Symfony\Component\Translation\Exception\NotFoundResourceException;
  14. use Symfony\Component\Config\Resource\FileResource;
  15. /**
  16. * XliffFileLoader loads translations from XLIFF files.
  17. *
  18. * @author Fabien Potencier <fabien@symfony.com>
  19. *
  20. * @api
  21. */
  22. class XliffFileLoader implements LoaderInterface
  23. {
  24. /**
  25. * {@inheritdoc}
  26. *
  27. * @api
  28. */
  29. public function load($resource, $locale, $domain = 'messages')
  30. {
  31. if (!stream_is_local($resource)) {
  32. throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
  33. }
  34. if (!file_exists($resource)) {
  35. throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
  36. }
  37. list($xml, $encoding) = $this->parseFile($resource);
  38. $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:1.2');
  39. $catalogue = new MessageCatalogue($locale);
  40. foreach ($xml->xpath('//xliff:trans-unit') as $translation) {
  41. $attributes = $translation->attributes();
  42. if (!(isset($attributes['resname']) || isset($translation->source)) || !isset($translation->target)) {
  43. continue;
  44. }
  45. $source = isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->source;
  46. $target = (string) $translation->target;
  47. // If the xlf file has another encoding specified, try to convert it because
  48. // simple_xml will always return utf-8 encoded values
  49. if ('UTF-8' !== $encoding && !empty($encoding)) {
  50. if (function_exists('mb_convert_encoding')) {
  51. $target = mb_convert_encoding($target, $encoding, 'UTF-8');
  52. } elseif (function_exists('iconv')) {
  53. $target = iconv('UTF-8', $encoding, $target);
  54. } else {
  55. throw new \RuntimeException('No suitable convert encoding function (use UTF-8 as your encoding or install the iconv or mbstring extension).');
  56. }
  57. }
  58. $catalogue->set((string) $source, $target, $domain);
  59. }
  60. $catalogue->addResource(new FileResource($resource));
  61. return $catalogue;
  62. }
  63. /**
  64. * Validates and parses the given file into a SimpleXMLElement
  65. *
  66. * @param string $file
  67. *
  68. * @throws \RuntimeException
  69. *
  70. * @return \SimpleXMLElement
  71. *
  72. * @throws InvalidResourceException
  73. */
  74. private function parseFile($file)
  75. {
  76. $internalErrors = libxml_use_internal_errors(true);
  77. $disableEntities = libxml_disable_entity_loader(true);
  78. libxml_clear_errors();
  79. $dom = new \DOMDocument();
  80. $dom->validateOnParse = true;
  81. if (!@$dom->loadXML(file_get_contents($file), LIBXML_NONET | (defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0))) {
  82. libxml_disable_entity_loader($disableEntities);
  83. throw new InvalidResourceException(implode("\n", $this->getXmlErrors($internalErrors)));
  84. }
  85. libxml_disable_entity_loader($disableEntities);
  86. foreach ($dom->childNodes as $child) {
  87. if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
  88. libxml_use_internal_errors($internalErrors);
  89. throw new InvalidResourceException('Document types are not allowed.');
  90. }
  91. }
  92. $location = str_replace('\\', '/', __DIR__).'/schema/dic/xliff-core/xml.xsd';
  93. $parts = explode('/', $location);
  94. if (0 === stripos($location, 'phar://')) {
  95. $tmpfile = tempnam(sys_get_temp_dir(), 'sf2');
  96. if ($tmpfile) {
  97. copy($location, $tmpfile);
  98. $parts = explode('/', str_replace('\\', '/', $tmpfile));
  99. }
  100. }
  101. $drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
  102. $location = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
  103. $source = file_get_contents(__DIR__.'/schema/dic/xliff-core/xliff-core-1.2-strict.xsd');
  104. $source = str_replace('http://www.w3.org/2001/xml.xsd', $location, $source);
  105. if (!@$dom->schemaValidateSource($source)) {
  106. throw new InvalidResourceException(implode("\n", $this->getXmlErrors($internalErrors)));
  107. }
  108. $dom->normalizeDocument();
  109. libxml_use_internal_errors($internalErrors);
  110. return array(simplexml_import_dom($dom), strtoupper($dom->encoding));
  111. }
  112. /**
  113. * Returns the XML errors of the internal XML parser
  114. *
  115. * @param Boolean $internalErrors
  116. *
  117. * @return array An array of errors
  118. */
  119. private function getXmlErrors($internalErrors)
  120. {
  121. $errors = array();
  122. foreach (libxml_get_errors() as $error) {
  123. $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
  124. LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
  125. $error->code,
  126. trim($error->message),
  127. $error->file ? $error->file : 'n/a',
  128. $error->line,
  129. $error->column
  130. );
  131. }
  132. libxml_clear_errors();
  133. libxml_use_internal_errors($internalErrors);
  134. return $errors;
  135. }
  136. }