Wordpress 2.8.2
[autoinstalls/wordpress.git] / wp-includes / pomo / po.php
1 <?php
2 /**
3  * Class for working with PO files
4  *
5  * @version $Id: po.php 123 2009-05-13 19:35:43Z nbachiyski $
6  * @package pomo
7  * @subpackage po
8  */
9
10 require_once dirname(__FILE__) . '/translations.php';
11
12 define('PO_MAX_LINE_LEN', 79);
13
14 ini_set('auto_detect_line_endings', 1);
15
16 /**
17  * Routines for working with PO files
18  */
19 class PO extends Gettext_Translations {
20         
21
22         /**
23          * Exports headers to a PO entry
24          *
25          * @return string msgid/msgstr PO entry for this PO file headers, doesn't contain newline at the end
26          */
27         function export_headers() {
28                 $header_string = '';
29                 foreach($this->headers as $header => $value) {
30                         $header_string.= "$header: $value\n";
31                 }
32                 $poified = PO::poify($header_string);
33                 return rtrim("msgid \"\"\nmsgstr $poified");
34         }
35
36         /**
37          * Exports all entries to PO format
38          *
39          * @return string sequence of mgsgid/msgstr PO strings, doesn't containt newline at the end
40          */
41         function export_entries() {
42                 //TODO sorting
43                 return implode("\n\n", array_map(array('PO', 'export_entry'), $this->entries));
44         }
45
46         /**
47          * Exports the whole PO file as a string
48          *
49          * @param bool $include_headers whether to include the headers in the export
50          * @return string ready for inclusion in PO file string for headers and all the enrtries
51          */
52         function export($include_headers = true) {
53                 $res = '';
54                 if ($include_headers) {
55                         $res .= $this->export_headers();
56                         $res .= "\n\n";
57                 }
58                 $res .= $this->export_entries();
59                 return $res;
60         }
61
62         /**
63          * Same as {@link export}, but writes the result to a file
64          *
65          * @param string $filename where to write the PO string
66          * @param bool $include_headers whether to include tje headers in the export
67          * @return bool true on success, false on error
68          */
69         function export_to_file($filename, $include_headers = true) {
70                 $fh = fopen($filename, 'w');
71                 if (false === $fh) return false;
72                 $export = $this->export($include_headers);
73                 $res = fwrite($fh, $export);
74                 if (false === $res) return false;
75                 return fclose($fh);
76         }
77
78         /**
79          * Formats a string in PO-style
80          *
81          * @static
82          * @param string $string the string to format
83          * @return string the poified string
84          */
85         function poify($string) {
86                 $quote = '"';
87                 $slash = '\\';
88                 $newline = "\n";
89
90                 $replaces = array(
91                         "$slash"        => "$slash$slash",
92                         "$quote"        => "$slash$quote",
93                         "\t"            => '\t',
94                 );
95
96                 $string = str_replace(array_keys($replaces), array_values($replaces), $string);
97
98                 $po = $quote.implode("${slash}n$quote$newline$quote", explode($newline, $string)).$quote;
99                 // add empty string on first line for readbility
100                 if (false !== strpos($string, $newline) &&
101                                 (substr_count($string, $newline) > 1 || !($newline === substr($string, -strlen($newline))))) {
102                         $po = "$quote$quote$newline$po";
103                 }
104                 // remove empty strings
105                 $po = str_replace("$newline$quote$quote", '', $po);
106                 return $po;
107         }
108         
109         /**
110          * Gives back the original string from a PO-formatted string
111          * 
112          * @static
113          * @param string $string PO-formatted string
114          * @return string enascaped string
115          */
116         function unpoify($string) {
117                 $escapes = array('t' => "\t", 'n' => "\n", '\\' => '\\');
118                 $lines = array_map('trim', explode("\n", $string));
119                 $lines = array_map(array('PO', 'trim_quotes'), $lines);
120                 $unpoified = '';
121                 $previous_is_backslash = false;
122                 foreach($lines as $line) {
123                         preg_match_all('/./u', $line, $chars);
124                         $chars = $chars[0];
125                         foreach($chars as $char) {
126                                 if (!$previous_is_backslash) {
127                                         if ('\\' == $char)
128                                                 $previous_is_backslash = true;
129                                         else
130                                                 $unpoified .= $char;
131                                 } else {
132                                         $previous_is_backslash = false;
133                                         $unpoified .= isset($escapes[$char])? $escapes[$char] : $char;
134                                 }
135                         }
136                 }
137                 return $unpoified;
138         }
139
140         /**
141          * Inserts $with in the beginning of every new line of $string and 
142          * returns the modified string
143          *
144          * @static
145          * @param string $string prepend lines in this string
146          * @param string $with prepend lines with this string
147          */
148         function prepend_each_line($string, $with) {
149                 $php_with = var_export($with, true);
150                 $lines = explode("\n", $string);
151                 // do not prepend the string on the last empty line, artefact by explode
152                 if ("\n" == substr($string, -1)) unset($lines[count($lines) - 1]);
153                 $res = implode("\n", array_map(create_function('$x', "return $php_with.\$x;"), $lines));
154                 // give back the empty line, we ignored above
155                 if ("\n" == substr($string, -1)) $res .= "\n";
156                 return $res;
157         }
158
159         /**
160          * Prepare a text as a comment -- wraps the lines and prepends #
161          * and a special character to each line
162          *
163          * @access private
164          * @param string $text the comment text
165          * @param string $char character to denote a special PO comment,
166          *      like :, default is a space
167          */
168         function comment_block($text, $char=' ') {
169                 $text = wordwrap($text, PO_MAX_LINE_LEN - 3);
170                 return PO::prepend_each_line($text, "#$char ");
171         }
172
173         /**
174          * Builds a string from the entry for inclusion in PO file
175          *
176          * @static
177          * @param object &$entry the entry to convert to po string
178          * @return string|bool PO-style formatted string for the entry or
179          *      false if the entry is empty
180          */
181         function export_entry(&$entry) {
182                 if (is_null($entry->singular)) return false;
183                 $po = array();
184                 if (!empty($entry->translator_comments)) $po[] = PO::comment_block($entry->translator_comments);
185                 if (!empty($entry->extracted_comments)) $po[] = PO::comment_block($entry->extracted_comments, '.');
186                 if (!empty($entry->references)) $po[] = PO::comment_block(implode(' ', $entry->references), ':');
187                 if (!empty($entry->flags)) $po[] = PO::comment_block(implode(", ", $entry->flags), ',');
188                 if (!is_null($entry->context)) $po[] = 'msgctxt '.PO::poify($entry->context);
189                 $po[] = 'msgid '.PO::poify($entry->singular);
190                 if (!$entry->is_plural) {
191                         $translation = empty($entry->translations)? '' : $entry->translations[0];
192                         $po[] = 'msgstr '.PO::poify($translation);
193                 } else {
194                         $po[] = 'msgid_plural '.PO::poify($entry->plural);
195                         $translations = empty($entry->translations)? array('', '') : $entry->translations;
196                         foreach($translations as $i => $translation) {
197                                 $po[] = "msgstr[$i] ".PO::poify($translation);
198                         }
199                 }
200                 return implode("\n", $po);
201         }
202
203         function import_from_file($filename) {
204                 $f = fopen($filename, 'r');
205                 if (!$f) return false;
206                 $lineno = 0;
207                 while (true) {
208                         $res = $this->read_entry($f, $lineno);
209                         if (!$res) break;
210                         if ($res['entry']->singular == '') {
211                                 $this->set_headers($this->make_headers($res['entry']->translations[0]));
212                         } else {
213                                 $this->add_entry($res['entry']);
214                         }
215                 }
216                 PO::read_line($f, 'clear');
217                 return $res !== false;
218         }
219         
220         function read_entry($f, $lineno = 0) {
221                 $entry = new Translation_Entry();
222                 // where were we in the last step
223                 // can be: comment, msgctxt, msgid, msgid_plural, msgstr, msgstr_plural
224                 $context = '';
225                 $msgstr_index = 0;
226                 $is_final = create_function('$context', 'return $context == "msgstr" || $context == "msgstr_plural";');
227                 while (true) {
228                         $lineno++;
229                         $line = PO::read_line($f);
230                         if (!$line)  {
231                                 if (feof($f)) {
232                                         if ($is_final($context))
233                                                 break;
234                                         elseif (!$context) // we haven't read a line and eof came
235                                                 return null;
236                                         else
237                                                 return false;
238                                 } else {
239                                         return false;
240                                 }
241                         }
242                         if ($line == "\n") continue;
243                         $line = trim($line);
244                         if (preg_match('/^#/', $line, $m)) {
245                                 // the comment is the start of a new entry
246                                 if ($is_final($context)) {
247                                         PO::read_line($f, 'put-back');
248                                         $lineno--;
249                                         break;
250                                 }
251                                 // comments have to be at the beginning
252                                 if ($context && $context != 'comment') {
253                                         return false;
254                                 }
255                                 // add comment
256                                 $this->add_comment_to_entry($entry, $line);;
257                         } elseif (preg_match('/^msgctxt\s+(".*")/', $line, $m)) {
258                                 if ($is_final($context)) {
259                                         PO::read_line($f, 'put-back');
260                                         $lineno--;
261                                         break;
262                                 }
263                                 if ($context && $context != 'comment') {
264                                         return false;
265                                 }
266                                 $context = 'msgctxt';
267                                 $entry->context .= PO::unpoify($m[1]);
268                         } elseif (preg_match('/^msgid\s+(".*")/', $line, $m)) {
269                                 if ($is_final($context)) {
270                                         PO::read_line($f, 'put-back');
271                                         $lineno--;
272                                         break;
273                                 }
274                                 if ($context && $context != 'msgctxt' && $context != 'comment') {
275                                         return false;
276                                 }
277                                 $context = 'msgid';
278                                 $entry->singular .= PO::unpoify($m[1]);
279                         } elseif (preg_match('/^msgid_plural\s+(".*")/', $line, $m)) {
280                                 if ($context != 'msgid') {
281                                         return false;
282                                 }
283                                 $context = 'msgid_plural';
284                                 $entry->is_plural = true;
285                                 $entry->plural .= PO::unpoify($m[1]);
286                         } elseif (preg_match('/^msgstr\s+(".*")/', $line, $m)) {
287                                 if ($context != 'msgid') {
288                                         return false;
289                                 }
290                                 $context = 'msgstr';
291                                 $entry->translations = array(PO::unpoify($m[1]));
292                         } elseif (preg_match('/^msgstr\[(\d+)\]\s+(".*")/', $line, $m)) {
293                                 if ($context != 'msgid_plural' && $context != 'msgstr_plural') {
294                                         return false;
295                                 }
296                                 $context = 'msgstr_plural';
297                                 $msgstr_index = $m[1];
298                                 $entry->translations[$m[1]] = PO::unpoify($m[2]);
299                         } elseif (preg_match('/^".*"$/', $line)) {
300                                 $unpoified = PO::unpoify($line);
301                                 switch ($context) {
302                                         case 'msgid':
303                                                 $entry->singular .= $unpoified; break;
304                                         case 'msgctxt':
305                                                 $entry->context .= $unpoified; break;
306                                         case 'msgid_plural':
307                                                 $entry->plural .= $unpoified; break;
308                                         case 'msgstr':
309                                                 $entry->translations[0] .= $unpoified; break;
310                                         case 'msgstr_plural':
311                                                 $entry->translations[$msgstr_index] .= $unpoified; break;
312                                         default:
313                                                 return false;
314                                 }
315                         } else {
316                                 return false;
317                         }
318                 }
319                 if (array() == array_filter($entry->translations)) $entry->translations = array();
320                 return array('entry' => $entry, 'lineno' => $lineno);
321         }
322         
323         function read_line($f, $action = 'read') {
324                 static $last_line = '';
325                 static $use_last_line = false;
326                 if ('clear' == $action) {
327                         $last_line = '';
328                         return true;
329                 }
330                 if ('put-back' == $action) {
331                         $use_last_line = true;
332                         return true;
333                 }
334                 $line = $use_last_line? $last_line : fgets($f);
335                 $last_line = $line;
336                 $use_last_line = false;
337                 return $line;
338         }
339         
340         function add_comment_to_entry(&$entry, $po_comment_line) {
341                 $first_two = substr($po_comment_line, 0, 2);
342                 $comment = trim(substr($po_comment_line, 2));
343                 if ('#:' == $first_two) {
344                         $entry->references = array_merge($entry->references, preg_split('/\s+/', $comment));
345                 } elseif ('#.' == $first_two) {
346                         $entry->extracted_comments = trim($entry->extracted_comments . "\n" . $comment);
347                 } elseif ('#,' == $first_two) {
348                         $entry->flags = array_merge($entry->flags, preg_split('/,\s*/', $comment));
349                 } else {
350                         $entry->translator_comments = trim($entry->translator_comments . "\n" . $comment);
351                 }
352         }
353         
354         function trim_quotes($s) {
355                 if ( substr($s, 0, 1) == '"') $s = substr($s, 1);
356                 if ( substr($s, -1, 1) == '"') $s = substr($s, 0, -1);
357                 return $s;
358         }
359 }
360 ?>