WordPress 4.6.3-scripts
[autoinstalls/wordpress.git] / wp-includes / pomo / po.php
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 dirname(__FILE__) . '/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                 return rtrim("{$before_headers}msgid \"\"\nmsgstr $poified");
42         }
43
44         /**
45          * Exports all entries to PO format
46          *
47          * @return string sequence of mgsgid/msgstr PO strings, doesn't containt newline at the end
48          */
49         function export_entries() {
50                 //TODO sorting
51                 return implode("\n\n", array_map(array('PO', 'export_entry'), $this->entries));
52         }
53
54         /**
55          * Exports the whole PO file as a string
56          *
57          * @param bool $include_headers whether to include the headers in the export
58          * @return string ready for inclusion in PO file string for headers and all the enrtries
59          */
60         function export($include_headers = true) {
61                 $res = '';
62                 if ($include_headers) {
63                         $res .= $this->export_headers();
64                         $res .= "\n\n";
65                 }
66                 $res .= $this->export_entries();
67                 return $res;
68         }
69
70         /**
71          * Same as {@link export}, but writes the result to a file
72          *
73          * @param string $filename where to write the PO string
74          * @param bool $include_headers whether to include tje headers in the export
75          * @return bool true on success, false on error
76          */
77         function export_to_file($filename, $include_headers = true) {
78                 $fh = fopen($filename, 'w');
79                 if (false === $fh) return false;
80                 $export = $this->export($include_headers);
81                 $res = fwrite($fh, $export);
82                 if (false === $res) return false;
83                 return fclose($fh);
84         }
85
86         /**
87          * Text to include as a comment before the start of the PO contents
88          *
89          * Doesn't need to include # in the beginning of lines, these are added automatically
90          */
91         function set_comment_before_headers( $text ) {
92                 $this->comments_before_headers = $text;
93         }
94
95         /**
96          * Formats a string in PO-style
97          *
98          * @static
99          * @param string $string the string to format
100          * @return string the poified string
101          */
102         public static function poify($string) {
103                 $quote = '"';
104                 $slash = '\\';
105                 $newline = "\n";
106
107                 $replaces = array(
108                         "$slash"        => "$slash$slash",
109                         "$quote"        => "$slash$quote",
110                         "\t"            => '\t',
111                 );
112
113                 $string = str_replace(array_keys($replaces), array_values($replaces), $string);
114
115                 $po = $quote.implode("${slash}n$quote$newline$quote", explode($newline, $string)).$quote;
116                 // add empty string on first line for readbility
117                 if (false !== strpos($string, $newline) &&
118                                 (substr_count($string, $newline) > 1 || !($newline === substr($string, -strlen($newline))))) {
119                         $po = "$quote$quote$newline$po";
120                 }
121                 // remove empty strings
122                 $po = str_replace("$newline$quote$quote", '', $po);
123                 return $po;
124         }
125
126         /**
127          * Gives back the original string from a PO-formatted string
128          *
129          * @static
130          * @param string $string PO-formatted string
131          * @return string enascaped string
132          */
133         public static function unpoify($string) {
134                 $escapes = array('t' => "\t", 'n' => "\n", 'r' => "\r", '\\' => '\\');
135                 $lines = array_map('trim', explode("\n", $string));
136                 $lines = array_map(array('PO', 'trim_quotes'), $lines);
137                 $unpoified = '';
138                 $previous_is_backslash = false;
139                 foreach($lines as $line) {
140                         preg_match_all('/./u', $line, $chars);
141                         $chars = $chars[0];
142                         foreach($chars as $char) {
143                                 if (!$previous_is_backslash) {
144                                         if ('\\' == $char)
145                                                 $previous_is_backslash = true;
146                                         else
147                                                 $unpoified .= $char;
148                                 } else {
149                                         $previous_is_backslash = false;
150                                         $unpoified .= isset($escapes[$char])? $escapes[$char] : $char;
151                                 }
152                         }
153                 }
154
155                 // Standardise the line endings on imported content, technically PO files shouldn't contain \r
156                 $unpoified = str_replace( array( "\r\n", "\r" ), "\n", $unpoified );
157
158                 return $unpoified;
159         }
160
161         /**
162          * Inserts $with in the beginning of every new line of $string and
163          * returns the modified string
164          *
165          * @static
166          * @param string $string prepend lines in this string
167          * @param string $with prepend lines with this string
168          */
169         public static function prepend_each_line($string, $with) {
170                 $php_with = var_export($with, true);
171                 $lines = explode("\n", $string);
172                 // do not prepend the string on the last empty line, artefact by explode
173                 if ("\n" == substr($string, -1)) unset($lines[count($lines) - 1]);
174                 $res = implode("\n", array_map(create_function('$x', "return $php_with.\$x;"), $lines));
175                 // give back the empty line, we ignored above
176                 if ("\n" == substr($string, -1)) $res .= "\n";
177                 return $res;
178         }
179
180         /**
181          * Prepare a text as a comment -- wraps the lines and prepends #
182          * and a special character to each line
183          *
184          * @access private
185          * @param string $text the comment text
186          * @param string $char character to denote a special PO comment,
187          *      like :, default is a space
188          */
189         public static function comment_block($text, $char=' ') {
190                 $text = wordwrap($text, PO_MAX_LINE_LEN - 3);
191                 return PO::prepend_each_line($text, "#$char ");
192         }
193
194         /**
195          * Builds a string from the entry for inclusion in PO file
196          *
197          * @static
198          * @param Translation_Entry &$entry the entry to convert to po string
199          * @return false|string PO-style formatted string for the entry or
200          *      false if the entry is empty
201          */
202         public static function export_entry(&$entry) {
203                 if ( null === $entry->singular || '' === $entry->singular ) return false;
204                 $po = array();
205                 if (!empty($entry->translator_comments)) $po[] = PO::comment_block($entry->translator_comments);
206                 if (!empty($entry->extracted_comments)) $po[] = PO::comment_block($entry->extracted_comments, '.');
207                 if (!empty($entry->references)) $po[] = PO::comment_block(implode(' ', $entry->references), ':');
208                 if (!empty($entry->flags)) $po[] = PO::comment_block(implode(", ", $entry->flags), ',');
209                 if ($entry->context) $po[] = 'msgctxt '.PO::poify($entry->context);
210                 $po[] = 'msgid '.PO::poify($entry->singular);
211                 if (!$entry->is_plural) {
212                         $translation = empty($entry->translations)? '' : $entry->translations[0];
213                         $translation = PO::match_begin_and_end_newlines( $translation, $entry->singular );
214                         $po[] = 'msgstr '.PO::poify($translation);
215                 } else {
216                         $po[] = 'msgid_plural '.PO::poify($entry->plural);
217                         $translations = empty($entry->translations)? array('', '') : $entry->translations;
218                         foreach($translations as $i => $translation) {
219                                 $translation = PO::match_begin_and_end_newlines( $translation, $entry->plural );
220                                 $po[] = "msgstr[$i] ".PO::poify($translation);
221                         }
222                 }
223                 return implode("\n", $po);
224         }
225
226         public static function match_begin_and_end_newlines( $translation, $original ) {
227                 if ( '' === $translation ) {
228                         return $translation;
229                 }
230
231                 $original_begin = "\n" === substr( $original, 0, 1 );
232                 $original_end = "\n" === substr( $original, -1 );
233                 $translation_begin = "\n" === substr( $translation, 0, 1 );
234                 $translation_end = "\n" === substr( $translation, -1 );
235
236                 if ( $original_begin ) {
237                         if ( ! $translation_begin ) {
238                                 $translation = "\n" . $translation;
239                         }
240                 } elseif ( $translation_begin ) {
241                         $translation = ltrim( $translation, "\n" );
242                 }
243
244                 if ( $original_end ) {
245                         if ( ! $translation_end ) {
246                                 $translation .= "\n";
247                         }
248                 } elseif ( $translation_end ) {
249                         $translation = rtrim( $translation, "\n" );
250                 }
251
252                 return $translation;
253         }
254
255         /**
256          * @param string $filename
257          * @return boolean
258          */
259         function import_from_file($filename) {
260                 $f = fopen($filename, 'r');
261                 if (!$f) return false;
262                 $lineno = 0;
263                 while (true) {
264                         $res = $this->read_entry($f, $lineno);
265                         if (!$res) break;
266                         if ($res['entry']->singular == '') {
267                                 $this->set_headers($this->make_headers($res['entry']->translations[0]));
268                         } else {
269                                 $this->add_entry($res['entry']);
270                         }
271                 }
272                 PO::read_line($f, 'clear');
273                 if ( false === $res ) {
274                         return false;
275                 }
276                 if ( ! $this->headers && ! $this->entries ) {
277                         return false;
278                 }
279                 return true;
280         }
281
282         /**
283          * @param resource $f
284          * @param int      $lineno
285          * @return null|false|array
286          */
287         function read_entry($f, $lineno = 0) {
288                 $entry = new Translation_Entry();
289                 // where were we in the last step
290                 // can be: comment, msgctxt, msgid, msgid_plural, msgstr, msgstr_plural
291                 $context = '';
292                 $msgstr_index = 0;
293                 $is_final = create_function('$context', 'return $context == "msgstr" || $context == "msgstr_plural";');
294                 while (true) {
295                         $lineno++;
296                         $line = PO::read_line($f);
297                         if (!$line)  {
298                                 if (feof($f)) {
299                                         if ($is_final($context))
300                                                 break;
301                                         elseif (!$context) // we haven't read a line and eof came
302                                                 return null;
303                                         else
304                                                 return false;
305                                 } else {
306                                         return false;
307                                 }
308                         }
309                         if ($line == "\n") continue;
310                         $line = trim($line);
311                         if (preg_match('/^#/', $line, $m)) {
312                                 // the comment is the start of a new entry
313                                 if ($is_final($context)) {
314                                         PO::read_line($f, 'put-back');
315                                         $lineno--;
316                                         break;
317                                 }
318                                 // comments have to be at the beginning
319                                 if ($context && $context != 'comment') {
320                                         return false;
321                                 }
322                                 // add comment
323                                 $this->add_comment_to_entry($entry, $line);
324                         } elseif (preg_match('/^msgctxt\s+(".*")/', $line, $m)) {
325                                 if ($is_final($context)) {
326                                         PO::read_line($f, 'put-back');
327                                         $lineno--;
328                                         break;
329                                 }
330                                 if ($context && $context != 'comment') {
331                                         return false;
332                                 }
333                                 $context = 'msgctxt';
334                                 $entry->context .= PO::unpoify($m[1]);
335                         } elseif (preg_match('/^msgid\s+(".*")/', $line, $m)) {
336                                 if ($is_final($context)) {
337                                         PO::read_line($f, 'put-back');
338                                         $lineno--;
339                                         break;
340                                 }
341                                 if ($context && $context != 'msgctxt' && $context != 'comment') {
342                                         return false;
343                                 }
344                                 $context = 'msgid';
345                                 $entry->singular .= PO::unpoify($m[1]);
346                         } elseif (preg_match('/^msgid_plural\s+(".*")/', $line, $m)) {
347                                 if ($context != 'msgid') {
348                                         return false;
349                                 }
350                                 $context = 'msgid_plural';
351                                 $entry->is_plural = true;
352                                 $entry->plural .= PO::unpoify($m[1]);
353                         } elseif (preg_match('/^msgstr\s+(".*")/', $line, $m)) {
354                                 if ($context != 'msgid') {
355                                         return false;
356                                 }
357                                 $context = 'msgstr';
358                                 $entry->translations = array(PO::unpoify($m[1]));
359                         } elseif (preg_match('/^msgstr\[(\d+)\]\s+(".*")/', $line, $m)) {
360                                 if ($context != 'msgid_plural' && $context != 'msgstr_plural') {
361                                         return false;
362                                 }
363                                 $context = 'msgstr_plural';
364                                 $msgstr_index = $m[1];
365                                 $entry->translations[$m[1]] = PO::unpoify($m[2]);
366                         } elseif (preg_match('/^".*"$/', $line)) {
367                                 $unpoified = PO::unpoify($line);
368                                 switch ($context) {
369                                         case 'msgid':
370                                                 $entry->singular .= $unpoified; break;
371                                         case 'msgctxt':
372                                                 $entry->context .= $unpoified; break;
373                                         case 'msgid_plural':
374                                                 $entry->plural .= $unpoified; break;
375                                         case 'msgstr':
376                                                 $entry->translations[0] .= $unpoified; break;
377                                         case 'msgstr_plural':
378                                                 $entry->translations[$msgstr_index] .= $unpoified; break;
379                                         default:
380                                                 return false;
381                                 }
382                         } else {
383                                 return false;
384                         }
385                 }
386                 if (array() == array_filter($entry->translations, create_function('$t', 'return $t || "0" === $t;'))) {
387                         $entry->translations = array();
388                 }
389                 return array('entry' => $entry, 'lineno' => $lineno);
390         }
391
392         /**
393          * @staticvar string   $last_line
394          * @staticvar boolean  $use_last_line
395          *
396          * @param     resource $f
397          * @param     string   $action
398          * @return boolean
399          */
400         function read_line($f, $action = 'read') {
401                 static $last_line = '';
402                 static $use_last_line = false;
403                 if ('clear' == $action) {
404                         $last_line = '';
405                         return true;
406                 }
407                 if ('put-back' == $action) {
408                         $use_last_line = true;
409                         return true;
410                 }
411                 $line = $use_last_line? $last_line : fgets($f);
412                 $line = ( "\r\n" == substr( $line, -2 ) ) ? rtrim( $line, "\r\n" ) . "\n" : $line;
413                 $last_line = $line;
414                 $use_last_line = false;
415                 return $line;
416         }
417
418         /**
419          * @param Translation_Entry $entry
420          * @param string            $po_comment_line
421          */
422         function add_comment_to_entry(&$entry, $po_comment_line) {
423                 $first_two = substr($po_comment_line, 0, 2);
424                 $comment = trim(substr($po_comment_line, 2));
425                 if ('#:' == $first_two) {
426                         $entry->references = array_merge($entry->references, preg_split('/\s+/', $comment));
427                 } elseif ('#.' == $first_two) {
428                         $entry->extracted_comments = trim($entry->extracted_comments . "\n" . $comment);
429                 } elseif ('#,' == $first_two) {
430                         $entry->flags = array_merge($entry->flags, preg_split('/,\s*/', $comment));
431                 } else {
432                         $entry->translator_comments = trim($entry->translator_comments . "\n" . $comment);
433                 }
434         }
435
436         /**
437          * @param string $s
438          * @return sring
439          */
440         public static function trim_quotes($s) {
441                 if ( substr($s, 0, 1) == '"') $s = substr($s, 1);
442                 if ( substr($s, -1, 1) == '"') $s = substr($s, 0, -1);
443                 return $s;
444         }
445 }
446 endif;