/src/I18n/Parser/PoFileParser.php

https://github.com/LubosRemplik/cakephp · PHP · 184 lines · 89 code · 18 blank · 77 comment · 11 complexity · 1275f3b15afe8453c8180dd1dbe0af84 MD5 · raw file

  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\I18n\Parser;
  16. use Cake\I18n\Translator;
  17. /**
  18. * Parses file in PO format
  19. *
  20. * @copyright Copyright (c) 2010, Union of RAD http://union-of-rad.org (http://lithify.me/)
  21. * @copyright Copyright (c) 2012, Clemens Tolboom
  22. * @copyright Copyright (c) 2014, Fabien Potencier https://github.com/symfony/Translation/blob/master/LICENSE
  23. */
  24. class PoFileParser
  25. {
  26. /**
  27. * Parses portable object (PO) format.
  28. *
  29. * From https://www.gnu.org/software/gettext/manual/gettext.html#PO-Files
  30. * we should be able to parse files having:
  31. *
  32. * white-space
  33. * # translator-comments
  34. * #. extracted-comments
  35. * #: reference...
  36. * #, flag...
  37. * #| msgid previous-untranslated-string
  38. * msgid untranslated-string
  39. * msgstr translated-string
  40. *
  41. * extra or different lines are:
  42. *
  43. * #| msgctxt previous-context
  44. * #| msgid previous-untranslated-string
  45. * msgctxt context
  46. *
  47. * #| msgid previous-untranslated-string-singular
  48. * #| msgid_plural previous-untranslated-string-plural
  49. * msgid untranslated-string-singular
  50. * msgid_plural untranslated-string-plural
  51. * msgstr[0] translated-string-case-0
  52. * ...
  53. * msgstr[N] translated-string-case-n
  54. *
  55. * The definition states:
  56. * - white-space and comments are optional.
  57. * - msgid "" that an empty singleline defines a header.
  58. *
  59. * This parser sacrifices some features of the reference implementation the
  60. * differences to that implementation are as follows.
  61. * - Translator and extracted comments are treated as being the same type.
  62. * - Message IDs are allowed to have other encodings as just US-ASCII.
  63. *
  64. * Items with an empty id are ignored.
  65. *
  66. * @param string $resource The file name to parse
  67. *
  68. * @return array
  69. */
  70. public function parse($resource)
  71. {
  72. $stream = fopen($resource, 'rb');
  73. $defaults = [
  74. 'ids' => [],
  75. 'translated' => null
  76. ];
  77. $messages = [];
  78. $item = $defaults;
  79. $stage = null;
  80. while ($line = fgets($stream)) {
  81. $line = trim($line);
  82. if ($line === '') {
  83. // Whitespace indicated current item is done
  84. $this->_addMessage($messages, $item);
  85. $item = $defaults;
  86. $stage = null;
  87. } elseif (substr($line, 0, 7) === 'msgid "') {
  88. // We start a new msg so save previous
  89. $this->_addMessage($messages, $item);
  90. $item['ids']['singular'] = substr($line, 7, -1);
  91. $stage = ['ids', 'singular'];
  92. } elseif (substr($line, 0, 8) === 'msgstr "') {
  93. $item['translated'] = substr($line, 8, -1);
  94. $stage = ['translated'];
  95. } elseif (substr($line, 0, 9) === 'msgctxt "') {
  96. $item['context'] = substr($line, 9, -1);
  97. $stage = ['context'];
  98. } elseif ($line[0] === '"') {
  99. switch (count($stage)) {
  100. case 2:
  101. $item[$stage[0]][$stage[1]] .= substr($line, 1, -1);
  102. break;
  103. case 1:
  104. $item[$stage[0]] .= substr($line, 1, -1);
  105. break;
  106. }
  107. } elseif (substr($line, 0, 14) === 'msgid_plural "') {
  108. $item['ids']['plural'] = substr($line, 14, -1);
  109. $stage = ['ids', 'plural'];
  110. } elseif (substr($line, 0, 7) === 'msgstr[') {
  111. $size = strpos($line, ']');
  112. $row = (int)substr($line, 7, 1);
  113. $item['translated'][$row] = substr($line, $size + 3, -1);
  114. $stage = ['translated', $row];
  115. }
  116. }
  117. // save last item
  118. $this->_addMessage($messages, $item);
  119. fclose($stream);
  120. return $messages;
  121. }
  122. /**
  123. * Saves a translation item to the messages.
  124. *
  125. * @param array $messages The messages array being collected from the file
  126. * @param array $item The current item being inspected
  127. * @return void
  128. */
  129. protected function _addMessage(array &$messages, array $item)
  130. {
  131. if (empty($item['ids']['singular']) && empty($item['ids']['plural'])) {
  132. return;
  133. }
  134. $singular = stripcslashes($item['ids']['singular']);
  135. $context = isset($item['context']) ? $item['context'] : null;
  136. $translation = $item['translated'];
  137. if (is_array($translation)) {
  138. $translation = $translation[0];
  139. }
  140. $translation = stripcslashes($translation);
  141. if ($context !== null && !isset($messages[$singular]['_context'][$context])) {
  142. $messages[$singular]['_context'][$context] = $translation;
  143. } elseif (!isset($messages[$singular]['_context'][''])) {
  144. $messages[$singular]['_context'][''] = $translation;
  145. }
  146. if (isset($item['ids']['plural'])) {
  147. $plurals = $item['translated'];
  148. // PO are by definition indexed so sort by index.
  149. ksort($plurals);
  150. // Make sure every index is filled.
  151. end($plurals);
  152. $count = key($plurals);
  153. // Fill missing spots with an empty string.
  154. $empties = array_fill(0, $count + 1, '');
  155. $plurals += $empties;
  156. ksort($plurals);
  157. $plurals = array_map('stripcslashes', $plurals);
  158. $key = stripcslashes($item['ids']['plural']);
  159. if ($context !== null) {
  160. $messages[Translator::PLURAL_PREFIX . $key]['_context'][$context] = $plurals;
  161. } else {
  162. $messages[Translator::PLURAL_PREFIX . $key]['_context'][''] = $plurals;
  163. }
  164. }
  165. }
  166. }