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