- Patch #1189184 by attiks, clemens.tolboom, penyaskito, Gábor Hojtsy, Sutharsan: OOP & PSR-0-ify gettext .po file parsing and generation.

8.0.x
Dries 2012-07-26 18:07:25 -04:00
parent b4f471e47b
commit 06aa2facb9
17 changed files with 2457 additions and 1149 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
<?php
use Drupal\Core\Database\Database;
use Drupal\locale\Gettext;
/**
* Indicates that a module has not been installed yet.
@ -700,13 +701,7 @@ function st($string, array $args = array(), array $options = array()) {
// that multiple files end with the same extension, even if unlikely.
$files = install_find_translation_files($install_state['parameters']['langcode']);
if (!empty($files)) {
// Include cross-dependent code from locale module and gettext.inc.
require_once DRUPAL_ROOT . '/core/modules/locale/locale.module';
require_once DRUPAL_ROOT . '/core/includes/gettext.inc';
foreach ($files as $file) {
_locale_import_read_po('mem-store', $file);
}
$strings = _locale_import_one_string('mem-report');
$strings = Gettext::filesToArray($install_state['parameters']['langcode'], $files);
}
}
}

View File

@ -0,0 +1,418 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoHeader
*/
namespace Drupal\Component\Gettext;
/**
* Gettext PO header handler.
*
* Possible Gettext PO header elements are explained in
* http://www.gnu.org/software/gettext/manual/gettext.html#Header-Entry,
* but we only support a subset of these directly.
*
* Example header:
*
* "Project-Id-Version: Drupal core (7.11)\n"
* "POT-Creation-Date: 2012-02-12 22:59+0000\n"
* "PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n"
* "Language-Team: Catalan\n"
* "MIME-Version: 1.0\n"
* "Content-Type: text/plain; charset=utf-8\n"
* "Content-Transfer-Encoding: 8bit\n"
* "Plural-Forms: nplurals=2; plural=(n>1);\n"
*/
class PoHeader {
/**
* Language code.
*
* @var string
*/
private $_langcode;
/**
* Formula for the plural form.
*
* @var string
*/
private $_pluralForms;
/**
* Author(s) of the file.
*
* @var string
*/
private $_authors;
/**
* Date the po file got created.
*
* @var string
*/
private $_po_date;
/**
* Human readable language name.
*
* @var string
*/
private $_languageName;
/**
* Name of the project the translation belongs to.
*
* @var string
*/
private $_projectName;
/**
* Constructor, creates a PoHeader with default values.
*
* @param string $langcode
* Language code.
*/
public function __construct($langcode = NULL) {
$this->_langcode = $langcode;
$this->_po_date = date("Y-m-d H:iO");
$this->_pluralForms = 'nplurals=2; plural=(n > 1);';
}
/**
* Get the plural form.
*
* @return string
* Plural form component from the header, for example:
* 'nplurals=2; plural=(n > 1);'.
*/
function getPluralForms() {
return $this->_pluralForms;
}
/**
* Set the human readable language name.
*
* @param string $languageName
* Human readable language name.
*/
function setLanguageName($languageName) {
$this->_languageName = $languageName;
}
/**
* Get the human readable language name.
*
* @return string
* The human readable language name.
*/
function getLanguageName() {
return $this->_languageName;
}
/**
* Set the project name.
*
* @param string $projectName
* Human readable project name.
*/
function setProjectName($projectName) {
$this->_projectName = $projectName;
}
/**
* Get the project name.
*
* @return string
* The human readable project name.
*/
function getProjectName() {
return $this->_projectName;
}
/**
* Populate internal values from a string.
*
* @param string $header
* Full header string with key-value pairs.
*/
public function setFromString($header) {
// Get an array of all header values for processing.
$values = $this->parseHeader($header);
// There is only one value relevant for our header implementation when
// reading, and that is the plural formula.
if (!empty($values['Plural-Forms'])) {
$this->_pluralForms = $values['Plural-Forms'];
}
}
/**
* Generate a Gettext PO formatted header string based on data set earlier.
*/
public function __toString() {
$output = '';
$isTemplate = empty($this->_languageName);
$output .= '# ' . ($isTemplate ? 'LANGUAGE' : $this->_languageName) . ' translation of ' . ($isTemplate ? 'PROJECT' : $this->_projectName) . "\n";
if (!empty($this->_authors)) {
$output .= '# Generated by ' . implode("\n# ", $this->_authors) . "\n";
}
$output .= "#\n";
// Add the actual header information.
$output .= "msgid \"\"\n";
$output .= "msgstr \"\"\n";
$output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
$output .= "\"POT-Creation-Date: " . $this->_po_date . "\\n\"\n";
$output .= "\"PO-Revision-Date: " . $this->_po_date . "\\n\"\n";
$output .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
$output .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n";
$output .= "\"MIME-Version: 1.0\\n\"\n";
$output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
$output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
$output .= "\"Plural-Forms: " . $this->_pluralForms . "\\n\"\n";
$output .= "\n";
return $output;
}
/**
* Parses a Plural-Forms entry from a Gettext Portable Object file header.
*
* @param string $pluralforms
* The Plural-Forms entry value.
*
* @return
* An array containing the number of plural forms and the converted version
* of the formula that can be evaluated with PHP later.
*/
function parsePluralForms($pluralforms) {
// First, delete all whitespace.
$pluralforms = strtr($pluralforms, array(" " => "", "\t" => ""));
// Select the parts that define nplurals and plural.
$nplurals = strstr($pluralforms, "nplurals=");
if (strpos($nplurals, ";")) {
// We want the string from the 10th char, because "nplurals=" length is 9.
$nplurals = substr($nplurals, 9, strpos($nplurals, ";") - 9);
}
else {
return FALSE;
}
$plural = strstr($pluralforms, "plural=");
if (strpos($plural, ";")) {
// We want the string from the 8th char, because "plural=" length is 7.
$plural = substr($plural, 7, strpos($plural, ";") - 7);
}
else {
return FALSE;
}
// Get PHP version of the plural formula.
$plural = $this->parseArithmetic($plural);
if ($plural !== FALSE) {
return array($nplurals, $plural);
}
else {
throw new Exception('The plural formula could not be parsed.');
}
}
/**
* Parses a Gettext Portable Object file header.
*
* @param string $header
* A string containing the complete header.
*
* @return array
* An associative array of key-value pairs.
*/
private function parseHeader($header) {
$header_parsed = array();
$lines = array_map('trim', explode("\n", $header));
foreach ($lines as $line) {
if ($line) {
list($tag, $contents) = explode(":", $line, 2);
$header_parsed[trim($tag)] = trim($contents);
}
}
return $header_parsed;
}
/**
* Parses and sanitizes an arithmetic formula into a PHP expression.
*
* While parsing, we ensure, that the operators have the right
* precedence and associativity.
*
* @param string $string
* A string containing the arithmetic formula.
*
* @return
* A version of the formula to evaluate with PHP later.
*/
private function parseArithmetic($string) {
// Operator precedence table.
$precedence = array("(" => -1, ")" => -1, "?" => 1, ":" => 1, "||" => 3, "&&" => 4, "==" => 5, "!=" => 5, "<" => 6, ">" => 6, "<=" => 6, ">=" => 6, "+" => 7, "-" => 7, "*" => 8, "/" => 8, "%" => 8);
// Right associativity.
$right_associativity = array("?" => 1, ":" => 1);
$tokens = $this->tokenizeFormula($string);
// Parse by converting into infix notation then back into postfix
// Operator stack - holds math operators and symbols.
$operator_stack = array();
// Element Stack - holds data to be operated on.
$element_stack = array();
foreach ($tokens as $token) {
$current_token = $token;
// Numbers and the $n variable are simply pushed into $element_stack.
if (is_numeric($token)) {
$element_stack[] = $current_token;
}
elseif ($current_token == "n") {
$element_stack[] = '$n';
}
elseif ($current_token == "(") {
$operator_stack[] = $current_token;
}
elseif ($current_token == ")") {
$topop = array_pop($operator_stack);
while (isset($topop) && ($topop != "(")) {
$element_stack[] = $topop;
$topop = array_pop($operator_stack);
}
}
elseif (!empty($precedence[$current_token])) {
// If it's an operator, then pop from $operator_stack into
// $element_stack until the precedence in $operator_stack is less
// than current, then push into $operator_stack.
$topop = array_pop($operator_stack);
while (isset($topop) && ($precedence[$topop] >= $precedence[$current_token]) && !(($precedence[$topop] == $precedence[$current_token]) && !empty($right_associativity[$topop]) && !empty($right_associativity[$current_token]))) {
$element_stack[] = $topop;
$topop = array_pop($operator_stack);
}
if ($topop) {
// Return element to top.
$operator_stack[] = $topop;
}
// Parentheses are not needed.
$operator_stack[] = $current_token;
}
else {
return FALSE;
}
}
// Flush operator stack.
$topop = array_pop($operator_stack);
while ($topop != NULL) {
$element_stack[] = $topop;
$topop = array_pop($operator_stack);
}
// Now extract formula from stack.
$previous_size = count($element_stack) + 1;
while (count($element_stack) < $previous_size) {
$previous_size = count($element_stack);
for ($i = 2; $i < count($element_stack); $i++) {
$op = $element_stack[$i];
if (!empty($precedence[$op])) {
$f = "";
if ($op == ":") {
$f = $element_stack[$i - 2] . "):" . $element_stack[$i - 1] . ")";
}
elseif ($op == "?") {
$f = "(" . $element_stack[$i - 2] . "?(" . $element_stack[$i - 1];
}
else {
$f = "(" . $element_stack[$i - 2] . $op . $element_stack[$i - 1] . ")";
}
array_splice($element_stack, $i - 2, 3, $f);
break;
}
}
}
// If only one element is left, the number of operators is appropriate.
if (count($element_stack) == 1) {
return $element_stack[0];
}
else {
return FALSE;
}
}
/**
* Tokenize the formula.
*
* @param string $formula
* A string containing the arithmetic formula.
*
* @return array
* List of arithmetic tokens identified in the formula.
*/
private function tokenizeFormula($formula) {
$formula = str_replace(" ", "", $formula);
$tokens = array();
for ($i = 0; $i < strlen($formula); $i++) {
if (is_numeric($formula[$i])) {
$num = $formula[$i];
$j = $i + 1;
while ($j < strlen($formula) && is_numeric($formula[$j])) {
$num .= $formula[$j];
$j++;
}
$i = $j - 1;
$tokens[] = $num;
}
elseif ($pos = strpos(" =<>!&|", $formula[$i])) {
$next = $formula[$i + 1];
switch ($pos) {
case 1:
case 2:
case 3:
case 4:
if ($next == '=') {
$tokens[] = $formula[$i] . '=';
$i++;
}
else {
$tokens[] = $formula[$i];
}
break;
case 5:
if ($next == '&') {
$tokens[] = '&&';
$i++;
}
else {
$tokens[] = $formula[$i];
}
break;
case 6:
if ($next == '|') {
$tokens[] = '||';
$i++;
}
else {
$tokens[] = $formula[$i];
}
break;
}
}
else {
$tokens[] = $formula[$i];
}
}
return $tokens;
}
}

