[ Index ] |
PHP Cross Reference of BackPress |
[Summary view] [Print] [Text view]
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;
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Dec 26 01:01:01 2024 | Cross-referenced by PHPXref 0.7.1 |