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