PoFileLoader.php 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. <?php
  2. namespace Cake\I18n\Loader;
  3. /**
  4. * @copyright Copyright (c) 2010, Union of RAD http://union-of-rad.org (http://lithify.me/)
  5. * @copyright Copyright (c) 2012, Clemens Tolboom
  6. * @copyright Copyright (c) 2014, Fabien Potencier https://github.com/symfony/Translation/blob/master/LICENSE
  7. */
  8. class PoFileLoader {
  9. /**
  10. * Parses portable object (PO) format.
  11. *
  12. * From http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files
  13. * we should be able to parse files having:
  14. *
  15. * white-space
  16. * # translator-comments
  17. * #. extracted-comments
  18. * #: reference...
  19. * #, flag...
  20. * #| msgid previous-untranslated-string
  21. * msgid untranslated-string
  22. * msgstr translated-string
  23. *
  24. * extra or different lines are:
  25. *
  26. * #| msgctxt previous-context
  27. * #| msgid previous-untranslated-string
  28. * msgctxt context
  29. *
  30. * #| msgid previous-untranslated-string-singular
  31. * #| msgid_plural previous-untranslated-string-plural
  32. * msgid untranslated-string-singular
  33. * msgid_plural untranslated-string-plural
  34. * msgstr[0] translated-string-case-0
  35. * ...
  36. * msgstr[N] translated-string-case-n
  37. *
  38. * The definition states:
  39. * - white-space and comments are optional.
  40. * - msgid "" that an empty singleline defines a header.
  41. *
  42. * This parser sacrifices some features of the reference implementation the
  43. * differences to that implementation are as follows.
  44. * - No support for comments spanning multiple lines.
  45. * - Translator and extracted comments are treated as being the same type.
  46. * - Message IDs are allowed to have other encodings as just US-ASCII.
  47. *
  48. * Items with an empty id are ignored.
  49. *
  50. * @param string $resource
  51. *
  52. * @return array
  53. */
  54. public function parse($resource) {
  55. $stream = fopen($resource, 'r');
  56. $defaults = [
  57. 'ids' => [],
  58. 'translated' => null
  59. ];
  60. $messages = [];
  61. $item = $defaults;
  62. while ($line = fgets($stream)) {
  63. $line = trim($line);
  64. if ($line === '') {
  65. // Whitespace indicated current item is done
  66. $this->_addMessage($messages, $item);
  67. $item = $defaults;
  68. } elseif (substr($line, 0, 7) === 'msgid "') {
  69. // We start a new msg so save previous
  70. $this->_addMessage($messages, $item);
  71. $item = $defaults;
  72. $item['ids']['singular'] = substr($line, 7, -1);
  73. } elseif (substr($line, 0, 8) === 'msgstr "') {
  74. $item['translated'] = substr($line, 8, -1);
  75. } elseif ($line[0] === '"') {
  76. $continues = isset($item['translated']) ? 'translated' : 'ids';
  77. if (is_array($item[$continues])) {
  78. end($item[$continues]);
  79. $item[$continues][key($item[$continues])] .= substr($line, 1, -1);
  80. } else {
  81. $item[$continues] .= substr($line, 1, -1);
  82. }
  83. } elseif (substr($line, 0, 14) === 'msgid_plural "') {
  84. $item['ids']['plural'] = substr($line, 14, -1);
  85. } elseif (substr($line, 0, 7) === 'msgstr[') {
  86. $size = strpos($line, ']');
  87. $item['translated'][(int)substr($line, 7, 1)] = substr($line, $size + 3, -1);
  88. }
  89. }
  90. // save last item
  91. $this->_addMessage($messages, $item);
  92. fclose($stream);
  93. return $messages;
  94. }
  95. /**
  96. * Saves a translation item to the messages.
  97. *
  98. * @param array $messages
  99. * @param array $item
  100. * @return void
  101. */
  102. protected function _addMessage(array &$messages, array $item) {
  103. if (is_array($item['translated'])) {
  104. $messages[stripcslashes($item['ids']['singular'])] = stripcslashes($item['translated'][0]);
  105. if (isset($item['ids']['plural'])) {
  106. $plurals = $item['translated'];
  107. // PO are by definition indexed so sort by index.
  108. ksort($plurals);
  109. // Make sure every index is filled.
  110. end($plurals);
  111. $count = key($plurals);
  112. // Fill missing spots with '-'.
  113. $empties = array_fill(0, $count + 1, '');
  114. $plurals += $empties;
  115. ksort($plurals);
  116. $messages[stripcslashes($item['ids']['plural'])] = stripcslashes(implode('&&&', $plurals));
  117. }
  118. } elseif (!empty($item['ids']['singular'])) {
  119. $messages[stripcslashes($item['ids']['singular'])] = stripcslashes($item['translated']);
  120. }
  121. }
  122. }