[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Thu Nov 21 08:20:01 2024 Cross-referenced by PHPXref