PoFileLoader.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Translation\Loader;
  11. /**
  12. * @copyright Copyright (c) 2010, Union of RAD https://github.com/UnionOfRAD/lithium
  13. * @copyright Copyright (c) 2012, Clemens Tolboom
  14. */
  15. class PoFileLoader extends FileLoader
  16. {
  17. /**
  18. * Parses portable object (PO) format.
  19. *
  20. * From https://www.gnu.org/software/gettext/manual/gettext.html#PO-Files
  21. * we should be able to parse files having:
  22. *
  23. * white-space
  24. * # translator-comments
  25. * #. extracted-comments
  26. * #: reference...
  27. * #, flag...
  28. * #| msgid previous-untranslated-string
  29. * msgid untranslated-string
  30. * msgstr translated-string
  31. *
  32. * extra or different lines are:
  33. *
  34. * #| msgctxt previous-context
  35. * #| msgid previous-untranslated-string
  36. * msgctxt context
  37. *
  38. * #| msgid previous-untranslated-string-singular
  39. * #| msgid_plural previous-untranslated-string-plural
  40. * msgid untranslated-string-singular
  41. * msgid_plural untranslated-string-plural
  42. * msgstr[0] translated-string-case-0
  43. * ...
  44. * msgstr[N] translated-string-case-n
  45. *
  46. * The definition states:
  47. * - white-space and comments are optional.
  48. * - msgid "" that an empty singleline defines a header.
  49. *
  50. * This parser sacrifices some features of the reference implementation the
  51. * differences to that implementation are as follows.
  52. * - No support for comments spanning multiple lines.
  53. * - Translator and extracted comments are treated as being the same type.
  54. * - Message IDs are allowed to have other encodings as just US-ASCII.
  55. * - Contexts (msgctxt) are parsed but discarded.
  56. *
  57. * Items with an empty id are ignored.
  58. */
  59. protected function loadResource(string $resource): array
  60. {
  61. $stream = fopen($resource, 'r');
  62. $defaults = [
  63. 'ids' => [],
  64. 'translated' => null,
  65. 'context' => null,
  66. ];
  67. $messages = [];
  68. $item = $defaults;
  69. $flags = [];
  70. while ($line = fgets($stream)) {
  71. $line = trim($line);
  72. if ('' === $line) {
  73. // Whitespace indicated current item is done
  74. $this->saveItem($messages, $item, $flags, $defaults);
  75. } elseif (str_starts_with($line, '#,')) {
  76. // flags belong to the next entry, so the previous one ends here
  77. if (null !== $item['translated']) {
  78. $this->saveItem($messages, $item, $flags, $defaults);
  79. }
  80. $flags = array_map('trim', explode(',', substr($line, 2)));
  81. } elseif (str_starts_with($line, 'msgctxt "')) {
  82. if (null !== $item['translated']) {
  83. $this->saveItem($messages, $item, $flags, $defaults);
  84. }
  85. $item['context'] = substr($line, 9, -1);
  86. } elseif (str_starts_with($line, 'msgid "')) {
  87. // We start a new msg so save previous
  88. if ($item['ids']) {
  89. $this->saveItem($messages, $item, $flags, $defaults);
  90. }
  91. $item['ids']['singular'] = substr($line, 7, -1);
  92. } elseif (str_starts_with($line, 'msgstr "')) {
  93. $item['translated'] = substr($line, 8, -1);
  94. } elseif ('"' === $line[0]) {
  95. $continues = isset($item['translated']) ? 'translated' : ($item['ids'] ? 'ids' : 'context');
  96. if (\is_array($item[$continues])) {
  97. end($item[$continues]);
  98. $item[$continues][key($item[$continues])] .= substr($line, 1, -1);
  99. } else {
  100. $item[$continues] .= substr($line, 1, -1);
  101. }
  102. } elseif (str_starts_with($line, 'msgid_plural "')) {
  103. $item['ids']['plural'] = substr($line, 14, -1);
  104. } elseif (str_starts_with($line, 'msgstr[')) {
  105. $size = strpos($line, ']');
  106. $item['translated'][(int) substr($line, 7, 1)] = substr($line, $size + 3, -1);
  107. }
  108. }
  109. // save last item
  110. $this->saveItem($messages, $item, $flags, $defaults);
  111. fclose($stream);
  112. return $messages;
  113. }
  114. private function saveItem(array &$messages, array &$item, array &$flags, array $defaults): void
  115. {
  116. if (!\in_array('fuzzy', $flags, true)) {
  117. $this->addMessage($messages, $item);
  118. }
  119. $item = $defaults;
  120. $flags = [];
  121. }
  122. /**
  123. * Save a translation item to the messages.
  124. *
  125. * A .po file could contain by error missing plural indexes. We need to
  126. * fix these before saving them.
  127. */
  128. private function addMessage(array &$messages, array $item): void
  129. {
  130. if (!empty($item['ids']['singular'])) {
  131. $id = stripcslashes($item['ids']['singular']);
  132. if (isset($item['ids']['plural'])) {
  133. $id .= '|'.stripcslashes($item['ids']['plural']);
  134. }
  135. $translated = (array) $item['translated'];
  136. // PO are by definition indexed so sort by index.
  137. ksort($translated);
  138. // Make sure every index is filled.
  139. end($translated);
  140. $count = key($translated);
  141. // Fill missing spots with '-'.
  142. $empties = array_fill(0, $count + 1, '-');
  143. $translated += $empties;
  144. ksort($translated);
  145. $messages[$id] = stripcslashes(implode('|', $translated));
  146. }
  147. }
  148. }