Chris@0: 1);\n" Chris@0: */ Chris@0: class PoHeader { Chris@0: Chris@0: /** Chris@0: * Language code. Chris@0: * Chris@0: * @var string Chris@0: */ Chris@17: protected $langcode; Chris@0: Chris@0: /** Chris@0: * Formula for the plural form. Chris@0: * Chris@0: * @var string Chris@0: */ Chris@17: protected $pluralForms; Chris@0: Chris@0: /** Chris@0: * Author(s) of the file. Chris@0: * Chris@0: * @var string Chris@0: */ Chris@17: protected $authors; Chris@0: Chris@0: /** Chris@0: * Date the po file got created. Chris@0: * Chris@0: * @var string Chris@0: */ Chris@17: protected $poDate; Chris@0: Chris@0: /** Chris@0: * Human readable language name. Chris@0: * Chris@0: * @var string Chris@0: */ Chris@17: protected $languageName; Chris@0: Chris@0: /** Chris@0: * Name of the project the translation belongs to. Chris@0: * Chris@0: * @var string Chris@0: */ Chris@17: protected $projectName; Chris@0: Chris@0: /** Chris@0: * Constructor, creates a PoHeader with default values. Chris@0: * Chris@0: * @param string $langcode Chris@0: * Language code. Chris@0: */ Chris@0: public function __construct($langcode = NULL) { Chris@17: $this->langcode = $langcode; Chris@0: // Ignore errors when run during site installation before Chris@0: // date_default_timezone_set() is called. Chris@17: $this->poDate = @date("Y-m-d H:iO"); Chris@17: $this->pluralForms = 'nplurals=2; plural=(n > 1);'; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the plural form. Chris@0: * Chris@0: * @return string Chris@0: * Plural form component from the header, for example: Chris@0: * 'nplurals=2; plural=(n > 1);'. Chris@0: */ Chris@0: public function getPluralForms() { Chris@17: return $this->pluralForms; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set the human readable language name. Chris@0: * Chris@0: * @param string $languageName Chris@0: * Human readable language name. Chris@0: */ Chris@0: public function setLanguageName($languageName) { Chris@17: $this->languageName = $languageName; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the human readable language name. Chris@0: * Chris@0: * @return string Chris@0: * The human readable language name. Chris@0: */ Chris@0: public function getLanguageName() { Chris@17: return $this->languageName; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set the project name. Chris@0: * Chris@0: * @param string $projectName Chris@0: * Human readable project name. Chris@0: */ Chris@0: public function setProjectName($projectName) { Chris@17: $this->projectName = $projectName; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the project name. Chris@0: * Chris@0: * @return string Chris@0: * The human readable project name. Chris@0: */ Chris@0: public function getProjectName() { Chris@17: return $this->projectName; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Populate internal values from a string. Chris@0: * Chris@0: * @param string $header Chris@0: * Full header string with key-value pairs. Chris@0: */ Chris@0: public function setFromString($header) { Chris@0: // Get an array of all header values for processing. Chris@0: $values = $this->parseHeader($header); Chris@0: Chris@0: // There is only one value relevant for our header implementation when Chris@0: // reading, and that is the plural formula. Chris@0: if (!empty($values['Plural-Forms'])) { Chris@17: $this->pluralForms = $values['Plural-Forms']; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Generate a Gettext PO formatted header string based on data set earlier. Chris@0: */ Chris@0: public function __toString() { Chris@0: $output = ''; Chris@0: Chris@17: $isTemplate = empty($this->languageName); Chris@0: Chris@17: $output .= '# ' . ($isTemplate ? 'LANGUAGE' : $this->languageName) . ' translation of ' . ($isTemplate ? 'PROJECT' : $this->projectName) . "\n"; Chris@17: if (!empty($this->authors)) { Chris@17: $output .= '# Generated by ' . implode("\n# ", $this->authors) . "\n"; Chris@0: } Chris@0: $output .= "#\n"; Chris@0: Chris@0: // Add the actual header information. Chris@0: $output .= "msgid \"\"\n"; Chris@0: $output .= "msgstr \"\"\n"; Chris@0: $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; Chris@17: $output .= "\"POT-Creation-Date: " . $this->poDate . "\\n\"\n"; Chris@17: $output .= "\"PO-Revision-Date: " . $this->poDate . "\\n\"\n"; Chris@0: $output .= "\"Last-Translator: NAME \\n\"\n"; Chris@0: $output .= "\"Language-Team: LANGUAGE \\n\"\n"; Chris@0: $output .= "\"MIME-Version: 1.0\\n\"\n"; Chris@0: $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; Chris@0: $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; Chris@17: $output .= "\"Plural-Forms: " . $this->pluralForms . "\\n\"\n"; Chris@0: $output .= "\n"; Chris@0: Chris@0: return $output; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses a Plural-Forms entry from a Gettext Portable Object file header. Chris@0: * Chris@0: * @param string $pluralforms Chris@0: * The Plural-Forms entry value. Chris@0: * Chris@0: * @return Chris@0: * An indexed array of parsed plural formula data. Containing: Chris@0: * - 'nplurals': The number of plural forms defined by the plural formula. Chris@0: * - 'plurals': Array of plural positions keyed by plural value. Chris@0: * Chris@14: * @throws \Exception Chris@0: */ Chris@0: public function parsePluralForms($pluralforms) { Chris@0: $plurals = []; Chris@0: // First, delete all whitespace. Chris@0: $pluralforms = strtr($pluralforms, [" " => "", "\t" => ""]); Chris@0: Chris@0: // Select the parts that define nplurals and plural. Chris@0: $nplurals = strstr($pluralforms, "nplurals="); Chris@0: if (strpos($nplurals, ";")) { Chris@0: // We want the string from the 10th char, because "nplurals=" length is 9. Chris@0: $nplurals = substr($nplurals, 9, strpos($nplurals, ";") - 9); Chris@0: } Chris@0: else { Chris@0: return FALSE; Chris@0: } Chris@0: $plural = strstr($pluralforms, "plural="); Chris@0: if (strpos($plural, ";")) { Chris@0: // We want the string from the 8th char, because "plural=" length is 7. Chris@0: $plural = substr($plural, 7, strpos($plural, ";") - 7); Chris@0: } Chris@0: else { Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: // If the number of plurals is zero, we return a default result. Chris@0: if ($nplurals == 0) { Chris@0: return [$nplurals, ['default' => 0]]; Chris@0: } Chris@0: Chris@0: // Calculate possible plural positions of different plural values. All known Chris@0: // plural formula's are repetitive above 100. Chris@0: // For data compression we store the last position the array value Chris@0: // changes and store it as default. Chris@0: $element_stack = $this->parseArithmetic($plural); Chris@0: if ($element_stack !== FALSE) { Chris@0: for ($i = 0; $i <= 199; $i++) { Chris@0: $plurals[$i] = $this->evaluatePlural($element_stack, $i); Chris@0: } Chris@0: $default = $plurals[$i - 1]; Chris@0: $plurals = array_filter($plurals, function ($value) use ($default) { Chris@0: return ($value != $default); Chris@0: }); Chris@0: $plurals['default'] = $default; Chris@0: Chris@0: return [$nplurals, $plurals]; Chris@0: } Chris@0: else { Chris@0: throw new \Exception('The plural formula could not be parsed.'); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses a Gettext Portable Object file header. Chris@0: * Chris@0: * @param string $header Chris@0: * A string containing the complete header. Chris@0: * Chris@0: * @return array Chris@0: * An associative array of key-value pairs. Chris@0: */ Chris@0: private function parseHeader($header) { Chris@0: $header_parsed = []; Chris@0: $lines = array_map('trim', explode("\n", $header)); Chris@0: foreach ($lines as $line) { Chris@0: if ($line) { Chris@0: list($tag, $contents) = explode(":", $line, 2); Chris@0: $header_parsed[trim($tag)] = trim($contents); Chris@0: } Chris@0: } Chris@0: return $header_parsed; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses and sanitizes an arithmetic formula into a plural element stack. Chris@0: * Chris@0: * While parsing, we ensure, that the operators have the right Chris@0: * precedence and associativity. Chris@0: * Chris@0: * @param string $string Chris@0: * A string containing the arithmetic formula. Chris@0: * Chris@0: * @return Chris@0: * A stack of values and operations to be evaluated. Chris@0: */ Chris@0: private function parseArithmetic($string) { Chris@0: // Operator precedence table. Chris@0: $precedence = ["(" => -1, ")" => -1, "?" => 1, ":" => 1, "||" => 3, "&&" => 4, "==" => 5, "!=" => 5, "<" => 6, ">" => 6, "<=" => 6, ">=" => 6, "+" => 7, "-" => 7, "*" => 8, "/" => 8, "%" => 8]; Chris@0: // Right associativity. Chris@0: $right_associativity = ["?" => 1, ":" => 1]; Chris@0: Chris@0: $tokens = $this->tokenizeFormula($string); Chris@0: Chris@0: // Parse by converting into infix notation then back into postfix Chris@0: // Operator stack - holds math operators and symbols. Chris@0: $operator_stack = []; Chris@0: // Element Stack - holds data to be operated on. Chris@0: $element_stack = []; Chris@0: Chris@0: foreach ($tokens as $token) { Chris@0: $current_token = $token; Chris@0: Chris@0: // Numbers and the $n variable are simply pushed into $element_stack. Chris@0: if (is_numeric($token)) { Chris@0: $element_stack[] = $current_token; Chris@0: } Chris@0: elseif ($current_token == "n") { Chris@0: $element_stack[] = '$n'; Chris@0: } Chris@0: elseif ($current_token == "(") { Chris@0: $operator_stack[] = $current_token; Chris@0: } Chris@0: elseif ($current_token == ")") { Chris@0: $topop = array_pop($operator_stack); Chris@0: while (isset($topop) && ($topop != "(")) { Chris@0: $element_stack[] = $topop; Chris@0: $topop = array_pop($operator_stack); Chris@0: } Chris@0: } Chris@0: elseif (!empty($precedence[$current_token])) { Chris@0: // If it's an operator, then pop from $operator_stack into Chris@0: // $element_stack until the precedence in $operator_stack is less Chris@0: // than current, then push into $operator_stack. Chris@0: $topop = array_pop($operator_stack); Chris@0: while (isset($topop) && ($precedence[$topop] >= $precedence[$current_token]) && !(($precedence[$topop] == $precedence[$current_token]) && !empty($right_associativity[$topop]) && !empty($right_associativity[$current_token]))) { Chris@0: $element_stack[] = $topop; Chris@0: $topop = array_pop($operator_stack); Chris@0: } Chris@0: if ($topop) { Chris@0: // Return element to top. Chris@0: $operator_stack[] = $topop; Chris@0: } Chris@0: // Parentheses are not needed. Chris@0: $operator_stack[] = $current_token; Chris@0: } Chris@0: else { Chris@0: return FALSE; Chris@0: } Chris@0: } Chris@0: Chris@0: // Flush operator stack. Chris@0: $topop = array_pop($operator_stack); Chris@0: while ($topop != NULL) { Chris@0: $element_stack[] = $topop; Chris@0: $topop = array_pop($operator_stack); Chris@0: } Chris@0: $return = $element_stack; Chris@0: Chris@0: // Now validate stack. Chris@0: $previous_size = count($element_stack) + 1; Chris@0: while (count($element_stack) < $previous_size) { Chris@0: $previous_size = count($element_stack); Chris@0: for ($i = 2; $i < count($element_stack); $i++) { Chris@0: $op = $element_stack[$i]; Chris@0: if (!empty($precedence[$op])) { Chris@0: if ($op == ":") { Chris@0: $f = $element_stack[$i - 2] . "):" . $element_stack[$i - 1] . ")"; Chris@0: } Chris@0: elseif ($op == "?") { Chris@0: $f = "(" . $element_stack[$i - 2] . "?(" . $element_stack[$i - 1]; Chris@0: } Chris@0: else { Chris@0: $f = "(" . $element_stack[$i - 2] . $op . $element_stack[$i - 1] . ")"; Chris@0: } Chris@0: array_splice($element_stack, $i - 2, 3, $f); Chris@0: break; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // If only one element is left, the number of operators is appropriate. Chris@0: return count($element_stack) == 1 ? $return : FALSE; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tokenize the formula. Chris@0: * Chris@0: * @param string $formula Chris@0: * A string containing the arithmetic formula. Chris@0: * Chris@0: * @return array Chris@0: * List of arithmetic tokens identified in the formula. Chris@0: */ Chris@0: private function tokenizeFormula($formula) { Chris@0: $formula = str_replace(" ", "", $formula); Chris@0: $tokens = []; Chris@0: for ($i = 0; $i < strlen($formula); $i++) { Chris@0: if (is_numeric($formula[$i])) { Chris@0: $num = $formula[$i]; Chris@0: $j = $i + 1; Chris@0: while ($j < strlen($formula) && is_numeric($formula[$j])) { Chris@0: $num .= $formula[$j]; Chris@0: $j++; Chris@0: } Chris@0: $i = $j - 1; Chris@0: $tokens[] = $num; Chris@0: } Chris@0: elseif ($pos = strpos(" =<>!&|", $formula[$i])) { Chris@0: $next = $formula[$i + 1]; Chris@0: switch ($pos) { Chris@0: case 1: Chris@0: case 2: Chris@0: case 3: Chris@0: case 4: Chris@0: if ($next == '=') { Chris@0: $tokens[] = $formula[$i] . '='; Chris@0: $i++; Chris@0: } Chris@0: else { Chris@0: $tokens[] = $formula[$i]; Chris@0: } Chris@0: break; Chris@0: case 5: Chris@0: if ($next == '&') { Chris@0: $tokens[] = '&&'; Chris@0: $i++; Chris@0: } Chris@0: else { Chris@0: $tokens[] = $formula[$i]; Chris@0: } Chris@0: break; Chris@0: case 6: Chris@0: if ($next == '|') { Chris@0: $tokens[] = '||'; Chris@0: $i++; Chris@0: } Chris@0: else { Chris@0: $tokens[] = $formula[$i]; Chris@0: } Chris@0: break; Chris@0: } Chris@0: } Chris@0: else { Chris@0: $tokens[] = $formula[$i]; Chris@0: } Chris@0: } Chris@0: return $tokens; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Evaluate the plural element stack using a plural value. Chris@0: * Chris@0: * Using an element stack, which represents a plural formula, we calculate Chris@0: * which plural string should be used for a given plural value. Chris@0: * Chris@0: * An example of plural formula parting and evaluation: Chris@0: * Plural formula: 'n!=1' Chris@0: * This formula is parsed by parseArithmetic() to a stack (array) of elements: Chris@0: * array( Chris@0: * 0 => '$n', Chris@0: * 1 => '1', Chris@0: * 2 => '!=', Chris@0: * ); Chris@0: * The evaluatePlural() method evaluates the $element_stack using the plural Chris@0: * value $n. Before the actual evaluation, the '$n' in the array is replaced Chris@0: * by the value of $n. Chris@0: * For example: $n = 2 results in: Chris@0: * array( Chris@0: * 0 => '2', Chris@0: * 1 => '1', Chris@0: * 2 => '!=', Chris@0: * ); Chris@0: * The stack is processed until only one element is (the result) is left. In Chris@0: * every iteration the top elements of the stack, up until the first operator, Chris@0: * are evaluated. After evaluation the arguments and the operator itself are Chris@0: * removed and replaced by the evaluation result. This is typically 2 Chris@0: * arguments and 1 element for the operator. Chris@0: * Because the operator is '!=' the example stack is evaluated as: Chris@0: * $f = (int) 2 != 1; Chris@0: * The resulting stack is: Chris@0: * array( Chris@0: * 0 => 1, Chris@0: * ); Chris@0: * With only one element left in the stack (the final result) the loop is Chris@0: * terminated and the result is returned. Chris@0: * Chris@0: * @param array $element_stack Chris@0: * Array of plural formula values and operators create by parseArithmetic(). Chris@0: * @param int $n Chris@0: * The @count number for which we are determining the right plural position. Chris@0: * Chris@0: * @return int Chris@0: * Number of the plural string to be used for the given plural value. Chris@0: * Chris@0: * @see parseArithmetic() Chris@14: * @throws \Exception Chris@0: */ Chris@0: protected function evaluatePlural($element_stack, $n) { Chris@0: $count = count($element_stack); Chris@0: $limit = $count; Chris@0: // Replace the '$n' value in the formula by the plural value. Chris@0: for ($i = 0; $i < $count; $i++) { Chris@0: if ($element_stack[$i] === '$n') { Chris@0: $element_stack[$i] = $n; Chris@0: } Chris@0: } Chris@0: Chris@0: // We process the stack until only one element is (the result) is left. Chris@0: // We limit the number of evaluation cycles to prevent an endless loop in Chris@0: // case the stack contains an error. Chris@0: while (isset($element_stack[1])) { Chris@0: for ($i = 2; $i < $count; $i++) { Chris@0: // There's no point in checking non-symbols. Also, switch(TRUE) would Chris@0: // match any case and so it would break. Chris@0: if (is_bool($element_stack[$i]) || is_numeric($element_stack[$i])) { Chris@0: continue; Chris@0: } Chris@0: $f = NULL; Chris@0: $length = 3; Chris@0: $delta = 2; Chris@0: switch ($element_stack[$i]) { Chris@0: case '==': Chris@0: $f = $element_stack[$i - 2] == $element_stack[$i - 1]; Chris@0: break; Chris@0: case '!=': Chris@0: $f = $element_stack[$i - 2] != $element_stack[$i - 1]; Chris@0: break; Chris@0: case '<=': Chris@0: $f = $element_stack[$i - 2] <= $element_stack[$i - 1]; Chris@0: break; Chris@0: case '>=': Chris@0: $f = $element_stack[$i - 2] >= $element_stack[$i - 1]; Chris@0: break; Chris@0: case '<': Chris@0: $f = $element_stack[$i - 2] < $element_stack[$i - 1]; Chris@0: break; Chris@0: case '>': Chris@0: $f = $element_stack[$i - 2] > $element_stack[$i - 1]; Chris@0: break; Chris@0: case '+': Chris@0: $f = $element_stack[$i - 2] + $element_stack[$i - 1]; Chris@0: break; Chris@0: case '-': Chris@0: $f = $element_stack[$i - 2] - $element_stack[$i - 1]; Chris@0: break; Chris@0: case '*': Chris@0: $f = $element_stack[$i - 2] * $element_stack[$i - 1]; Chris@0: break; Chris@0: case '/': Chris@0: $f = $element_stack[$i - 2] / $element_stack[$i - 1]; Chris@0: break; Chris@0: case '%': Chris@0: $f = $element_stack[$i - 2] % $element_stack[$i - 1]; Chris@0: break; Chris@0: case '&&': Chris@0: $f = $element_stack[$i - 2] && $element_stack[$i - 1]; Chris@0: break; Chris@0: case '||': Chris@0: $f = $element_stack[$i - 2] || $element_stack[$i - 1]; Chris@0: break; Chris@0: case ':': Chris@0: $f = $element_stack[$i - 3] ? $element_stack[$i - 2] : $element_stack[$i - 1]; Chris@0: // This operator has 3 preceding elements, instead of the default 2. Chris@0: $length = 5; Chris@0: $delta = 3; Chris@0: break; Chris@0: } Chris@0: Chris@0: // If the element is an operator we remove the processed elements and Chris@0: // store the result. Chris@0: if (isset($f)) { Chris@0: array_splice($element_stack, $i - $delta, $length, $f); Chris@0: break; Chris@0: } Chris@0: } Chris@0: } Chris@0: if (!$limit) { Chris@0: throw new \Exception('The plural formula could not be evaluated.'); Chris@0: } Chris@0: return (int) $element_stack[0]; Chris@0: } Chris@0: Chris@0: }