PageRenderTime 47ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/htdocs/wp-includes/pomo/po.php

https://gitlab.com/VTTE/sitios-vtte
PHP | 510 lines | 371 code | 29 blank | 110 comment | 77 complexity | bbf8e761ae21c447f30ce6c8c4eb7867 MD5 | raw file
  1. <?php
  2. /**
  3. * Class for working with PO files
  4. *
  5. * @version $Id: po.php 1158 2015-11-20 04:31:23Z dd32 $
  6. * @package pomo
  7. * @subpackage po
  8. */
  9. require_once __DIR__ . '/translations.php';
  10. if ( ! defined( 'PO_MAX_LINE_LEN' ) ) {
  11. define( 'PO_MAX_LINE_LEN', 79 );
  12. }
  13. ini_set( 'auto_detect_line_endings', 1 );
  14. /**
  15. * Routines for working with PO files
  16. */
  17. if ( ! class_exists( 'PO', false ) ) :
  18. class PO extends Gettext_Translations {
  19. var $comments_before_headers = '';
  20. /**
  21. * Exports headers to a PO entry
  22. *
  23. * @return string msgid/msgstr PO entry for this PO file headers, doesn't contain newline at the end
  24. */
  25. function export_headers() {
  26. $header_string = '';
  27. foreach ( $this->headers as $header => $value ) {
  28. $header_string .= "$header: $value\n";
  29. }
  30. $poified = PO::poify( $header_string );
  31. if ( $this->comments_before_headers ) {
  32. $before_headers = $this->prepend_each_line( rtrim( $this->comments_before_headers ) . "\n", '# ' );
  33. } else {
  34. $before_headers = '';
  35. }
  36. return rtrim( "{$before_headers}msgid \"\"\nmsgstr $poified" );
  37. }
  38. /**
  39. * Exports all entries to PO format
  40. *
  41. * @return string sequence of mgsgid/msgstr PO strings, doesn't containt newline at the end
  42. */
  43. function export_entries() {
  44. // TODO: Sorting.
  45. return implode( "\n\n", array_map( array( 'PO', 'export_entry' ), $this->entries ) );
  46. }
  47. /**
  48. * Exports the whole PO file as a string
  49. *
  50. * @param bool $include_headers whether to include the headers in the export
  51. * @return string ready for inclusion in PO file string for headers and all the enrtries
  52. */
  53. function export( $include_headers = true ) {
  54. $res = '';
  55. if ( $include_headers ) {
  56. $res .= $this->export_headers();
  57. $res .= "\n\n";
  58. }
  59. $res .= $this->export_entries();
  60. return $res;
  61. }
  62. /**
  63. * Same as {@link export}, but writes the result to a file
  64. *
  65. * @param string $filename where to write the PO string
  66. * @param bool $include_headers whether to include tje headers in the export
  67. * @return bool true on success, false on error
  68. */
  69. function export_to_file( $filename, $include_headers = true ) {
  70. $fh = fopen( $filename, 'w' );
  71. if ( false === $fh ) {
  72. return false;
  73. }
  74. $export = $this->export( $include_headers );
  75. $res = fwrite( $fh, $export );
  76. if ( false === $res ) {
  77. return false;
  78. }
  79. return fclose( $fh );
  80. }
  81. /**
  82. * Text to include as a comment before the start of the PO contents
  83. *
  84. * Doesn't need to include # in the beginning of lines, these are added automatically
  85. */
  86. function set_comment_before_headers( $text ) {
  87. $this->comments_before_headers = $text;
  88. }
  89. /**
  90. * Formats a string in PO-style
  91. *
  92. * @param string $string the string to format
  93. * @return string the poified string
  94. */
  95. public static function poify( $string ) {
  96. $quote = '"';
  97. $slash = '\\';
  98. $newline = "\n";
  99. $replaces = array(
  100. "$slash" => "$slash$slash",
  101. "$quote" => "$slash$quote",
  102. "\t" => '\t',
  103. );
  104. $string = str_replace( array_keys( $replaces ), array_values( $replaces ), $string );
  105. $po = $quote . implode( "${slash}n$quote$newline$quote", explode( $newline, $string ) ) . $quote;
  106. // Add empty string on first line for readbility.
  107. if ( false !== strpos( $string, $newline ) &&
  108. ( substr_count( $string, $newline ) > 1 || substr( $string, -strlen( $newline ) ) !== $newline ) ) {
  109. $po = "$quote$quote$newline$po";
  110. }
  111. // Remove empty strings.
  112. $po = str_replace( "$newline$quote$quote", '', $po );
  113. return $po;
  114. }
  115. /**
  116. * Gives back the original string from a PO-formatted string
  117. *
  118. * @param string $string PO-formatted string
  119. * @return string enascaped string
  120. */
  121. public static function unpoify( $string ) {
  122. $escapes = array(
  123. 't' => "\t",
  124. 'n' => "\n",
  125. 'r' => "\r",
  126. '\\' => '\\',
  127. );
  128. $lines = array_map( 'trim', explode( "\n", $string ) );
  129. $lines = array_map( array( 'PO', 'trim_quotes' ), $lines );
  130. $unpoified = '';
  131. $previous_is_backslash = false;
  132. foreach ( $lines as $line ) {
  133. preg_match_all( '/./u', $line, $chars );
  134. $chars = $chars[0];
  135. foreach ( $chars as $char ) {
  136. if ( ! $previous_is_backslash ) {
  137. if ( '\\' == $char ) {
  138. $previous_is_backslash = true;
  139. } else {
  140. $unpoified .= $char;
  141. }
  142. } else {
  143. $previous_is_backslash = false;
  144. $unpoified .= isset( $escapes[ $char ] ) ? $escapes[ $char ] : $char;
  145. }
  146. }
  147. }
  148. // Standardise the line endings on imported content, technically PO files shouldn't contain \r.
  149. $unpoified = str_replace( array( "\r\n", "\r" ), "\n", $unpoified );
  150. return $unpoified;
  151. }
  152. /**
  153. * Inserts $with in the beginning of every new line of $string and
  154. * returns the modified string
  155. *
  156. * @param string $string prepend lines in this string
  157. * @param string $with prepend lines with this string
  158. */
  159. public static function prepend_each_line( $string, $with ) {
  160. $lines = explode( "\n", $string );
  161. $append = '';
  162. if ( "\n" === substr( $string, -1 ) && '' === end( $lines ) ) {
  163. /*
  164. * Last line might be empty because $string was terminated
  165. * with a newline, remove it from the $lines array,
  166. * we'll restore state by re-terminating the string at the end.
  167. */
  168. array_pop( $lines );
  169. $append = "\n";
  170. }
  171. foreach ( $lines as &$line ) {
  172. $line = $with . $line;
  173. }
  174. unset( $line );
  175. return implode( "\n", $lines ) . $append;
  176. }
  177. /**
  178. * Prepare a text as a comment -- wraps the lines and prepends #
  179. * and a special character to each line
  180. *
  181. * @access private
  182. * @param string $text the comment text
  183. * @param string $char character to denote a special PO comment,
  184. * like :, default is a space
  185. */
  186. public static function comment_block( $text, $char = ' ' ) {
  187. $text = wordwrap( $text, PO_MAX_LINE_LEN - 3 );
  188. return PO::prepend_each_line( $text, "#$char " );
  189. }
  190. /**
  191. * Builds a string from the entry for inclusion in PO file
  192. *
  193. * @param Translation_Entry $entry the entry to convert to po string (passed by reference).
  194. * @return string|false PO-style formatted string for the entry or
  195. * false if the entry is empty
  196. */
  197. public static function export_entry( &$entry ) {
  198. if ( null === $entry->singular || '' === $entry->singular ) {
  199. return false;
  200. }
  201. $po = array();
  202. if ( ! empty( $entry->translator_comments ) ) {
  203. $po[] = PO::comment_block( $entry->translator_comments );
  204. }
  205. if ( ! empty( $entry->extracted_comments ) ) {
  206. $po[] = PO::comment_block( $entry->extracted_comments, '.' );
  207. }
  208. if ( ! empty( $entry->references ) ) {
  209. $po[] = PO::comment_block( implode( ' ', $entry->references ), ':' );
  210. }
  211. if ( ! empty( $entry->flags ) ) {
  212. $po[] = PO::comment_block( implode( ', ', $entry->flags ), ',' );
  213. }
  214. if ( $entry->context ) {
  215. $po[] = 'msgctxt ' . PO::poify( $entry->context );
  216. }
  217. $po[] = 'msgid ' . PO::poify( $entry->singular );
  218. if ( ! $entry->is_plural ) {
  219. $translation = empty( $entry->translations ) ? '' : $entry->translations[0];
  220. $translation = PO::match_begin_and_end_newlines( $translation, $entry->singular );
  221. $po[] = 'msgstr ' . PO::poify( $translation );
  222. } else {
  223. $po[] = 'msgid_plural ' . PO::poify( $entry->plural );
  224. $translations = empty( $entry->translations ) ? array( '', '' ) : $entry->translations;
  225. foreach ( $translations as $i => $translation ) {
  226. $translation = PO::match_begin_and_end_newlines( $translation, $entry->plural );
  227. $po[] = "msgstr[$i] " . PO::poify( $translation );
  228. }
  229. }
  230. return implode( "\n", $po );
  231. }
  232. public static function match_begin_and_end_newlines( $translation, $original ) {
  233. if ( '' === $translation ) {
  234. return $translation;
  235. }
  236. $original_begin = "\n" === substr( $original, 0, 1 );
  237. $original_end = "\n" === substr( $original, -1 );
  238. $translation_begin = "\n" === substr( $translation, 0, 1 );
  239. $translation_end = "\n" === substr( $translation, -1 );
  240. if ( $original_begin ) {
  241. if ( ! $translation_begin ) {
  242. $translation = "\n" . $translation;
  243. }
  244. } elseif ( $translation_begin ) {
  245. $translation = ltrim( $translation, "\n" );
  246. }
  247. if ( $original_end ) {
  248. if ( ! $translation_end ) {
  249. $translation .= "\n";
  250. }
  251. } elseif ( $translation_end ) {
  252. $translation = rtrim( $translation, "\n" );
  253. }
  254. return $translation;
  255. }
  256. /**
  257. * @param string $filename
  258. * @return boolean
  259. */
  260. function import_from_file( $filename ) {
  261. $f = fopen( $filename, 'r' );
  262. if ( ! $f ) {
  263. return false;
  264. }
  265. $lineno = 0;
  266. while ( true ) {
  267. $res = $this->read_entry( $f, $lineno );
  268. if ( ! $res ) {
  269. break;
  270. }
  271. if ( '' == $res['entry']->singular ) {
  272. $this->set_headers( $this->make_headers( $res['entry']->translations[0] ) );
  273. } else {
  274. $this->add_entry( $res['entry'] );
  275. }
  276. }
  277. PO::read_line( $f, 'clear' );
  278. if ( false === $res ) {
  279. return false;
  280. }
  281. if ( ! $this->headers && ! $this->entries ) {
  282. return false;
  283. }
  284. return true;
  285. }
  286. /**
  287. * Helper function for read_entry
  288. *
  289. * @param string $context
  290. * @return bool
  291. */
  292. protected static function is_final( $context ) {
  293. return ( 'msgstr' === $context ) || ( 'msgstr_plural' === $context );
  294. }
  295. /**
  296. * @param resource $f
  297. * @param int $lineno
  298. * @return null|false|array
  299. */
  300. function read_entry( $f, $lineno = 0 ) {
  301. $entry = new Translation_Entry();
  302. // Where were we in the last step.
  303. // Can be: comment, msgctxt, msgid, msgid_plural, msgstr, msgstr_plural.
  304. $context = '';
  305. $msgstr_index = 0;
  306. while ( true ) {
  307. $lineno++;
  308. $line = PO::read_line( $f );
  309. if ( ! $line ) {
  310. if ( feof( $f ) ) {
  311. if ( self::is_final( $context ) ) {
  312. break;
  313. } elseif ( ! $context ) { // We haven't read a line and EOF came.
  314. return null;
  315. } else {
  316. return false;
  317. }
  318. } else {
  319. return false;
  320. }
  321. }
  322. if ( "\n" === $line ) {
  323. continue;
  324. }
  325. $line = trim( $line );
  326. if ( preg_match( '/^#/', $line, $m ) ) {
  327. // The comment is the start of a new entry.
  328. if ( self::is_final( $context ) ) {
  329. PO::read_line( $f, 'put-back' );
  330. $lineno--;
  331. break;
  332. }
  333. // Comments have to be at the beginning.
  334. if ( $context && 'comment' !== $context ) {
  335. return false;
  336. }
  337. // Add comment.
  338. $this->add_comment_to_entry( $entry, $line );
  339. } elseif ( preg_match( '/^msgctxt\s+(".*")/', $line, $m ) ) {
  340. if ( self::is_final( $context ) ) {
  341. PO::read_line( $f, 'put-back' );
  342. $lineno--;
  343. break;
  344. }
  345. if ( $context && 'comment' !== $context ) {
  346. return false;
  347. }
  348. $context = 'msgctxt';
  349. $entry->context .= PO::unpoify( $m[1] );
  350. } elseif ( preg_match( '/^msgid\s+(".*")/', $line, $m ) ) {
  351. if ( self::is_final( $context ) ) {
  352. PO::read_line( $f, 'put-back' );
  353. $lineno--;
  354. break;
  355. }
  356. if ( $context && 'msgctxt' !== $context && 'comment' !== $context ) {
  357. return false;
  358. }
  359. $context = 'msgid';
  360. $entry->singular .= PO::unpoify( $m[1] );
  361. } elseif ( preg_match( '/^msgid_plural\s+(".*")/', $line, $m ) ) {
  362. if ( 'msgid' !== $context ) {
  363. return false;
  364. }
  365. $context = 'msgid_plural';
  366. $entry->is_plural = true;
  367. $entry->plural .= PO::unpoify( $m[1] );
  368. } elseif ( preg_match( '/^msgstr\s+(".*")/', $line, $m ) ) {
  369. if ( 'msgid' !== $context ) {
  370. return false;
  371. }
  372. $context = 'msgstr';
  373. $entry->translations = array( PO::unpoify( $m[1] ) );
  374. } elseif ( preg_match( '/^msgstr\[(\d+)\]\s+(".*")/', $line, $m ) ) {
  375. if ( 'msgid_plural' !== $context && 'msgstr_plural' !== $context ) {
  376. return false;
  377. }
  378. $context = 'msgstr_plural';
  379. $msgstr_index = $m[1];
  380. $entry->translations[ $m[1] ] = PO::unpoify( $m[2] );
  381. } elseif ( preg_match( '/^".*"$/', $line ) ) {
  382. $unpoified = PO::unpoify( $line );
  383. switch ( $context ) {
  384. case 'msgid':
  385. $entry->singular .= $unpoified;
  386. break;
  387. case 'msgctxt':
  388. $entry->context .= $unpoified;
  389. break;
  390. case 'msgid_plural':
  391. $entry->plural .= $unpoified;
  392. break;
  393. case 'msgstr':
  394. $entry->translations[0] .= $unpoified;
  395. break;
  396. case 'msgstr_plural':
  397. $entry->translations[ $msgstr_index ] .= $unpoified;
  398. break;
  399. default:
  400. return false;
  401. }
  402. } else {
  403. return false;
  404. }
  405. }
  406. $have_translations = false;
  407. foreach ( $entry->translations as $t ) {
  408. if ( $t || ( '0' === $t ) ) {
  409. $have_translations = true;
  410. break;
  411. }
  412. }
  413. if ( false === $have_translations ) {
  414. $entry->translations = array();
  415. }
  416. return array(
  417. 'entry' => $entry,
  418. 'lineno' => $lineno,
  419. );
  420. }
  421. /**
  422. * @staticvar string $last_line
  423. * @staticvar boolean $use_last_line
  424. *
  425. * @param resource $f
  426. * @param string $action
  427. * @return boolean
  428. */
  429. function read_line( $f, $action = 'read' ) {
  430. static $last_line = '';
  431. static $use_last_line = false;
  432. if ( 'clear' == $action ) {
  433. $last_line = '';
  434. return true;
  435. }
  436. if ( 'put-back' == $action ) {
  437. $use_last_line = true;
  438. return true;
  439. }
  440. $line = $use_last_line ? $last_line : fgets( $f );
  441. $line = ( "\r\n" == substr( $line, -2 ) ) ? rtrim( $line, "\r\n" ) . "\n" : $line;
  442. $last_line = $line;
  443. $use_last_line = false;
  444. return $line;
  445. }
  446. /**
  447. * @param Translation_Entry $entry
  448. * @param string $po_comment_line
  449. */
  450. function add_comment_to_entry( &$entry, $po_comment_line ) {
  451. $first_two = substr( $po_comment_line, 0, 2 );
  452. $comment = trim( substr( $po_comment_line, 2 ) );
  453. if ( '#:' == $first_two ) {
  454. $entry->references = array_merge( $entry->references, preg_split( '/\s+/', $comment ) );
  455. } elseif ( '#.' == $first_two ) {
  456. $entry->extracted_comments = trim( $entry->extracted_comments . "\n" . $comment );
  457. } elseif ( '#,' == $first_two ) {
  458. $entry->flags = array_merge( $entry->flags, preg_split( '/,\s*/', $comment ) );
  459. } else {
  460. $entry->translator_comments = trim( $entry->translator_comments . "\n" . $comment );
  461. }
  462. }
  463. /**
  464. * @param string $s
  465. * @return string
  466. */
  467. public static function trim_quotes( $s ) {
  468. if ( substr( $s, 0, 1 ) == '"' ) {
  469. $s = substr( $s, 1 );
  470. }
  471. if ( substr( $s, -1, 1 ) == '"' ) {
  472. $s = substr( $s, 0, -1 );
  473. }
  474. return $s;
  475. }
  476. }
  477. endif;