Completed
Pull Request — master (#58)
by
unknown
01:56
created

PoParser::eol()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
rs 10
cc 2
eloc 2
nc 2
nop 0
1
<?php
2
3
namespace Sepia;
4
5
/**
6
 *    Copyright (c) 2012 Raúl Ferràs [email protected]
7
 *    All rights reserved.
8
 *
9
 *    Redistribution and use in source and binary forms, with or without
10
 *    modification, are permitted provided that the following conditions
11
 *    are met:
12
 *    1. Redistributions of source code must retain the above copyright
13
 *       notice, this list of conditions and the following disclaimer.
14
 *    2. Redistributions in binary form must reproduce the above copyright
15
 *       notice, this list of conditions and the following disclaimer in the
16
 *       documentation and/or other materials provided with the distribution.
17
 *    3. Neither the name of copyright holders nor the names of its
18
 *       contributors may be used to endorse or promote products derived
19
 *       from this software without specific prior written permission.
20
 *
21
 *    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22
 *    ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
23
 *    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24
 *    PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS
25
 *    BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26
 *    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27
 *    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28
 *    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29
 *    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30
 *    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31
 *    POSSIBILITY OF SUCH DAMAGE.
32
 *
33
 * https://github.com/raulferras/PHP-po-parser
34
 *
35
 * Class to parse .po file and extract its strings.
36
 *
37
 * @method array headers() deprecated
38
 * @method null update_entry($original, $translation = null, $tcomment = array(), $ccomment = array()) deprecated
39
 * @method array read($filePath) deprecated
40
 * @version 5.0.0
41
 */
42
class PoParser
43
{
44
    protected $entries = array();
45
    protected $headers = array();
46
    protected $sourceHandle = null;
47
    protected $options = array();
48
    protected $lineEndings = array( 'unix'=>"\n", 'win'=>"\r\n" );
49
50
51
52
    /**
53
     * Reads and parses a string
54
     *
55
     * @param string po content
56
     * @param array $options
57
     * @throws \Exception.
58
     * @return array. List of entries found in string po formatted
0 ignored issues
show
Documentation introduced by
The doc-type array. could not be parsed: Unknown type name "array." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
59
     */
60
    public static function parseString($string, $options=array())
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$options" and equals sign; expected 1 but found 0
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$options"; expected 1 but found 0
Loading history...
61
    {
62
        $parser = new PoParser(new StringHandler($string), $options);
63
        $parser->parse();
64
        return $parser;
65
    }
66
67
68
69
   /**
70
     * Reads and parses a file
71
     *
72
     * @param string $filepath
73
     * @param array $options
74
     * @throws \Exception.
75
     * @return array. List of entries found in string po formatted
0 ignored issues
show
Documentation introduced by
The doc-type array. could not be parsed: Unknown type name "array." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
76
     */
77
    public static function parseFile($filepath, $options=array())
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$options" and equals sign; expected 1 but found 0
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$options"; expected 1 but found 0
Loading history...
78
    {
79
        $parser = new PoParser(new FileHandler($filepath), $options);
80
        $parser->parse();
81
        return $parser;
82
    }
83
84
85
    public function __construct(InterfaceHandler $handler=null, $options=array())
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$handler" and equals sign; expected 1 but found 0
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$handler"; expected 1 but found 0
Loading history...
Coding Style introduced by
Incorrect spacing between argument "$options" and equals sign; expected 1 but found 0
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$options"; expected 1 but found 0
Loading history...
86
    {
87
        $this->sourceHandle = $handler;
88
        $defaultOptions = array(
89
            'multiline-glue'=>'<##EOL##>',  // Token used to separate lines in msgid
90
            'context-glue'  => '<##EOC##>',  // Token used to separate ctxt from msgid
91
            'line-ending'   => 'unix'
92
        );
93
        $this->options = array_merge($defaultOptions, $options);
94
    }
95
96
97
    public function getOptions()
98
    {
99
        return $this->options;
100
    }
101
102
103
    public function getEntries()
104
    {
105
        return $this->entries;
106
    }
107
108
    /**
109
     * Reads and parses strings of a .po file.
110
     *
111
     * @param InterfaceHandler. Optional
112
     * @throws \Exception, \InvalidArgumentException
113
     * @return array. List of entries found in .po file.
0 ignored issues
show
Documentation introduced by
The doc-type array. could not be parsed: Unknown type name "array." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
114
     */
115
    public function parse(InterfaceHandler $handle=null )
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$handle" and equals sign; expected 1 but found 0
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$handle"; expected 1 but found 0
Loading history...
Coding Style introduced by
Expected 0 spaces between argument "$handle" and closing bracket; 1 found
Loading history...
116
    {
117
        if ($handle===null) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
118
119
            if ($this->sourceHandle===null) {
120
                throw new \InvalidArgumentException('Must provide a valid InterfaceHandler');
121
            }
122
            else {
123
                $handle = $this->sourceHandle;
124
            }
125
        }
126
127
        $headers         = array();
128
        $hash            = array();
129
        $entry           = array();
130
        $justNewEntry    = false; // A new entry has been just inserted.
131
        $firstLine       = true;
132
        $lastPreviousKey = null; // Used to remember last key in a multiline previous entry.
133
        $state           = null;
134
        $lineNumber      = 0;
135
136
        while (!$handle->ended()) {
137
            $line  = trim($handle->getNextLine());
138
            $split = preg_split('/\s+/ ', $line, 2);
139
            $key   = $split[0];
140
141
            // If a blank line is found, or a new msgid when already got one
142
            if ($line === '' || ($key=='msgid' && isset($entry['msgid']))) {
143
                // Two consecutive blank lines
144
                if ($justNewEntry) {
145
                    $lineNumber++;
146
                    continue;
147
                }
148
149
                if ($firstLine) {
150
                    $firstLine = false;
151
                    if (self::isHeader($entry)) {
152
                        array_shift($entry['msgstr']);
153
                        $headers = $entry['msgstr'];
154
                    } else {
155
                        $hash[] = $entry;
156
                    }
157
                } else {
158
                    // A new entry is found!
159
                    $hash[] = $entry;
160
                }
161
162
                $entry           = array();
163
                $state           = null;
164
                $justNewEntry    = true;
165
                $lastPreviousKey = null;
166
                if ($line==='') {
167
                    $lineNumber++;
168
                    continue;
169
                }
170
            }
171
172
            $justNewEntry = false;
173
            $data         = isset($split[1]) ? $split[1] : null;
174
175
            switch ($key) {
176
                // Flagged translation
177
                case '#,':
178
                    $entry['flags'] = preg_split('/,\s*/', $data);
179
                    break;
180
181
                // # Translator comments
182
                case '#':
183
                    $entry['tcomment'] = !isset($entry['tcomment']) ? array() : $entry['tcomment'];
184
                    $entry['tcomment'][] = $data;
185
                    break;
186
187
                // #. Comments extracted from source code
188
                case '#.':
189
                    $entry['ccomment'] = !isset($entry['ccomment']) ? array() : $entry['ccomment'];
190
                    $entry['ccomment'][] = $data;
191
                    break;
192
193
                // Reference
194
                case '#:':
195
                    $entry['reference'][] = addslashes($data);
196
                    break;
197
198
199
                case '#|':      // #| Previous untranslated string
200
                case '#~':      // #~ Old entry
201
                case '#~|':     // #~| Previous-Old untranslated string. Reported by @Cellard
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
202
203
                    switch ($key) {
204
                        case '#|':  $key = 'previous';
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
205
                                    break;
206
                        case '#~':  $key = 'obsolete';
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
207
                                    break;
208
                        case '#~|': $key = 'previous-obsolete';
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
209
                                    break;
210
                    }
211
212
                    $tmpParts = explode(' ', $data);
213
                    $tmpKey   = $tmpParts[0];
214
215
                    if (!in_array($tmpKey, array('msgid','msgid_plural','msgstr','msgctxt'))) {
216
                        $tmpKey = $lastPreviousKey; // If there is a multiline previous string we must remember what key was first line.
217
                        $str = $data;
218
                    } else {
219
                        $str = implode(' ', array_slice($tmpParts, 1));
220
                    }
221
222
                    $entry[$key] = isset($entry[$key])? $entry[$key]:array('msgid'=>array(),'msgstr'=>array());
223
224
                    if (strpos($key, 'obsolete')!==false) {
225
                        $entry['obsolete'] = true;
226
                        switch ($tmpKey) {
227
                            case 'msgid':
228
                                $entry['msgid'][] = $str;
229
                                $lastPreviousKey = $tmpKey;
230
                                break;
231
232
                            case 'msgstr':
233
                                if ($str == "\"\"") {
234
                                    $entry['msgstr'][] = trim($str, '"');
235
                                } else {
236
                                    $entry['msgstr'][] = $str;
237
                                }
238
                                $lastPreviousKey = $tmpKey;
239
                                break;
240
241
                            default:
242
                                break;
243
                        }
244
                    }
245
246
                    if ($key!=='obsolete') {
247
                        switch ($tmpKey) {
248
                            case 'msgid':
249
                            case 'msgid_plural':
250
                            case 'msgstr':
251
                                $entry[$key][$tmpKey][] = $str;
252
                                $lastPreviousKey = $tmpKey;
253
                                break;
254
255
                            default:
256
                                $entry[$key][$tmpKey] = $str;
257
                                break;
258
                        }
259
                    }
260
                    break;
261
262
263
                // context
264
                // Allows disambiguations of different messages that have same msgid.
265
                // Example:
266
                //
267
                // #: tools/observinglist.cpp:700
268
                // msgctxt "First letter in 'Scope'"
269
                // msgid "S"
270
                // msgstr ""
271
                //
272
                // #: skycomponents/horizoncomponent.cpp:429
273
                // msgctxt "South"
274
                // msgid "S"
275
                // msgstr ""
276
                case 'msgctxt':
277
                    // untranslated-string
278
                case 'msgid':
279
                    // untranslated-string-plural
280
                case 'msgid_plural':
281
                    $state = $key;
282
                    $entry[$state][] = $data;
283
                    break;
284
                // translated-string
285
                case 'msgstr':
286
                    $state = 'msgstr';
287
                    $entry[$state][] = $data;
288
                    break;
289
290
                default:
291
                    if (strpos($key, 'msgstr[') !== false) {
292
                        // translated-string-case-n
293
                        $state = $key;
294
                        $entry[$state][] = $data;
295
                    } else {
296
                        // "multiline" lines
297
                        switch ($state) {
298
                            case 'msgctxt':
299
                            case 'msgid':
300
                            case 'msgid_plural':
301
                            case (strpos($state, 'msgstr[') !== false):
302
                                if (is_string($entry[$state])) {
303
                                    // Convert it to array
304
                                    $entry[$state] = array($entry[$state]);
305
                                }
306
                                $entry[$state][] = $line;
307
                                break;
308
309
                            case 'msgstr':
310
                                // Special fix where msgid is ""
311
                                if ($entry['msgid'] == "\"\"") {
312
                                    $entry['msgstr'][] = trim($line, '"');
313
                                } else {
314
                                    $entry['msgstr'][] = $line;
315
                                }
316
                                break;
317
318
                            default:
319
                                throw new \Exception(
320
                                    'PoParser: Parse error! Unknown key "' . $key . '" on line ' . ($lineNumber+1)
321
                                );
322
                        }
323
                    }
324
                    break;
325
            }
326
327
            $lineNumber++;
328
        }
329
        $handle->close();
330
331
        // add final entry
332
        if ($state == 'msgstr') {
333
            $hash[] = $entry;
334
        }
335
336
        // - Cleanup header data
337
        $this->headers = array();
338
        foreach ($headers as $header) {
339
            $header = $this->clean( $header );
340
            $this->headers[] = "\"" . preg_replace("/\\n/", '\n', $header) . "\"";
341
        }
342
343
        // - Cleanup data,
344
        // - merge multiline entries
345
        // - Reindex hash for ksort
346
        $temp = $hash;
347
        $this->entries = array();
348
        foreach ($temp as $entry) {
349
            foreach ($entry as &$v) {
350
                $or = $v;
351
                $v = $this->clean($v);
352
                if ($v === false) {
353
                    // parse error
354
                    throw new \Exception(
355
                        'PoParser: Parse error! poparser::clean returned false on "' . htmlspecialchars($or) . '"'
356
                    );
357
                }
358
            }
359
360
            // check if msgid and a key starting with msgstr exists
361
            if (isset($entry['msgid']) && count(preg_grep('/^msgstr/', array_keys($entry)))) {
362
                $id = $this->getEntryId($entry);
363
                $this->entries[$id] = $entry;
364
            }
365
        }
366
367
        return $this->entries;
368
    }
369
370
    /**
371
     * Get headers from .po file
372
     *
373
     * @return array
374
     */
375
    public function getHeaders()
376
    {
377
        return $this->headers;
378
    }
379
380
    /**
381
     * Set new headers
382
     *
383
     * {code}
384
     *  array(
385
     *   '"Project-Id-Version: \n"',
386
     *   '"Report-Msgid-Bugs-To: \n"',
387
     *   '"POT-Creation-Date: \n"',
388
     *   '"PO-Revision-Date: \n"',
389
     *   '"Last-Translator: none\n"',
390
     *   '"Language-Team: \n"',
391
     *   '"MIME-Version: 1.0\n"',
392
     *   '"Content-Type: text/plain; charset=UTF-8\n"',
393
     *  );
394
     * {code}
395
     *
396
     * @param array $newHeaders
397
     * @return bool
398
     */
399
    public function setHeaders($newHeaders)
400
    {
401
        if (!is_array($newHeaders)) {
402
            return false;
403
        } else {
404
            $this->headers = $newHeaders;
405
            return true;
406
        }
407
    }
408
409
410
    /**
411
     * Updates an entry.
412
     * If entry not found returns false. If $createNew is true, a new entry will be created.
413
     * $entry is an array that can contain following indexes:
414
     *  - msgid: String Array. Required.
415
     *  - msgstr: String Array. Required.
416
     *  - reference: String Array.
417
     *  - msgctxt: String. Disambiguating context.
418
     *  - tcomment: String Array. Translator comments.
419
     *  - ccomment: String Array. Source comments.
420
     *  - msgid_plural: String Array.
421
     *  - flags: Array. List of entry flags. Example: array('fuzzy','php-format')
422
     *  - previous: Array: Contains previous untranslated strings in a sub array with msgid and msgstr.
423
     *
424
     * @param String  $msgid     Id of entry. Be aware that some entries have a multiline msgid. In that case \n must be replaced by the value of 'multiline-glue' option (by default "<##EOL##>").
425
     * @param Array   $entry     Array with all entry data. Fields not setted will be removed.
426
     * @param boolean $createNew If msgid not found, it will create a new entry. By default true. You want to set this to false if need to change the msgid of an entry.
427
     */
428
    public function setEntry($msgid, $entry, $createNew = true)
429
    {
430
        // In case of new entry
431
        if (!isset($this->entries[$msgid])) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
432
433
            if ($createNew==false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
434
                return;
435
            }
436
437
            $this->entries[$msgid] = $entry;
438
        }
439
        else {
440
            // Be able to change msgid.
441
            if( $msgid!==$entry['msgid'] ) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after IF keyword; 0 found
Loading history...
Coding Style introduced by
Expected 0 spaces before closing bracket; 1 found
Loading history...
442
                unset($this->entries[$msgid]);
443
                $new_msgid = is_array($entry['msgid'])? implode($this->options['multiline-glue'], $entry['msgid']):$entry['msgid'];
444
                $this->entries[$new_msgid] = $entry;
445
            }
446
            else {
447
                $this->entries[$msgid] = $entry;
448
            }
449
        }
