Skip to content

Commit 1a3d0b2

Browse files
authored
Added XliffCoventer (#5)
* Added XliffCoventer * Stylefix * Enable Transferable * BC fix * BC fix * typo * Fixes * cs * Verify methods exists * Made classes final and added composer alias * Do not support sf 2.7
1 parent e4a30a1 commit 1a3d0b2

File tree

8 files changed

+349
-7
lines changed

8 files changed

+349
-7
lines changed

composer.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"require": {
1212
"php": "^5.5 || ^7.0",
1313
"php-translation/common": "^0.2.1",
14-
"symfony/translation": "^2.7 || ^3.0"
14+
"symfony/translation": "^2.7 || ^3.0",
15+
"nyholm/nsa": "^1.0.1"
1516
},
1617
"require-dev": {
1718
"phpunit/phpunit": "^4.5 || ^5.4",
@@ -30,5 +31,11 @@
3031
"scripts": {
3132
"test": "vendor/bin/phpunit",
3233
"test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml"
34+
},
35+
"minimum-stability": "dev",
36+
"extra": {
37+
"branch-alias": {
38+
"dev-master": "0.2-dev"
39+
}
3340
}
3441
}

src/Dumper/XliffDumper.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the PHP Translation package.
5+
*
6+
* (c) PHP Translation team <[email protected]>
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 Translation\SymfonyStorage\Dumper;
13+
14+
use Symfony\Component\Translation\Dumper\XliffFileDumper;
15+
use Symfony\Component\Translation\MessageCatalogue;
16+
17+
/**
18+
* @author Tobias Nyholm <[email protected]>
19+
*/
20+
final class XliffDumper extends XliffFileDumper
21+
{
22+
/**
23+
* Alias for formatCatalogue to provide a BC bridge.
24+
*
25+
* @param MessageCatalogue $messages
26+
* @param string $domain
27+
* @param array $options
28+
*
29+
* @return string
30+
*/
31+
public function getFormattedCatalogue(MessageCatalogue $messages, $domain, array $options = [])
32+
{
33+
if (method_exists($this, 'formatCatalogue')) {
34+
return parent::formatCatalogue($messages, $domain, $options);
35+
}
36+
37+
return $this->format($messages, $domain);
38+
}
39+
}

src/FileStorage.php

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,18 @@
1313

1414
use Symfony\Bundle\FrameworkBundle\Translation\TranslationLoader as SymfonyTranslationLoader;
1515
use Symfony\Component\Translation\MessageCatalogue;
16+
use Symfony\Component\Translation\MessageCatalogueInterface;
1617
use Symfony\Component\Translation\Writer\TranslationWriter;
1718
use Translation\Common\Model\Message;
1819
use Translation\Common\Storage;
20+
use Translation\Common\TransferableStorage;
1921

2022
/**
2123
* This storage uses Symfony's writer and loader.
2224
*
2325
* @author Tobias Nyholm <[email protected]>
2426
*/
25-
final class FileStorage implements Storage
27+
final class FileStorage implements Storage, TransferableStorage
2628
{
2729
/**
2830
* @var TranslationWriter
@@ -45,9 +47,9 @@ final class FileStorage implements Storage
4547
private $catalogues;
4648

4749
/**
48-
* @param TranslationWriter $writer
49-
* @param mixed $loader
50-
* @param array $dir
50+
* @param TranslationWriter $writer
51+
* @param SymfonyTranslationLoader|TranslationLoader $loader
52+
* @param array $dir
5153
*/
5254
public function __construct(TranslationWriter $writer, $loader, array $dir)
5355
{
@@ -70,7 +72,6 @@ public function __construct(TranslationWriter $writer, $loader, array $dir)
7072
public function get($locale, $domain, $key)
7173
{
7274
$catalogue = $this->getCatalogue($locale);
73-
7475
$translation = $catalogue->get($key, $domain);
7576

7677
return new Message($key, $domain, $locale, $translation);
@@ -111,6 +112,26 @@ public function delete($locale, $domain, $key)
111112
$this->writeCatalogue($catalogue, $locale, $domain);
112113
}
113114

115+
/**
116+
* {@inheritdoc}
117+
*/
118+
public function export(MessageCatalogueInterface $catalogue)
119+
{
120+
$locale = $catalogue->getLocale();
121+
$catalogue->addCatalogue($this->getCatalogue($locale));
122+
}
123+
124+
/**
125+
* {@inheritdoc}
126+
*/
127+
public function import(MessageCatalogueInterface $catalogue)
128+
{
129+
$domains = $catalogue->getDomains();
130+
foreach ($domains as $domain) {
131+
$this->writeCatalogue($catalogue, $catalogue->getLocale(), $domain);
132+
}
133+
}
134+
114135
/**
115136
* Save catalogue back to file.
116137
*

src/Loader/XliffLoader.php

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the PHP Translation package.
5+
*
6+
* (c) PHP Translation team <[email protected]>
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 Translation\SymfonyStorage\Loader;
13+
14+
use Nyholm\NSA;
15+
use Symfony\Component\Translation\Loader\XliffFileLoader;
16+
use Symfony\Component\Translation\MessageCatalogue;
17+
use Symfony\Component\Translation\Exception\InvalidResourceException;
18+
19+
/**
20+
* This class is an ugly hack to allow loading Xliff from string content.
21+
*
22+
* @author Tobias Nyholm <[email protected]>
23+
*/
24+
final class XliffLoader extends XliffFileLoader
25+
{
26+
/**
27+
* @param string $content xml content
28+
* @param MessageCatalogue $catalogue
29+
* @param string $domain
30+
*/
31+
public function extractFromContent($content, MessageCatalogue $catalogue, $domain)
32+
{
33+
try {
34+
$dom = $this->loadFileContent($content);
35+
} catch (\InvalidArgumentException $e) {
36+
throw new InvalidResourceException(sprintf('Unable to load data: %s', $e->getMessage()), $e->getCode(), $e);
37+
}
38+
39+
if (!method_exists($this, 'getVersionNumber')) {
40+
// Symfony 2.7
41+
throw new \RuntimeException('Cannot use XliffLoader::extractFromContent with Symfony 2.7');
42+
}
43+
44+
$xliffVersion = NSA::invokeMethod($this, 'getVersionNumber', $dom);
45+
NSA::invokeMethod($this, 'validateSchema', $xliffVersion, $dom, NSA::invokeMethod($this, 'getSchema', $xliffVersion));
46+
47+
if ('1.2' === $xliffVersion) {
48+
NSA::invokeMethod($this, 'extractXliff1', $dom, $catalogue, $domain);
49+
}
50+
51+
if ('2.0' === $xliffVersion) {
52+
NSA::invokeMethod($this, 'extractXliff2', $dom, $catalogue, $domain);
53+
}
54+
}
55+
56+
/**
57+
* Loads an XML file.
58+
*
59+
* Taken and modified from Symfony\Component\Config\Util\XmlUtils
60+
*
61+
* @author Fabien Potencier <[email protected]>
62+
* @author Martin Hasoň <[email protected]>
63+
*
64+
* @param string $content An XML file path
65+
* @param string|callable|null $schemaOrCallable An XSD schema file path, a callable, or null to disable validation
66+
*
67+
* @return \DOMDocument
68+
*
69+
* @throws \InvalidArgumentException When loading of XML file returns error
70+
*/
71+
private function loadFileContent($content, $schemaOrCallable = null)
72+
{
73+
if ('' === trim($content)) {
74+
throw new \InvalidArgumentException('Content does not contain valid XML, it is empty.');
75+
}
76+
77+
$internalErrors = libxml_use_internal_errors(true);
78+
$disableEntities = libxml_disable_entity_loader(true);
79+
libxml_clear_errors();
80+
81+
$dom = new \DOMDocument();
82+
$dom->validateOnParse = true;
83+
if (!$dom->loadXML($content, LIBXML_NONET | (defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0))) {
84+
libxml_disable_entity_loader($disableEntities);
85+
86+
throw new \InvalidArgumentException(implode("\n", static::getXmlErrors($internalErrors)));
87+
}
88+
89+
$dom->normalizeDocument();
90+
91+
libxml_use_internal_errors($internalErrors);
92+
libxml_disable_entity_loader($disableEntities);
93+
94+
foreach ($dom->childNodes as $child) {
95+
if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
96+
throw new \InvalidArgumentException('Document types are not allowed.');
97+
}
98+
}
99+
100+
if (null !== $schemaOrCallable) {
101+
$internalErrors = libxml_use_internal_errors(true);
102+
libxml_clear_errors();
103+
104+
$e = null;
105+
if (is_callable($schemaOrCallable)) {
106+
try {
107+
$valid = call_user_func($schemaOrCallable, $dom, $internalErrors);
108+
} catch (\Exception $e) {
109+
$valid = false;
110+
}
111+
} elseif (!is_array($schemaOrCallable) && is_file((string) $schemaOrCallable)) {
112+
$schemaSource = file_get_contents((string) $schemaOrCallable);
113+
$valid = @$dom->schemaValidateSource($schemaSource);
114+
} else {
115+
libxml_use_internal_errors($internalErrors);
116+
117+
throw new \InvalidArgumentException('The schemaOrCallable argument has to be a valid path to XSD file or callable.');
118+
}
119+
120+
if (!$valid) {
121+
$messages = $this->getXmlErrors($internalErrors);
122+
if (empty($messages)) {
123+
$messages = ['The XML is not valid.'];
124+
}
125+
throw new \InvalidArgumentException(implode("\n", $messages), 0, $e);
126+
}
127+
}
128+
129+
libxml_clear_errors();
130+
libxml_use_internal_errors($internalErrors);
131+
132+
return $dom;
133+
}
134+
135+
private function getXmlErrors($internalErrors)
136+
{
137+
$errors = [];
138+
foreach (libxml_get_errors() as $error) {
139+
$errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
140+
LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
141+
$error->code,
142+
trim($error->message),
143+
$error->file ?: 'n/a',
144+
$error->line,
145+
$error->column
146+
);
147+
}
148+
149+
libxml_clear_errors();
150+
libxml_use_internal_errors($internalErrors);
151+
152+
return $errors;
153+
}
154+
}

src/XliffConverter.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the PHP Translation package.
5+
*
6+
* (c) PHP Translation team <[email protected]>
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 Translation\SymfonyStorage;
13+
14+
use Symfony\Component\Translation\MessageCatalogue;
15+
use Translation\SymfonyStorage\Dumper\XliffDumper;
16+
use Translation\SymfonyStorage\Loader\XliffLoader;
17+
18+
/**
19+
* Utility class to convert between a MessageCatalogue and XLIFF file content.
20+
*
21+
* @author Tobias Nyholm <[email protected]>
22+
*/
23+
final class XliffConverter
24+
{
25+
/**
26+
* Create a catalogue from the contents of a XLIFF file.
27+
*
28+
* @param string $content
29+
* @param string $locale
30+
* @param string $domain
31+
*
32+
* @return MessageCatalogue
33+
*/
34+
public static function contentToCatalogue($content, $locale, $domain)
35+
{
36+
$loader = new XliffLoader();
37+
$catalogue = new MessageCatalogue($locale);
38+
$loader->extractFromContent($content, $catalogue, $domain);
39+
40+
return $catalogue;
41+
}
42+
43+
/**
44+
* @param MessageCatalogue $catalogue
45+
* @param string $domain
46+
*
47+
* @return string
48+
*/
49+
public static function catalogueToContent(MessageCatalogue $catalogue, $domain)
50+
{
51+
$dumper = new XliffDumper();
52+
53+
return $dumper->getFormattedCatalogue($catalogue, $domain);
54+
}
55+
}

tests/FileStorageTest.php renamed to tests/Unit/FileStorageTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace Translation\SymfonyStorage\Tests;
12+
namespace Translation\SymfonyStorage\Tests\Unit;
1313

1414
use Symfony\Bundle\FrameworkBundle\Translation\TranslationLoader;
1515
use Symfony\Component\Translation\Writer\TranslationWriter;
1616
use Translation\SymfonyStorage\FileStorage;
1717

18+
/**
19+
* @author Tobias Nyholm <[email protected]>
20+
*/
1821
class FileStorageTest extends \PHPUnit_Framework_TestCase
1922
{
2023
public function testConstructor()

0 commit comments

Comments
 (0)