8000 [Translation] Added the gettext loaders · symfony/form@9af2342 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9af2342

Browse files
xaavstof
authored andcommitted
[Translation] Added the gettext loaders
1 parent a57a4af commit 9af2342

File tree

6 files changed

+325
-0
lines changed

6 files changed

+325
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
<parameter key="translation.loader.php.class">Symfony\Component\Translation\Loader\PhpFileLoader</parameter>
1212
<parameter key="translation.loader.yml.class">Symfony\Component\Translation\Loader\YamlFileLoader</parameter>
1313
<parameter key="translation.loader.xliff.class">Symfony\Component\Translation\Loader\XliffFileLoader</parameter>
14+
<parameter key="translation.loader.po.class">Symfony\Component\Translation\Loader\PoFileLoader</parameter>
15+
<parameter key="translation.loader.mo.class">Symfony\Component\Translation\Loader\MoFileLoader</parameter>
1416
<parameter key="translation.loader.qt.class">Symfony\Component\Translation\Loader\QtTranslationsLoader</parameter>
1517
<parameter key="translation.loader.csv.class">Symfony\Component\Translation\Loader\CsvFileLoader</parameter>
1618
<parameter key="translation.loader.rb.class">Symfony\Component\Translation\Loader\ResourceBundleLoader</parameter>
@@ -57,6 +59,14 @@
5759
<tag name="translation.loader" alias="xliff" />
5860
</service>
5961

