[ Index ]

PHP Cross Reference of BackPress

title

Body

[close]

/includes/pomo/ -> po.php (source)

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


Generated: Tue Mar 19 01:01:07 2024 Cross-referenced by PHPXref 0.7.1