home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress / wp-includes / pomo / po.php < prev    next >
Encoding:
PHP Script  |  2017-10-02  |  13.2 KB  |  470 lines

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