62+
<service id="translation.loader.po" class="%translation.loader.po.class%">
63+
<tag name="translation.loader" alias="po" />
64+
</service>
65+
66+
<service id="translation.loader.mo" class="%translation.loader.mo.class%">
67+
<tag name="translation.loader" alias="mo" />
68+
</service>
69+
6070
<service id="translation.loader.qt" class="%translation.loader.qt.class%">
6171
<tag name="translation.loader" alias="ts" />
6272
</service>
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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\Loader;
13+
14+
use Symfony\Component\Config\Resource\FileResource;
15+
16+
/**
17+
* @copyright Copyright (c) 2010, Union of RAD http://union-of-rad.org (http://lithify.me/)
18+
*/
19+
class MoFileLoader extends ArrayLoader implements LoaderInterface
20+
{
21+
/**
22+
* Magic used for validating the format of a MO file as well as
23+
* detecting if the machine used to create that file was little endian.
24+
*
25+
* @var float
26+
*/
27+
const MO_LITTLE_ENDIAN_MAGIC = 0x950412de;
28+
29+
/**
30+
* Magic used for validating the format of a MO file as well as
31+
* detecting if the machine used to create that file was big endian.
32+
*
33+
* @var float
34+
*/
35+
const MO_BIG_ENDIAN_MAGIC = 0xde120495;
36+
37+
/**
38+
* The size of the header of a MO file in bytes.
39+
*
40+
* @var integer Number of bytes.
41+
*/
42+
const MO_HEADER_SIZE = 28;
43+
44+
public function load($resource, $locale, $domain = 'messages')
45+
{
46+
$messages = $this->parse($resource);
47+
48+
// empty file
49+
if (null === $messages) {
50+
$messages = array();
51+
}
52+
53+
// not an array
54+
if (!is_array($messages)) {
55+
throw new \InvalidArgumentException(sprintf('The file "%s" must contain a valid mo file.', $resource));
56+
}
57+
58+
$catalogue = parent::load($messages, $locale, $domain);
59+
$catalogue->addResource(new FileResource($resource));
60+
61+
return $catalogue;
62+
}
63+
64+
/**
65+
* Parses machine object (MO) format, independent of the machine's endian it
66+
* was created on. Both 32bit and 64bit systems are supported.
67+
*
68+
* @param resource $stream
69+
* @return array
70+
* @throws InvalidArgumentException If stream content has an invalid format.
71+
*/
72+
private function parse($resource)
73+
{
74+
$stream = fopen($resource, 'r');
75+
76+
$stat = fstat($stream);
77+
78+
if ($stat['size'] < self::MO_HEADER_SIZE) {
79+
throw new \InvalidArgumentException("MO stream content has an invalid format.");
80+
}
81+
$magic = unpack('V1', fread($stream, 4));
82+
$magic = hexdec(substr(dechex(current($magic)), -8));
83+
84+
if ($magic == self::MO_LITTLE_ENDIAN_MAGIC) {
85+
$isBigEndian = false;
86+
} elseif ($magic == self::MO_BIG_ENDIAN_MAGIC) {
87+
$isBigEndian = true;
88+
} else {
89+
throw new \InvalidArgumentException("MO stream content has an invalid format.");
90+
}
91+
92+
$header = array(
93+
'formatRevision' => null,
94+
'count' => null,
95+
'offsetId' => null,
96+
'offsetTranslated' => null,
97+
'sizeHashes' => null,
98+
'offsetHashes' => null,
99+
);
100+
foreach ($header as &$value) {
101+
$value = $this->readLong($stream, $isBigEndian);
102+
}
103+
extract($header);
104+
$messages = array();
105+
106+
for ($i = 0; $i < $count; $i++) {
107+
$singularId = $pluralId = null;
108+
$translated = null;
109+
110+
fseek($stream, $offsetId + $i * 8);
111+
112+
$length = $this->readLong($stream, $isBigEndian);
113+
$offset = $this->readLong($stream, $isBigEndian);
114+
115+
if ($length < 1) {
116+
continue;
117+
}
118+
119+
fseek($stream, $offset);
120+
$singularId = fread($stream, $length);
121+
122+
if (strpos($singularId, "\000") !== false) {
123+
list($singularId, $pluralId) = explode("\000", $singularId);
124+
}
125+
126+
fseek($stream, $offsetTranslated + $i * 8);
127+
$length = $this->readLong($stream, $isBigEndian);
128+
$offset = $this->readLong($stream, $isBigEndian);
129+
130+
fseek($stream, $offset);
131+
$translated = fread($stream, $length);
132+
133+
if (strpos($translated, "\000") !== false) {
134+
$translated = explode("\000", $translated);
135+
}
136+
137+
$ids = array('singular' => $singularId, 'plural' => $pluralId);
138+
$item = compact('ids', 'translated');
139+
140+
if (is_array($item['translated'])) {
141+
$messages[$item['ids']['singular']] = stripslashes($item['translated'][0]);
142+
if (isset($item['ids']['plural'])) {
143+
$messages[$item['ids']['plural']] = stripslashes(end($item['translated']));
144+
}
145+
} elseif($item['ids']['singular']) {
146+
$messages[$item['ids']['singular']] = stripslashes($item['translated']);
147+
}
148+
}
149+
150+
fclose($stream);
151+
152+
return array_filter($messages);
153+
}
154+
155+
/**
156+
* Reads an unsigned long from stream respecting endianess.
157+
*
158+
* @param resource $stream
159+
* @param boolean $isBigEndian
160+
* @return integer
161+
*/
162+
private function readLong($stream, $isBigEndian)
163+
{
164+
$result = unpack($isBigEndian ? 'N1' : 'V1', fread($stream, 4));
165+
$result = current($result);
166+
167+
return (integer) substr($result, -8);
168+
}
169+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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\Loader;
13+
14+
use Symfony\Component\Config\Resource\FileResource;
15+
16+
/**
17+
* @copyright Copyright (c) 2010, Union of RAD http://union-of-rad.org (http://lithify.me/)
18+
*/
19+
class PoFileLoader extends ArrayLoader implements LoaderInterface
20+
{
21+
public function load($resource, $locale, $domain = 'messages')
22+
{
23+
$messages = $this->parse($resource);
24+
25+
// empty file
26+
if (null === $messages) {
27+
$messages = array();
28+
}
29+
30+
// not an array
31+
if (!is_array($messages)) {
32+
throw new \InvalidArgumentException(sprintf('The file "%s" must contain a valid po file.', $resource));
33+
}
34+
35+
$catalogue = parent::load($messages, $locale, $domain);
36+
$catalogue->addResource(new FileResource($resource));
37+
38+
return $catalogue;
39+
}
40+
41+
/**
42+
* Parses portable object (PO) format.
43+
*
44+
* This parser sacrifices some features of the reference implementation the
45+
* differences to that implementation are as follows.
46+
* - No support for comments spanning multiple lines.
47+
* - Translator and extracted comments are treated as being the same type.
48+
* - Message IDs are allowed to have other encodings as just US-ASCII.
49+
*
50+
* Items with an empty id are ignored.
51+
*
52+
* @param resource $stream
53+
* @return array
54+
*/
55+
private function parse($resource)
56+
{
57+
$stream = fopen($resource, 'r');
58+
59+
$defaults = array(
60+
'ids' => array(),
61+
'translated' => null,
62+
);
63+
64+
$messages = array();
65+
$item = $defaults;
66+
67+
while ($line = fgets($stream)) {
68+
$line = trim($line);
69+
70+
if ($line === '') {
71+
if (is_array($item['translated'])) {
72+
$messages[$item['ids']['singular']] = stripslashes($item['translated'][0]);
73+
if (isset($item['ids']['plural'])) {
74+
$messages[$item['ids']['plural']] = stripslashes(end($item['translated']));
75+
}
76+
} elseif($item['ids']['singular']) {
77+
$messages[$item['ids']['singular']] = stripslashes($item['translated']);
78+
}
79+
$item = $defaults;
80+
} elseif (substr($line, 0, 7) === 'msgid "') {
81+
$item['ids']['singular'] = substr($line, 7, -1);
82+
} elseif (substr($line, 0, 8) === 'msgstr "') {
83+
$item['translated'] = substr($line, 8, -1);
84+
} elseif ($line[0] === '"') {
85+
$continues = isset($item['translated']) ? 'translated' : 'ids';
86+
87+
if (is_array($item[$continues])) {
88+
end($item[$continues]);
89+
$item[$continues][key($item[$continues])] .= substr($line, 1, -1);
90+
} else {
91+
$item[$continues] .= substr($line, 1, -1);
92+
}
93+
} elseif (substr($line, 0, 14) === 'msgid_plural "') {
94+
$item['ids']['plural'] = substr($line, 14, -1);
95+
} elseif (substr($line, 0, 7) === 'msgstr[') {
96+
$item['translated'][(integer) substr($line, 7, 1)] = substr($line, 11, -1);
97+
}
98+
99+
}
100+
fclose($stream);
101+
102+
return array_filter($messages);
103+
}
104+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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\Tests\Component\Translation\Loader;
13+
14+
use Symfony\Component\Translation\Loader\PoFileLoader;
15+
use Symfony\Component\Config\Resource\FileResource;
16+
17+
class PoFileLoaderTest extends \PHPUnit_Framework_TestCase
18+
{
19+
public function testLoad()
20+
{
21+
$loader = new PoFileLoader();
22+
$resource = __DIR__.'/../fixtures/resources.po';
23+
$catalogue = $loader->load($resource, 'en', 'domain1');
24+
25+
$this->assertEquals(array('foo' => 'bar'), $catalogue->all('domain1'));
26+
$this->assertEquals('en', $catalogue->getLocale());
27+
$this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
28+
}
29+
30+
public function testLoadDoesNothingIfEmpty()
31+
{
32+
$loader = new PoFileLoader();
33+
$resource = __DIR__.'/../fixtures/empty.po';
34+
$catalogue = $loader->load($resource, 'en', 'domain1');
35+
36+
$this->assertEquals(array(), $catalogue->all('domain1'));
37+
$this->assertEquals('en', $catalogue->getLocale());
38+
$this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
39+
}
40+
}

tests/Symfony/Tests/Component/Translation/fixtures/empty.po

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
msgid "foo"
2+
msgstr "bar"

0 commit comments

Comments
 (0)
0