Skip to content

Commit 9d62908

Browse files
Nyholmnicolas-grekas
authored andcommitted
Moved PhpExtractor and PhpStringTokenParser to Translation component
1 parent cadc106 commit 9d62908

File tree

7 files changed

+546
-0
lines changed

7 files changed

+546
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ CHANGELOG
1313
* Added `TranslationWriterInterface`
1414
* Deprecated `TranslationWriter::writeTranslations` in favor of `TranslationWriter::write`
1515
* added support for adding custom message formatter and decoupling the default one.
16+
* Added `PhpExtractor`
17+
* Added `PhpStringTokenParser`
1618

1719
3.2.0
1820
-----

Extractor/PhpExtractor.php

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[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 Symfony\Component\Translation\Extractor;
13+
14+
use Symfony\Component\Finder\Finder;
15+
use Symfony\Component\Translation\MessageCatalogue;
16+
17+
/**
18+
* PhpExtractor extracts translation messages from a PHP template.
19+
*
20+
* @author Michel Salib <[email protected]>
21+
*/
22+
class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
23+
{
24+
const MESSAGE_TOKEN = 300;
25+
const METHOD_ARGUMENTS_TOKEN = 1000;
26+
const DOMAIN_TOKEN = 1001;
27+
28+
/**
29+
* Prefix for new found message.
30+
*
31+
* @var string
32+
*/
33+
private $prefix = '';
34+
35+
/**
36+
* The sequence that captures translation messages.
37+
*
38+
* @var array
39+
*/
40+
protected $sequences = array(
41+
array(
42+
'->',
43+
'trans',
44+
'(',
45+
self::MESSAGE_TOKEN,
46+
',',
47+
self::METHOD_ARGUMENTS_TOKEN,
48+
',',
49+
self::DOMAIN_TOKEN,
50+
),
51+
array(
52+
'->',
53+
'transChoice',
54+
'(',
55+
self::MESSAGE_TOKEN,
56+
',',
57+
self::METHOD_ARGUMENTS_TOKEN,
58+
',',
59+
self::METHOD_ARGUMENTS_TOKEN,
60+
',',
61+
self::DOMAIN_TOKEN,
62+
),
63+
array(
64+
'->',
65+
'trans',
66+
'(',
67+
self::MESSAGE_TOKEN,
68+
),
69+
array(
70+
'->',
71+
'transChoice',
72+
'(',
73+
self::MESSAGE_TOKEN,
74+
),
75+
);
76+
77+
/**
78+
* {@inheritdoc}
79+
*/
80+
public function extract($resource, MessageCatalogue $catalog)
81+
{
82+
$files = $this->extractFiles($resource);
83+
foreach ($files as $file) {
84+
$this->parseTokens(token_get_all(file_get_contents($file)), $catalog);
85+
86+
if (\PHP_VERSION_ID >= 70000) {
87+
// PHP 7 memory manager will not release after token_get_all(), see https://bugs.php.net/70098
88+
gc_mem_caches();
89+
}
90+
}
91+
}
92+
93+
/**
94+
* {@inheritdoc}
95+
*/
96+
public function setPrefix($prefix)
97+
{
98+
$this->prefix = $prefix;
99+
}
100+
101+
/**
102+
* Normalizes a token.
103+
*
104+
* @param mixed $token
105+
*
106+
* @return string
107+
*/
108+
protected function normalizeToken($token)
109+
{
110+
if (isset($token[1]) && 'b"' !== $token) {
111+
return $token[1];
112+
}
113+
114+
return $token;
115+
}
116+
117+
/**
118+
* Seeks to a non-whitespace token.
119+
*/
120+
private function seekToNextRelevantToken(\Iterator $tokenIterator)
121+
{
122+
for (; $tokenIterator->valid(); $tokenIterator->next()) {
123+
$t = $tokenIterator->current();
124+
if (T_WHITESPACE !== $t[0]) {
125+
break;
126+
}
127+
}
128+
}
129+
130+
private function skipMethodArgument(\Iterator $tokenIterator)
131+
{
132+
$openBraces = 0;
133+
134+
for (; $tokenIterator->valid(); $tokenIterator->next()) {
135+
$t = $tokenIterator->current();
136+
137+
if ('[' === $t[0] || '(' === $t[0]) {
138+
++$openBraces;
139+
}
140+
141+
if (']' === $t[0] || ')' === $t[0]) {
142+
--$openBraces;
143+
}
144+
145+
if ((0 === $openBraces && ',' === $t[0]) || (-1 === $openBraces && ')' === $t[0])) {
146+
break;
147+
}
148+
}
149+
}
150+
151+
/**
152+
* Extracts the message from the iterator while the tokens
153+
* match allowed message tokens.
154+
*/
155+
private function getValue(\Iterator $tokenIterator)
156+
{
157+
$message = '';
158+
$docToken = '';
159+
160+
for (; $tokenIterator->valid(); $tokenIterator->next()) {
161+
$t = $tokenIterator->current();
162+
if (!isset($t[1])) {
163+
break;
164+
}
165+
166+
switch ($t[0]) {
167+
case T_START_HEREDOC:
168+
$docToken = $t[1];
169+
break;
170+
case T_ENCAPSED_AND_WHITESPACE:
171+
case T_CONSTANT_ENCAPSED_STRING:
172+
$message .= $t[1];
173+
break;
174+
case T_END_HEREDOC:
175+
return PhpStringTokenParser::parseDocString($docToken, $message);
176+
default:
177+
break 2;
178+
}
179+
}
180+
181+
if ($message) {
182+
$message = PhpStringTokenParser::parse($message);
183+
}
184+
185+
return $message;
186+
}
187+
188+
/**
189+
* Extracts trans message from PHP tokens.
190+
*
191+
* @param array $tokens
192+
* @param MessageCatalogue $catalog
193+
*/
194+
protected function parseTokens($tokens, MessageCatalogue $catalog)
195+
{
196+
$tokenIterator = new \ArrayIterator($tokens);
197+
198+
for ($key = 0; $key < $tokenIterator->count(); ++$key) {
199+
foreach ($this->sequences as $sequence) {
200+
$message = '';
201+
$domain = 'messages';
202+
$tokenIterator->seek($key);
203+
204+
foreach ($sequence as $sequenceKey => $item) {
205+
$this->seekToNextRelevantToken($tokenIterator);
206+
207+
if ($this->normalizeToken($tokenIterator->current()) === $item) {
208+
$tokenIterator->next();
209+
continue;
210+
} elseif (self::MESSAGE_TOKEN === $item) {
211+
$message = $this->getValue($tokenIterator);
212+
213+
if (count($sequence) === ($sequenceKey + 1)) {
214+
break;
215+
}
216+
} elseif (self::METHOD_ARGUMENTS_TOKEN === $item) {
217+
$this->skipMethodArgument($tokenIterator);
218+
} elseif (self::DOMAIN_TOKEN === $item) {
219+
$domain = $this->getValue($tokenIterator);
220+
221+
break;
222+
} else {
223+
break;
224+
}
225+
}
226+
227+
if ($message) {
228+
$catalog->set($message, $this->prefix.$message, $domain);
229+
break;
230+
}
231+
}
232+
}
233+
}
234+
235+
/**
236+
* @param string $file
237+
*
238+
* @return bool
239+
*
240+
* @throws \InvalidArgumentException
241+
*/
242+
protected function canBeExtracted($file)
243+
{
244+
return $this->isFile($file) && 'php' === pathinfo($file, PATHINFO_EXTENSION);
245+
}
246+
247+
/**
248+
* @param string|array $directory
249+
*
250+
* @return array
251+
*/
252+
protected function extractFromDirectory($directory)
253+
{
254+
$finder = new Finder();
255+
256+
return $finder->files()->name('*.php')->in($directory);
257+
}
258+
}

0 commit comments

Comments
 (0)