View File

@ -0,0 +1,287 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoItem.
*/
namespace Drupal\Component\Gettext;
/**
* PoItem handles one translation.
*
* @todo: This class contains some really old legacy code.
* @see https://drupal.org/node/1637662
*/
class PoItem {
/**
* The language code this translation is in.
*
* @car string
*/
private $_langcode;
/**
* The context this translation belongs to.
*
* @var string
*/
private $_context = '';
/**
* The source string or array of strings if it has plurals.
*
* @var string or array
* @see $_plural
*/
private $_source;
/**
* Flag indicating if this translation has plurals.
*
* @var boolean
*/
private $_plural;
/**
* The comment of this translation.
*
* @var string
*/
private $_comment;
/**
* The translation string or array of strings if it has plurals.
*
* @var string or array
* @see $_plural
*/
private $_translation;
/**
* Get the language code of the currently used language.
*
* @return string with langcode
*/
function getLangcode() {
return $this->_langcode;
}
/**
* Set the language code of the current language.
*
* @param string $langcode
*/
function setLangcode($langcode) {
$this->_langcode = $langcode;
}
/**
* Get the context this translation belongs to.
*
* @return string $context
*/
function getContext() {
return $this->_context;
}
/**
* Set the context this translation belongs to.
*
* @param string $context
*/
function setContext($context) {
$this->_context = $context;
}
/**
* Get the source string or the array of strings if the translation has
* plurals.
*
* @return string or array $translation
*/
function getSource() {
return $this->_source;
}
/**
* Set the source string or the array of strings if the translation has
* plurals.
*
* @param string or array $source
*/
function setSource($source) {
$this->_source = $source;
}
/**
* Get the translation string or the array of strings if the translation has
* plurals.
*
* @return string or array $translation
*/
function getTranslation() {
return $this->_translation;
}
/**
* Set the translation string or the array of strings if the translation has
* plurals.
*
* @param string or array $translation
*/
function setTranslation($translation) {
$this->_translation = $translation;
}
/**
* Set if the translation has plural values.
*
* @param boolean $plural
*/
function setPlural($plural) {
$this->_plural = $plural;
}
/**
* Get if the translation has plural values.
*
* @return boolean $plural
*/
function isPlural() {
return $this->_plural;
}
/**
* Get the comment of this translation.
*
* @return String $comment
*/
function getComment() {
return $this->_comment;
}
/**
* Set the comment of this translation.
*
* @param String $comment
*/
function setComment($comment) {
$this->_comment = $comment;
}
/**
* Create the PoItem from a structured array.
*
* @param array values
*/
public function setFromArray(array $values = array()) {
if (isset($values['context'])) {
$this->setContext($values['context']);
}
if (isset($values['source'])) {
$this->setSource($values['source']);
}
if (isset($values['translation'])) {
$this->setTranslation($values['translation']);
}
if (isset($values['comment'])){
$this->setComment($values['comment']);
}
if (isset($this->_source) &&
strpos($this->_source, LOCALE_PLURAL_DELIMITER) !== FALSE) {
$this->setSource(explode(LOCALE_PLURAL_DELIMITER, $this->_source));
$this->setTranslation(explode(LOCALE_PLURAL_DELIMITER, $this->_translation));
$this->setPlural(count($this->_translation) > 1);
}
}
/**
* Output the PoItem as a string.
*/
public function __toString() {
return $this->formatItem();
}
/**
* Format the POItem as a string.
*/
private function formatItem() {
$output = '';
// Format string context.
if (!empty($this->_context)) {
$output .= 'msgctxt ' . $this->formatString($this->_context);
}
// Format translation.
if ($this->_plural) {
$output .= $this->formatPlural();
}
else {
$output .= $this->formatSingular();
}
// Add one empty line to separate the translations.
$output .= "\n";
return $output;
}
/**
* Formats a plural translation.
*/
private function formatPlural() {
$output = '';
// Format source strings.
$output .= 'msgid ' . $this->formatString($this->_source[0]);
$output .= 'msgid_plural ' . $this->formatString($this->_source[1]);
foreach ($this->_translation as $i => $trans) {
if (isset($this->_translation[$i])) {
$output .= 'msgstr[' . $i . '] ' . $this->formatString($trans);
}
else {
$output .= 'msgstr[' . $i . '] ""' . "\n";
}
}
return $output;
}
/**
* Formats a singular translation.
*/
private function formatSingular() {
$output = '';
$output .= 'msgid '. $this->formatString($this->_source);
$output .= 'msgstr '. (isset($this->_translation) ? $this->formatString($this->_translation) : '');
return $output;
}
/**
* Formats a string for output on multiple lines.
*/
private function formatString($string) {
// Escape characters for processing.
$string = addcslashes($string, "\0..\37\\\"");
// Always include a line break after the explicit \n line breaks from
// the source string. Otherwise wrap at 70 chars to accommodate the extra
// format overhead too.
$parts = explode("\n", wordwrap(str_replace('\n', "\\n\n", $string), 70, " \n"));
// Multiline string should be exported starting with a "" and newline to
// have all lines aligned on the same column.
if (count($parts) > 1) {
return "\"\"\n\"" . implode("\"\n\"", $parts) . "\"\n";
}
// Single line strings are output on the same line.
else {
return "\"$parts[0]\"\n";
}
}
}

