[ Index ]

PHP Cross Reference of YOURLS

title

Body

[close]

/includes/vendor/pomo/pomo/src/ -> PO.php (source)

   1  <?php
   2  /**
   3   * This file is part of the POMO package.
   4   *
   5   * @copyright 2014 POMO
   6   * @license GPL
   7   */
   8  
   9  namespace POMO;
  10  
  11  use POMO\Translations\EntryTranslations;
  12  use POMO\Translations\GettextTranslations;
  13  
  14  ini_set('auto_detect_line_endings', 1);
  15  
  16  /**
  17   * Class for working with PO files.
  18   */
  19  class PO extends GettextTranslations
  20  {
  21      const MAX_LINE_LEN = 79;
  22  
  23      public $comments_before_headers = '';
  24  
  25      /**
  26       * Exports headers to a PO entry.
  27       *
  28       * @return string msgid/msgstr PO entry for this PO file headers, doesn't
  29       *                contain newline at the end
  30       */
  31      public function export_headers()
  32      {
  33          $header_string = '';
  34          foreach ($this->headers as $header => $value) {
  35              $header_string .= "$header: $value\n";
  36          }
  37          $poified = self::poify($header_string);
  38          if ($this->comments_before_headers) {
  39              $before_headers = self::prepend_each_line(
  40                  rtrim($this->comments_before_headers)."\n",
  41                  '# '
  42              );
  43          } else {
  44              $before_headers = '';
  45          }
  46  
  47          return rtrim("{$before_headers}msgid \"\"\nmsgstr $poified");
  48      }
  49  
  50      /**
  51       * Exports all entries to PO format.
  52       *
  53       * @return string sequence of mgsgid/msgstr PO strings, doesn't containt
  54       *                newline at the end
  55       */
  56      public function export_entries()
  57      {
  58          //TODO: sorting
  59          return implode("\n\n", array_map(
  60              array(__NAMESPACE__.'\PO', 'export_entry'),
  61              $this->entries
  62          ));
  63      }
  64  
  65      /**
  66       * Exports the whole PO file as a string.
  67       *
  68       * @param bool $include_headers whether to include the headers in the
  69       *                              export
  70       *
  71       * @return string ready for inclusion in PO file string for headers and all
  72       *                the enrtries
  73       */
  74      public function export($include_headers = true)
  75      {
  76          $res = '';
  77          if ($include_headers) {
  78              $res .= $this->export_headers();
  79              $res .= "\n\n";
  80          }
  81          $res .= $this->export_entries();
  82  
  83          return $res;
  84      }
  85  
  86      /**
  87       * Same as {@link export}, but writes the result to a file.
  88       *
  89       * @param string $filename        where to write the PO string
  90       * @param bool   $include_headers whether to include tje headers in the
  91       *                                export
  92       *
  93       * @return bool true on success, false on error
  94       */
  95      public function export_to_file($filename, $include_headers = true)
  96      {
  97          $fh = fopen($filename, 'w');
  98          if (false === $fh) {
  99              return false;
 100          }
 101          $export = $this->export($include_headers);
 102          $res = fwrite($fh, $export);
 103          if (false === $res) {
 104              return false;
 105          }
 106  
 107          return fclose($fh);
 108      }
 109  
 110      /**
 111       * Text to include as a comment before the start of the PO contents.
 112       *
 113       * Doesn't need to include # in the beginning of lines, these are added
 114       * automatically
 115       *
 116       * @param string $text Comment text
 117       */
 118      public function set_comment_before_headers($text)
 119      {
 120          $this->comments_before_headers = $text;
 121      }
 122  
 123      /**
 124       * Formats a string in PO-style.
 125       *
 126       * @param string $string the string to format
 127       *
 128       * @return string the poified string
 129       */
 130      public static function poify($string)
 131      {
 132          $quote = '"';
 133          $slash = '\\';
 134          $newline = "\n";
 135  
 136          $replaces = array(
 137              "$slash"    => "$slash$slash",
 138              "$quote"    => "$slash$quote",
 139              "\t"        => '\t',
 140          );
 141  
 142          $string = str_replace(
 143              array_keys($replaces),
 144              array_values($replaces),
 145              $string
 146          );
 147  
 148          $po = $quote.implode(
 149              "$slash}n$quote$newline$quote",
 150              explode($newline, $string)
 151          ).$quote;
 152          // add empty string on first line for readbility
 153          if (false !== strpos($string, $newline) &&
 154                  (substr_count($string, $newline) > 1 ||
 155                  !($newline === substr($string, -strlen($newline))))) {
 156              $po = "$quote$quote$newline$po";
 157          }
 158          // remove empty strings
 159          $po = str_replace("$newline$quote$quote", '', $po);
 160  
 161          return $po;
 162      }
 163  
 164      /**
 165       * Gives back the original string from a PO-formatted string.
 166       *
 167       * @param string $string PO-formatted string
 168       *
 169       * @return string enascaped string
 170       */
 171      public static function unpoify($string)
 172      {
 173          $escapes = array('t' => "\t", 'n' => "\n", 'r' => "\r", '\\' => '\\');
 174          $lines = array_map('trim', explode("\n", $string));
 175          $lines = array_map(array(__NAMESPACE__.'\PO', 'trim_quotes'), $lines);
 176          $unpoified = '';
 177          $previous_is_backslash = false;
 178          foreach ($lines as $line) {
 179              preg_match_all('/./u', $line, $chars);
 180              $chars = $chars[0];
 181              foreach ($chars as $char) {
 182                  if (!$previous_is_backslash) {
 183                      if ('\\' == $char) {
 184                          $previous_is_backslash = true;
 185                      } else {
 186                          $unpoified .= $char;
 187                      }
 188                  } else {
 189                      $previous_is_backslash = false;
 190                      $unpoified .= isset($escapes[$char]) ? $escapes[$char] : $char;
 191                  }
 192              }
 193          }
 194  
 195          // Standardise the line endings on imported content, technically PO files shouldn't contain \r
 196          $unpoified = str_replace(array("\r\n", "\r"), "\n", $unpoified);
 197  
 198          return $unpoified;
 199      }
 200  
 201      /**
 202       * Inserts $with in the beginning of every new line of $string and
 203       * returns the modified string.
 204       *
 205       * @param string $string prepend lines in this string
 206       * @param string $with   prepend lines with this string
 207       *
 208       * @return string The modified string
 209       */
 210      public static function prepend_each_line($string, $with)
 211      {
 212          $lines = explode("\n", $string);
 213          $append = '';
 214          if ("\n" === substr($string, -1) && '' === end($lines)) {
 215              // Last line might be empty because $string was terminated
 216              // with a newline, remove it from the $lines array,
 217              // we'll restore state by re-terminating the string at the end
 218              array_pop($lines);
 219              $append = "\n";
 220          }
 221          foreach ($lines as &$line) {
 222              $line = $with.$line;
 223          }
 224          unset($line);
 225  
 226          return implode("\n", $lines).$append;
 227      }
 228  
 229      /**
 230       * Prepare a text as a comment -- wraps the lines and prepends #
 231       * and a special character to each line.
 232       *
 233       * @param string $text the comment text
 234       * @param string $char character to denote a special PO comment,
 235       *                     like :, default is a space
 236       *
 237       * @return string The modified string
 238       */
 239      private static function comment_block($text, $char = ' ')
 240      {
 241          $text = wordwrap($text, self::MAX_LINE_LEN - 3);
 242  
 243          return self::prepend_each_line($text, "#$char ");
 244      }
 245  
 246      /**
 247       * Builds a string from the entry for inclusion in PO file.
 248       *
 249       * @static
 250       *
 251       * @param EntryTranslations &$entry the entry to convert to po string
 252       *
 253       * @return false|string PO-style formatted string for the entry or
 254       *                      false if the entry is empty
 255       */
 256      public static function export_entry(EntryTranslations &$entry)
 257      {
 258          if (null === $entry->singular || '' === $entry->singular) {
 259              return false;
 260          }
 261          $po = array();
 262          if (!empty($entry->translator_comments)) {
 263              $po[] = self::comment_block($entry->translator_comments);
 264          }
 265          if (!empty($entry->extracted_comments)) {
 266              $po[] = self::comment_block($entry->extracted_comments, '.');
 267          }
 268          if (!empty($entry->references)) {
 269              $po[] = self::comment_block(implode(' ', $entry->references), ':');
 270          }
 271          if (!empty($entry->flags)) {
 272              $po[] = self::comment_block(implode(', ', $entry->flags), ',');
 273          }
 274          if (!is_null($entry->context)) {
 275              $po[] = 'msgctxt '.self::poify($entry->context);
 276          }
 277          $po[] = 'msgid '.self::poify($entry->singular);
 278          if (!$entry->is_plural) {
 279              $translation = empty($entry->translations) ?
 280                  '' :
 281                  $entry->translations[0];
 282              $translation = self::match_begin_and_end_newlines($translation, $entry->singular);
 283              $po[] = 'msgstr '.self::poify($translation);
 284          } else {
 285              $po[] = 'msgid_plural '.self::poify($entry->plural);
 286              $translations = empty($entry->translations) ?
 287                  array('', '') :
 288                  $entry->translations;
 289              foreach ($translations as $i => $translation) {
 290                  $translation = self::match_begin_and_end_newlines($translation, $entry->plural);
 291                  $po[] = "msgstr[$i] ".self::poify($translation);
 292              }
 293          }
 294  
 295          return implode("\n", $po);
 296      }
 297  
 298      /**
 299       * @param $translation
 300       * @param $original
 301       *
 302       * @return string
 303       */
 304      public static function match_begin_and_end_newlines($translation, $original)
 305      {
 306          if ('' === $translation) {
 307              return $translation;
 308          }
 309  
 310          $original_begin = "\n" === substr($original, 0, 1);
 311          $original_end = "\n" === substr($original, -1);
 312          $translation_begin = "\n" === substr($translation, 0, 1);
 313          $translation_end = "\n" === substr($translation, -1);
 314          if ($original_begin) {
 315              if (!$translation_begin) {
 316                  $translation = "\n".$translation;
 317              }
 318          } elseif ($translation_begin) {
 319              $translation = ltrim($translation, "\n");
 320          }
 321          if ($original_end) {
 322              if (!$translation_end) {
 323                  $translation .= "\n";
 324              }
 325          } elseif ($translation_end) {
 326              $translation = rtrim($translation, "\n");
 327          }
 328  
 329          return $translation;
 330      }
 331  
 332      /**
 333       * @param string $filename
 334       *
 335       * @return bool
 336       */
 337      public function import_from_file($filename)
 338      {
 339          $f = fopen($filename, 'r');
 340          if (!$f) {
 341              return false;
 342          }
 343          $lineno = 0;
 344          $res = false;
 345          while (true) {
 346              $res = $this->read_entry($f, $lineno);
 347              if (!$res) {
 348                  break;
 349              }
 350              if ($res['entry']->singular == '') {
 351                  $this->set_headers(
 352                      $this->make_headers($res['entry']->translations[0])
 353                  );
 354              } else {
 355                  $this->add_entry($res['entry']);
 356              }
 357          }
 358          self::read_line($f, 'clear');
 359          if (false === $res) {
 360              return false;
 361          }
 362          if (!$this->headers && !$this->entries) {
 363              return false;
 364          }
 365  
 366          return true;
 367      }
 368  
 369      /**
 370       * Helper function for read_entry.
 371       *
 372       * @param string $context
 373       *
 374       * @return bool
 375       */
 376      protected static function is_final($context)
 377      {
 378          return ($context === 'msgstr') || ($context === 'msgstr_plural');
 379      }
 380  
 381      /**
 382       * @param resource $f
 383       * @param int      $lineno
 384       *
 385       * @return null|false|array
 386       */
 387      public function read_entry($f, $lineno = 0)
 388      {
 389          $entry = new EntryTranslations();
 390          // where were we in the last step
 391          // can be: comment, msgctxt, msgid, msgid_plural, msgstr, msgstr_plural
 392          $context = '';
 393          $msgstr_index = 0;
 394          while (true) {
 395              $lineno++;
 396              $line = self::read_line($f);
 397              if (!$line) {
 398                  if (feof($f)) {
 399                      if (self::is_final($context)) {
 400                          break;
 401                      } elseif (!$context) { // we haven't read a line and eof came
 402                          return;
 403                      } else {
 404                          return false;
 405                      }
 406                  } else {
 407                      return false;
 408                  }
 409              }
 410              if ($line == "\n") {
 411                  continue;
 412              }
 413  
 414              $line = trim($line);
 415              if (preg_match('/^#/', $line, $m)) {
 416                  // the comment is the start of a new entry
 417                  if (self::is_final($context)) {
 418                      self::read_line($f, 'put-back');
 419                      $lineno--;
 420                      break;
 421                  }
 422                  // comments have to be at the beginning
 423                  if ($context && $context != 'comment') {
 424                      return false;
 425                  }
 426                  // add comment
 427                  $this->add_comment_to_entry($entry, $line);
 428              } elseif (preg_match('/^msgctxt\s+(".*")/', $line, $m)) {
 429                  if (self::is_final($context)) {
 430                      self::read_line($f, 'put-back');
 431                      $lineno--;
 432                      break;
 433                  }
 434                  if ($context && $context != 'comment') {
 435                      return false;
 436                  }
 437                  $context = 'msgctxt';
 438                  $entry->context .= self::unpoify($m[1]);
 439              } elseif (preg_match('/^msgid\s+(".*")/', $line, $m)) {
 440                  if (self::is_final($context)) {
 441                      self::read_line($f, 'put-back');
 442                      $lineno--;
 443                      break;
 444                  }
 445                  if ($context &&
 446                      $context != 'msgctxt' &&
 447                      $context != 'comment') {
 448                      return false;
 449                  }
 450                  $context = 'msgid';
 451                  $entry->singular .= self::unpoify($m[1]);
 452              } elseif (preg_match('/^msgid_plural\s+(".*")/', $line, $m)) {
 453                  if ($context != 'msgid') {
 454                      return false;
 455                  }
 456                  $context = 'msgid_plural';
 457                  $entry->is_plural = true;
 458                  $entry->plural .= self::unpoify($m[1]);
 459              } elseif (preg_match('/^msgstr\s+(".*")/', $line, $m)) {
 460                  if ($context != 'msgid') {
 461                      return false;
 462                  }
 463                  $context = 'msgstr';
 464                  $entry->translations = array(self::unpoify($m[1]));
 465              } elseif (preg_match('/^msgstr\[(\d+)\]\s+(".*")/', $line, $m)) {
 466                  if ($context != 'msgid_plural' && $context != 'msgstr_plural') {
 467                      return false;
 468                  }
 469                  $context = 'msgstr_plural';
 470                  $msgstr_index = $m[1];
 471                  $entry->translations[$m[1]] = self::unpoify($m[2]);
 472              } elseif (preg_match('/^".*"$/', $line)) {
 473                  $unpoified = self::unpoify($line);
 474                  switch ($context) {
 475                      case 'msgid':
 476                          $entry->singular .= $unpoified;
 477                          break;
 478                      case 'msgctxt':
 479                          $entry->context .= $unpoified;
 480                          break;
 481                      case 'msgid_plural':
 482                          $entry->plural .= $unpoified;
 483                          break;
 484                      case 'msgstr':
 485                          $entry->translations[0] .= $unpoified;
 486                          break;
 487                      case 'msgstr_plural':
 488                          $entry->translations[$msgstr_index] .= $unpoified;
 489                          break;
 490                      default:
 491                          return false;
 492                  }
 493              } else {
 494                  return false;
 495              }
 496          }
 497  
 498          $have_translations = false;
 499          foreach ($entry->translations as $t) {
 500              if ($t || ('0' === $t)) {
 501                  $have_translations = true;
 502                  break;
 503              }
 504          }
 505          if (false === $have_translations) {
 506              $entry->translations = array();
 507          }
 508  
 509          return array('entry' => $entry, 'lineno' => $lineno);
 510      }
 511  
 512      /**
 513       * @param resource $f
 514       * @param string   $action
 515       *
 516       * @return bool
 517       */
 518      public static function read_line($f, $action = 'read')
 519      {
 520          static $last_line = '';
 521          static $use_last_line = false;
 522          if ('clear' == $action) {
 523              $last_line = '';
 524  
 525              return true;
 526          }
 527          if ('put-back' == $action) {
 528              $use_last_line = true;
 529  
 530              return true;
 531          }
 532          $line = $use_last_line ? $last_line : fgets($f);
 533          $line = ("\r\n" == substr($line, -2)) ?
 534              rtrim($line, "\r\n")."\n" :
 535              $line;
 536          $last_line = $line;
 537          $use_last_line = false;
 538  
 539          return $line;
 540      }
 541  
 542      /**
 543       * @param EntryTranslations $entry
 544       * @param string            $po_comment_line
 545       */
 546      public function add_comment_to_entry(EntryTranslations &$entry, $po_comment_line)
 547      {
 548          $first_two = substr($po_comment_line, 0, 2);
 549          $comment = trim(substr($po_comment_line, 2));
 550          if ('#:' == $first_two) {
 551              $entry->references = array_merge(
 552                  $entry->references,
 553                  preg_split('/\s+/', $comment)
 554              );
 555          } elseif ('#.' == $first_two) {
 556              $entry->extracted_comments = trim(
 557                  $entry->extracted_comments."\n".$comment
 558              );
 559          } elseif ('#,' == $first_two) {
 560              $entry->flags = array_merge(
 561                  $entry->flags,
 562                  preg_split('/,\s*/', $comment)
 563              );
 564          } else {
 565              $entry->translator_comments = trim(
 566                  $entry->translator_comments."\n".$comment
 567              );
 568          }
 569      }
 570  
 571      /**
 572       * @param string $s
 573       *
 574       * @return string
 575       */
 576      public static function trim_quotes($s)
 577      {
 578          if (substr($s, 0, 1) == '"') {
 579              $s = substr($s, 1);
 580          }
 581          if (substr($s, -1, 1) == '"') {
 582              $s = substr($s, 0, -1);
 583          }
 584  
 585          return $s;
 586      }
 587  }


Generated: Wed Sep 28 05:10:02 2022 Cross-referenced by PHPXref 0.7.1