450
    }
451
452
453
    public function setEntryPlural($msgid, $plural = false)
454
    {
455
        if ($plural) {
456
            $this->entries[$msgid]['msgid_plural'] = $plural;
457
        } else {
458
            unset($this->entries[$msgid]['msgid_plural']);
459
        }
460
    }
461
462
    public function setEntryContext($msgid, $context = false)
463
    {
464
        if ($context) {
465
            $this->entries[$msgid]['msgctxt'][0] = $context;
466
        } else {
467
            unset($this->entries[$msgid]['msgctxt']);
468
        }
469
    }
470
471
472
    /**
473
    *   Gets entries.
474
    */
475
    public function entries()
476
    {
477
        return $this->entries;
478
    }
479
480
481
482
483
484
    /**
485
     *  Writes entries to a po file
486
     *
487
     * @example
488
     *        $pofile = new PoParser();
489
     *        $pofile->parse('ca.po');
490
     *
491
     *        // Modify an antry
492
     *        $pofile->updateEntry( $msgid, $msgstr );
493
     *        // Save Changes back into `ca.po`
494
     *        $pofile->write('ca.po');
495
     * @param string $filepath
496
     * @throws \Exception
497
     * @return boolean
498
    */
499
    public function writeFile($filepath)
500
    {
501
        $output = $this->compile();
502
        $result = file_put_contents($filepath, $output);
503
        if ($result===false) {
504
            throw new \Exception('Could not write into file '.htmlspecialchars($filepath));
505
        }
506
        return true;
507
    }
