* * NB: * - If no $output_file specified, output filename is same as $input_file (but .mo) * - Returns true/false for success/failure * - No warranty, but if it breaks, please let me know * * More info: * https://github.com/josscrowcroft/php.mo * * Based on php-msgfmt by Matthias Bauer (Copyright © 2007), a command-line PHP tool * for converting .po files to .mo. * (http://wordpress-soc-2007.googlecode.com/svn/trunk/moeffju/php-msgfmt/msgfmt.php) * * License: GPL v3 http://www.opensource.org/licenses/gpl-3.0.html */ /** * The main .po to .mo function. */ function phpmo_convert($input, $output = false) { if (! $output) { $output = str_replace('.po', '.mo', $input); } $hash = phpmo_parse_po_file($input); if ($hash === false) { return false; } else { phpmo_write_mo_file($hash, $output); return true; } } function phpmo_clean_helper($x) { if (is_array($x)) { foreach ($x as $k => $v) { $x[$k] = phpmo_clean_helper($v); } } else { if ($x && $x[0] == '"') { $x = substr($x, 1, -1); } $x = str_replace("\"\n\"", '', $x); $x = str_replace('$', '\\$', $x); $x = str_replace('\\\\', '\\', $x); $x = preg_replace('/\+(["\$n])/', '\$1', $x); } return $x; } /* Parse gettext .po files. */ /* @link http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files */ function phpmo_parse_po_file($in) { // read .po file $fh = fopen($in, 'r'); if ($fh === false) { // Could not open file resource return false; } // results array $hash = []; // temporary array $temp = []; // state $state = null; $fuzzy = false; // iterate over lines while (($line = fgets($fh, 65536)) !== false) { $line = trim($line); if ($line === '') { continue; } $pair = preg_split('/\s/', $line, 2); $key = $pair[0]; $data = $pair[1] ?? null; if ($data === null) { continue; } switch ($key) { case '#,': // flag... $fuzzy = in_array('fuzzy', preg_split('/,\s*/', $data)); case '#': // translator-comments case '#.': // extracted-comments case '#:': // reference... case '#|': // msgid previous-untranslated-string // start a new entry if (sizeof($temp) && array_key_exists('msgid', $temp) && array_key_exists('msgstr', $temp)) { if (! $fuzzy) { $hash[] = $temp; } $temp = []; $state = null; $fuzzy = false; } break; case 'msgctxt': // context case 'msgid': // untranslated-string if (sizeof($temp)) { $hash[] = $temp; $temp = []; } case 'msgid_plural': // untranslated-string-plural $state = $key; $temp[$state] = $data; break; case 'msgstr': // translated-string $state = 'msgstr'; $temp[$state][] = $data; break; default: if (strpos($key, 'msgstr[') !== FALSE) { // translated-string-case-n $state = 'msgstr'; $temp[$state][] = $data; } else { // continued lines switch ($state) { case 'msgctxt': case 'msgid': case 'msgid_plural': $temp[$state] .= "\n" . $line; break; case 'msgstr': $temp[$state][sizeof($temp[$state]) - 1] .= "\n" . $line; break; default: // parse error fclose($fh); return FALSE; } } break; } } fclose($fh); // add final entry if ($state == 'msgstr') { $hash[] = $temp; } // Cleanup data, merge multiline entries, reindex hash for ksort $temp = $hash; $hash = []; foreach ($temp as $entry) { foreach ($entry as &$v) { $v = phpmo_clean_helper($v); if ($v === FALSE) { // parse error return FALSE; } } $hash[$entry['msgid']] = $entry; } return $hash; } function phpmo_po_text($hash) { $text = ''; foreach ($hash as $id => $value) { $msgid = phpmo_clean_helper(addcslashes($value['msgid'] ?? '', '"')); $msgstr = phpmo_clean_helper(addcslashes($value['msgstr'][0] ?? '', '"')); $text .= "msgid \"$msgid\"\nmsgstr \"$msgstr\"\n\n"; } return $text; } /* Write a gettext .po file. */ /* @link http://www.gnu.org/software/gettext/manual/gettext.html#MO-Files */ function phpmo_write_po_file($hash, $out) { $text = phpmo_po_text($hash); return file_put_contents($out, $text); } /* Write a GNU gettext style machine object. */ /* @link http://www.gnu.org/software/gettext/manual/gettext.html#MO-Files */ function phpmo_write_mo_file($hash, $out) { // sort by msgid ksort($hash, SORT_STRING); // our mo file data $mo = ''; // header data $offsets = []; $ids = ''; $strings = ''; foreach ($hash as $index => &$entry) { //don't add empty translations if (! $entry['msgstr'] || ! $entry['msgstr'][0]) { unset($hash[$index]); continue; } $id = $entry['msgid']; if (isset($entry['msgid_plural'])) { $id .= "\x00" . $entry['msgid_plural']; } // context is merged into id, separated by EOT (\x04) if (array_key_exists('msgctxt', $entry)) { $id = $entry['msgctxt'] . "\x04" . $id; } // plural msgstrs are NUL-separated $str = implode("\x00", $entry['msgstr']); $str = str_replace('\n',"\n", $str); //restore newlines // keep track of offsets $offsets[] = [ strlen($ids ), strlen($id), strlen($strings), strlen($str), ]; // plural msgids are not stored (?) $ids .= $id . "\x00"; $strings .= $str . "\x00"; } // keys start after the header (7 words) + index tables ($#hash * 4 words) $key_start = 7 * 4 + sizeof($hash) * 4 * 4; // values start right after the keys $value_start = $key_start + strlen($ids); // first all key offsets, then all value offsets $key_offsets = []; $value_offsets = []; // calculate foreach ($offsets as $v) { list($o1, $l1, $o2, $l2) = $v; $key_offsets[] = $l1; $key_offsets[] = $o1 + $key_start; $value_offsets[] = $l2; $value_offsets[] = $o2 + $value_start; } $offsets = array_merge($key_offsets, $value_offsets); // write header $mo .= pack('Iiiiiii', 0x950412de, // magic number 0, // version sizeof($hash), // number of entries in the catalog 7 * 4, // key index offset 7 * 4 + sizeof($hash) * 8, // value index offset, 0, // hashtable size (unused, thus 0) $key_start // hashtable offset ); // offsets foreach ($offsets as $offset) { $mo .= pack('i', $offset); } // ids $mo .= $ids; // strings $mo .= $strings; return file_put_contents($out, $mo); }