3 * @copyright Copyright (C) 2010-2021, the Friendica project
5 * @license GNU AGPL version 3 or any later version
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.
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.
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/>.
22 namespace Friendica\Console;
24 use Geekwright\Po\PoFile;
25 use Geekwright\Po\PoTokens;
28 * Read a messages.po file and create strings.php in the same directory
30 class PoToPhp extends \Asika\SimpleConsole\Console
32 protected $helpOptions = ['h', 'help', '?'];
34 const DQ_ESCAPE = "__DQ__";
36 protected function getHelp()
39 console php2po - Generate a strings.php file from a messages.po file
41 bin/console php2po <path/to/messages.po> [-h|--help|-?] [-v]
44 Read a messages.po file and create the according strings.php in the same directory
47 -h|--help|-? Show help information
48 -v Show more debug information.
53 protected function doExecute()
55 if ($this->getOption('v')) {
56 $this->out('Class: ' . __CLASS__);
57 $this->out('Arguments: ' . var_export($this->args, true));
58 $this->out('Options: ' . var_export($this->options, true));
61 if (count($this->args) == 0) {
62 $this->out($this->getHelp());
66 if (count($this->args) > 1) {
67 throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments');
70 $pofile = realpath($this->getArgument(0));
72 if (!file_exists($pofile)) {
73 throw new \RuntimeException('Supplied file path doesn\'t exist.');
76 if (!is_writable(dirname($pofile))) {
77 throw new \RuntimeException('Supplied directory isn\'t writable.');
80 $outfile = dirname($pofile) . DIRECTORY_SEPARATOR . 'strings.php';
82 if (basename(dirname($pofile)) == 'C') {
85 $lang = str_replace('-', '_', basename(dirname($pofile)));
88 $this->out('Out to ' . $outfile);
90 $out = $this->poFile2Php($lang, $pofile);
92 if (!file_put_contents($outfile, $out)) {
93 throw new \RuntimeException('Unable to write to ' . $outfile);
99 private function poFile2Php($lang, $infile): string
101 $poFile = new PoFile();
102 $poFile->readPoFile($infile);
106 $pluralForms = $poFile->getHeaderEntry()->getHeader('plural-forms');
109 throw new \RuntimeException('No Plural-Forms header detected');
112 $regex = 'nplurals=([0-9]*); *plural=(.*?)[\\\\;]';
114 if (!preg_match('|' . $regex . '|', $pluralForms, $match)) {
115 throw new \RuntimeException('Unexpected Plural-Forms header value, expected "' . $regex . '", found ' . $pluralForms);
118 $out .= $this->createPluralSelectFunctionString($match[2], $lang);
120 foreach ($poFile->getEntries() as $entry) {
121 if (!implode('', $entry->getAsStringArray(PoTokens::TRANSLATED))) {
122 // Skip completely untranslated entries
126 $out .= '$a->strings[' . self::escapePhpString($entry->getAsString(PoTokens::MESSAGE)) . '] = ';
128 $msgid_plural = $entry->get(PoTokens::PLURAL);
129 if (empty($msgid_plural)) {
130 $out .= self::escapePhpString($entry->getAsString(PoTokens::TRANSLATED)) . ';' . "\n";
133 foreach($entry->getAsStringArray(PoTokens::TRANSLATED) as $key => $msgstr) {
134 $out .= "\t" . $key . ' => ' . self::escapePhpString($msgstr) . ',' . "\n";
144 private function createPluralSelectFunctionString(string $pluralForms, string $lang): string
146 $return = $this->convertCPluralConditionToPhpReturnStatement(
150 $fnname = 'string_plural_select_' . $lang;
151 $out = 'if(! function_exists("' . $fnname . '")) {' . "\n";
152 $out .= 'function ' . $fnname . '($n){' . "\n";
153 $out .= ' $n = intval($n);' . "\n";
154 $out .= ' ' . $return . "\n";
160 private static function escapePhpString($string): string
162 return "'" . strtr($string, ['\'' => '\\\'']) . "'";
166 * Converts C-style plural condition in .po files to a PHP-style plural return statement
168 * Adapted from https://github.com/friendica/friendica/issues/9747#issuecomment-769604485
169 * Many thanks to Christian Archer (https://github.com/sunchaserinfo)
171 * @param string $cond
174 private function convertCPluralConditionToPhpReturnStatement(string $cond)
176 $cond = str_replace('n', '$n', $cond);
179 self::parse($cond, $tree);
181 return is_string($tree) ? "return intval({$tree});" : self::render($tree);
185 * Parses the condition into an array if there's at least a ternary operator, to a string otherwise
187 * Warning: Black recursive magic
189 * @param string $string
190 * @param array|string $node
192 private static function parse(string $string, &$node = [])
194 // Removes extra outward parentheses
195 if (strpos($string, '(') === 0 && strrpos($string, ')') === strlen($string) - 1) {
196 $string = substr($string, 1, -1);
199 $q = strpos($string, '?');
200 $s = strpos($string, ':');
202 if ($q === false && $s === false) {
207 if ($q === false || $s < $q) {
208 list($then, $else) = explode(':', $string, 2);
209 $node['then'] = $then;
211 self::parse($else, $parsedElse);
212 $node['else'] = $parsedElse;
214 list($if, $thenelse) = explode('?', $string, 2);
216 self::parse($thenelse, $node);
221 * Renders the parsed condition tree into a return statement
223 * Warning: Black recursive magic
228 private static function render($tree): string
230 if (is_array($tree)) {
231 $if = trim($tree['if']);
232 $then = trim($tree['then']);
233 $else = self::render($tree['else']);
235 return "if ({$if}) { return {$then}; } else {$else}";
240 return " { return {$tree}; }";