Browse Source

Automatically generate *.mo files if they don't exist

Nathaniel van Diepen 6 years ago
parent
commit
9cf1a6b552
2 changed files with 203 additions and 2 deletions
  1. 17 2
      Http/response.class.php
  2. 186 0
      msgfmt.class.php

+ 17 - 2
Http/response.class.php

@@ -1,8 +1,9 @@
 <?php
 	namespace Juju\Http;
 	require_once(realpath(dirname(__DIR__).'/events.trait.php'));
+	require_once(realpath(dirname(__DIR__).'/msgfmt.class.php'));
 	require_once(realpath(dirname(__DIR__).'/App/router.class.php'));
-	use \Juju\{App\Router, Events};
+	use \Juju\{App\Router, Events, MsgFmt};
 
 	class Response implements \JsonSerializable {
 		use Events;
@@ -153,6 +154,19 @@
 			}
 			return $this;
 		}
+		public static function gen_locale(string $folder){
+			$path = realpath($folder);
+			if($path !== false){
+				foreach(glob("{$path}/*/LC_MESSAGES/*.po") as $po_file){
+					$mo_file = dirname($po_file).'/'.basename($po_file, '.po').'.mo';
+					if(!file_exists($mo_file)){
+						MsgFmt::convert($po_file, $mo_file);
+					}
+				}
+			}else{
+				trigger_error("Locale folder {$folder} missing", E_USER_WARNING);
+			}
+		}
 		public static function locale(string $locale, string $domain, string $folder, string $codeset = 'UTF-8'){
 			$path = realpath($folder);
 			if($path !== false){
@@ -160,6 +174,7 @@
 				textdomain($domain);
 				bind_textdomain_codeset($domain, $codeset);
 				$orig_locale = $locale;
+				self::gen_locale($path);
 				if(!file_exists("{$path}/$locale/LC_MESSAGES/{$domain}.mo")){
 					$locale = \Locale::getPrimaryLanguage($locale);
 					if(!file_exists("{$path}/$locale/LC_MESSAGES/{$domain}.mo")){
@@ -182,4 +197,4 @@
 			\Locale::setDefault($locale);
 		}
 	}
-?>
+?>

+ 186 - 0
msgfmt.class.php

@@ -0,0 +1,186 @@
+<?php
+	namespace Juju;
+
+	class MsgFmt {
+		public static function from(string $popath){
+			if(!file_exists($popath)){
+				throw new \Exception("PO file {$popath} does not exist.");
+			}
+			$hash = [];
+			$temp = [];
+			$state = null;
+			$fuzzy = false;
+			// iterate over lines
+			foreach(explode(
+				"\n",
+				str_replace(
+					["\r\n", "\r"],
+					["\n", "\n"],
+					file_get_contents($popath)
+				)
+			) as $line){
+				$line = trim($line);
+				if ($line !== ''){
+					$exp = explode(' ', $line, 2);
+					$key = $exp[0];
+					if(isset($exp[1])){
+						$data = $exp[1];
+					}
+					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
+						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
+										return false;
+								}
+							}
+						break;
+					}
+				}
+			}
+			// 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 &$value){
+					$value = self::clean($value);
+					if($value === false){ // parse error
+						return false;
+					}
+				}
+				$hash[$entry['msgid']] = $entry;
+			}
+			return $hash;
+		}
+		public static function to(string $mopath, array $hash){
+			// sort by msgid
+			ksort($hash, SORT_STRING);
+			// our mo file data
+			$mo = '';
+			// header data
+			$offsets = [];
+			$ids = '';
+			$strings = '';
+			foreach($hash as $entry){
+				$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']);
+				// 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 $value){
+				list($o1, $l1, $o2, $l2) = $value;
+				$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;
+			file_put_contents($mopath, $mo);
+		}
+		public static function convert(string $popath, string $mopath = null){
+			if(is_null($mopath)){
+				$mopath = dirname($popath).'/'.basename($popath, '.po').'.mo';
+			}
+			self::to($mopath, self::from($popath));
+		}
+		private static function clean($value){
+			if(is_array($value)){
+				foreach($value as $k => $v){
+					$value[$k] = self::clean($v);
+				}
+			}else{
+				if ($value[0] == '"'){
+					$value = substr($value, 1, -1);
+				}
+				$value = (string)str_replace(
+					'$',
+					'\\$',
+					str_replace("\"\n\"", '', $value)
+				);
+			}
+			return $value;
+		}
+	}
+?>