msgfmt.class.php 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. <?php
  2. namespace Juju;
  3. class MsgFmt {
  4. public static function from(string $popath){
  5. if(!file_exists($popath)){
  6. throw new \Exception("PO file {$popath} does not exist.");
  7. }
  8. $hash = [];
  9. $temp = [];
  10. $state = null;
  11. $fuzzy = false;
  12. // iterate over lines
  13. foreach(explode(
  14. "\n",
  15. str_replace(
  16. ["\r\n", "\r"],
  17. ["\n", "\n"],
  18. file_get_contents($popath)
  19. )
  20. ) as $line){
  21. $line = trim($line);
  22. if ($line !== ''){
  23. $exp = explode(' ', $line, 2);
  24. $key = $exp[0];
  25. if(isset($exp[1])){
  26. $data = $exp[1];
  27. }
  28. switch($key){
  29. case '#,': // flag...
  30. $fuzzy = in_array('fuzzy', preg_split('/,\s*/', $data));
  31. case '#': // translator-comments
  32. case '#.': // extracted-comments
  33. case '#:': // reference...
  34. case '#|': // msgid previous-untranslated-string
  35. // start a new entry
  36. if(sizeof($temp) && array_key_exists('msgid', $temp) && array_key_exists('msgstr', $temp)){
  37. if (!$fuzzy){
  38. $hash[] = $temp;
  39. }
  40. $temp = [];
  41. $state = null;
  42. $fuzzy = false;
  43. }
  44. break;
  45. case 'msgctxt': // context
  46. case 'msgid': // untranslated-string
  47. case 'msgid_plural': // untranslated-string-plural
  48. $state = $key;
  49. $temp[$state] = $data;
  50. break;
  51. case 'msgstr': // translated-string
  52. $state= 'msgstr';
  53. $temp[$state][] = $data;
  54. break;
  55. default:
  56. if(strpos($key, 'msgstr[') !== false){ // translated-string-case-n
  57. $state= 'msgstr';
  58. $temp[$state][] = $data;
  59. }else{ // continued lines
  60. switch($state){
  61. case 'msgctxt':
  62. case 'msgid':
  63. case 'msgid_plural':
  64. $temp[$state] .= "\n" . $line;
  65. break;
  66. case 'msgstr':
  67. $temp[$state][sizeof($temp[$state]) - 1] .= "\n" . $line;
  68. break;
  69. default: // parse error
  70. return false;
  71. }
  72. }
  73. break;
  74. }
  75. }
  76. }
  77. // add final entry
  78. if ($state === 'msgstr'){
  79. $hash[] = $temp;
  80. }
  81. // Cleanup data, merge multiline entries, reindex hash for ksort
  82. $temp= $hash;
  83. $hash= [];
  84. foreach($temp as $entry){
  85. foreach($entry as &$value){
  86. $value = self::clean($value);
  87. if($value === false){ // parse error
  88. return false;
  89. }
  90. }
  91. $hash[$entry['msgid']] = $entry;
  92. }
  93. return $hash;
  94. }
  95. public static function to(string $mopath, array $hash){
  96. // sort by msgid
  97. ksort($hash, SORT_STRING);
  98. // our mo file data
  99. $mo = '';
  100. // header data
  101. $offsets = [];
  102. $ids = '';
  103. $strings = '';
  104. foreach($hash as $entry){
  105. $id = $entry['msgid'];
  106. if(isset($entry['msgid_plural'])){
  107. $id .= "\x00" . $entry['msgid_plural'];
  108. }
  109. // context is merged into id, separated by EOT (\x04)
  110. if(array_key_exists('msgctxt', $entry)){
  111. $id = $entry['msgctxt'] . "\x04" . $id;
  112. }
  113. // plural msgstrs are NUL-separated
  114. $str = implode("\x00", $entry['msgstr']);
  115. // keep track of offsets
  116. $offsets[]= [
  117. strlen($ids),
  118. strlen($id),
  119. strlen($strings),
  120. strlen($str)
  121. ];
  122. // plural msgids are not stored (?)
  123. $ids .= $id . "\x00";
  124. $strings .= $str . "\x00";
  125. }
  126. // keys start after the header (7 words) + index tables ($#hash * 4 words)
  127. $key_start = 7 * 4 + sizeof($hash) * 4 * 4;
  128. // values start right after the keys
  129. $value_start = $key_start + strlen($ids);
  130. // first all key offsets, then all value offsets
  131. $key_offsets = [];
  132. $value_offsets = [];
  133. // calculate
  134. foreach($offsets as $value){
  135. list($o1, $l1, $o2, $l2) = $value;
  136. $key_offsets[] = $l1;
  137. $key_offsets[] = $o1 + $key_start;
  138. $value_offsets[] = $l2;
  139. $value_offsets[] = $o2 + $value_start;
  140. }
  141. $offsets= array_merge($key_offsets, $value_offsets);
  142. // write header
  143. $mo .= pack('Iiiiiii', 0x950412de, // magic number
  144. 0, // version
  145. sizeof($hash), // number of entries in the catalog
  146. 7 * 4, // key index offset
  147. 7 * 4 + sizeof($hash) * 8, // value index offset,
  148. 0, // hashtable size (unused, thus 0)
  149. $key_start // hashtable offset
  150. );
  151. // offsets
  152. foreach($offsets as $offset){
  153. $mo .= pack('i', $offset);
  154. }
  155. // ids
  156. $mo .= $ids;
  157. // strings
  158. $mo .= $strings;
  159. file_put_contents($mopath, $mo);
  160. }
  161. public static function convert(string $popath, string $mopath = null){
  162. if(is_null($mopath)){
  163. $mopath = dirname($popath).'/'.basename($popath, '.po').'.mo';
  164. }
  165. self::to($mopath, self::from($popath));
  166. }
  167. private static function clean($value){
  168. if(is_array($value)){
  169. foreach($value as $k => $v){
  170. $value[$k] = self::clean($v);
  171. }
  172. }else{
  173. if ($value[0] == '"'){
  174. $value = substr($value, 1, -1);
  175. }
  176. $value = (string)str_replace(
  177. '$',
  178. '\\$',
  179. str_replace("\"\n\"", '', $value)
  180. );
  181. }
  182. return $value;
  183. }
  184. }
  185. ?>