Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 namespace Drupal\Component\Gettext;
|
Chris@0
|
4
|
Chris@17
|
5 use Drupal\Component\Render\FormattableMarkup;
|
Chris@0
|
6
|
Chris@0
|
7 /**
|
Chris@0
|
8 * Implements Gettext PO stream reader.
|
Chris@0
|
9 *
|
Chris@0
|
10 * The PO file format parsing is implemented according to the documentation at
|
Chris@0
|
11 * http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files
|
Chris@0
|
12 */
|
Chris@0
|
13 class PoStreamReader implements PoStreamInterface, PoReaderInterface {
|
Chris@0
|
14
|
Chris@0
|
15 /**
|
Chris@0
|
16 * Source line number of the stream being parsed.
|
Chris@0
|
17 *
|
Chris@0
|
18 * @var int
|
Chris@0
|
19 */
|
Chris@17
|
20 protected $lineNumber = 0;
|
Chris@0
|
21
|
Chris@0
|
22 /**
|
Chris@0
|
23 * Parser context for the stream reader state machine.
|
Chris@0
|
24 *
|
Chris@0
|
25 * Possible contexts are:
|
Chris@0
|
26 * - 'COMMENT' (#)
|
Chris@0
|
27 * - 'MSGID' (msgid)
|
Chris@0
|
28 * - 'MSGID_PLURAL' (msgid_plural)
|
Chris@0
|
29 * - 'MSGCTXT' (msgctxt)
|
Chris@0
|
30 * - 'MSGSTR' (msgstr or msgstr[])
|
Chris@0
|
31 * - 'MSGSTR_ARR' (msgstr_arg)
|
Chris@0
|
32 *
|
Chris@0
|
33 * @var string
|
Chris@0
|
34 */
|
Chris@17
|
35 protected $context = 'COMMENT';
|
Chris@0
|
36
|
Chris@0
|
37 /**
|
Chris@0
|
38 * Current entry being read. Incomplete.
|
Chris@0
|
39 *
|
Chris@0
|
40 * @var array
|
Chris@0
|
41 */
|
Chris@17
|
42 protected $currentItem = [];
|
Chris@0
|
43
|
Chris@0
|
44 /**
|
Chris@0
|
45 * Current plural index for plural translations.
|
Chris@0
|
46 *
|
Chris@0
|
47 * @var int
|
Chris@0
|
48 */
|
Chris@17
|
49 protected $currentPluralIndex = 0;
|
Chris@0
|
50
|
Chris@0
|
51 /**
|
Chris@0
|
52 * URI of the PO stream that is being read.
|
Chris@0
|
53 *
|
Chris@0
|
54 * @var string
|
Chris@0
|
55 */
|
Chris@17
|
56 protected $uri = '';
|
Chris@0
|
57
|
Chris@0
|
58 /**
|
Chris@0
|
59 * Language code for the PO stream being read.
|
Chris@0
|
60 *
|
Chris@0
|
61 * @var string
|
Chris@0
|
62 */
|
Chris@17
|
63 protected $langcode = NULL;
|
Chris@0
|
64
|
Chris@0
|
65 /**
|
Chris@0
|
66 * File handle of the current PO stream.
|
Chris@0
|
67 *
|
Chris@0
|
68 * @var resource
|
Chris@0
|
69 */
|
Chris@17
|
70 protected $fd;
|
Chris@0
|
71
|
Chris@0
|
72 /**
|
Chris@0
|
73 * The PO stream header.
|
Chris@0
|
74 *
|
Chris@0
|
75 * @var \Drupal\Component\Gettext\PoHeader
|
Chris@0
|
76 */
|
Chris@17
|
77 protected $header;
|
Chris@0
|
78
|
Chris@0
|
79 /**
|
Chris@0
|
80 * Object wrapper for the last read source/translation pair.
|
Chris@0
|
81 *
|
Chris@0
|
82 * @var \Drupal\Component\Gettext\PoItem
|
Chris@0
|
83 */
|
Chris@17
|
84 protected $lastItem;
|
Chris@0
|
85
|
Chris@0
|
86 /**
|
Chris@0
|
87 * Indicator of whether the stream reading is finished.
|
Chris@0
|
88 *
|
Chris@0
|
89 * @var bool
|
Chris@0
|
90 */
|
Chris@17
|
91 protected $finished;
|
Chris@0
|
92
|
Chris@0
|
93 /**
|
Chris@0
|
94 * Array of translated error strings recorded on reading this stream so far.
|
Chris@0
|
95 *
|
Chris@0
|
96 * @var array
|
Chris@0
|
97 */
|
Chris@17
|
98 protected $errors;
|
Chris@0
|
99
|
Chris@0
|
100 /**
|
Chris@0
|
101 * {@inheritdoc}
|
Chris@0
|
102 */
|
Chris@0
|
103 public function getLangcode() {
|
Chris@17
|
104 return $this->langcode;
|
Chris@0
|
105 }
|
Chris@0
|
106
|
Chris@0
|
107 /**
|
Chris@0
|
108 * {@inheritdoc}
|
Chris@0
|
109 */
|
Chris@0
|
110 public function setLangcode($langcode) {
|
Chris@17
|
111 $this->langcode = $langcode;
|
Chris@0
|
112 }
|
Chris@0
|
113
|
Chris@0
|
114 /**
|
Chris@0
|
115 * {@inheritdoc}
|
Chris@0
|
116 */
|
Chris@0
|
117 public function getHeader() {
|
Chris@17
|
118 return $this->header;
|
Chris@0
|
119 }
|
Chris@0
|
120
|
Chris@0
|
121 /**
|
Chris@0
|
122 * Implements Drupal\Component\Gettext\PoMetadataInterface::setHeader().
|
Chris@0
|
123 *
|
Chris@0
|
124 * Not applicable to stream reading and therefore not implemented.
|
Chris@0
|
125 */
|
Chris@0
|
126 public function setHeader(PoHeader $header) {
|
Chris@0
|
127 }
|
Chris@0
|
128
|
Chris@0
|
129 /**
|
Chris@0
|
130 * {@inheritdoc}
|
Chris@0
|
131 */
|
Chris@0
|
132 public function getURI() {
|
Chris@17
|
133 return $this->uri;
|
Chris@0
|
134 }
|
Chris@0
|
135
|
Chris@0
|
136 /**
|
Chris@0
|
137 * {@inheritdoc}
|
Chris@0
|
138 */
|
Chris@0
|
139 public function setURI($uri) {
|
Chris@17
|
140 $this->uri = $uri;
|
Chris@0
|
141 }
|
Chris@0
|
142
|
Chris@0
|
143 /**
|
Chris@0
|
144 * Implements Drupal\Component\Gettext\PoStreamInterface::open().
|
Chris@0
|
145 *
|
Chris@0
|
146 * Opens the stream and reads the header. The stream is ready for reading
|
Chris@0
|
147 * items after.
|
Chris@0
|
148 *
|
Chris@14
|
149 * @throws \Exception
|
Chris@0
|
150 * If the URI is not yet set.
|
Chris@0
|
151 */
|
Chris@0
|
152 public function open() {
|
Chris@17
|
153 if (!empty($this->uri)) {
|
Chris@17
|
154 $this->fd = fopen($this->uri, 'rb');
|
Chris@0
|
155 $this->readHeader();
|
Chris@0
|
156 }
|
Chris@0
|
157 else {
|
Chris@0
|
158 throw new \Exception('Cannot open stream without URI set.');
|
Chris@0
|
159 }
|
Chris@0
|
160 }
|
Chris@0
|
161
|
Chris@0
|
162 /**
|
Chris@0
|
163 * Implements Drupal\Component\Gettext\PoStreamInterface::close().
|
Chris@0
|
164 *
|
Chris@14
|
165 * @throws \Exception
|
Chris@0
|
166 * If the stream is not open.
|
Chris@0
|
167 */
|
Chris@0
|
168 public function close() {
|
Chris@17
|
169 if ($this->fd) {
|
Chris@17
|
170 fclose($this->fd);
|
Chris@0
|
171 }
|
Chris@0
|
172 else {
|
Chris@0
|
173 throw new \Exception('Cannot close stream that is not open.');
|
Chris@0
|
174 }
|
Chris@0
|
175 }
|
Chris@0
|
176
|
Chris@0
|
177 /**
|
Chris@0
|
178 * {@inheritdoc}
|
Chris@0
|
179 */
|
Chris@0
|
180 public function readItem() {
|
Chris@0
|
181 // Clear out the last item.
|
Chris@17
|
182 $this->lastItem = NULL;
|
Chris@0
|
183
|
Chris@0
|
184 // Read until finished with the stream or a complete item was identified.
|
Chris@17
|
185 while (!$this->finished && is_null($this->lastItem)) {
|
Chris@0
|
186 $this->readLine();
|
Chris@0
|
187 }
|
Chris@0
|
188
|
Chris@17
|
189 return $this->lastItem;
|
Chris@0
|
190 }
|
Chris@0
|
191
|
Chris@0
|
192 /**
|
Chris@0
|
193 * Sets the seek position for the current PO stream.
|
Chris@0
|
194 *
|
Chris@0
|
195 * @param int $seek
|
Chris@0
|
196 * The new seek position to set.
|
Chris@0
|
197 */
|
Chris@0
|
198 public function setSeek($seek) {
|
Chris@17
|
199 fseek($this->fd, $seek);
|
Chris@0
|
200 }
|
Chris@0
|
201
|
Chris@0
|
202 /**
|
Chris@0
|
203 * Gets the pointer position of the current PO stream.
|
Chris@0
|
204 */
|
Chris@0
|
205 public function getSeek() {
|
Chris@17
|
206 return ftell($this->fd);
|
Chris@0
|
207 }
|
Chris@0
|
208
|
Chris@0
|
209 /**
|
Chris@0
|
210 * Read the header from the PO stream.
|
Chris@0
|
211 *
|
Chris@0
|
212 * The header is a special case PoItem, using the empty string as source and
|
Chris@0
|
213 * key-value pairs as translation. We just reuse the item reader logic to
|
Chris@0
|
214 * read the header.
|
Chris@0
|
215 */
|
Chris@0
|
216 private function readHeader() {
|
Chris@0
|
217 $item = $this->readItem();
|
Chris@0
|
218 // Handle the case properly when the .po file is empty (0 bytes).
|
Chris@0
|
219 if (!$item) {
|
Chris@0
|
220 return;
|
Chris@0
|
221 }
|
Chris@0
|
222 $header = new PoHeader();
|
Chris@0
|
223 $header->setFromString(trim($item->getTranslation()));
|
Chris@17
|
224 $this->header = $header;
|
Chris@0
|
225 }
|
Chris@0
|
226
|
Chris@0
|
227 /**
|
Chris@0
|
228 * Reads a line from the PO stream and stores data internally.
|
Chris@0
|
229 *
|
Chris@17
|
230 * Expands $this->current_item based on new data for the current item. If
|
Chris@0
|
231 * this line ends the current item, it is saved with setItemFromArray() with
|
Chris@17
|
232 * data from $this->current_item.
|
Chris@0
|
233 *
|
Chris@0
|
234 * An internal state machine is maintained in this reader using
|
Chris@17
|
235 * $this->context as the reading state. PO items are in between COMMENT
|
Chris@0
|
236 * states (when items have at least one line or comment in between them) or
|
Chris@0
|
237 * indicated by MSGSTR or MSGSTR_ARR followed immediately by an MSGID or
|
Chris@0
|
238 * MSGCTXT (when items closely follow each other).
|
Chris@0
|
239 *
|
Chris@0
|
240 * @return
|
Chris@0
|
241 * FALSE if an error was logged, NULL otherwise. The errors are considered
|
Chris@0
|
242 * non-blocking, so reading can continue, while the errors are collected
|
Chris@0
|
243 * for later presentation.
|
Chris@0
|
244 */
|
Chris@0
|
245 private function readLine() {
|
Chris@0
|
246 // Read a line and set the stream finished indicator if it was not
|
Chris@0
|
247 // possible anymore.
|
Chris@17
|
248 $line = fgets($this->fd);
|
Chris@17
|
249 $this->finished = ($line === FALSE);
|
Chris@0
|
250
|
Chris@17
|
251 if (!$this->finished) {
|
Chris@0
|
252
|
Chris@17
|
253 if ($this->lineNumber == 0) {
|
Chris@0
|
254 // The first line might come with a UTF-8 BOM, which should be removed.
|
Chris@0
|
255 $line = str_replace("\xEF\xBB\xBF", '', $line);
|
Chris@0
|
256 // Current plurality for 'msgstr[]'.
|
Chris@17
|
257 $this->currentPluralIndex = 0;
|
Chris@0
|
258 }
|
Chris@0
|
259
|
Chris@0
|
260 // Track the line number for error reporting.
|
Chris@17
|
261 $this->lineNumber++;
|
Chris@0
|
262
|
Chris@0
|
263 // Initialize common values for error logging.
|
Chris@0
|
264 $log_vars = [
|
Chris@0
|
265 '%uri' => $this->getURI(),
|
Chris@17
|
266 '%line' => $this->lineNumber,
|
Chris@0
|
267 ];
|
Chris@0
|
268
|
Chris@0
|
269 // Trim away the linefeed. \\n might appear at the end of the string if
|
Chris@0
|
270 // another line continuing the same string follows. We can remove that.
|
Chris@0
|
271 $line = trim(strtr($line, ["\\\n" => ""]));
|
Chris@0
|
272
|
Chris@0
|
273 if (!strncmp('#', $line, 1)) {
|
Chris@0
|
274 // Lines starting with '#' are comments.
|
Chris@0
|
275
|
Chris@17
|
276 if ($this->context == 'COMMENT') {
|
Chris@0
|
277 // Already in comment context, add to current comment.
|
Chris@17
|
278 $this->currentItem['#'][] = substr($line, 1);
|
Chris@0
|
279 }
|
Chris@17
|
280 elseif (($this->context == 'MSGSTR') || ($this->context == 'MSGSTR_ARR')) {
|
Chris@0
|
281 // We are currently in string context, save current item.
|
Chris@17
|
282 $this->setItemFromArray($this->currentItem);
|
Chris@0
|
283
|
Chris@0
|
284 // Start a new entry for the comment.
|
Chris@17
|
285 $this->currentItem = [];
|
Chris@17
|
286 $this->currentItem['#'][] = substr($line, 1);
|
Chris@0
|
287
|
Chris@17
|
288 $this->context = 'COMMENT';
|
Chris@0
|
289 return;
|
Chris@0
|
290 }
|
Chris@0
|
291 else {
|
Chris@0
|
292 // A comment following any other context is a syntax error.
|
Chris@17
|
293 $this->errors[] = new FormattableMarkup('The translation stream %uri contains an error: "msgstr" was expected but not found on line %line.', $log_vars);
|
Chris@0
|
294 return FALSE;
|
Chris@0
|
295 }
|
Chris@0
|
296 return;
|
Chris@0
|
297 }
|
Chris@0
|
298 elseif (!strncmp('msgid_plural', $line, 12)) {
|
Chris@0
|
299 // A plural form for the current source string.
|
Chris@0
|
300
|
Chris@17
|
301 if ($this->context != 'MSGID') {
|
Chris@0
|
302 // A plural form can only be added to an msgid directly.
|
Chris@17
|
303 $this->errors[] = new FormattableMarkup('The translation stream %uri contains an error: "msgid_plural" was expected but not found on line %line.', $log_vars);
|
Chris@0
|
304 return FALSE;
|
Chris@0
|
305 }
|
Chris@0
|
306
|
Chris@0
|
307 // Remove 'msgid_plural' and trim away whitespace.
|
Chris@0
|
308 $line = trim(substr($line, 12));
|
Chris@0
|
309
|
Chris@0
|
310 // Only the plural source string is left, parse it.
|
Chris@0
|
311 $quoted = $this->parseQuoted($line);
|
Chris@0
|
312 if ($quoted === FALSE) {
|
Chris@0
|
313 // The plural form must be wrapped in quotes.
|
Chris@17
|
314 $this->errors[] = new FormattableMarkup('The translation stream %uri contains a syntax error on line %line.', $log_vars);
|
Chris@0
|
315 return FALSE;
|
Chris@0
|
316 }
|
Chris@0
|
317
|
Chris@0
|
318 // Append the plural source to the current entry.
|
Chris@17
|
319 if (is_string($this->currentItem['msgid'])) {
|
Chris@0
|
320 // The first value was stored as string. Now we know the context is
|
Chris@0
|
321 // plural, it is converted to array.
|
Chris@17
|
322 $this->currentItem['msgid'] = [$this->currentItem['msgid']];
|
Chris@0
|
323 }
|
Chris@17
|
324 $this->currentItem['msgid'][] = $quoted;
|
Chris@0
|
325
|
Chris@17
|
326 $this->context = 'MSGID_PLURAL';
|
Chris@0
|
327 return;
|
Chris@0
|
328 }
|
Chris@0
|
329 elseif (!strncmp('msgid', $line, 5)) {
|
Chris@0
|
330 // Starting a new message.
|
Chris@0
|
331
|
Chris@17
|
332 if (($this->context == 'MSGSTR') || ($this->context == 'MSGSTR_ARR')) {
|
Chris@0
|
333 // We are currently in string context, save current item.
|
Chris@17
|
334 $this->setItemFromArray($this->currentItem);
|
Chris@0
|
335
|
Chris@0
|
336 // Start a new context for the msgid.
|
Chris@17
|
337 $this->currentItem = [];
|
Chris@0
|
338 }
|
Chris@17
|
339 elseif ($this->context == 'MSGID') {
|
Chris@0
|
340 // We are currently already in the context, meaning we passed an id with no data.
|
Chris@17
|
341 $this->errors[] = new FormattableMarkup('The translation stream %uri contains an error: "msgid" is unexpected on line %line.', $log_vars);
|
Chris@0
|
342 return FALSE;
|
Chris@0
|
343 }
|
Chris@0
|
344
|
Chris@0
|
345 // Remove 'msgid' and trim away whitespace.
|
Chris@0
|
346 $line = trim(substr($line, 5));
|
Chris@0
|
347
|
Chris@0
|
348 // Only the message id string is left, parse it.
|
Chris@0
|
349 $quoted = $this->parseQuoted($line);
|
Chris@0
|
350 if ($quoted === FALSE) {
|
Chris@0
|
351 // The message id must be wrapped in quotes.
|
Chris@17
|
352 $this->errors[] = new FormattableMarkup('The translation stream %uri contains an error: invalid format for "msgid" on line %line.', $log_vars, $log_vars);
|
Chris@0
|
353 return FALSE;
|
Chris@0
|
354 }
|
Chris@0
|
355
|
Chris@17
|
356 $this->currentItem['msgid'] = $quoted;
|
Chris@17
|
357 $this->context = 'MSGID';
|
Chris@0
|
358 return;
|
Chris@0
|
359 }
|
Chris@0
|
360 elseif (!strncmp('msgctxt', $line, 7)) {
|
Chris@0
|
361 // Starting a new context.
|
Chris@0
|
362
|
Chris@17
|
363 if (($this->context == 'MSGSTR') || ($this->context == 'MSGSTR_ARR')) {
|
Chris@0
|
364 // We are currently in string context, save current item.
|
Chris@17
|
365 $this->setItemFromArray($this->currentItem);
|
Chris@17
|
366 $this->currentItem = [];
|
Chris@0
|
367 }
|
Chris@17
|
368 elseif (!empty($this->currentItem['msgctxt'])) {
|
Chris@0
|
369 // A context cannot apply to another context.
|
Chris@17
|
370 $this->errors[] = new FormattableMarkup('The translation stream %uri contains an error: "msgctxt" is unexpected on line %line.', $log_vars);
|
Chris@0
|
371 return FALSE;
|
Chris@0
|
372 }
|
Chris@0
|
373
|
Chris@0
|
374 // Remove 'msgctxt' and trim away whitespaces.
|
Chris@0
|
375 $line = trim(substr($line, 7));
|
Chris@0
|
376
|
Chris@0
|
377 // Only the msgctxt string is left, parse it.
|
Chris@0
|
378 $quoted = $this->parseQuoted($line);
|
Chris@0
|
379 if ($quoted === FALSE) {
|
Chris@0
|
380 // The context string must be quoted.
|
Chris@17
|
381 $this->errors[] = new FormattableMarkup('The translation stream %uri contains an error: invalid format for "msgctxt" on line %line.', $log_vars);
|
Chris@0
|
382 return FALSE;
|
Chris@0
|
383 }
|
Chris@0
|
384
|
Chris@17
|
385 $this->currentItem['msgctxt'] = $quoted;
|
Chris@0
|
386
|
Chris@17
|
387 $this->context = 'MSGCTXT';
|
Chris@0
|
388 return;
|
Chris@0
|
389 }
|
Chris@0
|
390 elseif (!strncmp('msgstr[', $line, 7)) {
|
Chris@0
|
391 // A message string for a specific plurality.
|
Chris@0
|
392
|
Chris@17
|
393 if (($this->context != 'MSGID') &&
|
Chris@17
|
394 ($this->context != 'MSGCTXT') &&
|
Chris@17
|
395 ($this->context != 'MSGID_PLURAL') &&
|
Chris@17
|
396 ($this->context != 'MSGSTR_ARR')) {
|
Chris@17
|
397 // Plural message strings must come after msgid, msgctxt,
|
Chris@0
|
398 // msgid_plural, or other msgstr[] entries.
|
Chris@17
|
399 $this->errors[] = new FormattableMarkup('The translation stream %uri contains an error: "msgstr[]" is unexpected on line %line.', $log_vars);
|
Chris@0
|
400 return FALSE;
|
Chris@0
|
401 }
|
Chris@0
|
402
|
Chris@0
|
403 // Ensure the plurality is terminated.
|
Chris@0
|
404 if (strpos($line, ']') === FALSE) {
|
Chris@17
|
405 $this->errors[] = new FormattableMarkup('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars);
|
Chris@0
|
406 return FALSE;
|
Chris@0
|
407 }
|
Chris@0
|
408
|
Chris@0
|
409 // Extract the plurality.
|
Chris@0
|
410 $frombracket = strstr($line, '[');
|
Chris@17
|
411 $this->currentPluralIndex = substr($frombracket, 1, strpos($frombracket, ']') - 1);
|
Chris@0
|
412
|
Chris@0
|
413 // Skip to the next whitespace and trim away any further whitespace,
|
Chris@0
|
414 // bringing $line to the message text only.
|
Chris@0
|
415 $line = trim(strstr($line, " "));
|
Chris@0
|
416
|
Chris@0
|
417 $quoted = $this->parseQuoted($line);
|
Chris@0
|
418 if ($quoted === FALSE) {
|
Chris@0
|
419 // The string must be quoted.
|
Chris@17
|
420 $this->errors[] = new FormattableMarkup('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars);
|
Chris@0
|
421 return FALSE;
|
Chris@0
|
422 }
|
Chris@17
|
423 if (!isset($this->currentItem['msgstr']) || !is_array($this->currentItem['msgstr'])) {
|
Chris@17
|
424 $this->currentItem['msgstr'] = [];
|
Chris@0
|
425 }
|
Chris@0
|
426
|
Chris@17
|
427 $this->currentItem['msgstr'][$this->currentPluralIndex] = $quoted;
|
Chris@0
|
428
|
Chris@17
|
429 $this->context = 'MSGSTR_ARR';
|
Chris@0
|
430 return;
|
Chris@0
|
431 }
|
Chris@0
|
432 elseif (!strncmp("msgstr", $line, 6)) {
|
Chris@0
|
433 // A string pair for an msgid (with optional context).
|
Chris@0
|
434
|
Chris@17
|
435 if (($this->context != 'MSGID') && ($this->context != 'MSGCTXT')) {
|
Chris@0
|
436 // Strings are only valid within an id or context scope.
|
Chris@17
|
437 $this->errors[] = new FormattableMarkup('The translation stream %uri contains an error: "msgstr" is unexpected on line %line.', $log_vars);
|
Chris@0
|
438 return FALSE;
|
Chris@0
|
439 }
|
Chris@0
|
440
|
Chris@0
|
441 // Remove 'msgstr' and trim away away whitespaces.
|
Chris@0
|
442 $line = trim(substr($line, 6));
|
Chris@0
|
443
|
Chris@0
|
444 // Only the msgstr string is left, parse it.
|
Chris@0
|
445 $quoted = $this->parseQuoted($line);
|
Chris@0
|
446 if ($quoted === FALSE) {
|
Chris@0
|
447 // The string must be quoted.
|
Chris@17
|
448 $this->errors[] = new FormattableMarkup('The translation stream %uri contains an error: invalid format for "msgstr" on line %line.', $log_vars);
|
Chris@0
|
449 return FALSE;
|
Chris@0
|
450 }
|
Chris@0
|
451
|
Chris@17
|
452 $this->currentItem['msgstr'] = $quoted;
|
Chris@0
|
453
|
Chris@17
|
454 $this->context = 'MSGSTR';
|
Chris@0
|
455 return;
|
Chris@0
|
456 }
|
Chris@0
|
457 elseif ($line != '') {
|
Chris@0
|
458 // Anything that is not a token may be a continuation of a previous token.
|
Chris@0
|
459
|
Chris@0
|
460 $quoted = $this->parseQuoted($line);
|
Chris@0
|
461 if ($quoted === FALSE) {
|
Chris@0
|
462 // This string must be quoted.
|
Chris@17
|
463 $this->errors[] = new FormattableMarkup('The translation stream %uri contains an error: string continuation expected on line %line.', $log_vars);
|
Chris@0
|
464 return FALSE;
|
Chris@0
|
465 }
|
Chris@0
|
466
|
Chris@0
|
467 // Append the string to the current item.
|
Chris@17
|
468 if (($this->context == 'MSGID') || ($this->context == 'MSGID_PLURAL')) {
|
Chris@17
|
469 if (is_array($this->currentItem['msgid'])) {
|
Chris@0
|
470 // Add string to last array element for plural sources.
|
Chris@17
|
471 $last_index = count($this->currentItem['msgid']) - 1;
|
Chris@17
|
472 $this->currentItem['msgid'][$last_index] .= $quoted;
|
Chris@0
|
473 }
|
Chris@0
|
474 else {
|
Chris@0
|
475 // Singular source, just append the string.
|
Chris@17
|
476 $this->currentItem['msgid'] .= $quoted;
|
Chris@0
|
477 }
|
Chris@0
|
478 }
|
Chris@17
|
479 elseif ($this->context == 'MSGCTXT') {
|
Chris@0
|
480 // Multiline context name.
|
Chris@17
|
481 $this->currentItem['msgctxt'] .= $quoted;
|
Chris@0
|
482 }
|
Chris@17
|
483 elseif ($this->context == 'MSGSTR') {
|
Chris@0
|
484 // Multiline translation string.
|
Chris@17
|
485 $this->currentItem['msgstr'] .= $quoted;
|
Chris@0
|
486 }
|
Chris@17
|
487 elseif ($this->context == 'MSGSTR_ARR') {
|
Chris@0
|
488 // Multiline plural translation string.
|
Chris@17
|
489 $this->currentItem['msgstr'][$this->currentPluralIndex] .= $quoted;
|
Chris@0
|
490 }
|
Chris@0
|
491 else {
|
Chris@0
|
492 // No valid context to append to.
|
Chris@17
|
493 $this->errors[] = new FormattableMarkup('The translation stream %uri contains an error: unexpected string on line %line.', $log_vars);
|
Chris@0
|
494 return FALSE;
|
Chris@0
|
495 }
|
Chris@0
|
496 return;
|
Chris@0
|
497 }
|
Chris@0
|
498 }
|
Chris@0
|
499
|
Chris@0
|
500 // Empty line read or EOF of PO stream, close out the last entry.
|
Chris@17
|
501 if (($this->context == 'MSGSTR') || ($this->context == 'MSGSTR_ARR')) {
|
Chris@17
|
502 $this->setItemFromArray($this->currentItem);
|
Chris@17
|
503 $this->currentItem = [];
|
Chris@0
|
504 }
|
Chris@17
|
505 elseif ($this->context != 'COMMENT') {
|
Chris@17
|
506 $this->errors[] = new FormattableMarkup('The translation stream %uri ended unexpectedly at line %line.', $log_vars);
|
Chris@0
|
507 return FALSE;
|
Chris@0
|
508 }
|
Chris@14
|
509
|
Chris@14
|
510 return;
|
Chris@0
|
511 }
|
Chris@0
|
512
|
Chris@0
|
513 /**
|
Chris@0
|
514 * Store the parsed values as a PoItem object.
|
Chris@0
|
515 */
|
Chris@0
|
516 public function setItemFromArray($value) {
|
Chris@0
|
517 $plural = FALSE;
|
Chris@0
|
518
|
Chris@0
|
519 $comments = '';
|
Chris@0
|
520 if (isset($value['#'])) {
|
Chris@0
|
521 $comments = $this->shortenComments($value['#']);
|
Chris@0
|
522 }
|
Chris@0
|
523
|
Chris@0
|
524 if (is_array($value['msgstr'])) {
|
Chris@0
|
525 // Sort plural variants by their form index.
|
Chris@0
|
526 ksort($value['msgstr']);
|
Chris@0
|
527 $plural = TRUE;
|
Chris@0
|
528 }
|
Chris@0
|
529
|
Chris@0
|
530 $item = new PoItem();
|
Chris@0
|
531 $item->setContext(isset($value['msgctxt']) ? $value['msgctxt'] : '');
|
Chris@0
|
532 $item->setSource($value['msgid']);
|
Chris@0
|
533 $item->setTranslation($value['msgstr']);
|
Chris@0
|
534 $item->setPlural($plural);
|
Chris@0
|
535 $item->setComment($comments);
|
Chris@17
|
536 $item->setLangcode($this->langcode);
|
Chris@0
|
537
|
Chris@17
|
538 $this->lastItem = $item;
|
Chris@0
|
539
|
Chris@17
|
540 $this->context = 'COMMENT';
|
Chris@0
|
541 }
|
Chris@0
|
542
|
Chris@0
|
543 /**
|
Chris@0
|
544 * Parses a string in quotes.
|
Chris@0
|
545 *
|
Chris@0
|
546 * @param $string
|
Chris@0
|
547 * A string specified with enclosing quotes.
|
Chris@0
|
548 *
|
Chris@0
|
549 * @return
|
Chris@0
|
550 * The string parsed from inside the quotes.
|
Chris@0
|
551 */
|
Chris@0
|
552 public function parseQuoted($string) {
|
Chris@0
|
553 if (substr($string, 0, 1) != substr($string, -1, 1)) {
|
Chris@0
|
554 // Start and end quotes must be the same.
|
Chris@0
|
555 return FALSE;
|
Chris@0
|
556 }
|
Chris@0
|
557 $quote = substr($string, 0, 1);
|
Chris@0
|
558 $string = substr($string, 1, -1);
|
Chris@0
|
559 if ($quote == '"') {
|
Chris@0
|
560 // Double quotes: strip slashes.
|
Chris@0
|
561 return stripcslashes($string);
|
Chris@0
|
562 }
|
Chris@0
|
563 elseif ($quote == "'") {
|
Chris@0
|
564 // Simple quote: return as-is.
|
Chris@0
|
565 return $string;
|
Chris@0
|
566 }
|
Chris@0
|
567 else {
|
Chris@0
|
568 // Unrecognized quote.
|
Chris@0
|
569 return FALSE;
|
Chris@0
|
570 }
|
Chris@0
|
571 }
|
Chris@0
|
572
|
Chris@0
|
573 /**
|
Chris@0
|
574 * Generates a short, one-string version of the passed comment array.
|
Chris@0
|
575 *
|
Chris@0
|
576 * @param $comment
|
Chris@0
|
577 * An array of strings containing a comment.
|
Chris@0
|
578 *
|
Chris@0
|
579 * @return
|
Chris@0
|
580 * Short one-string version of the comment.
|
Chris@0
|
581 */
|
Chris@0
|
582 private function shortenComments($comment) {
|
Chris@0
|
583 $comm = '';
|
Chris@0
|
584 while (count($comment)) {
|
Chris@0
|
585 $test = $comm . substr(array_shift($comment), 1) . ', ';
|
Chris@0
|
586 if (strlen($comm) < 130) {
|
Chris@0
|
587 $comm = $test;
|
Chris@0
|
588 }
|
Chris@0
|
589 else {
|
Chris@0
|
590 break;
|
Chris@0
|
591 }
|
Chris@0
|
592 }
|
Chris@0
|
593 return trim(substr($comm, 0, -2));
|
Chris@0
|
594 }
|
Chris@0
|
595
|
Chris@0
|
596 }
|