Skip to content

Commit e53bf58

Browse files
javiereguiluzfabpot
authored andcommitted
[Translation] Improved the performance of the lint:xliff command
1 parent 683bbf9 commit e53bf58

File tree

3 files changed

+173
-128
lines changed

3 files changed

+173
-128
lines changed

src/Symfony/Component/Translation/Command/XliffLintCommand.php

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\Console\Input\InputOption;
1818
use Symfony\Component\Console\Output\OutputInterface;
1919
use Symfony\Component\Console\Style\SymfonyStyle;
20+
use Symfony\Component\Translation\Util\XliffUtils;
2021

2122
/**
2223
* Validates XLIFF files syntax and outputs encountered errors.
@@ -127,18 +128,14 @@ private function validate($content, $file = null)
127128
}
128129
}
129130

130-
$document->schemaValidate(__DIR__.'/../Resources/schemas/xliff-core-1.2-strict.xsd');
131-
foreach (libxml_get_errors() as $xmlError) {
131+
foreach (XliffUtils::validateSchema($document) as $xmlError) {
132132
$errors[] = array(
133-
'line' => $xmlError->line,
134-
'column' => $xmlError->column,
135-
'message' => trim($xmlError->message),
133+
'line' => $xmlError['line'],
134+
'column' => $xmlError['column'],
135+
'message' => $xmlError['message'],
136136
);
137137
}
138138

139-
libxml_clear_errors();
140-
libxml_use_internal_errors(false);
141-
142139
return array('file' => $file, 'valid' => 0 === count($errors), 'messages' => $errors);
143140
}
144141

src/Symfony/Component/Translation/Loader/XliffFileLoader.php

Lines changed: 5 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
use Symfony\Component\Translation\MessageCatalogue;
1616
use Symfony\Component\Translation\Exception\InvalidResourceException;
1717
use Symfony\Component\Translation\Exception\NotFoundResourceException;
18-
use Symfony\Component\Translation\Exception\InvalidArgumentException;
1918
use Symfony\Component\Config\Resource\FileResource;
19+
use Symfony\Component\Translation\Util\XliffUtils;
2020

2121
/**
2222
* XliffFileLoader loads translations from XLIFF files.
@@ -56,8 +56,10 @@ private function extract($resource, MessageCatalogue $catalogue, $domain)
5656
throw new InvalidResourceException(sprintf('Unable to load "%s": %s', $resource, $e->getMessage()), $e->getCode(), $e);
5757
}
5858

59-
$xliffVersion = $this->getVersionNumber($dom);
60-
$this->validateSchema($xliffVersion, $dom, $this->getSchema($xliffVersion));
59+
$xliffVersion = XliffUtils::getVersionNumber($dom);
60+
if ($errors = XliffUtils::validateSchema($dom)) {
61+
throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: %s', $xliffVersion, XliffUtils::getErrorsAsString($errors)));
62+
}
6163

6264
if ('1.2' === $xliffVersion) {
6365
$this->extractXliff1($dom, $catalogue, $domain);
@@ -169,123 +171,6 @@ private function utf8ToCharset(string $content, string $encoding = null): string
169171
return $content;
170172
}
171173

172-
/**
173-
* Validates and parses the given file into a DOMDocument.
174-
*
175-
* @throws InvalidResourceException
176-
*/
177-
private function validateSchema(string $file, \DOMDocument $dom, string $schema)
178-
{
179-
$internalErrors = libxml_use_internal_errors(true);
180-
181-
$disableEntities = libxml_disable_entity_loader(false);
182-
183-
if (!@$dom->schemaValidateSource($schema)) {
184-
libxml_disable_entity_loader($disableEntities);
185-
186-
throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: %s', $file, implode("\n", $this->getXmlErrors($internalErrors))));
187-
}
188-
189-
libxml_disable_entity_loader($disableEntities);
190-
191-
$dom->normalizeDocument();
192-
193-
libxml_clear_errors();
194-
libxml_use_internal_errors($internalErrors);
195-
}
196-
197-
private function getSchema($xliffVersion)
198-
{
199-
if ('1.2' === $xliffVersion) {
200-
$schemaSource = file_get_contents(__DIR__.'/schema/dic/xliff-core/xliff-core-1.2-strict.xsd');
201-
$xmlUri = 'http://www.w3.org/2001/xml.xsd';
202-
} elseif ('2.0' === $xliffVersion) {
203-
$schemaSource = file_get_contents(__DIR__.'/schema/dic/xliff-core/xliff-core-2.0.xsd');
204-
$xmlUri = 'informativeCopiesOf3rdPartySchemas/w3c/xml.xsd';
205-
} else {
206-
throw new InvalidArgumentException(sprintf('No support implemented for loading XLIFF version "%s".', $xliffVersion));
207-
}
208-
209-
return $this->fixXmlLocation($schemaSource, $xmlUri);
210-
}
211-
212-
/**
213-
* Internally changes the URI of a dependent xsd to be loaded locally.
214-
*/
215-
private function fixXmlLocation(string $schemaSource, string $xmlUri): string
216-
{
217-
$newPath = str_replace('\\', '/', __DIR__).'/schema/dic/xliff-core/xml.xsd';
218-
$parts = explode('/', $newPath);
219-
$locationstart = 'file:///';
220-
if (0 === stripos($newPath, 'phar://')) {
221-
$tmpfile = tempnam(sys_get_temp_dir(), 'symfony');
222-
if ($tmpfile) {
223-
copy($newPath, $tmpfile);
224-
$parts = explode('/', str_replace('\\', '/', $tmpfile));
225-
} else {
226-
array_shift($parts);
227-
$locationstart = 'phar:///';
228-
}
229-
}
230-
231-
$drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
232-
$newPath = $locationstart.$drive.implode('/', array_map('rawurlencode', $parts));
233-
234-
return str_replace($xmlUri, $newPath, $schemaSource);
235-
}
236-
237-
/**
238-
* Returns the XML errors of the internal XML parser.
239-
*/
240-
private function getXmlErrors(bool $internalErrors): array
241-
{
242-
$errors = array();
243-
foreach (libxml_get_errors() as $error) {
244-
$errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
245-
LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
246-
$error->code,
247-
trim($error->message),
248-
$error->file ?: 'n/a',
249-
$error->line,
250-
$error->column
251-
);
252-
}
253-
254-
libxml_clear_errors();
255-
libxml_use_internal_errors($internalErrors);
256-
257-
return $errors;
258-
}
259-
260-
/**
261-
* Gets xliff file version based on the root "version" attribute.
262-
* Defaults to 1.2 for backwards compatibility.
263-
*
264-
* @throws InvalidArgumentException
265-
*/
266-
private function getVersionNumber(\DOMDocument $dom): string
267-
{
268-
/** @var \DOMNode $xliff */
269-
foreach ($dom->getElementsByTagName('xliff') as $xliff) {
270-
$version = $xliff->attributes->getNamedItem('version');
271-
if ($version) {
272-
return $version->nodeValue;
273-
}
274-
275-
$namespace = $xliff->attributes->getNamedItem('xmlns');
276-
if ($namespace) {
277-
if (0 !== substr_compare('urn:oasis:names:tc:xliff:document:', $namespace->nodeValue, 0, 34)) {
278-
throw new InvalidArgumentException(sprintf('Not a valid XLIFF namespace "%s"', $namespace));
279-
}
280-
281-
return substr($namespace, 34);
282-
}
283-
}
284-
285-
// Falls back to v1.2
286-
return '1.2';
287-
}
288-
289174
private function parseNotesMetadata(\SimpleXMLElement $noteElement = null, string $encoding = null): array
290175
{
291176
$notes = array();
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Translation\Util;
13+
14+
use Symfony\Component\Translation\Exception\InvalidArgumentException;
15+
use Symfony\Component\Translation\Exception\InvalidResourceException;
16+
17+
/**
18+
* Provides some utility methods for XLIFF translation files, such as validating
19+
* their contents according to the XSD schema.
20+
*
21+
* @author Fabien Potencier <fabien@symfony.com>
22+
*/
23+
class XliffUtils
24+
{
25+
/**
26+
* Gets xliff file version based on the root "version" attribute.
27+
*
28+
* Defaults to 1.2 for backwards compatibility.
29+
*
30+
* @throws InvalidArgumentException
31+
*/
32+
public static function getVersionNumber(\DOMDocument $dom): string
33+
{
34+
/** @var \DOMNode $xliff */
35+
foreach ($dom->getElementsByTagName('xliff') as $xliff) {
36+
$version = $xliff->attributes->getNamedItem('version');
37+
if ($version) {
38+
return $version->nodeValue;
39+
}
40+
41+
$namespace = $xliff->attributes->getNamedItem('xmlns');
42+
if ($namespace) {
43+
if (0 !== substr_compare('urn:oasis:names:tc:xliff:document:', $namespace->nodeValue, 0, 34)) {
44+
throw new InvalidArgumentException(sprintf('Not a valid XLIFF namespace "%s"', $namespace));
45+
}
46+
47+
return substr($namespace, 34);
48+
}
49+
}
50+
51+
// Falls back to v1.2
52+
return '1.2';
53+
}
54+
55+
/**
56+
* Validates and parses the given file into a DOMDocument.
57+
*
58+
* @throws InvalidResourceException
59+
*/
60+
public static function validateSchema(\DOMDocument $dom): array
61+
{
62+
$xliffVersion = static::getVersionNumber($dom);
63+
$internalErrors = libxml_use_internal_errors(true);
64+
$disableEntities = libxml_disable_entity_loader(false);
65+
66+
$isValid = @$dom->schemaValidateSource(self::getSchema($xliffVersion));
67+
if (!$isValid) {
68+
libxml_disable_entity_loader($disableEntities);
69+
70+
return self::getXmlErrors($internalErrors);
71+
}
72+
73+
libxml_disable_entity_loader($disableEntities);
74+
75+
$dom->normalizeDocument();
76+
77+
libxml_clear_errors();
78+
libxml_use_internal_errors($internalErrors);
79+
80+
return array();
81+
}
82+
83+
public static function getErrorsAsString(array $xmlErrors): string
84+
{
85+
$errorsAsString = '';
86+
87+
foreach ($xmlErrors as $error) {
88+
$errorsAsString .= sprintf("[%s %s] %s (in %s - line %d, column %d)\n",
89+
LIBXML_ERR_WARNING === $error['level'] ? 'WARNING' : 'ERROR',
90+
$error['code'],
91+
$error['message'],
92+
$error['file'],
93+
$error['line'],
94+
$error['column']
95+
);
96+
}
97+
98+
return $errorsAsString;
99+
}
100+
101+
private static function getSchema(string $xliffVersion): string
102+
{
103+
if ('1.2' === $xliffVersion) {
104+
$schemaSource = file_get_contents(__DIR__.'/../Loader/schema/dic/xliff-core/xliff-core-1.2-strict.xsd');
105+
$xmlUri = 'http://www.w3.org/2001/xml.xsd';
106+
} elseif ('2.0' === $xliffVersion) {
107+
$schemaSource = file_get_contents(__DIR__.'/../Loader/schema/dic/xliff-core/xliff-core-2.0.xsd');
108+
$xmlUri = 'informativeCopiesOf3rdPartySchemas/w3c/xml.xsd';
109+
} else {
110+
throw new InvalidArgumentException(sprintf('No support implemented for loading XLIFF version "%s".', $xliffVersion));
111+
}
112+
113+
return self::fixXmlLocation($schemaSource, $xmlUri);
114+
}
115+
116+
/**
117+
* Internally changes the URI of a dependent xsd to be loaded locally.
118+
*/
119+
private static function fixXmlLocation(string $schemaSource, string $xmlUri): string
120+
{
121+
$newPath = str_replace('\\', '/', __DIR__).'/../Loader/schema/dic/xliff-core/xml.xsd';
122+
$parts = explode('/', $newPath);
123+
$locationstart = 'file:///';
124+
if (0 === stripos($newPath, 'phar://')) {
125+
$tmpfile = tempnam(sys_get_temp_dir(), 'symfony');
126+
if ($tmpfile) {
127+
copy($newPath, $tmpfile);
128+
$parts = explode('/', str_replace('\\', '/', $tmpfile));
129+
} else {
130+
array_shift($parts);
131+
$locationstart = 'phar:///';
132+
}
133+
}
134+
135+
$drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
136+
$newPath = $locationstart.$drive.implode('/', array_map('rawurlencode', $parts));
137+
138+
return str_replace($xmlUri, $newPath, $schemaSource);
139+
}
140+
141+
/**
142+
* Returns the XML errors of the internal XML parser.
143+
*/
144+
private static function getXmlErrors(bool $internalErrors): array
145+
{
146+
$errors = array();
147+
foreach (libxml_get_errors() as $error) {
148+
$errors[] = array(
149+
'level' => LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
150+
'code' => $error->code,
151+
'message' => trim($error->message),
152+
'file' => $error->file ?: 'n/a',
153+
'line' => $error->line,
154+
'column' => $error->column,
155+
);
156+
}
157+
158+
libxml_clear_errors();
159+
libxml_use_internal_errors($internalErrors);
160+
161+
return $errors;
162+
}
163+
}

0 commit comments

Comments
 (0)