508
509
510
511
512
    /**
513
     * Compiles entries into a string
514
     *
515
     * @throws \Exception
516
     * @return string
517
     */
518
    public function compile()
519
    {
520
        $output = '';
521
522
        if (count($this->headers) > 0) {
523
            $output.= "msgid \"\"".$this->eol();
524
            $output.= "msgstr \"\"".$this->eol();
525
            foreach ($this->headers as $header) {
526
                $output.= $header . $this->eol();
527
            }
528
            $output.= $this->eol();
529
        }
530
531
532
        $entriesCount = count($this->entries);
533
        $counter = 0;
534
        foreach ($this->entries as $entry) {
535
            $isObsolete = isset($entry['obsolete']) && $entry['obsolete'];
536
            $isPlural = isset($entry['msgid_plural']);
537
538
            if (isset($entry['previous'])) {
539
                foreach ($entry['previous'] as $key => $data) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
540
541
                    if (is_string($data)) {
542
                        $output.= "#| " . $key . " " . $this->cleanExport($data) . $this->eol();
543
                    } elseif (is_array($data) && count($data)>0) {
544
                        foreach ($data as $line) {
545
                            $output.= "#| " . $key . " " . $this->cleanExport($line) . $this->eol();
546
                        }
547
                    }
548
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
549
                }
550
            }
551
552
            if (isset($entry['tcomment'])) {
553
                foreach ($entry['tcomment'] as $comment) {
554
                    $output.= "# " . $comment . $this->eol();
555
                }
556
            }
557
558
            if (isset($entry['ccomment'])) {
559
                foreach ($entry['ccomment'] as $comment) {
560
                    $output.= '#. ' . $comment . $this->eol();
561
                }
562
            }
563
564
            if (isset($entry['reference'])) {
565
                foreach ($entry['reference'] as $ref) {
566
                    $output.= '#: ' . $ref . $this->eol();
567
                }
568
            }
569
570
            if (isset($entry['flags']) && !empty($entry['flags'])) {
571
                $output.= "#, " . implode(', ', $entry['flags']) . $this->eol();
572
            }
573
574
            if (isset($entry['@'])) {
575
                $output.= "#@ " . $entry['@'] . $this->eol();
576
            }
577
578
            if (isset($entry['msgctxt'])) {
579
                $output.= 'msgctxt ' . $this->cleanExport($entry['msgctxt'][0]) . $this->eol();
580
            }
581
582
583
            if ($isObsolete) {
584
                $output.= "#~ ";
585
            }
586
587
            if (isset($entry['msgid'])) {
588
                // Special clean for msgid
589
                if (is_string($entry['msgid'])) {
590
                    $msgid = explode($this->eol(), $entry['msgid']);
591
                } elseif (is_array($entry['msgid'])) {
592
                    $msgid = $entry['msgid'];
593
                } else {
594
                    throw new \Exception('msgid not string or array');
595
                }
596
597
                $output.= 'msgid ';
598
                foreach ($msgid as $i => $id) {
599
                    if ($i > 0 && $isObsolete) {
600
                        $output.= "#~ ";
601
                    }
602
                    $output.= $this->cleanExport($id) . $this->eol();
603
                }
604
            }
605
606
            if (isset($entry['msgid_plural'])) {
607
                // Special clean for msgid_plural
608
                if (is_string($entry['msgid_plural'])) {
609
                    $msgidPlural = explode($this->eol(), $entry['msgid_plural']);
610
                } elseif (is_array($entry['msgid_plural'])) {
611
                    $msgidPlural = $entry['msgid_plural'];
612
                } else {
613
                    throw new \Exception('msgid_plural not string or array');
614
                }
615
616
                $output.= 'msgid_plural ';
617
                foreach ($msgidPlural as $plural) {
618
                    $output.= $this->cleanExport($plural) . $this->eol();
619
                }
620
            }
621
622
            if (count(preg_grep('/^msgstr/', array_keys($entry)))) { // checks if there is a key starting with msgstr
623
                if ($isPlural) {
624
                    $noTranslation = true;
625
                    foreach ($entry as $key => $value) {
626
                        if (strpos($key, 'msgstr[') === false) continue;
0 ignored issues
show
Coding Style Best Practice introduced by
It is generally a best practice to always use braces with control structures.

Adding braces to control structures avoids accidental mistakes as your code changes:

// Without braces (not recommended)
if (true)
    doSomething();

// Recommended
if (true) {
    doSomething();
}
Loading history...
627
                        $output.= $key." ";
628
                        $noTranslation = false;
629
                        foreach ($value as $i => $t) {
630
                            $output.= $this->cleanExport($t) . $this->eol();
631
                        }
632
                    }
633
                    if ($noTranslation) {
634
                        $output.= 'msgstr[0] '.$this->cleanExport('').$this->eol();
635
                        $output.= 'msgstr[1] '.$this->cleanExport('').$this->eol();
636
                    }
637
                } else {
638
                    foreach ((array)$entry['msgstr'] as $i => $t) {
639
                        if ($i == 0) {
640
                            if ($isObsolete) {
641
                                $output.= "#~ ";
642
                            }
643
644
                            $output.= 'msgstr ' . $this->cleanExport($t) . $this->eol();
645
                        } else {
646
                            if ($isObsolete) {
647
                                $output.= "#~ ";
648
                            }
649
650
                            $output.= $this->cleanExport($t) . $this->eol();
651
                        }
652
                    }
653
                }
654
            }
655
656
            $counter++;
657
            // Avoid inserting an extra newline at end of file
658
            if ($counter < $entriesCount) {
659
                $output.= $this->eol();
660
            }
661
        }
