Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / lib / Drupal / Component / Gettext / PoHeader.php
1 <?php
2
3 namespace Drupal\Component\Gettext;
4
5 /**
6  * Gettext PO header handler.
7  *
8  * Possible Gettext PO header elements are explained in
9  * http://www.gnu.org/software/gettext/manual/gettext.html#Header-Entry,
10  * but we only support a subset of these directly.
11  *
12  * Example header:
13  *
14  * "Project-Id-Version: Drupal core (7.11)\n"
15  * "POT-Creation-Date: 2012-02-12 22:59+0000\n"
16  * "PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n"
17  * "Language-Team: Catalan\n"
18  * "MIME-Version: 1.0\n"
19  * "Content-Type: text/plain; charset=utf-8\n"
20  * "Content-Transfer-Encoding: 8bit\n"
21  * "Plural-Forms: nplurals=2; plural=(n>1);\n"
22  */
23 class PoHeader {
24
25   /**
26    * Language code.
27    *
28    * @var string
29    */
30   protected $langcode;
31
32   /**
33    * Formula for the plural form.
34    *
35    * @var string
36    */
37   protected $pluralForms;
38
39   /**
40    * Author(s) of the file.
41    *
42    * @var string
43    */
44   protected $authors;
45
46   /**
47    * Date the po file got created.
48    *
49    * @var string
50    */
51   protected $poDate;
52
53   /**
54    * Human readable language name.
55    *
56    * @var string
57    */
58   protected $languageName;
59
60   /**
61    * Name of the project the translation belongs to.
62    *
63    * @var string
64    */
65   protected $projectName;
66
67   /**
68    * Constructor, creates a PoHeader with default values.
69    *
70    * @param string $langcode
71    *   Language code.
72    */
73   public function __construct($langcode = NULL) {
74     $this->langcode = $langcode;
75     // Ignore errors when run during site installation before
76     // date_default_timezone_set() is called.
77     $this->poDate = @date("Y-m-d H:iO");
78     $this->pluralForms = 'nplurals=2; plural=(n > 1);';
79   }
80
81   /**
82    * Gets the plural form.
83    *
84    * @return string
85    *   Plural form component from the header, for example:
86    *   'nplurals=2; plural=(n > 1);'.
87    */
88   public function getPluralForms() {
89     return $this->pluralForms;
90   }
91
92   /**
93    * Set the human readable language name.
94    *
95    * @param string $languageName
96    *   Human readable language name.
97    */
98   public function setLanguageName($languageName) {
99     $this->languageName = $languageName;
100   }
101
102   /**
103    * Gets the human readable language name.
104    *
105    * @return string
106    *   The human readable language name.
107    */
108   public function getLanguageName() {
109     return $this->languageName;
110   }
111
112   /**
113    * Set the project name.
114    *
115    * @param string $projectName
116    *   Human readable project name.
117    */
118   public function setProjectName($projectName) {
119     $this->projectName = $projectName;
120   }
121
122   /**
123    * Gets the project name.
124    *
125    * @return string
126    *   The human readable project name.
127    */
128   public function getProjectName() {
129     return $this->projectName;
130   }
131
132   /**
133    * Populate internal values from a string.
134    *
135    * @param string $header
136    *   Full header string with key-value pairs.
137    */
138   public function setFromString($header) {
139     // Get an array of all header values for processing.
140     $values = $this->parseHeader($header);
141
142     // There is only one value relevant for our header implementation when
143     // reading, and that is the plural formula.
144     if (!empty($values['Plural-Forms'])) {
145       $this->pluralForms = $values['Plural-Forms'];
146     }
147   }
148
149   /**
150    * Generate a Gettext PO formatted header string based on data set earlier.
151    */
152   public function __toString() {
153     $output = '';
154
155     $isTemplate = empty($this->languageName);
156
157     $output .= '# ' . ($isTemplate ? 'LANGUAGE' : $this->languageName) . ' translation of ' . ($isTemplate ? 'PROJECT' : $this->projectName) . "\n";
158     if (!empty($this->authors)) {
159       $output .= '# Generated by ' . implode("\n# ", $this->authors) . "\n";
160     }
161     $output .= "#\n";
162
163     // Add the actual header information.
164     $output .= "msgid \"\"\n";
165     $output .= "msgstr \"\"\n";
166     $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
167     $output .= "\"POT-Creation-Date: " . $this->poDate . "\\n\"\n";
168     $output .= "\"PO-Revision-Date: " . $this->poDate . "\\n\"\n";
169     $output .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
170     $output .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n";
171     $output .= "\"MIME-Version: 1.0\\n\"\n";
172     $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
173     $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
174     $output .= "\"Plural-Forms: " . $this->pluralForms . "\\n\"\n";
175     $output .= "\n";
176
177     return $output;
178   }
179
180   /**
181    * Parses a Plural-Forms entry from a Gettext Portable Object file header.
182    *
183    * @param string $pluralforms
184    *   The Plural-Forms entry value.
185    *
186    * @return
187    *   An indexed array of parsed plural formula data. Containing:
188    *   - 'nplurals': The number of plural forms defined by the plural formula.
189    *   - 'plurals': Array of plural positions keyed by plural value.
190    *
191    * @throws \Exception
192    */
193   public function parsePluralForms($pluralforms) {
194     $plurals = [];
195     // First, delete all whitespace.
196     $pluralforms = strtr($pluralforms, [" " => "", "\t" => ""]);
197
198     // Select the parts that define nplurals and plural.
199     $nplurals = strstr($pluralforms, "nplurals=");
200     if (strpos($nplurals, ";")) {
201       // We want the string from the 10th char, because "nplurals=" length is 9.
202       $nplurals = substr($nplurals, 9, strpos($nplurals, ";") - 9);
203     }
204     else {
205       return FALSE;
206     }
207     $plural = strstr($pluralforms, "plural=");
208     if (strpos($plural, ";")) {
209       // We want the string from the 8th char, because "plural=" length is 7.
210       $plural = substr($plural, 7, strpos($plural, ";") - 7);
211     }
212     else {
213       return FALSE;
214     }
215
216     // If the number of plurals is zero, we return a default result.
217     if ($nplurals == 0) {
218       return [$nplurals, ['default' => 0]];
219     }
220
221     // Calculate possible plural positions of different plural values. All known
222     // plural formula's are repetitive above 100.
223     // For data compression we store the last position the array value
224     // changes and store it as default.
225     $element_stack = $this->parseArithmetic($plural);
226     if ($element_stack !== FALSE) {
227       for ($i = 0; $i <= 199; $i++) {
228         $plurals[$i] = $this->evaluatePlural($element_stack, $i);
229       }
230       $default = $plurals[$i - 1];
231       $plurals = array_filter($plurals, function ($value) use ($default) {
232         return ($value != $default);
233       });
234       $plurals['default'] = $default;
235
236       return [$nplurals, $plurals];
237     }
238     else {
239       throw new \Exception('The plural formula could not be parsed.');
240     }
241   }
242
243   /**
244    * Parses a Gettext Portable Object file header.
245    *
246    * @param string $header
247    *   A string containing the complete header.
248    *
249    * @return array
250    *   An associative array of key-value pairs.
251    */
252   private function parseHeader($header) {
253     $header_parsed = [];
254     $lines = array_map('trim', explode("\n", $header));
255     foreach ($lines as $line) {
256       if ($line) {
257         list($tag, $contents) = explode(":", $line, 2);
258         $header_parsed[trim($tag)] = trim($contents);
259       }
260     }
261     return $header_parsed;
262   }
263
264   /**
265    * Parses and sanitizes an arithmetic formula into a plural element stack.
266    *
267    * While parsing, we ensure, that the operators have the right
268    * precedence and associativity.
269    *
270    * @param string $string
271    *   A string containing the arithmetic formula.
272    *
273    * @return
274    *   A stack of values and operations to be evaluated.
275    */
276   private function parseArithmetic($string) {
277     // Operator precedence table.
278     $precedence = ["(" => -1, ")" => -1, "?" => 1, ":" => 1, "||" => 3, "&&" => 4, "==" => 5, "!=" => 5, "<" => 6, ">" => 6, "<=" => 6, ">=" => 6, "+" => 7, "-" => 7, "*" => 8, "/" => 8, "%" => 8];
279     // Right associativity.
280     $right_associativity = ["?" => 1, ":" => 1];
281
282     $tokens = $this->tokenizeFormula($string);
283
284     // Parse by converting into infix notation then back into postfix
285     // Operator stack - holds math operators and symbols.
286     $operator_stack = [];
287     // Element Stack - holds data to be operated on.
288     $element_stack = [];
289
290     foreach ($tokens as $token) {
291       $current_token = $token;
292
293       // Numbers and the $n variable are simply pushed into $element_stack.
294       if (is_numeric($token)) {
295         $element_stack[] = $current_token;
296       }
297       elseif ($current_token == "n") {
298         $element_stack[] = '$n';
299       }
300       elseif ($current_token == "(") {
301         $operator_stack[] = $current_token;
302       }
303       elseif ($current_token == ")") {
304         $topop = array_pop($operator_stack);
305         while (isset($topop) && ($topop != "(")) {
306           $element_stack[] = $topop;
307           $topop = array_pop($operator_stack);
308         }
309       }
310       elseif (!empty($precedence[$current_token])) {
311         // If it's an operator, then pop from $operator_stack into
312         // $element_stack until the precedence in $operator_stack is less
313         // than current, then push into $operator_stack.
314         $topop = array_pop($operator_stack);
315         while (isset($topop) && ($precedence[$topop] >= $precedence[$current_token]) && !(($precedence[$topop] == $precedence[$current_token]) && !empty($right_associativity[$topop]) && !empty($right_associativity[$current_token]))) {
316           $element_stack[] = $topop;
317           $topop = array_pop($operator_stack);
318         }
319         if ($topop) {
320           // Return element to top.
321           $operator_stack[] = $topop;
322         }
323         // Parentheses are not needed.
324         $operator_stack[] = $current_token;
325       }
326       else {
327         return FALSE;
328       }
329     }
330
331     // Flush operator stack.
332     $topop = array_pop($operator_stack);
333     while ($topop != NULL) {
334       $element_stack[] = $topop;
335       $topop = array_pop($operator_stack);
336     }
337     $return = $element_stack;
338
339     // Now validate stack.
340     $previous_size = count($element_stack) + 1;
341     while (count($element_stack) < $previous_size) {
342       $previous_size = count($element_stack);
343       for ($i = 2; $i < count($element_stack); $i++) {
344         $op = $element_stack[$i];
345         if (!empty($precedence[$op])) {
346           if ($op == ":") {
347             $f = $element_stack[$i - 2] . "):" . $element_stack[$i - 1] . ")";
348           }
349           elseif ($op == "?") {
350             $f = "(" . $element_stack[$i - 2] . "?(" . $element_stack[$i - 1];
351           }
352           else {
353             $f = "(" . $element_stack[$i - 2] . $op . $element_stack[$i - 1] . ")";
354           }
355           array_splice($element_stack, $i - 2, 3, $f);
356           break;
357         }
358       }
359     }
360
361     // If only one element is left, the number of operators is appropriate.
362     return count($element_stack) == 1 ? $return : FALSE;
363   }
364
365   /**
366    * Tokenize the formula.
367    *
368    * @param string $formula
369    *   A string containing the arithmetic formula.
370    *
371    * @return array
372    *   List of arithmetic tokens identified in the formula.
373    */
374   private function tokenizeFormula($formula) {
375     $formula = str_replace(" ", "", $formula);
376     $tokens = [];
377     for ($i = 0; $i < strlen($formula); $i++) {
378       if (is_numeric($formula[$i])) {
379         $num = $formula[$i];
380         $j = $i + 1;
381         while ($j < strlen($formula) && is_numeric($formula[$j])) {
382           $num .= $formula[$j];
383           $j++;
384         }
385         $i = $j - 1;
386         $tokens[] = $num;
387       }
388       elseif ($pos = strpos(" =<>!&|", $formula[$i])) {
389         $next = $formula[$i + 1];
390         switch ($pos) {
391           case 1:
392           case 2:
393           case 3:
394           case 4:
395             if ($next == '=') {
396               $tokens[] = $formula[$i] . '=';
397               $i++;
398             }
399             else {
400               $tokens[] = $formula[$i];
401             }
402             break;
403           case 5:
404             if ($next == '&') {
405               $tokens[] = '&&';
406               $i++;
407             }
408             else {
409               $tokens[] = $formula[$i];
410             }
411             break;
412           case 6:
413             if ($next == '|') {
414               $tokens[] = '||';
415               $i++;
416             }
417             else {
418               $tokens[] = $formula[$i];
419             }
420             break;
421         }
422       }
423       else {
424         $tokens[] = $formula[$i];
425       }
426     }
427     return $tokens;
428   }
429
430   /**
431    * Evaluate the plural element stack using a plural value.
432    *
433    * Using an element stack, which represents a plural formula, we calculate
434    * which plural string should be used for a given plural value.
435    *
436    * An example of plural formula parting and evaluation:
437    *   Plural formula: 'n!=1'
438    * This formula is parsed by parseArithmetic() to a stack (array) of elements:
439    *   array(
440    *     0 => '$n',
441    *     1 => '1',
442    *     2 => '!=',
443    *   );
444    * The evaluatePlural() method evaluates the $element_stack using the plural
445    * value $n. Before the actual evaluation, the '$n' in the array is replaced
446    * by the value of $n.
447    *   For example: $n = 2 results in:
448    *   array(
449    *     0 => '2',
450    *     1 => '1',
451    *     2 => '!=',
452    *   );
453    * The stack is processed until only one element is (the result) is left. In
454    * every iteration the top elements of the stack, up until the first operator,
455    * are evaluated. After evaluation the arguments and the operator itself are
456    * removed and replaced by the evaluation result. This is typically 2
457    * arguments and 1 element for the operator.
458    *   Because the operator is '!=' the example stack is evaluated as:
459    *   $f = (int) 2 != 1;
460    *   The resulting stack is:
461    *   array(
462    *     0 => 1,
463    *   );
464    * With only one element left in the stack (the final result) the loop is
465    * terminated and the result is returned.
466    *
467    * @param array $element_stack
468    *   Array of plural formula values and operators create by parseArithmetic().
469    * @param int $n
470    *   The @count number for which we are determining the right plural position.
471    *
472    * @return int
473    *   Number of the plural string to be used for the given plural value.
474    *
475    * @see parseArithmetic()
476    * @throws \Exception
477    */
478   protected function evaluatePlural($element_stack, $n) {
479     $count = count($element_stack);
480     $limit = $count;
481     // Replace the '$n' value in the formula by the plural value.
482     for ($i = 0; $i < $count; $i++) {
483       if ($element_stack[$i] === '$n') {
484         $element_stack[$i] = $n;
485       }
486     }
487
488     // We process the stack until only one element is (the result) is left.
489     // We limit the number of evaluation cycles to prevent an endless loop in
490     // case the stack contains an error.
491     while (isset($element_stack[1])) {
492       for ($i = 2; $i < $count; $i++) {
493         // There's no point in checking non-symbols. Also, switch(TRUE) would
494         // match any case and so it would break.
495         if (is_bool($element_stack[$i]) || is_numeric($element_stack[$i])) {
496           continue;
497         }
498         $f = NULL;
499         $length = 3;
500         $delta = 2;
501         switch ($element_stack[$i]) {
502           case '==':
503             $f = $element_stack[$i - 2] == $element_stack[$i - 1];
504             break;
505           case '!=':
506             $f = $element_stack[$i - 2] != $element_stack[$i - 1];
507             break;
508           case '<=':
509             $f = $element_stack[$i - 2] <= $element_stack[$i - 1];
510             break;
511           case '>=':
512             $f = $element_stack[$i - 2] >= $element_stack[$i - 1];
513             break;
514           case '<':
515             $f = $element_stack[$i - 2] < $element_stack[$i - 1];
516             break;
517           case '>':
518             $f = $element_stack[$i - 2] > $element_stack[$i - 1];
519             break;
520           case '+':
521             $f = $element_stack[$i - 2] + $element_stack[$i - 1];
522             break;
523           case '-':
524             $f = $element_stack[$i - 2] - $element_stack[$i - 1];
525             break;
526           case '*':
527             $f = $element_stack[$i - 2] * $element_stack[$i - 1];
528             break;
529           case '/':
530             $f = $element_stack[$i - 2] / $element_stack[$i - 1];
531             break;
532           case '%':
533             $f = $element_stack[$i - 2] % $element_stack[$i - 1];
534             break;
535           case '&&':
536             $f = $element_stack[$i - 2] && $element_stack[$i - 1];
537             break;
538           case '||':
539             $f = $element_stack[$i - 2] || $element_stack[$i - 1];
540             break;
541           case ':':
542             $f = $element_stack[$i - 3] ? $element_stack[$i - 2] : $element_stack[$i - 1];
543             // This operator has 3 preceding elements, instead of the default 2.
544             $length = 5;
545             $delta = 3;
546             break;
547         }
548
549         // If the element is an operator we remove the processed elements and
550         // store the result.
551         if (isset($f)) {
552           array_splice($element_stack, $i - $delta, $length, $f);
553           break;
554         }
555       }
556     }
557     if (!$limit) {
558       throw new \Exception('The plural formula could not be evaluated.');
559     }
560     return (int) $element_stack[0];
561   }
562
563 }