]> git.mxchange.org Git - friendica.git/blob - src/Console/PoToPhp.php
Merge branch '2021.03-rc' into copyright-2021
[friendica.git] / src / Console / PoToPhp.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2021, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Console;
23
24 /**
25  * Read a messages.po file and create strings.php in the same directory
26  */
27 class PoToPhp extends \Asika\SimpleConsole\Console
28 {
29         protected $helpOptions = ['h', 'help', '?'];
30
31         const DQ_ESCAPE = "__DQ__";
32
33         protected function getHelp()
34         {
35                 $help = <<<HELP
36 console php2po - Generate a strings.php file from a messages.po file
37 Usage
38         bin/console php2po <path/to/messages.po> [-h|--help|-?] [-v]
39
40 Description
41         Read a messages.po file and create the according strings.php in the same directory
42
43 Options
44         -h|--help|-?  Show help information
45         -v            Show more debug information.
46 HELP;
47                 return $help;
48         }
49
50         protected function doExecute()
51         {
52                 if ($this->getOption('v')) {
53                         $this->out('Class: ' . __CLASS__);
54                         $this->out('Arguments: ' . var_export($this->args, true));
55                         $this->out('Options: ' . var_export($this->options, true));
56                 }
57
58                 if (count($this->args) == 0) {
59                         $this->out($this->getHelp());
60                         return 0;
61                 }
62
63                 if (count($this->args) > 1) {
64                         throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments');
65                 }
66
67                 $pofile = realpath($this->getArgument(0));
68
69                 if (!file_exists($pofile)) {
70                         throw new \RuntimeException('Supplied file path doesn\'t exist.');
71                 }
72
73                 if (!is_writable(dirname($pofile))) {
74                         throw new \RuntimeException('Supplied directory isn\'t writable.');
75                 }
76
77                 $outfile = dirname($pofile) . DIRECTORY_SEPARATOR . 'strings.php';
78
79                 if (basename(dirname($pofile)) == 'C') {
80                         $lang = 'en';
81                 } else {
82                         $lang = str_replace('-', '_', basename(dirname($pofile)));
83                 }
84
85                 $this->out('Out to ' . $outfile);
86
87                 $out = "<?php\n\n";
88
89                 $infile = file($pofile);
90                 $k = '';
91                 $v = '';
92                 $arr = false;
93                 $ink = false;
94                 $inv = false;
95                 $escape_s_exp = '|[^\\\\]\$[a-z]|';
96
97                 foreach ($infile as $l) {
98                         $l = str_replace('\"', self::DQ_ESCAPE, $l);
99                         $len = strlen($l);
100                         if ($l[0] == "#") {
101                                 $l = "";
102                         }
103
104                         if (substr($l, 0, 15) == '"Plural-Forms: ') {
105                                 $match = [];
106                                 preg_match("|nplurals=([0-9]*); *plural=(.*?)[;\\\\]|", $l, $match);
107                                 $return = $this->convertCPluralConditionToPhpReturnStatement($match[2]);
108                                 // define plural select function if not already defined
109                                 $fnname = 'string_plural_select_' . $lang;
110                                 $out .= 'if(! function_exists("' . $fnname . '")) {' . "\n";
111                                 $out .= 'function ' . $fnname . '($n){' . "\n";
112                                 $out .= '       $n = intval($n);' . "\n";
113                                 $out .= '       ' . $return . "\n";
114                                 $out .= '}}' . "\n";
115                         }
116
117                         if ($k != '' && substr($l, 0, 7) == 'msgstr ') {
118                                 $v = substr($l, 8, $len - 10);
119                                 $v = preg_replace_callback($escape_s_exp, [$this, 'escapeDollar'], $v);
120
121                                 if ($v != '') {
122                                         $out .= '$a->strings["' . $k . '"] = "' . $v . '"';
123                                 } else {
124                                         $k = '';
125                                         $ink = false;
126                                 }
127                         }
128
129                         if ($k != "" && substr($l, 0, 7) == 'msgstr[') {
130                                 if ($ink) {
131                                         $ink = false;
132                                         $out .= '$a->strings["' . $k . '"] = ';
133                                 }
134                                 if ($inv) {
135                                         $inv = false;
136                                         $out .= '"' . $v . '"';
137                                 }
138
139                                 if (!$arr) {
140                                         $arr = true;
141                                         $out .= "[\n";
142                                 }
143
144                                 $match = [];
145                                 preg_match("|\[([0-9]*)\] (.*)|", $l, $match);
146                                 if ($match[2] !== '""') {
147                                         $out .= "\t"
148                                                 . preg_replace_callback($escape_s_exp, [$this, 'escapeDollar'], $match[1])
149                                                 . ' => '
150                                                 . preg_replace_callback($escape_s_exp, [$this, 'escapeDollar'], $match[2])
151                                                 . ",\n";
152                                 }
153                         }
154
155                         if (substr($l, 0, 6) == 'msgid_') {
156                                 $ink = false;
157                                 $out .= '$a->strings["' . $k . '"] = ';
158                         }
159
160                         if ($ink) {
161                                 $k .= trim($l, "\"\r\n");
162                                 $k = preg_replace_callback($escape_s_exp, [$this, 'escapeDollar'], $k);
163                         }
164
165                         if (substr($l, 0, 6) == 'msgid ') {
166                                 if ($inv) {
167                                         $inv = false;
168                                         $out .= '"' . $v . '"';
169                                 }
170
171                                 if ($k != "") {
172                                         $out .= ($arr) ? "];\n" : ";\n";
173                                 }
174
175                                 $arr = false;
176                                 $k = str_replace("msgid ", "", $l);
177                                 if ($k != '""') {
178                                         $k = trim($k, "\"\r\n");
179                                 } else {
180                                         $k = '';
181                                 }
182
183                                 $k = preg_replace_callback($escape_s_exp, [$this, 'escapeDollar'], $k);
184                                 $ink = true;
185                         }
186
187                         if ($inv && substr($l, 0, 6) != "msgstr") {
188                                 $v .= trim($l, "\"\r\n");
189                                 $v = preg_replace_callback($escape_s_exp, [$this, 'escapeDollar'], $v);
190                         }
191                 }
192
193                 if ($inv) {
194                         $out .= '"' . $v . '"';
195                 }
196
197                 if ($k != '') {
198                         $out .= ($arr ? "];\n" : ";\n");
199                 }
200
201                 $out = str_replace(self::DQ_ESCAPE, '\"', $out);
202                 if (!file_put_contents($outfile, $out)) {
203                         throw new \RuntimeException('Unable to write to ' . $outfile);
204                 }
205
206                 return 0;
207         }
208
209         private function escapeDollar($match)
210         {
211                 return str_replace('$', '\$', $match[0]);
212         }
213
214         /**
215          * Converts C-style plural condition in .po files to a PHP-style plural return statement
216          *
217          * Adapted from https://github.com/friendica/friendica/issues/9747#issuecomment-769604485
218          * Many thanks to Christian Archer (https://github.com/sunchaserinfo)
219          *
220          * @param string $cond
221          * @return string
222          */
223         private function convertCPluralConditionToPhpReturnStatement(string $cond)
224         {
225                 $cond = str_replace('n', '$n', $cond);
226
227                 /**
228                  * Parses the condition into an array if there's at least a ternary operator, to a string otherwise
229                  *
230                  * Warning: Black recursive magic
231                  *
232                  * @param string $string
233                  * @param array|string $node
234                  */
235                 function parse(string $string, &$node = [])
236                 {
237                         // Removes extra outward parentheses
238                         if (strpos($string, '(') === 0 && strrpos($string, ')') === strlen($string) - 1) {
239                                 $string = substr($string, 1, -1);
240                         }
241
242                         $q = strpos($string, '?');
243                         $s = strpos($string, ':');
244
245                         if ($q === false && $s === false) {
246                                 $node = $string;
247                                 return;
248                         }
249
250                         if ($q === false || $s < $q) {
251                                 list($then, $else) = explode(':', $string, 2);
252                                 $node['then'] = $then;
253                                 $parsedElse = [];
254                                 parse($else, $parsedElse);
255                                 $node['else'] = $parsedElse;
256                         } else {
257                                 list($if, $thenelse) = explode('?', $string, 2);
258                                 $node['if'] = $if;
259                                 parse($thenelse, $node);
260                         }
261                 }
262
263                 /**
264                  * Renders the parsed condition tree into a return statement
265                  *
266                  * Warning: Black recursive magic
267                  *
268                  * @param $tree
269                  * @return string
270                  */
271                 function render($tree)
272                 {
273                         if (is_array($tree)) {
274                                 $if = trim($tree['if']);
275                                 $then = trim($tree['then']);
276                                 $else = render($tree['else']);
277
278                                 return "if ({$if}) { return {$then}; } else {$else}";
279                         }
280
281                         $tree = trim($tree);
282
283                         return " { return {$tree}; }";
284                 }
285
286                 $tree = [];
287                 parse($cond, $tree);
288
289                 return is_string($tree) ? "return intval({$tree});" : render($tree);
290         }
291 }