View File

@ -0,0 +1,95 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoMemoryWriter.
*/
namespace Drupal\Component\Gettext;
use Drupal\Component\Gettext\PoWriterInterface;
use Drupal\Component\Gettext\PoHeader;
use Drupal\Component\Gettext\PoItem;
/**
* Defines a Gettext PO memory writer, to be used by the installer.
*/
class PoMemoryWriter implements PoWriterInterface {
/**
* Array to hold all PoItem elements.
*
* @var array
*/
private $_items;
/**
* Constructor, initialize empty items.
*/
function __construct() {
$this->_items = array();
}
/**
* Implements Drupal\Component\Gettext\PoWriterInterface::writeItem().
*/
public function writeItem(PoItem $item) {
if (is_array($item->source)) {
$item->source = implode(LOCALE_PLURAL_DELIMITER, $item->source);
$item->translation = implode(LOCALE_PLURAL_DELIMITER, $item->translation);
}
$this->_items[isset($item->context) ? $item->context : ''][$item->source] = $item->translation;
}
/**
* Implements Drupal\Component\Gettext\PoWriterInterface::writeItems().
*/
public function writeItems(PoReaderInterface $reader, $count = -1) {
$forever = $count == -1;
while (($count-- > 0 || $forever) && ($item = $reader->readItem())) {
$this->writeItem($item);
}
}
/**
* Get all stored PoItem's.
*
* @return array PoItem
*/
public function getData() {
return $this->_items;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface:setLangcode().
*
* Not implemented. Not relevant for the MemoryWriter.
*/
function setLangcode($langcode) {
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface:getLangcode().
*
* Not implemented. Not relevant for the MemoryWriter.
*/
function getLangcode() {
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface:getHeader().
*
* Not implemented. Not relevant for the MemoryWriter.
*/
function getHeader() {
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface:setHeader().
*
* Not implemented. Not relevant for the MemoryWriter.
*/
function setHeader(PoHeader $header) {
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoMetadataInterface.
*/
namespace Drupal\Component\Gettext;
use Drupal\Component\Gettext\PoHeader;
/**
* Methods required for both reader and writer implementations.
*
* @see Drupal\Component\Gettext\PoReaderInterface
* @see Drupal\Component\Gettext\PoWriterInterface
*/
interface PoMetadataInterface {
/**
* Set language code.
*
* @param string $langcode
* Language code string.
*/
function setLangcode($langcode);
/**
* Get language code.
*
* @return string
* Language code string.
*/
function getLangcode();
/**
* Set header metadata.
*
* @param Drupal\Component\Gettext\PoHeader $header
* Header object representing metadata in a PO header.
*/
function setHeader(PoHeader $header);
/**
* Get header metadata.
*
* @return Drupal\Component\Gettext\PoHeader $header
* Header instance representing metadata in a PO header.
*/
function getHeader();
}

View File

@ -0,0 +1,24 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoReaderInterface.
*/
namespace Drupal\Component\Gettext;
use Drupal\Component\Gettext\PoMetadataInterface;
/**
* Shared interface definition for all Gettext PO Readers.
*/
interface PoReaderInterface extends PoMetadataInterface {
/**
* Reads and returns a PoItem (source/translation pair).
*
* @return Drupal\Component\Gettext\PoItem
* Wrapper for item data instance.
*/
function readItem();
}

View File

@ -0,0 +1,43 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoStreamInterface.
*/
namespace Drupal\Component\Gettext;
/**
* Common functions for file/stream based PO readers/writers.
*
* @see PoReaderInterface
* @see PoWriterInterface
*/
interface PoStreamInterface {
/**
* Open the stream. Set the URI for the stream earlier with setURI().
*/
function open();
/**
* Close the stream.
*/
function close();
/**
* Get the URI of the PO stream that is being read or written.
*
* @return
* URI string for this stream.
*/
function getURI();
/**
* Set the URI of the PO stream that is going to be read or written.
*
* @param $uri
* URI string to set for this stream.
*/
function setURI($uri);
}

View File

@ -0,0 +1,589 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoStreamReader.
*/
namespace Drupal\Component\Gettext;
use Drupal\Component\Gettext\PoReaderInterface;
use Drupal\Component\Gettext\PoStreamInterface;
use Drupal\Component\Gettext\PoHeader;
/**
* Implements Gettext PO stream reader.
*
* The PO file format parsing is implemented according to the documentation at
* http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files
*/
class PoStreamReader implements PoStreamInterface, PoReaderInterface {
/**
* Source line number of the stream being parsed.
*
* @var int
*/
private $_line_number = 0;
/**
* Parser context for the stream reader state machine.
*
* Possible contexts are:
* - 'COMMENT' (#)
* - 'MSGID' (msgid)
* - 'MSGID_PLURAL' (msgid_plural)
* - 'MSGCTXT' (msgctxt)
* - 'MSGSTR' (msgstr or msgstr[])
* - 'MSGSTR_ARR' (msgstr_arg)
*
* @var string
*/
private $_context = 'COMMENT';
/**
* Current entry being read. Incomplete.
*
* @var array
*/
private $_current_item = array();
/**
* Current plural index for plural translations.
*
* @var int
*/
private $_current_plural_index = 0;
/**
* URI of the PO stream that is being read.
*
* @var string
*/
private $_uri = '';
/**
* Language code for the PO stream being read.
*
* @var string
*/
private $_langcode = NULL;
/**
* Size of the current PO stream.
*
* @var int
*/
private $_size;
/**
* File handle of the current PO stream.
*
* @var resource
*/
private $_fd;
/**
* The PO stream header.
*
* @var Drupal\Component\Gettext\PoHeader
*/
private $_header;
/**
* Object wrapper for the last read source/translation pair.
*
* @var Drupal\Component\Gettext\PoItem
*/
private $_last_item;
/**
* Indicator of whether the stream reading is finished.
*
* @var boolean
*/
private $_finished;
/**
* Array of translated error strings recorded on reading this stream so far.
*
* @var array
*/
private $_errors;
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::getLangcode().
*/
public function getLangcode() {
return $this->_langcode;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::setLangcode().
*/
public function setLangcode($langcode) {
$this->_langcode = $langcode;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::getHeader().
*/
public function getHeader() {
return $this->_header;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::setHeader().
*
* Not applicable to stream reading and therefore not implemented.
*/
public function setHeader(PoHeader $header) {
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::getURI().
*/
public function getURI() {
return $this->_uri;
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::setURI().
*/
public function setURI($uri) {
$this->_uri = $uri;
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::open().
*
* Opens the stream and reads the header. The stream is ready for reading
* items after.
*
* @throws Exception
* If the URI is not yet set.
*/
public function open() {
if (!empty($this->_uri)) {
$this->_fd = fopen($this->_uri, 'rb');
$this->_size = ftell($this->_fd);
$this->readHeader();
}
else {
throw new Exception('Cannot open stream without URI set.');
}
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::close().
*
* @throws Exception
* If the stream is not open.
*/
public function close() {
if ($this->_fd) {
fclose($this->_fd);
}
else {
throw new Exception('Cannot close stream that is not open.');
}
}
/**
* Implements Drupal\Component\Gettext\PoReaderInterface::readItem().
*/
public function readItem() {
// Clear out the last item.
$this->_last_item = NULL;
// Read until finished with the stream or a complete item was identified.
while (!$this->_finished && is_null($this->_last_item)) {
$this->readLine();
}
return $this->_last_item;
}
/**
* Read the header from the PO stream.
*
* The header is a special case PoItem, using the empty string as source and
* key-value pairs as translation. We just reuse the item reader logic to
* read the header.
*/
private function readHeader() {
$item = $this->readItem();
$header = new PoHeader;
$header->setFromString(trim($item->getTranslation()));
$this->_header = $header;
}
/**
* Reads a line from the PO stream and stores data internally.
*
* Expands $this->_current_item based on new data for the current item. If
* this line ends the current item, it is saved with setItemFromArray() with
* data from $this->_current_item.
*
* An internal state machine is maintained in this reader using $this->_context
* as the reading state. PO items are inbetween COMMENT states (when items have
* at least one line or comment inbetween them or indicated by MSGSTR or
* MSGSTR_ARR followed immediately by an MSGID or MSGCTXT (when items closely
* follow each other).
*
* @return
* FALSE if an error was logged, NULL otherwise. The errors are considered
* non-blocking, so reading can continue, while the errors are collected
* for later presentation.
*/
private function readLine() {
// Read a line and set the stream finished indicator if it was not
// possible anymore.
$line = fgets($this->_fd);
$this->_finished = ($line === FALSE);
if (!$this->_finished) {
if ($this->_line_number == 0) {
// The first line might come with a UTF-8 BOM, which should be removed.
$line = str_replace("\xEF\xBB\xBF", '', $line);
// Current plurality for 'msgstr[]'.
$this->_current_plural_index = 0;
}
// Track the line number for error reporting.
$this->_line_number++;
// Initialize common values for error logging.
$log_vars = array(
'%uri' => $this->getURI(),
'%line' => $this->_line_number,
);
$t = get_t();
// Trim away the linefeed. \\n might appear at the end of the string if
// another line continuing the same string follows. We can remove that.
$line = trim(strtr($line, array("\\\n" => "")));
if (!strncmp('#', $line, 1)) {
// Lines starting with '#' are comments.
if ($this->_context == 'COMMENT') {
// Already in comment context, add to current comment.
$this->_current_item['#'][] = substr($line, 1);
}
elseif (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
// We are currently in string context, save current item.
$this->setItemFromArray($this->_current_item);
// Start a new entry for the comment.
$this->_current_item = array();
$this->_current_item['#'][] = substr($line, 1);
$this->_context = 'COMMENT';
return;
}
else {
// A comment following any other context is a syntax error.
$this->_errors[] = $t('The translation stream %uri contains an error: "msgstr" was expected but not found on line %line.', $log_vars);
return FALSE;
}
return;
}
elseif (!strncmp('msgid_plural', $line, 12)) {
// A plural form for the current source string.
if ($this->_context != 'MSGID') {
// A plural form can only be added to an msgid directly.
$this->_errors[] = $t('The translation stream %uri contains an error: "msgid_plural" was expected but not found on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgid_plural' and trim away whitespace.
$line = trim(substr($line, 12));
// Only the plural source string is left, parse it.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The plural form must be wrapped in quotes.
$this->_errors[] = $t('The translation stream %uri contains a syntax error on line %line.', $log_vars);
return FALSE;
}
// Append the plural source to the current entry.
if (is_string($this->_current_item['msgid'])) {
// The first value was stored as string. Now we know the context is
// plural, it is converted to array.
$this->_current_item['msgid'] = array($this->_current_item['msgid']);
}
$this->_current_item['msgid'][] = $quoted;
$this->_context = 'MSGID_PLURAL';
return;
}
elseif (!strncmp('msgid', $line, 5)) {
// Starting a new message.
if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
// We are currently in string context, save current item.
$this->setItemFromArray($this->_current_item);
// Start a new context for the msgid.
$this->_current_item = array();
}
elseif ($this->_context == 'MSGID') {
// We are currently already in the context, meaning we passed an id with no data.
$this->_errors[] = $t('The translation stream %uri contains an error: "msgid" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgid' and trim away whitespace.
$line = trim(substr($line, 5));
// Only the message id string is left, parse it.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The message id must be wrapped in quotes.
$this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgid" on line %line.', $log_vars, $log_vars);
return FALSE;
}
$this->_current_item['msgid'] = $quoted;
$this->_context = 'MSGID';
return;
}
elseif (!strncmp('msgctxt', $line, 7)) {
// Starting a new context.
if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
// We are currently in string context, save current item.
$this->setItemFromArray($this->_current_item);
$this->_current_item = array();
}
elseif (!empty($this->_current_item['msgctxt'])) {
// A context cannot apply to another context.
$this->_errors[] = $t('The translation stream %uri contains an error: "msgctxt" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgctxt' and trim away whitespaces.
$line = trim(substr($line, 7));
// Only the msgctxt string is left, parse it.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The context string must be quoted.
$this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgctxt" on line %line.', $log_vars);
return FALSE;
}
$this->_current_item['msgctxt'] = $quoted;
$this->_context = 'MSGCTXT';
return;
}
elseif (!strncmp('msgstr[', $line, 7)) {
// A message string for a specific plurality.
if (($this->_context != 'MSGID') &&
($this->_context != 'MSGCTXT') &&
($this->_context != 'MSGID_PLURAL') &&
($this->_context != 'MSGSTR_ARR')) {
// Plural message strings must come after msgid, msgxtxt,
// msgid_plural, or other msgstr[] entries.
$this->_errors[] = $t('The translation stream %uri contains an error: "msgstr[]" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Ensure the plurality is terminated.
if (strpos($line, ']') === FALSE) {
$this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars);
return FALSE;
}
// Extract the plurality.
$frombracket = strstr($line, '[');
$this->_current_plural_index = substr($frombracket, 1, strpos($frombracket, ']') - 1);
// Skip to the next whitespace and trim away any further whitespace,
// bringing $line to the message text only.
$line = trim(strstr($line, " "));
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The string must be quoted.
$this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars);
return FALSE;
}
if (!isset($this->_current_item['msgstr']) || !is_array($this->_current_item['msgstr'])) {
$this->_current_item['msgstr'] = array();
}
$this->_current_item['msgstr'][$this->_current_plural_index] = $quoted;
$this->_context = 'MSGSTR_ARR';
return;
}
elseif (!strncmp("msgstr", $line, 6)) {
// A string pair for an msgidid (with optional context).
if (($this->_context != 'MSGID') && ($this->_context != 'MSGCTXT')) {
// Strings are only valid within an id or context scope.
$this->_errors[] = $t('The translation stream %uri contains an error: "msgstr" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgstr' and trim away away whitespaces.
$line = trim(substr($line, 6));
// Only the msgstr string is left, parse it.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// The string must be quoted.
$this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgstr" on line %line.', $log_vars);
return FALSE;
}
$this->_current_item['msgstr'] = $quoted;
$this->_context = 'MSGSTR';
return;
}
elseif ($line != '') {
// Anything that is not a token may be a continuation of a previous token.
$quoted = $this->parseQuoted($line);
if ($quoted === FALSE) {
// This string must be quoted.
$this->_errors[] = $t('The translation stream %uri contains an error: string continuation expected on line %line.', $log_vars);
return FALSE;
}
// Append the string to the current item.
if (($this->_context == 'MSGID') || ($this->_context == 'MSGID_PLURAL')) {
if (is_array($this->_current_item['msgid'])) {
// Add string to last array element for plural sources.
$last_index = count($this->_current_item['msgid']) - 1;
$this->_current_item['msgid'][$last_index] .= $quoted;
}
else {
// Singular source, just append the string.
$this->_current_item['msgid'] .= $quoted;
}
}
elseif ($this->_context == 'MSGCTXT') {
// Multiline context name.
$this->_current_item['msgctxt'] .= $quoted;
}
elseif ($this->_context == 'MSGSTR') {
// Multiline translation string.
$this->_current_item['msgstr'] .= $quoted;
}
elseif ($this->_context == 'MSGSTR_ARR') {
// Multiline plural translation string.
$this->_current_item['msgstr'][$this->_current_plural_index] .= $quoted;
}
else {
// No valid context to append to.
$this->_errors[] = $t('The translation stream %uri contains an error: unexpected string on line %line.', $log_vars);
return FALSE;
}
return;
}
}
// Empty line read or EOF of PO stream, close out the last entry.
if (($this->_context == 'MSGSTR') || ($this->_context == 'MSGSTR_ARR')) {
$this->setItemFromArray($this->_current_item);
$this->_current_item = array();
}
elseif ($this->_context != 'COMMENT') {
$this->_errors[] = $t('The translation stream %uri ended unexpectedly at line %line.', $log_vars);
return FALSE;
}
}
/**
* Store the parsed values as a PoItem object.
*/
public function setItemFromArray($value) {
$plural = FALSE;
$comments = '';
if (isset($value['#'])) {
$comments = $this->shortenComments($value['#']);
}
if (is_array($value['msgstr'])) {
// Sort plural variants by their form index.
ksort($value['msgstr']);
$plural = TRUE;
}
$item = new PoItem();
$item->setContext(isset($value['msgctxt']) ? $value['msgctxt'] : '');
$item->setSource($value['msgid']);
$item->setTranslation($value['msgstr']);
$item->setPlural($plural);
$item->setComment($comments);
$item->setLangcode($this->_langcode);
$this->_last_item = $item;
$this->_context = 'COMMENT';
}
/**
* Parses a string in quotes.
*
* @param $string
* A string specified with enclosing quotes.
*
* @return
* The string parsed from inside the quotes.
*/
function parseQuoted($string) {
if (substr($string, 0, 1) != substr($string, -1, 1)) {
// Start and end quotes must be the same.
return FALSE;
}
$quote = substr($string, 0, 1);
$string = substr($string, 1, -1);
if ($quote == '"') {
// Double quotes: strip slashes.
return stripcslashes($string);
}
elseif ($quote == "'") {
// Simple quote: return as-is.
return $string;
}
else {
// Unrecognized quote.
return FALSE;
}
}
/**
* Generates a short, one-string version of the passed comment array.
*
* @param $comment
* An array of strings containing a comment.
*
* @return
* Short one-string version of the comment.
*/
private function shortenComments($comment) {
$comm = '';
while (count($comment)) {
$test = $comm . substr(array_shift($comment), 1) . ', ';
if (strlen($comm) < 130) {
$comm = $test;
}
else {
break;
}
}
return trim(substr($comm, 0, -2));
}
}

View File

@ -0,0 +1,168 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoStreamWriter.
*/
namespace Drupal\Component\Gettext;
use Drupal\Component\Gettext\PoHeader;
use Drupal\Component\Gettext\PoItem;
use Drupal\Component\Gettext\PoReaderInterface;
use Drupal\Component\Gettext\PoWriterInterface;
use Drupal\Component\Gettext\PoStreamInterface;
/**
* Defines a Gettext PO stream writer.
*/
class PoStreamWriter implements PoWriterInterface, PoStreamInterface {
/**
* URI of the PO stream that is being written.
*
* @var string
*/
private $_uri;
/**
* The Gettext PO header.
*
* @var Drupal\Component\Gettext\PoHeader
*/
private $_header;
/**
* File handle of the current PO stream.
*
* @var resource
*/
private $_fd;
/**
* Get the PO header of the current stream.
*
* @return Drupal\Component\Gettext\PoHeader
* The Gettext PO header.
*/
public function getHeader() {
return $this->_header;
}
/**
* Set the PO header for the current stream.
*
* @param Drupal\Component\Gettext\PoHeader $header
* The Gettext PO header to set.
*/
public function setHeader(PoHeader $header) {
$this->_header = $header;
}
/**
* Get the current language code used.
*
* @return string
* The language code.
*/
public function getLangcode() {
return $this->_langcode;
}
/**
* Set the language code.
*
* @param string $langcode
* The language code.
*/
public function setLangcode($langcode) {
$this->_langcode = $langcode;
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::open().
*/
public function open() {
// Open in write mode. Will overwrite the stream if it already exists.
$this->_fd = fopen($this->getURI(), 'w');
// Write the header at the start.
$this->writeHeader();
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::close().
*
* @throws Exception
* If the stream is not open.
*/
public function close() {
if ($this->_fd) {
fclose($this->_fd);
}
else {
throw new Exception('Cannot close stream that is not open.');
}
}
/**
* Write data to the stream.
*
* @param string $data
* Piece of string to write to the stream. If the value is not directly a
* string, casting will happen in writing.
*
* @throws Exception
* If writing the data is not possible.
*/
private function write($data) {
$result = fputs($this->_fd, $data);
if ($result === FALSE) {
throw new Exception('Unable to write data: ' . substr($data, 0, 20));
}
}
/**
* Write the PO header to the stream.
*/
private function writeHeader() {
$this->write($this->_header);
}
/**
* Implements Drupal\Component\Gettext\PoWriterInterface::writeItem().
*/
public function writeItem(PoItem $item) {
$this->write($item);
}
/**
* Implements Drupal\Component\Gettext\PoWriterInterface::writeItems().
*/
public function writeItems(PoReaderInterface $reader, $count = -1) {
$forever = $count == -1;
while (($count-- > 0 || $forever) && ($item = $reader->readItem())) {
$this->writeItem($item);
}
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::getURI().
*
* @throws Exception
* If the URI is not set.
*/
public function getURI() {
if (empty($this->_uri)) {
throw new Exception('No URI set.');
}
return $this->_uri;
}
/**
* Implements Drupal\Component\Gettext\PoStreamInterface::setURI().
*/
public function setURI($uri) {
$this->_uri = $uri;
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* @file
* Definition of Drupal\Component\Gettext\PoWriterInterface.
*/
namespace Drupal\Component\Gettext;
use Drupal\Component\Gettext\PoMetadataInterface;
use Drupal\Component\Gettext\PoItem;
/**
* Shared interface definition for all Gettext PO Writers.
*/
interface PoWriterInterface extends PoMetadataInterface {
/**
* Writes the given item.
*
* @param PoItem $item
* One specific item to write.
*/
function writeItem(PoItem $item);
/**
* Writes all or the given amount of items.
*
* @param PoReaderInterface $reader
* Reader to read PoItems from.
* @param $count
* Amount of items to read from $reader to write. If -1, all items are
* read from $reader.
*/
function writeItems(PoReaderInterface $reader, $count = -1);
}

View File

@ -0,0 +1,108 @@
<?php
/**
* @file
* Definition of Drupal\locale\Gettext.
*/
namespace Drupal\locale;
use Drupal\Component\Gettext\PoStreamReader;
use Drupal\Component\Gettext\PoMemoryWriter;
use Drupal\locale\PoDatabaseWriter;
/**
* Static class providing Drupal specific Gettext functionality.
*
* The operations are related to pumping data from a source to a destination,
* for example:
* - Remote files http://*.po to memory
* - File public://*.po to database
*/
class Gettext {
/**
* Reads the given Gettext PO files into a data structure.
*
* @param string $langcode
* Language code string.
* @param array $files
* List of file objects with uri properties pointing to read.
*
* @return array
* Structured array as produced by a PoMemoryWriter.
*
* @see Drupal\Component\Gettext\PoMemoryWriter
*/
static function filesToArray($langcode, array $files) {
$writer = new PoMemoryWriter();
$writer->setLangcode($langcode);
foreach ($files as $file) {
$reader = new PoStreamReader();
$reader->setURI($file->uri);
$reader->setLangcode($langcode);
$reader->open();
$writer->writeItems($reader, -1);
}
return $writer->getData();
}
/**
* Reads the given PO files into the database.
*
* @param stdClass $file
* File object with an uri property pointing at the file's path.
* @param string $langcode
* Language code string.
* @param array $overwrite_options
* Overwrite options array as defined in Drupal\locale\PoDatabaseWriter.
* @param boolean $customized
* Flag indicating whether the string imported from $file are customized
* translations or come from a community source. Use LOCALE_CUSTOMIZED or
* LOCALE_NOT_CUSTOMIZED.
*
* @return array
* Report array as defined in Drupal\locale\PoDatabaseWriter.
*
* @see Drupal\locale\PoDatabaseWriter
*/
static function fileToDatabase($file, $langcode, $overwrite_options, $customized = LOCALE_NOT_CUSTOMIZED) {
// Instantiate and initialize the stream reader for this file.
$reader = new PoStreamReader();
$reader->setLangcode($langcode);
$reader->setURI($file->uri);
try {
$reader->open();
}
catch (Exception $exception) {
throw $exception;
}
$header = $reader->getHeader();
if (!$header) {
throw new Exception('Missing or malformed header.');
}
// Initialize the database writer.
$writer = new PoDatabaseWriter();
$writer->setLangcode($langcode);
$options = array(
'overwrite_options' => $overwrite_options,
'customized' => $customized,
);
$writer->setOptions($options);
$writer->setHeader($header);
// Attempt to pipe all items from the file to the database.
try {
$writer->writeItems($reader, -1);
}
catch (Exception $exception) {
throw $exception;
}
// Report back with an array of status information.
return $writer->getReport();
}
}

View File

@ -0,0 +1,180 @@
<?php
/**
* @file
* Definition of Drupal\locale\PoDatabaseReader.
*/
namespace Drupal\locale;
use Drupal\Component\Gettext\PoHeader;
use Drupal\Component\Gettext\PoItem;
use Drupal\Component\Gettext\PoReaderInterface;
/**
* Gettext PO reader working with the locale module database.
*/
class PoDatabaseReader implements PoReaderInterface {
/**
* An associative array indicating which type of strings should be read.
*
* Elements of the array:
* - not_customized: boolean indicating if not customized strings should be
* read.
* - customized: boolean indicating if customized strings should be read.
* - no_translated: boolean indicating if non-translated should be read.
*
* The three options define three distinct sets of strings, which combined
* cover all strings.
*
* @var array
*/
private $_options;
/**
* Language code of the language being read from the database.
*
* @var string
*/
private $_langcode;
/**
* Store the result of the query so it can be iterated later.
*
* @var resource
*/
private $_result;
/**
* Constructor, initializes with default options.
*/
function __construct() {
$this->setOptions(array());
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::getLangcode().
*/
public function getLangcode() {
return $this->_langcode;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::setLangcode().
*/
public function setLangcode($langcode) {
$this->_langcode = $langcode;
}
/**
* Get the options used by the reader.
*/
function getOptions() {
return $this->_options;
}
/**
* Set the options for the current reader.
*/
function setOptions(array $options) {
$options += array(
'customized' => FALSE,
'not_customized' => FALSE,
'not_translated' => FALSE,
);
$this->_options = $options;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::getHeader().
*/
function getHeader() {
return new PoHeader($this->getLangcode());
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::setHeader().
*
* @throws Exception
* Always, because you cannot set the PO header of a reader.
*/
function setHeader(PoHeader $header) {
throw new Exception('You cannot set the PO header in a reader.');
}
/**
* Builds and executes a database query based on options set earlier.
*/
private function buildQuery() {
$langcode = $this->_langcode;
$options = $this->_options;
if (array_sum($options) == 0) {
// If user asked to not include anything in the translation files,
// that would not make sense, so just fall back on providing a template.
$langcode = NULL;
}
// Build and execute query to collect source strings and translations.
$query = db_select('locales_source', 's');
if (!empty($langcode)) {
if ($options['not_translated']) {
// Left join to keep untranslated strings in.
$query->leftJoin('locales_target', 't', 's.lid = t.lid AND t.language = :language', array(':language' => $langcode));
}
else {
// Inner join to filter for only translations.
$query->innerJoin('locales_target', 't', 's.lid = t.lid AND t.language = :language', array(':language' => $langcode));
}
if ($options['customized']) {
if (!$options['not_customized']) {
// Filter for customized strings only.
$query->condition('t.customized', LOCALE_CUSTOMIZED);
}
// Else no filtering needed in this case.
}
else {
if ($options['not_customized']) {
// Filter for non-customized strings only.
$query->condition('t.customized', LOCALE_NOT_CUSTOMIZED);
}
else {
// Filter for strings without translation.
$query->isNull('t.translation');
}
}
$query->fields('t', array('translation'));
}
else {
$query->leftJoin('locales_target', 't', 's.lid = t.lid');
}
$query->fields('s', array('lid', 'source', 'context', 'location'));
$this->_result = $query->execute();
}
/**
* Get the database result resource for the given language and options.
*/
private function getResult() {
if (!isset($this->_result)) {
$this->buildQuery();
}
return $this->_result;
}
/**
* Implements Drupal\Component\Gettext\PoReaderInterface::readItem().
*/
function readItem() {
$result = $this->getResult();
$values = $result->fetchAssoc();
if ($values) {
$poItem = new PoItem();
$poItem->setFromArray($values);
return $poItem;
}
}
}

View File

@ -0,0 +1,306 @@
<?php
/**
* @file
* Definition of Drupal\locale\PoDatabaseWriter.
*/
namespace Drupal\locale;
use Drupal\Component\Gettext\PoHeader;
use Drupal\Component\Gettext\PoItem;
use Drupal\Component\Gettext\PoReaderInterface;
use Drupal\Component\Gettext\PoWriterInterface;
/**
* Gettext PO writer working with the locale module database.
*/
class PoDatabaseWriter implements PoWriterInterface {
/**
* An associative array indicating what data should be overwritten, if any.
*
* Elements of the array:
* - override_options
* - not_customized: boolean indicating that not customized strings should
* be overwritten.
* - customized: boolean indicating that customized strings should be
* overwritten.
* - customized: the strings being imported should be saved as customized.
* One of LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED.
*
* @var array
*/
private $_options;
/**
* Language code of the language being written to the database.
*
* @var string
*/
private $_langcode;
/**
* Header of the po file written to the database.
*
* @var Drupal\Component\Gettext\PoHeader
*/
private $_header;
/**
* Associative array summarizing the number of changes done.
*
* Keys for the array:
* - additions: number of source strings newly added
* - updates: number of translations updated
* - deletes: number of translations deleted
* - skips: number of strings skipped due to disallowed HTML
*
* @var array
*/
private $_report;
/**
* Constructor, initialize reporting array.
*/
function __construct() {
$this->setReport();
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::getLangcode().
*/
public function getLangcode() {
return $this->_langcode;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::setLangcode().
*/
public function setLangcode($langcode) {
$this->_langcode = $langcode;
}
/**
* Get the report of the write operations.
*/
public function getReport() {
return $this->_report;
}
/**
* Set the report array of write operations.
*
* @param array $report
* Associative array with result information.
*/
function setReport($report = array()) {
$report += array(
'additions' => 0,
'updates' => 0,
'deletes' => 0,
'skips' => 0,
);
$this->_report = $report;
}
/**
* Get the options used by the writer.
*/
function getOptions() {
return $this->_options;
}
/**
* Set the options for the current writer.
*/
function setOptions(array $options) {
if (!isset($options['overwrite_options'])) {
$options['overwrite_options'] = array();
}
$options['overwrite_options'] += array(
'not_customized' => FALSE,
'customized' => FALSE,
);
$options += array(
'customized' => LOCALE_NOT_CUSTOMIZED,
);
$this->_options = $options;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::getHeader().
*/
function getHeader() {
return $this->_header;
}
/**
* Implements Drupal\Component\Gettext\PoMetadataInterface::setHeader().
*
* Sets the header and configure Drupal accordingly.
*
* Before being able to process the given header we need to know in what
* context this database write is done. For this the options must be set.
*
* A langcode is required to set the current header's PluralForm.
*
* @param Drupal\Component\Gettext\PoHeader $header
* Header metadata.
*
* @throws Exception
*/
function setHeader(PoHeader $header) {
$this->_header = $header;
$locale_plurals = variable_get('locale_translation_plurals', array());
// Check for options.
$options = $this->getOptions();
if (empty($options)) {
throw new Exception("Options should be set before assigning a PoHeader.");
}
$overwrite_options = $options['overwrite_options'];
// Check for langcode.
$langcode = $this->_langcode;
if (empty($langcode)) {
throw new Exception("Langcode should be set before assigning a PoHeader.");
}
if (array_sum($overwrite_options) || empty($locale_plurals[$langcode]['plurals'])) {
// Get and store the plural formula if available.
$plural = $header->getPluralForms();
if (isset($plural) && $p = $header->parsePluralForms($plural)) {
list($nplurals, $formula) = $p;
$locale_plurals[$langcode] = array(
'plurals' => $nplurals,
'formula' => $formula,
);
variable_set('locale_translation_plurals', $locale_plurals);
}
}
}
/**
* Implements Drupal\Component\Gettext\PoWriterInterface::writeItem().
*/
function writeItem(PoItem $item) {
if ($item->isPlural()) {
$item->setSource(join(LOCALE_PLURAL_DELIMITER, $item->getSource()));
$item->setTranslation(join(LOCALE_PLURAL_DELIMITER, $item->getTranslation()));
}
$this->importString($item);
}
/**
* Implements Drupal\Component\Gettext\PoWriterInterface::writeItems().
*/
public function writeItems(PoReaderInterface $reader, $count = -1) {
$forever = $count == -1;
while (($count-- > 0 || $forever) && ($item = $reader->readItem())) {
$this->writeItem($item);
}
}
/**
* Imports one string into the database.
*
* @param Drupal\Component\Gettext\PoItem $item
* The item being imported.
*
* @return int
* The string ID of the existing string modified or the new string added.
*/
private function importString(PoItem $item) {
// Initialize overwrite options if not set.
$this->_options['overwrite_options'] += array(
'not_customized' => FALSE,
'customized' => FALSE,
);
$overwrite_options = $this->_options['overwrite_options'];
$customized = $this->_options['customized'];
$context = $item->getContext();
$source = $item->getSource();
$translation = $item->getTranslation();
// Look up the source string and any existing translation.
$string = db_query("SELECT s.lid, t.customized FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.source = :source AND s.context = :context", array(
':source' => $source,
':context' => $context,
':language' => $this->_langcode,
))
->fetchObject();
if (!empty($translation)) {
// Skip this string unless it passes a check for dangerous code.
if (!locale_string_is_safe($translation)) {
watchdog('locale', 'Import of string "%string" was skipped because of disallowed or malformed HTML.', array('%string' => $translation), WATCHDOG_ERROR);
$this->_report['skips']++;
return 0;
}
elseif (isset($string->lid)) {
if (!isset($string->customized)) {
// No translation in this language.
db_insert('locales_target')
->fields(array(
'lid' => $string->lid,
'language' => $this->_langcode,
'translation' => $translation,
'customized' => $customized,
))
->execute();
$this->_report['additions']++;
}
elseif ($overwrite_options[$string->customized ? 'customized' : 'not_customized']) {
// Translation exists, only overwrite if instructed.
db_update('locales_target')
->fields(array(
'translation' => $translation,
'customized' => $customized,
))
->condition('language', $this->_langcode)
->condition('lid', $string->lid)
->execute();
$this->_report['updates']++;
}
return $string->lid;
}
else {
// No such source string in the database yet.
$lid = db_insert('locales_source')
->fields(array(
'source' => $source,
'context' => $context,
))
->execute();
db_insert('locales_target')
->fields(array(
'lid' => $lid,
'language' => $this->_langcode,
'translation' => $translation,
'customized' => $customized,
))
->execute();
$this->_report['additions']++;
return $lid;
}
}
elseif (isset($string->lid) && isset($string->customized) && $overwrite_options[$string->customized ? 'customized' : 'not_customized']) {
// Empty translation, remove existing if instructed.
db_delete('locales_target')
->condition('language', $this->_langcode)
->condition('lid', $string->lid)
->execute();
$this->_report['deletes']++;
return $string->lid;
}
}
}

View File

@ -197,6 +197,7 @@ class LocaleImportFunctionalTest extends WebTestBase {
// Ensure the translation file was automatically imported when language was
// added.
$this->assertText(t('One translation file imported.'), t('Language file automatically imported.'));
$this->assertText(t('A translation string was skipped because of disallowed or malformed HTML'), t('Language file automatically imported.'));
// Ensure strings were successfully imported.
$search = array(
@ -206,6 +207,27 @@ class LocaleImportFunctionalTest extends WebTestBase {
);
$this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
$this->assertNoText(t('No strings available.'), t('String successfully imported.'));
// Ensure multiline string was imported.
$search = array(
'string' => 'Source string for multiline translation',
'langcode' => $langcode,
'translation' => 'all',
);
$this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
$this->assertText('Multiline translation string to make sure that import works with it.', t('String successfully imported.'));
// Ensure 'Allowed HTML source string' was imported but the translation for
// 'Another allowed HTML source string' was not because it contains invalid
// HTML.
$search = array(
'string' => 'HTML source string',
'langcode' => $langcode,
'translation' => 'all',
);
$this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
$this->assertText('Allowed HTML source string', t('String successfully imported.'));
$this->assertNoText('Another allowed HTML source string', t('String with disallowed translation not imported.'));
}
/**

View File

@ -5,7 +5,10 @@
* Mass import-export and batch import functionality for Gettext .po files.
*/
include_once DRUPAL_ROOT . '/core/includes/gettext.inc';
use Drupal\Component\Gettext\PoStreamWriter;
use Drupal\locale\Gettext;
use Drupal\locale\PoDatabaseReader;
/**
* User interface for the translation import screen.
@ -107,7 +110,39 @@ function locale_translate_import_form_submit($form, &$form_state) {
$customized = $form_state['values']['customized'] ? LOCALE_CUSTOMIZED : LOCALE_NOT_CUSTOMIZED;
// Now import strings into the language
if ($return = _locale_import_po($file, $language->langcode, $form_state['values']['overwrite_options'], $customized) == FALSE) {
try {
// Try to allocate enough time to parse and import the data.
drupal_set_time_limit(240);
$report = GetText::fileToDatabase($file, $language->langcode, $form_state['values']['overwrite_options'], $customized);
$additions = $report['additions'];
$updates = $report['updates'];
$deletes = $report['deletes'];
$skips = $report['skips'];
menu_router_rebuild();
// Clear cache and force refresh of JavaScript translations.
_locale_invalidate_js($language->langcode);
cache()->deletePrefix('locale:');
drupal_set_message(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)));
watchdog('locale', 'Imported %file into %locale: %number new strings added, %update updated and %delete removed.', array('%file' => $file->filename, '%locale' => $language->langcode, '%number' => $additions, '%update' => $updates, '%delete' => $deletes));
if ($skips) {
if (module_exists('dblog')) {
$skip_message = format_plural($skips, 'A translation string was skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog')));
}
else {
$skip_message = format_plural($skips, 'A translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.');
}
drupal_set_message($skip_message, 'error');
watchdog('locale', '@count disallowed HTML string(s) in %file', array('@count' => $skips, '%file' => $file->uri), WATCHDOG_WARNING);
}
$variables = array('%filename' => $file->filename);
drupal_set_message(t('The translation import of %filename is done.', $variables));
watchdog('locale', 'The translation import of %filename is done.', $variables);
} catch (Exception $exc) {
drupal_set_message(print_r($exc, TRUE));
$variables = array('%filename' => $file->filename);
drupal_set_message(t('The translation import of %filename failed.', $variables), 'error');
watchdog('locale', 'The translation import of %filename failed.', $variables, WATCHDOG_ERROR);
@ -207,7 +242,44 @@ function locale_translate_export_form_submit($form, &$form_state) {
$language = NULL;
}
$content_options = isset($form_state['values']['content_options']) ? $form_state['values']['content_options'] : array();
_locale_export_po($language, _locale_export_po_generate($language, _locale_export_get_strings($language, $content_options)));
$reader = new PoDatabaseReader();
$languageName = '';
if ($language != NULL) {
$reader->setLangcode($language->langcode);
$reader->setOptions($content_options);
$languages = language_list();
$languageName = isset($languages[$language->langcode]) ? $languages[$language->langcode]->name : '';
$filename = $language->langcode .'.po';
}
else {
// Template required.
$filename = 'drupal.pot';
}
$item = $reader->readItem();
if (!empty($item)) {
$uri = tempnam('temporary://', 'po_');
$header = $reader->getHeader();
$header->setProjectName(variable_get('site_name', 'Drupal'));
$header->setLanguageName($languageName);
$writer = new PoStreamWriter;
$writer->setUri($uri);
$writer->setHeader($header);
$writer->open();
$writer->writeItem($item);
$writer->writeItems($reader);
$writer->close();
header("Content-Disposition: attachment; filename=$filename");
header("Content-Type: text/plain; charset=utf-8");
print file_get_contents($uri);
drupal_exit();
}
else {
drupal_set_message('Nothing to export.');
}
}
/**
@ -257,7 +329,7 @@ function locale_translate_batch_import_files($langcode = NULL, $finish_feedback
->fetchAllAssoc('uri');
foreach ($result as $uri => $info) {
if (isset($files[$uri]) && filemtime($uri) <= $info->timestamp) {
// The file is already imported and it did not change since the import.
// The file is already imported and not changed since the last import.
// Remove it from file list and don't import it again.
unset($files[$uri]);
}
@ -318,22 +390,32 @@ function locale_translate_batch_build($files, $finish_feedback = FALSE) {
/**
* Perform interface translation import as a batch step.
*
* The given filepath is matched against ending with '{langcode}.po'. When
* matched the filepath is added to batch context.
*
* @param $filepath
* Path to a file to import.
* @param $results
* @param $context
* Contains a list of files imported.
*/
function locale_translate_batch_import($filepath, &$context) {
// The filename is either {langcode}.po or {prefix}.{langcode}.po, so
// we can extract the language code to use for the import from the end.
if (preg_match('!(/|\.)([^\./]+)\.po$!', $filepath, $langcode)) {
$file = locale_translate_file_create($filepath, $langcode[2]);
$success = _locale_import_read_po('db-store', $file, array(), $langcode[2]);
if ($success == NULL) {
$file->langcode = $langcode[2];
$file = entity_create('file', array('filename' => drupal_basename($filepath), 'uri' => $filepath));
// We need only the last match
$langcode = array_pop($langcode);
try {
$report = GetText::fileToDatabase($file, $langcode, array(), LOCALE_NOT_CUSTOMIZED);
$file->langcode = $langcode;
$file->timestamp = filemtime($file->uri);
locale_translate_update_file_history($file);
$context['results']['files'][$filepath] = $filepath;
$context['results']['stats'][$filepath] = $report;
} catch (Exception $exception) {
$context['results']['files'][$filepath] = $filepath;
$context['results']['failed_files'][$filepath] = $filepath;
}
$context['results'][] = $filepath;
}
}
@ -342,7 +424,30 @@ function locale_translate_batch_import($filepath, &$context) {
*/
function locale_translate_batch_finished($success, $results) {
if ($success) {
drupal_set_message(format_plural(count($results), 'One translation file imported.', '@count translation files imported.'));
$additions = $updates = $deletes = $skips = 0;
drupal_set_message(format_plural(count($results['files']), 'One translation file imported.', '@count translation files imported.'));
$skipped_files = array();
foreach ($results['stats'] as $filepath => $report) {
$additions += $report['additions'];
$updates += $report['updates'];
$deletes += $report['deletes'];
$skips += $report['skips'];
if ($report['skips'] > 0) {
$skipped_files[] = $filepath;
}
}
drupal_set_message(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)));
watchdog('locale', 'The translation was succesfully imported. %number new strings added, %update updated and %delete removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes));
if ($skips) {
if (module_exists('dblog')) {
$skip_message = format_plural($skips, 'A translation string was skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href="@url">See the log</a> for details.', array('@url' => url('admin/reports/dblog')));
}
else {
$skip_message = format_plural($skips, 'A translation string was skipped because of disallowed or malformed HTML. See the log for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.');
}
drupal_set_message($skip_message, 'error');
watchdog('locale', '@count disallowed HTML string(s) in files: @files.', array('@count' => $skips, '@files' => implode(',', $skipped_files)), WATCHDOG_WARNING);
}
}
}

View File

@ -26,3 +26,15 @@ msgstr "samedi"
msgid "Sunday"
msgstr "dimanche"
msgid "Allowed HTML source string"
msgstr "<strong>Allowed HTML translation string</strong>"
msgid "Another allowed HTML source string"
msgstr "<script>Disallowed HTML translation string</script>"
msgid "Source string for multiline translation"
msgstr ""
"Multiline translation string "
"to make sure that "
"import works with it."