PageRenderTime 54ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/wp-content/plugins/loco-translate/src/gettext/Data.php

https://gitlab.com/campus-academy/krowkaramel
PHP | 397 lines | 238 code | 38 blank | 121 comment | 33 complexity | 99b458df19150d8f87a2ae5627e28143 MD5 | raw file
  1. <?php
  2. loco_require_lib('compiled/gettext.php');
  3. /**
  4. * Wrapper for array forms of parsed PO data
  5. */
  6. class Loco_gettext_Data extends LocoPoIterator implements JsonSerializable {
  7. /**
  8. * Normalize file extension to internal type
  9. * @param Loco_fs_File
  10. * @return string Normalized file extension "po", "pot" or "mo"
  11. * @throws Loco_error_Exception
  12. */
  13. public static function ext( Loco_fs_File $file ){
  14. $ext = rtrim( strtolower( $file->extension() ), '~' );
  15. if( 'po' === $ext || 'pot' === $ext || 'mo' === $ext ){
  16. // We could validate file location here, but file type restriction should be sufficient
  17. return $ext;
  18. }
  19. // translators: Error thrown when attempting to parse a file that is not PO, POT or MO
  20. throw new Loco_error_Exception( sprintf( __('%s is not a Gettext file','loco-translate'), $file->basename() ) );
  21. }
  22. /**
  23. * @param Loco_fs_File
  24. * @return Loco_gettext_Data
  25. */
  26. public static function load( Loco_fs_File $file ){
  27. $type = strtoupper( self::ext($file) );
  28. // catch parse errors so we can inform user of which file is bad
  29. try {
  30. if( 'MO' === $type ){
  31. return self::fromBinary( $file->getContents() );
  32. }
  33. else {
  34. return self::fromSource( $file->getContents() );
  35. }
  36. }
  37. catch( Loco_error_ParseException $e ){
  38. $path = $file->getRelativePath( loco_constant('WP_CONTENT_DIR') );
  39. Loco_error_AdminNotices::debug( sprintf('Failed to parse %s as a %s file; %s',$path,$type,$e->getMessage()) );
  40. throw new Loco_error_ParseException( sprintf('Invalid %s file: %s',$type,basename($path)) );
  41. }
  42. }
  43. /**
  44. * Like load but just pulls header, saving a full parse. PO only
  45. * @param Loco_fs_File
  46. * @return LocoPoHeaders
  47. * @throws InvalidArgumentException
  48. */
  49. public static function head( Loco_fs_File $file ){
  50. if( 'mo' === self::ext($file) ){
  51. throw new InvalidArgumentException('PO only');
  52. }
  53. $p = new LocoPoParser( $file->getContents() );
  54. $p->parse(0);
  55. return $p->getHeader();
  56. }
  57. /**
  58. * @param string assumed PO source
  59. * @return Loco_gettext_Data
  60. */
  61. public static function fromSource( $src ){
  62. $p = new LocoPoParser($src);
  63. return new Loco_gettext_Data( $p->parse() );
  64. }
  65. /**
  66. * @param string assumed MO bytes
  67. * @return Loco_gettext_Data
  68. */
  69. public static function fromBinary( $bin ){
  70. $p = new LocoMoParser($bin);
  71. return new Loco_gettext_Data( $p->parse() );
  72. }
  73. /**
  74. * Create a dummy/empty instance with minimum content to be a valid PO file.
  75. * @return Loco_gettext_Data
  76. */
  77. public static function dummy(){
  78. return new Loco_gettext_Data( [ ['source'=>'','target'=>'Language:'] ] );
  79. }
  80. /**
  81. * Ensure PO source is UTF-8.
  82. * Required if we want PO code when we're not parsing it. e.g. source view
  83. * @param string
  84. * @return string
  85. */
  86. public static function ensureUtf8( $src ){
  87. $src = loco_remove_bom($src,$cs);
  88. if( ! $cs ){
  89. // read PO header, requiring partial parse
  90. try {
  91. $cs = LocoPoHeaders::fromSource($src)->getCharset();
  92. }
  93. catch( Loco_error_ParseException $e ){
  94. Loco_error_AdminNotices::debug( $e->getMessage() );
  95. }
  96. }
  97. return loco_convert_utf8($src,$cs,false);
  98. }
  99. /**
  100. * Compile messages to binary MO format
  101. * @return string MO file source
  102. * @throws Loco_error_Exception
  103. */
  104. public function msgfmt(){
  105. if( 2 !== strlen("\xC2\xA3") ){
  106. throw new Loco_error_Exception('Refusing to compile MO file. Please disable mbstring.func_overload'); // @codeCoverageIgnore
  107. }
  108. $mo = new LocoMo( $this, $this->getHeaders() );
  109. $opts = Loco_data_Settings::get();
  110. if( $opts->gen_hash ){
  111. $mo->enableHash();
  112. }
  113. if( $opts->use_fuzzy ){
  114. $mo->useFuzzy();
  115. }
  116. /*/ TODO optionally exclude .js strings
  117. if( $opts->purge_js ){
  118. $mo->filter....
  119. }*/
  120. return $mo->compile();
  121. }
  122. /**
  123. * Get final UTF-8 string for writing to file
  124. * @param bool whether to sort output, generally only for extracting strings
  125. * @return string
  126. */
  127. public function msgcat( $sort = false ){
  128. // set maximum line width, zero or >= 15
  129. $this->wrap( Loco_data_Settings::get()->po_width );
  130. // concat with default text sorting if specified
  131. $po = $this->render( $sort ? [ 'LocoPoIterator', 'compare' ] : null );
  132. // Prepend byte order mark only if configured
  133. if( Loco_data_Settings::get()->po_utf8_bom ){
  134. $po = "\xEF\xBB\xBF".$po;
  135. }
  136. return $po;
  137. }
  138. /**
  139. * Compile JED flavour JSON
  140. * @param string text domain for JED metadata
  141. * @param string source file that uses included strings
  142. * @return string
  143. */
  144. public function msgjed( $domain = 'messages', $source = '' ){
  145. $head = $this->getHeaders();
  146. $head['domain'] = $domain;
  147. $data = $this->exportJed();
  148. // Pretty formatting for debugging. Doing as per WordPress and always escaping Unicode.
  149. $json_options = 0;
  150. if( Loco_data_Settings::get()->jed_pretty ){
  151. $json_options |= loco_constant('JSON_PRETTY_PRINT') | loco_constant('JSON_UNESCAPED_SLASHES'); // | loco_constant('JSON_UNESCAPED_UNICODE');
  152. }
  153. // PO should have a date if localised properly
  154. return json_encode( [
  155. 'translation-revision-date' => $head['PO-Revision-Date'],
  156. 'generator' => $head['X-Generator'],
  157. 'source' => $source,
  158. 'domain' => $domain,
  159. 'locale_data' => [
  160. $domain => $data,
  161. ],
  162. ], $json_options );
  163. }
  164. /**
  165. * @return array
  166. */
  167. #[ReturnTypeWillChange]
  168. public function jsonSerialize(){
  169. $po = $this->getArrayCopy();
  170. // exporting headers non-scalar so js doesn't have to parse them
  171. try {
  172. $headers = $this->getHeaders();
  173. if( count($headers) && '' === $po[0]['source'] ){
  174. $po[0]['target'] = $headers->getArrayCopy();
  175. }
  176. }
  177. // suppress header errors when serializing
  178. // @codeCoverageIgnoreStart
  179. catch( Exception $e ){ }
  180. // @codeCoverageIgnoreEnd
  181. return $po;
  182. }
  183. /**
  184. * Create a signature for use in comparing source strings between documents
  185. * @return string
  186. */
  187. public function getSourceDigest(){
  188. $data = $this->getHashes();
  189. return md5( implode("\1",$data) );
  190. }
  191. /**
  192. * @param Loco_Locale
  193. * @param string[] custom headers
  194. * @return Loco_gettext_Data
  195. */
  196. public function localize( Loco_Locale $locale, array $custom = [] ){
  197. $date = gmdate('Y-m-d H:i').'+0000';
  198. // headers that must always be set if absent
  199. $defaults = [
  200. 'Project-Id-Version' => '',
  201. 'Report-Msgid-Bugs-To' => '',
  202. 'POT-Creation-Date' => $date,
  203. ];
  204. // headers that must always override when localizing
  205. $required = [
  206. 'PO-Revision-Date' => $date,
  207. 'Last-Translator' => '',
  208. 'Language-Team' => $locale->getName(),
  209. 'Language' => (string) $locale,
  210. 'Plural-Forms' => $locale->getPluralFormsHeader(),
  211. 'MIME-Version' => '1.0',
  212. 'Content-Type' => 'text/plain; charset=UTF-8',
  213. 'Content-Transfer-Encoding' => '8bit',
  214. 'X-Generator' => 'Loco https://localise.biz/',
  215. 'X-Loco-Version' => sprintf('%s; wp-%s', loco_plugin_version(), $GLOBALS['wp_version'] ),
  216. ];
  217. // Allow some existing headers to remain if PO was previously localized to the same language
  218. $headers = $this->getHeaders();
  219. $previous = Loco_Locale::parse( $headers->trimmed('Language') );
  220. if( $previous->lang === $locale->lang ){
  221. $header = $headers->trimmed('Plural-Forms');
  222. if( preg_match('/^\\s*nplurals\\s*=\\s*\\d+\\s*;\\s*plural\\s*=/', $header) ) {
  223. $required['Plural-Forms'] = $header;
  224. }
  225. if( $previous->region === $locale->region && $previous->variant === $locale->variant ){
  226. unset( $required['Language-Team'] );
  227. }
  228. }
  229. // set user's preferred Last-Translator credit if configured
  230. if( function_exists('get_current_user_id') && get_current_user_id() ){
  231. $prefs = Loco_data_Preferences::get();
  232. $credit = (string) $prefs->credit;
  233. if( '' === $credit ){
  234. $credit = $prefs->default_credit();
  235. }
  236. // filter credit with current user name and email
  237. $user = wp_get_current_user();
  238. $credit = apply_filters( 'loco_current_translator', $credit, $user->get('display_name'), $user->get('email') );
  239. if( '' !== $credit ){
  240. $required['Last-Translator'] = $credit;
  241. }
  242. }
  243. $headers = $this->applyHeaders($required,$defaults,$custom);
  244. // avoid non-empty POT placeholders that won't have been set from $defaults
  245. if( 'PACKAGE VERSION' === $headers['Project-Id-Version'] ){
  246. $headers['Project-Id-Version'] = '';
  247. }
  248. // finally allow headers to be modified via filter
  249. $replaced = apply_filters( 'loco_po_headers', $headers );
  250. if( $replaced instanceof LocoPoHeaders && $replaced !== $headers ){
  251. $this->setHeaders($replaced);
  252. }
  253. return $this->initPo();
  254. }
  255. /**
  256. * @param string
  257. * @return Loco_gettext_Data
  258. */
  259. public function templatize( $domain = '' ){
  260. $date = gmdate('Y-m-d H:i').'+0000'; // <- forcing UCT
  261. $defaults = [
  262. 'Project-Id-Version' => 'PACKAGE VERSION',
  263. 'Report-Msgid-Bugs-To' => '',
  264. ];
  265. $required = [
  266. 'POT-Creation-Date' => $date,
  267. 'PO-Revision-Date' => 'YEAR-MO-DA HO:MI+ZONE',
  268. 'Last-Translator' => 'FULL NAME <EMAIL@ADDRESS>',
  269. 'Language-Team' => '',
  270. 'Language' => '',
  271. 'Plural-Forms' => 'nplurals=INTEGER; plural=EXPRESSION;',
  272. 'MIME-Version' => '1.0',
  273. 'Content-Type' => 'text/plain; charset=UTF-8',
  274. 'Content-Transfer-Encoding' => '8bit',
  275. 'X-Generator' => 'Loco https://localise.biz/',
  276. 'X-Loco-Version' => sprintf('%s; wp-%s', loco_plugin_version(), $GLOBALS['wp_version'] ),
  277. 'X-Domain' => $domain,
  278. ];
  279. $headers = $this->applyHeaders($required,$defaults);
  280. // finally allow headers to be modified via filter
  281. $replaced = apply_filters( 'loco_pot_headers', $headers );
  282. if( $replaced instanceof LocoPoHeaders && $replaced !== $headers ){
  283. $this->setHeaders($replaced);
  284. }
  285. return $this->initPot();
  286. }
  287. /**
  288. * @param string[] Required headers
  289. * @param string[] Default headers
  290. * @param string[] Custom headers
  291. * @return LocoPoHeaders
  292. */
  293. private function applyHeaders( array $required = [], array $defaults = [], array $custom = [] ){
  294. $headers = $this->getHeaders();
  295. // only set absent or empty headers from default list
  296. foreach( $defaults as $key => $value ){
  297. if( ! $headers[$key] ){
  298. $headers[$key] = $value;
  299. }
  300. }
  301. // add required headers with custom ones overriding
  302. if( $custom ){
  303. $required = array_merge( $required, $custom );
  304. }
  305. // TODO fix ordering weirdness here. required headers seem to get appended wrongly
  306. foreach( $required as $key => $value ){
  307. $headers[$key] = $value;
  308. }
  309. return $headers;
  310. }
  311. /**
  312. * Remap proprietary base path when PO file is moving to another location.
  313. *
  314. * @param Loco_fs_File the file that was originally extracted to (POT)
  315. * @param Loco_fs_File the file that must now target references relative to itself
  316. * @param string vendor name used in header keys
  317. * @return bool whether base header was altered
  318. */
  319. public function rebaseHeader( Loco_fs_File $origin, Loco_fs_File $target, $vendor ){
  320. $base = $target->getParent();
  321. $head = $this->getHeaders();
  322. $key = $head->normalize('X-'.$vendor.'-Basepath');
  323. if( $key ){
  324. $oldRelBase = $head[$key];
  325. $oldAbsBase = new Loco_fs_Directory($oldRelBase);
  326. $oldAbsBase->normalize( $origin->getParent() );
  327. $newRelBase = $oldAbsBase->getRelativePath($base);
  328. // new base path is relative to $target location
  329. $head[$key] = $newRelBase;
  330. return true;
  331. }
  332. return false;
  333. }
  334. /**
  335. * Inherit meta values from header given, but leave standard headers intact.
  336. * @param LocoPoHeaders source header
  337. */
  338. public function inheritHeader( LocoPoHeaders $source ){
  339. $target = $this->getHeaders();
  340. foreach( $source as $key => $value ){
  341. if( 'X-' === substr($key,0,2) ) {
  342. $target[$key] = $value;
  343. }
  344. }
  345. }
  346. /**
  347. * @param string date format as Gettext states "YEAR-MO-DA HO:MI+ZONE"
  348. * @return int
  349. */
  350. public static function parseDate( $podate ){
  351. if( method_exists('DateTime','createFromFormat') ){
  352. $objdate = DateTime::createFromFormat('Y-m-d H:iO', $podate);
  353. if( $objdate instanceof DateTime ){
  354. return $objdate->getTimestamp();
  355. }
  356. }
  357. return strtotime($podate);
  358. }
  359. }