662
663
        return $output;
664
    }
665
666
    /**
667
     * Returns configured line ending (option 'line-ending' ['win', 'unix'])
668
     *
669
     *
670
     * @return string
671
     */
672
    protected function eol()
673
    {
674
        return $this->lineEndings[$this->options['line-ending']]?:"\n";
675
    }
676
677
    /**
678
     * Prepares a string to be outputed into a file.
679
     *
680
     * @param string $string The string to be converted.
681
     * @return string
682
     */
683
    protected function cleanExport($string)
684
    {
685
        $quote = '"';
686
        $slash = '\\';
687
        $newline = "\n";
688
689
        $replaces = array(
690
            "$slash" => "$slash$slash",
691
            "$quote" => "$slash$quote",
692
            "\t" => '\t',
693
        );
694
695
        $string = str_replace(array_keys($replaces), array_values($replaces), $string);
696
697
        $po = $quote . implode("${slash}n$quote$newline$quote", explode($newline, $string)) . $quote;
698
699
        // remove empty strings
700
        return str_replace("$newline$quote$quote", '', $po);
701
    }
702
703
704
    /**
705
     * Generates the internal key for a msgid.
706
     *
707
     * @param array $entry
708
     * @return string
709
     */
710
    protected function getEntryId(array $entry)
