diff --git a/core/includes/gettext.sketch.inc b/core/includes/gettext.sketch.inc new file mode 100644 index 0000000..07ca70f --- /dev/null +++ b/core/includes/gettext.sketch.inc @@ -0,0 +1,195 @@ +supportBatch() && $destination->supportBatch() && $source->size() > $threshold) { + // Transfer data in batches. + // Built and execute batch. + $batch = gettext_transfer_batch_setup($source, $destination); + batch_set($batch); + } + else { + // Transfer all translations without interruption. + // Process the translations one by one to keep the memory footprint low. + while ($translation = $source->read()) { + $destination->write($translation); + } + } +} + +/** + * Set up a batch process to transfer Gettext data. + */ +function gettext_transfer_batch_setup($source, $destination) { + $batch = array( + 'operations' => array( + array('gettext_transfer_batch_op', array($source, $destination)), + ), + 'finished' => 'gettext_transfer_batch_finished', + ); + return $batch; +} + +/** + * Batch operation transferring gettext data. + * + * @todo Add source filter conditions (context, language(?)) as parameter. + */ +function gettext_transfer_batch_op($source, $destination) { + // Initialize sandbox for batch reading. + if (empty($context['sandbox'])) { + $context['sandbox']['transfer'] = array( + 'chunk' => min($source->chunkSize(), $destination->chunkSize()), + ); + } + + // Execute one transfer cycle, which will process bite size 'chunks'. + $success = gettext_transfer_execute($source, $destination, $context['sandbox']['transfer']); + + // After a number of cycles executing gettext_transfer_execute() the transfer + // is completed. + // See gettextapi_import_batch_op() for details. + if ($context['sandbox']['transfer']['finished']) { + $context['finished'] = 1; + + if ($success) { + // Collect statistics from $context['sandbox']['transfer'] + // and tell the world about what great work we did. + } + } +} + +/** + * Batch wrap-up for gettext data transfer. + */ +function gettext_transfer_batch_finished($success, $results, $operations) { + if ($success) { + // Call post processing handler defined by the destination object. + // Can we get the callback and arguments via $results? + $post_process_callback = $results['post_process_callback']; + $arguments = $results['post_process_arguments']; + $post_process_callback($arguments); + + // We did it, Magoo! :) + } + else { + // Sadly reporting where it went wrong. + } +} + +/** + * Post processing for gettext language import into the database. + * + * To be called after successfull (batch) import. + */ +function gettext_post_process_import($langcode) { + // After succesfull import we refresh all affected parts of the system. + _locale_invalidate_js($langcode); + cache_clear_all('locale:', 'cache', TRUE); + menu_rebuild(); +} + +/** + * Transfer gettext data from source to destination in bite size chunks. + */ +function gettext_transfer_execute($source, $destination, &$transfer) { + // If transfer is not yet started we set the header data. + // The header (if supported) is written before the first translation is written. + if (!$destination->inProgress()) { + // Transfer translation header data. + $destination->setMetaData($source->getMetaData(), $langcode); + } + + $chunk = $transfer['chunk']; + // Transfer translations as long as valid data is available and + // the bite size chunk is not yet swallowed. + while ($source->valid() && $chunk > 0) { + // Get one translation from source, write it to destination. + $translation = $source->read(); + $destination->write($translation); + $chunk--; + } + + // Report the percentage of completion for progress reporting. + // Ai, user interface. Why do we bother ;) + $transfer['state']['percentage_of_completion'] = $source->poc(); + + // Close connections when we are done and report the results. + if ($source->finished()) { + $destination->finish(); + $transfer['finished'] = TRUE; + } + + // Report statistics including errors and error log. + $transfer['result'] = $destination->statistics(); +} diff --git a/core/lib/Drupal/Core/Gettext/BatchStateInterface.php b/core/lib/Drupal/Core/Gettext/BatchStateInterface.php new file mode 100644 index 0000000..27ac8f9 --- /dev/null +++ b/core/lib/Drupal/Core/Gettext/BatchStateInterface.php @@ -0,0 +1,8 @@ +stream_open($uri, 'r', $options, $opened_url); + $this->_stream = $s; + $state = array( + 'uri' => $uri, + 'mode' => $mode, + 'options' => $options, + 'opened_url' => $opened_url, + ); + $state += $this->_addition_state; + $this->_addition_state = $state; + } + } + + public function getStream() { + return $this->_stream; + } + + public function getBatchState() { + $state = $this->_addition_state; + if ($this->getStream()) { + $state['position'] = $this->getStream()->stream_tell(); + $state['uri'] = $this->getStream()->getURI(); + } + return $state; + } + + public function setBatchState(array $state = array()) { + $current_state = $this->getBatchState(); + $state += $current_state; + $this->_setBatchState($state); + } + + private function _setBatchState($state) { + if ($this->getStream()) { + $this->getStream()->stream_close(); + $this->open($state['uri'], $state['mode'], $state['options'], $opened_url); + $this->getStream()->stream_seek($state['position'], SEEK_SET); + } + return $this->getBatchState(); + } + +} diff --git a/core/lib/Drupal/Core/Gettext/PODbReader.php b/core/lib/Drupal/Core/Gettext/PODbReader.php new file mode 100644 index 0000000..23add52 --- /dev/null +++ b/core/lib/Drupal/Core/Gettext/PODbReader.php @@ -0,0 +1,169 @@ +_langcode = $langcode; + $this->setOptions($options); + } + + function getOptions() { + return $this->_options; + } + + private function setOptions(array $options) { + if (!isset($options['override_options'])) { + $options['override_options'] = array(); + } + if (!isset($options['customized'])) { + $options['customized'] = LOCALE_NOT_CUSTOMIZED; + } + $this->_options = array( + 'override_options' => $options['override_options'], + 'customized' => $options['customized'], + ); + } + + function setState(array $state) { + $this->_lid = $state['lid']; + $this->setOptions($state['options']); + $this->buildQuery(); + } + + function getState() { + return array( + 'lid' => $this->_lid, + 'options' => $this->_options, + ); + } + + function getHeader() { + return new POHeader(); + } + + /** + * Generates a structured array of all translated strings for the language. + * + * @param $language + * Language object to generate the output for, or NULL if generating + * translation template. + * @param $options + * (optional) An associative array specifying what to include in the output: + * - customized: include customized strings (if TRUE) + * - uncustomized: include non-customized string (if TRUE) + * - untranslated: include untranslated source strings (if TRUE) + * Ignored if $language is NULL. + * + * @return + * An array of translated strings that can be used to generate an export. + */ + private function buildQuery() { + $langcode = $this->_langcode; + $options = $this->_options; + + // Assume FALSE for all options if not provided by the API. + $options += array( + 'customized' => FALSE, + 'not_customized' => FALSE, + 'not_translated' => FALSE, + ); + 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. + $language = NULL; + } + + // Build and execute query to collect source strings and translations. + $query = db_select('locales_source', 's'); + if (!empty($language)) { + 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')); + + // TODO: we need to order by lid + // This does not seem to work + $query->orderBy('s.lid'); + $query->condition('s.lid', $this->_lid, '>'); + + $this->_result = $query->execute(); + //echo "Executing: (lid = $this->_lid) : \n" . $this->_result->getQueryString() . "\n"; + } + + private function getResult() { + if (!isset($this->_result)) { + $this->buildQuery(); + } + return $this->_result; + } + + function readItem() { + $result = $this->getResult(); + $values = $result->fetchAssoc(); + + if ($values) { + $poItem = new POItem(); + $poItem->fromArray($values); + // Manage state + $this->_lid = $values['lid']; + return $poItem; + } + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Gettext/PODbWriter.php b/core/lib/Drupal/Core/Gettext/PODbWriter.php new file mode 100644 index 0000000..396c497 --- /dev/null +++ b/core/lib/Drupal/Core/Gettext/PODbWriter.php @@ -0,0 +1,189 @@ +_langcode = $langcode; + $this->setOptions($options); + } + + function getOptions() { + return $this->_options; + } + + function setOptions(array $options) { + if (!isset($options['override_options'])) { + $options['override_options'] = array(); + } + if (!isset($options['customized'])) { + $options['customized'] = LOCALE_NOT_CUSTOMIZED; + } + $this->_options = $options; + } + + function getHeader() { + return new POHeader($this->_langcode); + } + + function writeItem(POItem $item) { + $report = NULL; + if ($item->plural) { + // We must iterate of plurals? + $item->source = join(LOCALE_PLURAL_DELIMITER, $item->source); + $item->translation = join(LOCALE_PLURAL_DELIMITER, $item->translation); + } + else { + $this->_locale_import_one_string_db($report, $this->_langcode, $item->context, $item->source, $item->translation, 'location', $this->_options['override_options'], $this->_options['customized']); + } + } + + public function writeItems(PoReaderInterface $reader, $count = 10) { + $forever = $count == -1; + while (($count-- > 0 || $forever) && ($item = $reader->readItem())) { + $this->writeItem($item); + } + } + + /** + * Imports one string into the database. + * + * @param $report + * Report array summarizing the number of changes done in the form: + * array(inserts, updates, deletes). + * @param $langcode + * Language code to import string into. + * @param $context + * The context of this string. + * @param $source + * Source string. + * @param $translation + * Translation to language specified in $langcode. + * @param $location + * Location value to save with source string. + * @param $overwrite_options + * An associative array indicating what data should be overwritten, if any. + * - not_customized: not customized strings should be overwritten. + * - customized: customized strings should be overwritten. + * @param $customized + * (optional) Whether the strings being imported should be saved as customized. + * Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. + * + * @return + * The string ID of the existing string modified or the new string added. + */ + function _locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $location, $overwrite_options, $customized = LOCALE_NOT_CUSTOMIZED) { + + // Initialize overwrite options if not set. + $overwrite_options += array( + 'not_customized' => FALSE, + 'customized' => FALSE, + ); + + // 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' => $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); + $report['skips']++; + return 0; + } + elseif (isset($string->lid)) { + // We have this source string saved already. + db_update('locales_source') + ->fields(array( + 'location' => $location, + )) + ->condition('lid', $string->lid) + ->execute(); + + if (!isset($string->customized)) { + // No translation in this language. + db_insert('locales_target') + ->fields(array( + 'lid' => $string->lid, + 'language' => $langcode, + 'translation' => $translation, + 'customized' => $customized, + )) + ->execute(); + + $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', $langcode) + ->condition('lid', $string->lid) + ->execute(); + + $report['updates']++; + } + return $string->lid; + } + else { + // No such source string in the database yet. + $lid = db_insert('locales_source') + ->fields(array( + 'location' => $location, + 'source' => $source, + 'context' => (string) $context, + )) + ->execute(); + + db_insert('locales_target') + ->fields(array( + 'lid' => $lid, + 'language' => $langcode, + 'translation' => $translation, + 'customized' => $customized, + )) + ->execute(); + + $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', $langcode) + ->condition('lid', $string->lid) + ->execute(); + + $report['deletes']++; + return $string->lid; + } + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Gettext/POHeader.php b/core/lib/Drupal/Core/Gettext/POHeader.php new file mode 100644 index 0000000..a46030d --- /dev/null +++ b/core/lib/Drupal/Core/Gettext/POHeader.php @@ -0,0 +1,115 @@ +1);\n" + + * @author clemens + */ +class POHeader { + + private $_langcode; + private $_projectIdVersion; + private $_potCreationDate; + private $_poRevisionDate; + private $_languageTeam; + private $_mimeVersion; + private $_contentType; + private $_contentTransferEncoding; + private $_pluralForms; + private $_authors; + private $_po_date; + private $_plurals; + + public function __construct($langcode = NULL) { + $this->_authors = array(); + $this->_po_date = date("Y-m-d H:iO"); + $this->_plurals = 'nplurals=2; plural=(n > 1);'; + ; + } + + static public function mapping() { + return array( + 'Project-Id-Version' => '_projectIdVersion', + 'POT-Creation-Date' => '_potCreationDate', + 'PO-Revision-Date' => '_poRevisionDate', + 'Language-Team' => '_languageTeam', + 'MIME-Version' => '_mimeVersion', + 'Content-Type' => '_contentType', + 'Content-Transfer-Encoding' => '_contentTransferEncoding', + 'Plural-Forms' => '_pluralForms', + ); + } + + /** + * Compile the PO header. + */ + private function compileHeader() { + $output = ''; + + // Add language description and author as comment. + $languages = language_list(); + $language_name = isset($languages[$this->langcode]) ? $languages[$this->langcode]->name : ''; + $output .= '# ' . $language_name . ' translation of ' . variable_get('site_name', 'Drupal') . "\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 \\n\"\n"; + $output .= "\"Language-Team: LANGUAGE \\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->_plurals . "\\n\"\n"; + $output .= "\n"; + + return $output; + } + + /** + * Stores a given PO Header string + * + * TODO: the header string is cleaned by the parser :( + * we need to accept unclean version too + * + * @param type $header + */ + public function setFromString($header) { + $items = split("\n", $header); + $headers = array(); + foreach ($items as $line) { + list( $key, $value) = split(':', $line, 2); + $headers[trim($key, ' "')] = trim($value, "\n"); + } + $mapping = self::mapping(); + foreach ($mapping as $key => $var) { + if (isset($headers[$key])) { + $this->{$var} = $headers[$key]; + } + } + } + + public function __toString() { + $result = $this->compileHeader() . "\n"; + return $result; + } +} + +?> diff --git a/core/lib/Drupal/Core/Gettext/POItem.php b/core/lib/Drupal/Core/Gettext/POItem.php new file mode 100644 index 0000000..ea7ced1 --- /dev/null +++ b/core/lib/Drupal/Core/Gettext/POItem.php @@ -0,0 +1,139 @@ +1);\n" + + * @author clemens + */ +class POItem { + + public $context; + public $source; + public $this; + public $plural; + public $comment; + + static public function mapping() { + return array( + 'msgctxt' => 'context', + 'msgid' => 'source', + 'msgstr' => 'translation', + '#' => 'comment', + ); + } + + public function fromArray(array $values = array()) { + foreach ($values as $key => $value) { + $this->{$key} = $value; + } + } + + public function __toString() { + return $this->compileTranslation(); + } + + /** + * Compile PO translations strings from a translation object. + * + * Translation object consists of: + * source string (singular) or array of strings (plural) + * translation string (singular) or array of strings (plural) + * plural TRUE: source and translation are plurals + * context source context string + */ + private function compileTranslation() { + $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]); + + // Format translation strings. + $plurals = variable_get('locale_translation_plurals', array()); + // @todo What to when $plurals[$this->langcode] is not set? + // This (currently) happens if a language is created manually or importing a malformed po. + $nplurals = $plurals[$this->langcode]['plurals']; + for ($i = 0; $i < $nplurals; $i++) { + if (isset($this->translation[$i])) { + $output .= 'msgstr[' . $i . '] ' . $this->formatString($this->translation[$i]); + } + else { + $output .= 'msgstr[' . $i . '] ""' . "\n"; + } + } + + return $output; + } + + /** + * Formats a singular translation. + */ + private function formatSingular() { + $output = ''; + $output .= 'msgid ' . $this->formatString($this->source); + $output .= 'msgstr ' . $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"; + } + } + +} + +?> diff --git a/core/lib/Drupal/Core/Gettext/PoFileReader.php b/core/lib/Drupal/Core/Gettext/PoFileReader.php new file mode 100644 index 0000000..c466a36 --- /dev/null +++ b/core/lib/Drupal/Core/Gettext/PoFileReader.php @@ -0,0 +1,482 @@ +_uri = $uri; + $this->_langcode = $langcode; + + $this->open(); + $this->readHeader(); + } + + public function open() { + $this->_fd = fopen($this->_uri, 'rb'); + $this->_size = ftell($this->_fd); + } + + public function setState(array $state) { + $this->_uri = $state['uri']; + $this->open(); + $this->readHeader(); + if (isset($state['seekpos'])) { + fseek($this->_fd, $state['seekpos']); + } + if (isset($state['lineno'])) { + $this->lineno = $state['lineno']; + } + } + + public function getState() { + return array( + 'uri' => $this->_uri, + 'seekpos' => ftell($this->_fd), + 'lineno' => $this->lineno, + ); + } + + /** + * Return a translation object (singular or plural) + * + * @todo Define a translation object for this purpose? + * Or use a standard class for better performance? + */ + public function readItem() { + $translation = $this->readTranslation(); + return $translation; + } + + private function readTranslation() { + $this->latestTranslation = $this->translation; + $this->translation = NULL; + while (!$this->finished && is_null($this->translation)) { + $this->readLine(); + } + return $this->translation; + } + + public function getHeader() { + return $this->_header; + } + + /** + * Reads the header from the given input stream. + * + * We need to read the optional first COMMENT + * Next read a MSGID and a MSGSTR + * + * TODO: is a header required? + */ + private function readHeader() { + $translation = $this->readTranslation(); + $header = new POHeader; + $header->setFromString(trim($translation->translation)); + $this->_header = $header; + } + + /** + * Reads a line from a PO file. + * + * While reading a line it's content is processed according to current + * context. + * + * The parser context. Can be: + * - 'COMMENT' (#) + * - 'MSGID' (msgid) + * - 'MSGID_PLURAL' (msgid_plural) + * - 'MSGCTXT' (msgctxt) + * - 'MSGSTR' (msgstr or msgstr[]) + * - 'MSGSTR_ARR' (msgstr_arg) + * + * @return boolean FALSE or NULL + */ + private function readLine() { + // a string or boolean FALSE + $line = fgets($this->_fd); + $this->finished = is_bool($line); + if (!$this->finished) { + + if ($this->lineno == 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->plural = 0; + } + + $this->lineno++; + + // Trim away the linefeed. + $line = trim(strtr($line, array("\\\n" => ""))); + + if (!strncmp('#', $line, 1)) { + // Lines starting with '#' are comments. + + if ($this->context == 'COMMENT') { + // Already in comment token, insert the comment. + $this->current['#'][] = substr($line, 1); + } + elseif (($this->context == 'MSGSTR') || ($this->context == 'MSGSTR_ARR')) { + // We are currently in string token, close it out. + $this->saveOneString(); + + // Start a new entry for the comment. + $this->current = array(); + $this->current['#'][] = substr($line, 1); + + $this->context = 'COMMENT'; + return TRUE; + } + else { + // A comment following any other token is a syntax error. + $this->log('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $this->lineno); + return FALSE; + } + return; + } + elseif (!strncmp('msgid_plural', $line, 12)) { + // A plural form for the current message. + + if ($this->context != 'MSGID') { + // A plural form cannot be added to anything else but the id directly. + $this->log('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $this->lineno); + return FALSE; + } + + // Remove 'msgid_plural' and trim away whitespace. + $line = trim(substr($line, 12)); + // At this point, $line should now contain only the plural form. + + $quoted = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The plural form must be wrapped in quotes. + $this->log('The translation file %filename contains a syntax error on line %line.', $this->lineno); + return FALSE; + } + + // Append the plural form to the current entry. + if (is_string($this->current['msgid'])) { + // The first value was stored as string. Now we know the context is + // plural, it is converted to array. + $this->current['msgid'] = array($this->current['msgid']); + } + $this->current['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 a message string, close it out. + $this->saveOneString(); + + // Start a new context for the id. + $current = array(); + } + elseif ($this->context == 'MSGID') { + // We are currently already in the context, meaning we passed an id with no data. + $this->log('The translation file %filename contains an error: "msgid" is unexpected on line %line.', $this->lineno); + return FALSE; + } + + // Remove 'msgid' and trim away whitespace. + $line = trim(substr($line, 5)); + // At this point, $line should now contain only the message id. + + $quoted = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The message id must be wrapped in quotes. + $this->log('The translation file %filename contains a syntax error on line %line.', $this->lineno); + return FALSE; + } + + $this->current['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 a message, start a new one. + $this->saveOneString($current); + $this->current = array(); + } + elseif (!empty($this->current['msgctxt'])) { + // A context cannot apply to another context. + $this->log('The translation file %filename contains an error: "msgctxt" is unexpected on line %line.', $this->lineno); + return FALSE; + } + + // Remove 'msgctxt' and trim away whitespaces. + $line = trim(substr($line, 7)); + // At this point, $line should now contain the context. + + $quoted = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The context string must be quoted. + $this->log('The translation file %filename contains a syntax error on line %line.', $this->lineno); + return FALSE; + } + + $this->current['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')) { + // Message strings must come after msgid, msgxtxt, msgid_plural, or other msgstr[] entries. + $this->log('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', $this->lineno); + return FALSE; + } + + // Ensure the plurality is terminated. + if (strpos($line, ']') === FALSE) { + $this->log('The translation file %filename contains a syntax error on line %line.', $this->lineno); + return FALSE; + } + + // Extract the plurality. + $frombracket = strstr($line, '['); + $this->plural = substr($frombracket, 1, strpos($frombracket, ']') - 1); + + // Skip to the next whitespace and trim away any further whitespace, bringing $line to the message data. + $line = trim(strstr($line, " ")); + + $quoted = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The string must be quoted. + $this->log('The translation file %filename contains a syntax error on line %line.', $this->lineno); + return FALSE; + } + if (!is_array($this->current['msgstr'])) { + $this->current['msgstr'] = array(); + } + + $this->current['msgstr'][$this->plural] = $quoted; + + $this->context = 'MSGSTR_ARR'; + return; + } + elseif (!strncmp("msgstr", $line, 6)) { + // A string for the an id or context. + + if (($this->context != 'MSGID') && ($this->context != 'MSGCTXT')) { + // Strings are only valid within an id or context scope. + $this->log('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', $this->lineno); + return FALSE; + } + + // Remove 'msgstr' and trim away away whitespaces. + $line = trim(substr($line, 6)); + // At this point, $line should now contain the message. + + $quoted = $this->parseQuoted($line); + if ($quoted === FALSE) { + // The string must be quoted. + $this->log('The translation file %filename contains a syntax error on line %line.', $this->lineno); + return FALSE; + } + + $this->current['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) { + // The string must be quoted. + $this->log('The translation file %filename contains a syntax error on line %line.', $this->lineno); + return FALSE; + } + + // Append the string to the current context. + if (($this->context == 'MSGID') || ($this->context == 'MSGID_PLURAL')) { + if (is_array($this->current['msgid'])) { + // Add string to last array element. + $last_index = count($this->current['msgid']) - 1; + $this->current['msgid'][$last_index] .= $quoted; + } + else { + $this->current['msgid'] .= $quoted; + } + } + elseif ($this->context == 'MSGCTXT') { + $this->current['msgctxt'] .= $quoted; + } + elseif ($this->context == 'MSGSTR') { + $this->current['msgstr'] .= $quoted; + } + elseif ($this->context == 'MSGSTR_ARR') { + $this->current['msgstr'][$this->plural] .= $quoted; + } + else { + // No valid context to append to. + $this->log('The translation file %filename contains an error: there is an unexpected string on line %line.', $this->lineno); + return FALSE; + } + return; + } + } + + // Empty line read or EOF of PO file, closed out the last entry. + if (($this->context == 'MSGSTR') || ($this->context == 'MSGSTR_ARR')) { + $this->saveOneString($this->current); + } + elseif ($this->context != 'COMMENT') { + $this->log('The translation file %filename ended unexpectedly at line %line.', $this->lineno); + return FALSE; + } + } + + /** + * Sets an error message if an error occurred during locale file parsing. + * + * @param $message + * The message to be translated. + * @param $lineno + * An optional line number argument. + */ + protected function log($message, $lineno = NULL) { + if (isset($lineno)) { + $vars['%line'] = $lineno; + } + $t = get_t(); + $this->errorLog[] = $t($message, $vars); + } + + /** + * Store the parsed values as translation object. + */ + public function saveOneString() { + $value = $this->current; + $plural = FALSE; + + $comments = empty($value['#']) ? $this->shortenComments($value['#']) : ''; + + if (is_array($value['msgstr'])) { + // Sort plural variants by their form index. + ksort($value['msgstr']); + $plural = TRUE; + } + + $translation = new POItem; + $translation->context = isset($value['msgctxt']) ? $value['msgctxt'] : ''; + $translation->source = $value['msgid']; + $translation->translation = $value['msgstr']; + $translation->plural = $plural; + $translation->comment = $comments; + + $this->translation = $translation; + + $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)) { + return FALSE; // Start and end quotes must be the same + } + $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 { + return FALSE; // Unrecognized quote + } + } + + /** + * 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($comments) { + $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)); + } + +} diff --git a/core/lib/Drupal/Core/Gettext/PoFileWriter.php b/core/lib/Drupal/Core/Gettext/PoFileWriter.php new file mode 100644 index 0000000..d9090de --- /dev/null +++ b/core/lib/Drupal/Core/Gettext/PoFileWriter.php @@ -0,0 +1,84 @@ +_uri = $uri; + $this->_header = $header; + $this->open(); + } + + private function open() { + // Open in append mode + $this->_fd = fopen($this->_uri, 'a'); + // If file is new position == 0 + $this->_seekpos = ftell($this->_fd); + if ($this->_seekpos == 0) { + $this->writeHeader(); + } + else { + $reader = new PoFileReader($this->uri); + $this->_header = $reader->getHeader(); + } + } + + public function close() { + fclose($this->_fd); + } + + public function setState(array $state) { + $this->_uri = $state['uri']; + $this->open(); + } + + public function getState() { + return array( + 'uri' => $this->_uri, + 'seekpos' => ftell($this->_fd), + ); + } + + private function write($data) { + $result = fputs($this->_fd, $data); + if ($result === FALSE) { + // TODO: better context for message + throw new \Exception("Unable to write data : " . substr($data, 0, 20)); + } + $this->_seekpos = ftell($this->_fd); + } + + private function writeHeader() { + $this->write($this->_header); + } + + public function writeItem(POItem $item) { + $this->write($item); + } + + public function writeItems($reader, $count = 10) { + $forever = $count == -1; + while (($count-- > 0 || $forever) && ($item = $reader->readItem())) { + $this->writeItem($item); + } + } + +} diff --git a/core/lib/Drupal/Core/Gettext/PoReaderInterface.php b/core/lib/Drupal/Core/Gettext/PoReaderInterface.php new file mode 100644 index 0000000..03b725b --- /dev/null +++ b/core/lib/Drupal/Core/Gettext/PoReaderInterface.php @@ -0,0 +1,24 @@ +gettextInterface = $interface; + } + + /** + * Implements magic function __destruct(). + */ + public function __destruct() { + $this->gettextInterface->close(); + } + + /** + * Return a translation object (singular or plural) + * + * @todo Define a translation object for this purpose? + * Or use a standard class for better performance? + */ + public function read() { + } + + /** + * Return header/meta data (date, plural formula, etc.) + */ + public function getMetaData() { + return $this->metaData; + } + + /** + * Return TRUE if the file is opened or the transfer has started. + */ + public function inProgress() { + return $this->inProgress; + } + + /** + * Return the name of an error callback function + */ + public function errorCallback() { + return ''; + } + + /** + * Return arguments for an error callback function + */ + public function errorArguments() { + return array(); + } + + /** + * Return the name of a post processing callback function + */ + public function postProcessCallback() { + return ''; + } + + /** + * Return arguments for a post processing callback function + */ + public function postProcessArguments() { + return array(); + } + + /** + * Return the calculated or estimated size in number of translations. Zero for unknown. To be generated without opening the connection. e.g. use file size not number of lines. + */ + public function size() { + return $this->sourceSize; + } + + /** + * Return a bite size. Based on experience and size to be transfered within a reasonable time. Size in number of source/translations pairs. e.g. database records in locales_source or locales_target tables. A set of plural's are counted as one. + */ + public function biteSize() { + return $this->bite_size; + } + + /** + * Accept a set of data filter arguments: Language, Context + */ + public function setFilter($arguments) { + $this->filter = $arguments; + } + + /** + * Return Is valid. Valid data is available, not EOF, no errors, etc. + */ + public function valid() { + return $this->valid; + } + + /** + * Get percentage of completion (read) + */ + public function poc() { + if (!$this->$inProgress) { + return 0; + } + if ($this->finished) { + return 1; + } + // If reading is not finished, we limit the percentage to max. 95% + // Percentages above 100% are a result of low estimate of source size and + // will be suppressed. + return min(0.95, $this->index/$this->sourceSize); + } + + /** + * Return syntax errors + */ + public function getLog($category = NULL) { + return $this->errorLog; + } + + /** + * Internal: log syntax errors + */ + protected function log($line, $message) { + $this->errorLog[$line] = $message; + } + +} diff --git a/core/lib/Drupal/Core/Gettext/Writer.php b/core/lib/Drupal/Core/Gettext/Writer.php new file mode 100644 index 0000000..34c0c3d --- /dev/null +++ b/core/lib/Drupal/Core/Gettext/Writer.php @@ -0,0 +1,172 @@ +gettextInterface = $interface; + $this->langcode = $langcode; + + $languages = language_list(); + if (isset($languages[$langcode])) { + $this->language = $languages[$langcode]; + } + else { + // @todo throw error: Unknown language code. + } + + // Set default meta data. + $this->metaData = array( + 'authors' => array(), + 'po_date' => date("Y-m-d H:iO"), + 'plurals' => 'nplurals=2; plural=(n > 1);', + ); + } + + /** + * Implements magic function __destruct(). + */ + public function __destruct() { + $this->gettextInterface->close(); + } + + /** + * Write translation string (singular or plural) + * + * @todo Define a translation object for this purpose? + * Or use a standard class for better performance? + */ + public function write($translation) { + + } + + /** + * Set header/meta data (date, plural formula, etc.) + */ + public function setMetaData(array $data) { + $this->metaData = array_merge($this->metaData, $data); + } + + /** + * Return TRUE if the file is opened or the transfer has started. + */ + public function inProgress() { + return $this->inProgress; + } + + /** + * Return the name of an error callback function + */ + public function errorCallback() { + return ''; + } + + /** + * Return arguments for an error callback function + */ + public function errorArguments() { + return array(); + } + + /** + * Return the name of a post processing callback function + */ + public function postProcessCallback() { + return ''; + } + + /** + * Return arguments for a post processing callback function + */ + public function postProcessArguments() { + return array(); + } + + /** + * Return a bite size. Based on experience and size to be transfered within a reasonable time. Size in number of source/translations pairs. e.g. database records in locales_source or locales_target tables. A set of plural's are counted as one. + */ + public function biteSize() { + return $this->bite_size; + } + + /** + * Return the language code as defined by the data (e.g. po header). Use language filter (see below) as fallback. + */ + public function langcode() { + return $this->langcode; + } + + /** + * Accept write mode argument (replace, keep changes, skip existing) + * + * @todo Move this to __constructor()? + */ + public function setWriteMode($mode) { + $this->writeMode = $mode; + } + + /** + * Return Is valid. Valid data is available, not EOF, no errors, etc. + * + * @todo Needed for Writer? + */ + public function valid() { + return $this->valid; + } + + /** + * Get statistics (added, replaced, ignored, error, error log) + */ + public function statistics() { + return array( + 'added' => $this->resultsAdded, + 'replaced' => $this->resultsReplaced, + 'ignored' => $this->resultsIgnored, + 'error' => $this->resultsError, + ); + } + + /** + * Return syntax errors + */ + public function getLog($category = NULL) { + return $this->errorLog; + } + + /** + * Internal: log syntax errors + */ + protected function log($line, $message) { + $this->errorLog[$line] = $message; + } + +} diff --git a/core/lib/Drupal/Core/Gettext/testGettext.php b/core/lib/Drupal/Core/Gettext/testGettext.php new file mode 100644 index 0000000..6b9ce3f --- /dev/null +++ b/core/lib/Drupal/Core/Gettext/testGettext.php @@ -0,0 +1,522 @@ + 'home', + 'translation' => 'thuis', + 'plural' => 0, + 'context' => '', + ), + (object) array( + 'source' => 'delete', + 'translation' => 'verwijderen', + 'plural' => 0, + 'context' => '', + ), + (object) array( + 'source' => array('1 day', '@count days'), + 'translation' => array('1 dag', '@count dagen'), + 'plural' => 1, + 'context' => '', + ), + ); + + return array( + 'langcode' => $langcode, + 'items' => $translations, + ); +} + +/** + * A simple writer of the pot structure + * + * TODO: the header is not specified enough. + * + * @param type $uri + */ +function testGettextWrite($uri) { + $struct = gettext_struct(); + + $file = new GettextFileInterface($uri); + $target = new PoWriter($file, $langcode); + $header = array( + 'authors' => array('me', 'you'), + 'po_date' => date("Y-m-d H:iO"), + 'plurals' => 'nplurals=2; plural=(n > 1);', + ); + $target->setMetaData($header, $struct['langcode']); + + $translations = $struct['items']; + foreach ($translations as $translation) { + $target->write($translation); + } +} + +/** + * Processes a File into a database + * + * @param type $uri + * @param type $dbwriter + * @param type $type + */ +function testProcessFileToDb($uri, $dbwriter, $type = 'po') { + // Producer + $producer = testFileToStruct($uri, $type); + // Consumer + $dbwriter->batchProcess($producer, $type); +} + +function testWriteToPublic() { + logLine(__FUNCTION__, '='); + // Write into public + $file = 'public://text-file.txt.po'; + testGettextWrite($file); + echo file_get_contents($file); + echo "\n\n"; + + $uri = 'public://infinite-text-file.txt.po'; + testInfiniteProducer($uri, 10); + + echo file_get_contents($uri); +} + +function testBatchStreamManager() { + logLine(__FUNCTION__, '='); + $uri = 'public://infinite-text-file.txt.po'; + echo file_get_contents($uri); + + echo "Creating new BatchStreamManager()\n"; + $bsm = new BatchStreamManager(); + echo " state:\n"; + print_r($bsm->getBatchState()); + + echo "Opening stream $uri\n"; +// $uri, $mode, $options, &$opened_url + $bsm->open($uri, 'r', 0, $full_path); + echo " state:\n"; + print_r($bsm->getBatchState()); + + $my_s = $bsm->getStream(); + echo "Reading 10 bytes\n"; + $bytes = $my_s->stream_read(10); + echo " Bytes read: '$bytes'\n"; + + echo " state:\n"; + print_r($bsm->getBatchState()); + + echo "Moving to position 50\n"; + $state = $bsm->getBatchState(); + $state['position'] = 50; + $bsm->setBatchState($state); + $my_s = $bsm->getStream(); + + echo " state:\n"; + print_r($bsm->getBatchState()); + + echo "Reading 15 bytes\n"; + $bytes = $my_s->stream_read(15); + echo " Bytes read: '$bytes'\n"; + + echo " state:\n"; + print_r($bsm->getBatchState()); +} + +function getReadStream($uri) { + logLine(__FUNCTION__, '='); + $s = new GettextFileInterface($uri); + return $s; +} + +function testPoReader() { + logLine(__FUNCTION__, '='); + + $uri = 'public://test.po.txt'; + + logLine("Reading : $uri", '='); + logLine("File contents first 500 bytes"); + $contents = file_get_contents($uri); + echo substr($contents, 0, 500) . "\n"; + + logLine("Using PoFileReader"); + $reader = new PoFileReader($uri); + + echo $reader->getHeader(); + print_r($translation); + + $i = 0; + while (($item = $reader->readItem()) && $i++ < 4) { + printItem($item, $i); + } +} + +function testHeader() { + logLine(__FUNCTION__, '='); + $h = new POHeader(); + + echo "----------------\n"; + $h->setFromString(''); + echo "empty header\n"; + echo $h; + + echo "----------------\n"; + $h->setFromString('"Project-Id-Version: ' . __FILE__ . '\n"'); + echo "-- one item -- \n"; + echo $h; +} + +function testFileToDb() { + logLine(__FUNCTION__, '='); + $uri = 'public://test.po.txt'; + logLine("Reading : $uri"); + logLine("File contents first 500 bytes"); + $contents = file_get_contents($uri); + echo substr($contents, 0, 500) . "\n"; + + logLine("Using new classes"); + $reader = new PoFileReader($uri); + + $writer = new PODbWriter('ca'); + + printItem($reader->getHeader()); + + $i = 0; + while (($item = $reader->readItem()) && $i < 4) { + printItem($item, $i++); + $writer->writeItem($item); + } +} + +function testDbDump() { + logLine(__FUNCTION__, '='); + $reader = new PODbReader('en'); + echo $reader->getHeader() . "\n"; + + $i = 0; + while (($item = $reader->readItem()) && $i < 4) { + printItem($item, $i++); + } + + echo "Saving state to simulate a batch\n"; + $state = $reader->getState(); + + echo "Create a new PODbReader so simulate a batch\n"; + $reader = new PODbReader('en'); + + // Set the state + $reader->setState($state); + $i = 0; + while (($item = $reader->readItem()) && $i < 4) { + printItem($item, $i++); + } +} + +function printItem($item, $context = 0) { + logLine(__FUNCTION__, '='); + if ($item) { + logLine("$context : $item->lid"); + print_r($item); + } +} + +function readItem($reader, $context = 0) { + $item = $reader->readItem(); + if ($item) { + printItem($item); + } +} + +function dumpState($state) { + logLine(__FUNCTION__, '='); + print_r($state); +} + +function testPOFileReader($uri) { + logLine(__FUNCTION__, '='); + + $reader = new \Drupal\Core\Gettext\PoFileReader($uri); + dumpState($reader->getState()); + echo $reader->getHeader() . "\n"; + dumpState($reader->getState()); + $i = 0; + while (($item = readItem($reader)) && $i < 4) { + printItem($item, $i++); + } + dumpState($reader->getState()); +} + +function getLanguages($langcode = NULL) { + logLine(__FUNCTION__, '='); + if (!is_null($langcode)) { + return array($langcode); + } + return array( + 'nl', + 'ar', + 'ca', + 'en', // does not exists on d.o (should it be?) + 'NOP', // does really not exists on d.o + ); +} + +function getRemoteUris($langcode = NULL) { + logLine(__FUNCTION__, '='); + $langcodes = getLanguages($langcode); + $uris = array(); + foreach ($langcodes as $langcode) { + $uri = "http://ftp.drupal.org/files/translations/7.x/drupal/drupal-7.11.$langcode.po"; + $uris[$langcode] = $uri; + } + return $uris; +} + +function getPublicUri($name, $langcode) { + logLine(__FUNCTION__, '='); + $result = "public://$name-$langcode.po"; + logLine($result); + return $result; +} + +function getRemoteUri($langcode) { + $uris = getRemoteUris($langcode); + return $uris[$langcode]; +} + +function testRemotePOPumper() { + logLine(__FUNCTION__, '='); + $uris = getRemoteUris(); + foreach ($uris as $langcode => $uri) { + logLine("langcode: $langcode"); + $reader = new PoFileReader($uri); + $writer = new PODbWriter($langcode); + $writer->writeItems($reader, 10); + } +} + +function testPOFileWriter() { + logLine(__FUNCTION__, '='); + $src = "http://ftp.drupal.org/files/translations/7.x/drupal/drupal-7.11.ar.po"; + + $reader = new \Drupal\Core\Gettext\PoFileReader($src); + $header = $reader->getHeader(); + + $dst = 'public://drupal-7.11.ar.po'; + zapUri($dst); + $writer = new PoFileWriter($dst, $header); + + $i = 0; + while (($item = $reader->readItem()) && $i < 4) { + printItem($item, $i); + $i++; + $writer->writeItem($item); + dumpState($writer->getState()); + } + + $writer->close(); +} + +function testDbToFile() { + logLine(__FUNCTION__, '='); + $reader = new PODbReader('ca'); + + $dst = 'public://drupal-7.11.dummy.po'; + $header = $reader->getHeader(); + + $writer = new Drupal\Core\Gettext\PoFileWriter($dst, $header); + + $i = 0; + while (($item = $reader->readItem()) && $i < 4) { + printItem($item, $i); + $i++; + $writer->writeItem($item); + dumpState($writer->getState()); + } + + $writer->writeItems($reader, 10); + + $writer->close(); +} + +function testBatchSimulation() { + logLine(__FUNCTION__, '='); + + // Grab first langcode + $uris = getRemoteUris(); + $langcode = key($uris); + $src = current($uris); + + logLine("Opening $langcode : $src"); + $reader = new PoFileReader($src); + $header = $reader->getHeader(); + logLine($header); + + $dst = getPublicUri(__FUNCTION__, $langcode); + + zapUri($dst); + logLine("Writing $langcode : $dst"); + $writer = new PoFileWriter($dst, $header); + + logLine('Written header only', '='); + echo file_get_contents($dst); + + processN($writer, $reader, 2); + + dumpFileContents($dst); + + $state = $reader->getState(); + dumpState($state); + + logLine('Replacing reader', '='); + $reader = new PoFileReader($src); + + logLine('setting state back'); + $reader->setState($state); + dumpState($state); + + processN($writer, $reader, 3); + $reader->setState($state); + dumpState($state); + + dumpFileContents($dst); +} + +function testDBReaderState() { + $langcode = 'nl'; + $reader = new PODbReader($langcode); + logLine("Init PODbReader", '='); + $state = $reader->getState(); + dumpState($state); + + + $uri = getPublicUri(__FUNCTION__, $langcode); + zapUri($uri); + $writer = new PoFileWriter($uri, $reader->getHeader()); + + processN($writer, $reader, 4); + + logLine("Read some", '='); + $state = $reader->getState(); + dumpState($state); + + $reader = new PODbReader($langcode); + $reader->setState($state); + processN($writer, $reader, 4); + $state = $reader->getState(); + dumpState($state); + + logLine("File contents from $uri", '='); + echo file_get_contents($uri); +} + +function zapUri($uri) { + logLine("Truncate $uri", '='); + ftruncate(fopen($uri, 'w')); +} + +function processN($writer, $reader, $count = 10) { + if ($count == -1) { + logLine("processing items: __ALL__"); + } + else { + logLine("processing items: $count"); + } + $writer->writeItems($reader, $count); +} + +function dumpFileContents($uri) { + logLine("Written: $uri", '='); + echo file_get_contents($uri); +} + +function pumpAround($langcode) { + logLine(__FUNCTION__, '='); + $uri = getRemoteUri($langcode); + + logLine("Reading from $uri"); + $reader = new PoFileReader($uri, $langcode); + $header = $reader->getHeader(); + + $uri = getPublicUri(__FUNCTION__ . '-remote', $langcode); + zapUri($uri); + logLine("Writing to $uri"); + $writer = new PoFileWriter($uri, $header); + + processN($writer, $reader, -1); + + logLine("Reading from $uri"); + $reader = new PoFileReader($uri, $langcode); + logLine("Writing to DB"); + $writer = new PODbWriter($langcode); + + processN($writer, $reader, -1); + + + logLine("Reading from DB"); + $reader = new PODbReader($langcode); + $header = $reader->getHeader(); + + $uri = getPublicUri(__FUNCTION__ . '-db', $langcode); + zapUri($uri); + logLine("Writing to $uri"); + $writer = new PoFileWriter($uri, $header); + processN($writer, $reader, -1); +} + +testDBReaderState(); +testBatchStreamManager(); +testPoReader(); +testHeader(); +testFileToDb(); +testDbDump(); +testPOFileReader(); +testPOFileWriter(); +testDbToFile(); +testRemotePOPumper(); +testBatchSimulation(); + +pumpAround('ar'); +pumpAround('ca'); +pumpAround('nl'); diff --git a/core/modules/simpletest/tests/gettext.test b/core/modules/simpletest/tests/gettext.test new file mode 100644 index 0000000..0bd968e --- /dev/null +++ b/core/modules/simpletest/tests/gettext.test @@ -0,0 +1,32 @@ + 'Gettext', + 'description' => 'Gettext handling unit tests.', + 'group' => 'System', + ); + } + + function setUp() { + parent::setUp(); + } + + /** + * Test loading a small Po. + */ + function testLoadSmallPo() { + } +} + diff --git a/core/vendor/Symfony/Component/Config b/core/vendor/Symfony/Component/Config new file mode 120000 index 0000000..3421ac7 --- /dev/null +++ b/core/vendor/Symfony/Component/Config @@ -0,0 +1 @@ +../../../../../symfony/src/Symfony/Component/Config \ No newline at end of file diff --git a/core/vendor/Symfony/Component/Translation b/core/vendor/Symfony/Component/Translation new file mode 120000 index 0000000..5b0c441 --- /dev/null +++ b/core/vendor/Symfony/Component/Translation @@ -0,0 +1 @@ +../../../../../symfony/src/Symfony/Component/Translation \ No newline at end of file