ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

po.php (14723B)


      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 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 		public $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.
    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 bool
    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 bool
    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;