711
    {
712
        if (isset($entry['msgctxt'])) {
713
            $id = implode($this->options['multiline-glue'], (array)$entry['msgctxt']) . $this->options['context-glue'] . implode($this->options['multiline-glue'], (array)$entry['msgid']);
714
        } else {
715
            $id = implode($this->options['multiline-glue'], (array)$entry['msgid']);
716
        }
717
718
        return $id;
719
    }
720
721
722
    /**
723
     * Undos `cleanExport` actions on a string.
724
     *
725
     * @param string|array $x
726
     * @return string|array.
0 ignored issues
show
Documentation introduced by
The doc-type string|array. could not be parsed: Unknown type name "array." at position 7. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
727
     */
728
    protected function clean($x)
729
    {
730
        if (is_array($x)) {
731
            foreach ($x as $k => $v) {
732
                $x[$k] = $this->clean($v);
733
            }
734
        } else {
735
            // Remove double quotes from start and end of string
736
            if ($x == '') {
737
                return '';
738
            }
739
740
            if ($x[0] == '"') {
741
                $x = substr($x, 1, -1);
742
            }
743
744
            // Escapes C-style escape sequences (\a,\b,\f,\n,\r,\t,\v) and converts them to their actual meaning
745
            $x = stripcslashes($x);
746
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
747
        }
748
749
        return $x;
750
    }
