<?php

/**
 * php.mo 0.1 by Joss Crowcroft (http://www.josscrowcroft.com).
 * 
 * Converts gettext translation '.po' files to binary '.mo' files in PHP.
 * 
 * Usage: 
 * <?php require('php-mo.php'); phpmo_convert( 'input.po', [ 'output.mo' ] ); ?>
 * 
 * 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);
}
