Mercurial > hg > isophonics-drupal-site
comparison core/lib/Drupal/Component/Gettext/PoHeader.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 1fec387a4317 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
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 private $_langcode; | |
31 | |
32 /** | |
33 * Formula for the plural form. | |
34 * | |
35 * @var string | |
36 */ | |
37 private $_pluralForms; | |
38 | |
39 /** | |
40 * Author(s) of the file. | |
41 * | |
42 * @var string | |
43 */ | |
44 private $_authors; | |
45 | |
46 /** | |
47 * Date the po file got created. | |
48 * | |
49 * @var string | |
50 */ | |
51 private $_po_date; | |
52 | |
53 /** | |
54 * Human readable language name. | |
55 * | |
56 * @var string | |
57 */ | |
58 private $_languageName; | |
59 | |
60 /** | |
61 * Name of the project the translation belongs to. | |
62 * | |
63 * @var string | |
64 */ | |
65 private $_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->_po_date = @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->_po_date . "\\n\"\n"; | |
168 $output .= "\"PO-Revision-Date: " . $this->_po_date . "\\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 } |