751
752
753
    /**
754
     * Checks if entry is a header by
755
     *
756
     * @param array $entry
757
     * @return bool
758
     */
759
    protected static function isHeader(array $entry)
760
    {
761
        if (empty($entry) || !isset($entry['msgstr'])) {
762
            return false;
763
        }
764
765
        $headerKeys = array(
766
            'Project-Id-Version:' => false,
767
            //  'Report-Msgid-Bugs-To:' => false,
768
            //  'POT-Creation-Date:'    => false,
769
            'PO-Revision-Date:' => false,
770
            //  'Last-Translator:'      => false,
771
            //  'Language-Team:'        => false,
772
            'MIME-Version:' => false,
773
            //  'Content-Type:'         => false,
774
            //  'Content-Transfer-Encoding:' => false,
775
            //  'Plural-Forms:'         => false
776
        );
777
        $count = count($headerKeys);
778
        $keys = array_keys($headerKeys);
779
780
        $headerItems = 0;
781
        foreach ($entry['msgstr'] as $str) {
782
            $tokens = explode(':', $str);
783
            $tokens[0] = trim($tokens[0], "\"") . ':';
784
785
            if (in_array($tokens[0], $keys)) {
786
                $headerItems++;
787
                unset($headerKeys[$tokens[0]]);
788
                $keys = array_keys($headerKeys);
789
            }
790
        }
791
        return ($headerItems == $count) ? true : false;
792
    }
793
}
794