diff --git a/core/modules/locale/lib/Drupal/locale/LocaleLookup.php b/core/modules/locale/lib/Drupal/locale/LocaleLookup.php index 77aa4ddace0eff4928aa464a9563c2998cb49202..30d12eca419a58cf5c98aeec88f779e5121e81d5 100644 --- a/core/modules/locale/lib/Drupal/locale/LocaleLookup.php +++ b/core/modules/locale/lib/Drupal/locale/LocaleLookup.php @@ -8,6 +8,8 @@ namespace Drupal\locale; use Drupal\Core\Utility\CacheArray; +use Drupal\locale\SourceString; +use Drupal\locale\TranslationString; /** * Extends CacheArray to allow for dynamic building of the locale cache. @@ -26,12 +28,20 @@ class LocaleLookup extends CacheArray { */ protected $context; + /** + * The locale storage + * + * @var Drupal\locale\StringStorageInterface + */ + protected $stringStorage; + /** * Constructs a LocaleCache object. */ - public function __construct($langcode, $context) { + public function __construct($langcode, $context, $stringStorage) { $this->langcode = $langcode; $this->context = (string) $context; + $this->stringStorage = $stringStorage; // Add the current user's role IDs to the cache key, this ensures that, for // example, strings for admin menu items and settings forms are not cached @@ -44,36 +54,30 @@ public function __construct($langcode, $context) { * Overrides DrupalCacheArray::resolveCacheMiss(). */ protected function resolveCacheMiss($offset) { - $translation = db_query("SELECT s.lid, t.translation, s.version 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( - ':language' => $this->langcode, - ':source' => $offset, - ':context' => $this->context, - ))->fetchObject(); + $translation = $this->stringStorage->findTranslation(array( + // These are the search conditions. + 'language' => $this->langcode, + 'source' => $offset, + 'context' => $this->context + ), array( + // Search options. We just need this limited set of fields. + 'fields' => array('lid', 'version', 'translation'), + )); + if ($translation) { - if ($translation->version != VERSION) { - // This is the first use of this string under current Drupal version. - // Update the {locales_source} table to indicate the string is current. - db_update('locales_source') - ->fields(array('version' => VERSION)) - ->condition('lid', $translation->lid) - ->execute(); - } + $this->stringStorage->checkVersion($translation, VERSION); $value = !empty($translation->translation) ? $translation->translation : TRUE; } else { // We don't have the source string, update the {locales_source} table to // indicate the string is not translated. - db_merge('locales_source') - ->insertFields(array( - 'location' => request_uri(), - 'version' => VERSION, - )) - ->key(array( - 'source' => $offset, - 'context' => $this->context, - )) - ->execute(); - $value = TRUE; + $this->stringStorage->createString(array( + 'source' => $offset, + 'context' => $this->context, + 'location' => request_uri(), + 'version' => VERSION + ))->save(); + $value = TRUE; } $this->storage[$offset] = $value; // Disabling the usage of string caching allows a module to watch for diff --git a/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php b/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php index c0dfc2f2ff57c4847afd5a8181d7a9943cb4c4a9..74cf40d07ebaa1cd0857ca540e53901c377d6ffa 100644 --- a/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php +++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php @@ -10,6 +10,8 @@ use Drupal\Component\Gettext\PoHeader; use Drupal\Component\Gettext\PoItem; use Drupal\Component\Gettext\PoReaderInterface; +use Drupal\locale\TranslationString; +use PDO; /** * Gettext PO reader working with the locale module database. @@ -106,71 +108,67 @@ function setHeader(PoHeader $header) { /** * Builds and executes a database query based on options set earlier. */ - private function buildQuery() { + private function loadStrings() { $langcode = $this->_langcode; $options = $this->_options; + $conditions = array(); 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; + // Force option to get both translated and untranslated strings. + $options['not_translated'] = TRUE; } - // 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)); - } + $conditions['language'] = $langcode; + // Translate some options into field conditions. if ($options['customized']) { if (!$options['not_customized']) { // Filter for customized strings only. - $query->condition('t.customized', LOCALE_CUSTOMIZED); + $conditions['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); + $conditions['customized'] = LOCALE_NOT_CUSTOMIZED; } else { // Filter for strings without translation. - $query->isNull('t.translation'); + $conditions['translated'] = FALSE; } } - $query->fields('t', array('translation')); + if (!$options['not_translated']) { + // Filter for string with translation. + $conditions['translated'] = TRUE; + } + return locale_storage()->getTranslations($conditions); } else { - $query->leftJoin('locales_target', 't', 's.lid = t.lid'); + // If no language, we don't need any of the target fields. + return locale_storage()->getStrings($conditions); } - $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() { + private function readString() { if (!isset($this->_result)) { - $this->buildQuery(); + $this->_result = $this->loadStrings(); } - return $this->_result; + return array_shift($this->_result); } /** * Implements Drupal\Component\Gettext\PoReaderInterface::readItem(). */ function readItem() { - $result = $this->getResult(); - $values = $result->fetchAssoc(); - if ($values) { + if ($string = $this->readString()) { + $values = (array)$string; $poItem = new PoItem(); $poItem->setFromArray($values); return $poItem; diff --git a/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php index 33f05d9f9d7c648bcda21743dc05c21b62eba33c..7afa1db50e250ba49e8ba5ebc24c523b16632790 100644 --- a/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php +++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php @@ -11,6 +11,8 @@ use Drupal\Component\Gettext\PoItem; use Drupal\Component\Gettext\PoReaderInterface; use Drupal\Component\Gettext\PoWriterInterface; +use Drupal\locale\SourceString; +use Drupal\locale\TranslationString; /** * Gettext PO writer working with the locale module database. @@ -226,12 +228,11 @@ private function importString(PoItem $item) { // 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(); + $string = locale_storage()->findTranslation(array( + 'language' => $this->_langcode, + 'source' => $source, + 'context' => $context + )); if (!empty($translation)) { // Skip this string unless it passes a check for dangerous code. @@ -240,64 +241,43 @@ private function importString(PoItem $item) { $this->_report['skips']++; return 0; } - elseif (isset($string->lid)) { - if (!isset($string->customized)) { + elseif ($string) { + $string->setString($translation); + if ($string->isNew()) { // No translation in this language. - db_insert('locales_target') - ->fields(array( - 'lid' => $string->lid, - 'language' => $this->_langcode, - 'translation' => $translation, - 'customized' => $customized, - )) - ->execute(); - + $string->setValues(array( + 'language' => $this->_langcode, + 'customized' => $customized + )); + $string->save(); $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(); - + $string->customized = $customized; + $string->save(); $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(); + $string = locale_storage()->createString(array('source' => $source, 'context' => $context)) + ->save(); + $target = locale_storage()->createTranslation(array( + 'lid' => $string->getId(), + 'language' => $this->_langcode, + 'translation' => $translation, + 'customized' => $customized, + ))->save(); $this->_report['additions']++; - return $lid; + return $string->lid; } } - elseif (isset($string->lid) && isset($string->customized) && $overwrite_options[$string->customized ? 'customized' : 'not_customized']) { + elseif ($string && !$string->isNew() && $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(); - + $string->delete(); $this->_report['deletes']++; return $string->lid; } diff --git a/core/modules/locale/lib/Drupal/locale/SourceString.php b/core/modules/locale/lib/Drupal/locale/SourceString.php new file mode 100644 index 0000000000000000000000000000000000000000..40254f4bae440fb3d6864b4ea3248575c4d3a40c --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/SourceString.php @@ -0,0 +1,57 @@ +source); + } + + /** + * Implements Drupal\locale\StringInterface::isTranslation(). + */ + public function isTranslation() { + return FALSE; + } + + /** + * Implements Drupal\locale\LocaleString::getString(). + */ + public function getString() { + return isset($this->source) ? $this->source : ''; + } + + /** + * Implements Drupal\locale\LocaleString::setString(). + */ + public function setString($string) { + $this->source = $string; + return $this; + } + + /** + * Implements Drupal\locale\LocaleString::isNew(). + */ + public function isNew() { + return empty($this->lid); + } + +} diff --git a/core/modules/locale/lib/Drupal/locale/StringBase.php b/core/modules/locale/lib/Drupal/locale/StringBase.php new file mode 100644 index 0000000000000000000000000000000000000000..fdb35e84a13c1d771181230a941441b3184e61a3 --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/StringBase.php @@ -0,0 +1,179 @@ +setValues((array)$values); + } + + /** + * Implements Drupal\locale\StringInterface::getId(). + */ + public function getId() { + return isset($this->lid) ? $this->lid : NULL; + } + + /** + * Implements Drupal\locale\StringInterface::setId(). + */ + public function setId($lid) { + $this->lid = $lid; + return $this; + } + + /** + * Implements Drupal\locale\StringInterface::getVersion(). + */ + public function getVersion() { + return isset($this->version) ? $this->version : NULL; + } + + /** + * Implements Drupal\locale\StringInterface::setVersion(). + */ + public function setVersion($version) { + $this->version = $version; + return $this; + } + + /** + * Implements Drupal\locale\StringInterface::getPlurals(). + */ + public function getPlurals() { + return explode(LOCALE_PLURAL_DELIMITER, $this->getString()); + } + + /** + * Implements Drupal\locale\StringInterface::setPlurals(). + */ + public function setPlurals($plurals) { + $this->setString(implode(LOCALE_PLURAL_DELIMITER, $plurals)); + return $this; + } + + /** + * Implements Drupal\locale\StringInterface::setStorage(). + */ + public function setStorage($storage) { + $this->storage = $storage; + return $this; + } + + /** + * Implements Drupal\locale\StringInterface::setValues(). + */ + public function setValues(array $values, $override = TRUE) { + foreach ($values as $key => $value) { + if (property_exists($this, $key) && ($override || !isset($this->$key))) { + $this->$key = $value; + } + } + return $this; + } + + /** + * Implements Drupal\locale\StringInterface::getValues(). + */ + public function getValues(array $fields) { + $values = array(); + foreach ($fields as $field) { + if (isset($this->$field)) { + $values[$field] = $this->$field; + } + } + return $values; + } + + /** + * Implements Drupal\locale\LocaleString::save(). + */ + public function save() { + $this->getStorage()->save($this); + return $this; + } + + /** + * Implements Drupal\locale\LocaleString::delete(). + */ + public function delete() { + if (!$this->isNew()) { + $this->getStorage()->delete($this); + } + return $this; + } + + /** + * Gets the storage to which this string is bound. + * + * @throws Drupal\locale\StringStorageException + * In case the string doesn't have an storage set, an exception is thrown. + */ + protected function getStorage() { + if (isset($this->storage)) { + return $this->storage; + } + else { + throw new StringStorageException(format_string('The string cannot be saved nor deleted because its not bound to an storage: @string', array( + '@string' => $string->getString() + ))); + } + } + +} diff --git a/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php b/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php new file mode 100644 index 0000000000000000000000000000000000000000..01927c7f400828db68f830818dd3546cff325db3 --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php @@ -0,0 +1,514 @@ +connection = $connection; + $this->options = $options; + } + + /** + * Implements Drupal\locale\StringStorageInterface::getStrings(). + */ + public function getStrings(array $conditions = array(), array $options = array()) { + $options += array('source' => TRUE); + return $this->dbStringLoad($conditions, $options, 'Drupal\locale\SourceString'); + } + + /** + * Implements Drupal\locale\StringStorageInterface::getTranslations(). + */ + public function getTranslations(array $conditions = array(), array $options = array()) { + $options += array('source' => TRUE, 'translation' => TRUE); + return $this->dbStringLoad($conditions, $options, 'Drupal\locale\TranslationString'); + } + + /** + * Implements Drupal\locale\StringStorageInterface::findString(). + */ + public function findString(array $conditions, array $options = array()) { + $options += array('source' => TRUE); + $string = $this->dbStringSelect($conditions, $options) + ->execute() + ->fetchObject('Drupal\locale\SourceString'); + if ($string) { + $string->setStorage($this); + } + return $string; + } + + /** + * Implements Drupal\locale\StringStorageInterface::findTranslation(). + */ + public function findTranslation(array $conditions, array $options = array()) { + $options += array('source' => TRUE, 'translation' => TRUE); + $string = $this->dbStringSelect($conditions, $options) + ->execute() + ->fetchObject('Drupal\locale\TranslationString'); + if ($string) { + $string->setStorage($this); + } + return $string; + } + + /** + * Implements Drupal\locale\StringStorageInterface::countStrings(). + */ + public function countStrings() { + return $this->dbExecute("SELECT COUNT(*) FROM {locales_source}")->fetchField(); + } + + /** + * Implements Drupal\locale\StringStorageInterface::countTranslations(). + */ + public function countTranslations() { + return $this->dbExecute("SELECT t.language, COUNT(*) AS translated FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid GROUP BY t.language")->fetchAllKeyed(); + } + + /** + * Implements Drupal\locale\StringStorageInterface::checkVersion(). + */ + public function checkVersion($string, $version) { + if ($string->getId() && $string->getVersion() != $version) { + $string->setVersion($version); + $this->connection->update('locales_source', $this->options) + ->condition('lid', $string->getId()) + ->fields(array('version' => $version)) + ->execute(); + } + } + + /** + * Implements Drupal\locale\StringStorageInterface::save(). + */ + public function save($string) { + if ($string->isNew()) { + $result = $this->dbStringInsert($string); + if ($string->isSource() && $result) { + // Only for source strings, we set the locale identifier. + $string->setId($result); + } + $string->setStorage($this); + } + else { + $this->dbStringUpdate($string); + } + return $this; + } + + /** + * Implements Drupal\locale\StringStorageInterface::delete(). + */ + public function delete($string) { + if ($keys = $this->dbStringKeys($string)) { + $this->dbDelete('locales_target', $keys)->execute(); + if ($string->isSource()) { + $this->dbDelete('locales_source', $keys)->execute(); + $string->setId(NULL); + } + } + else { + throw new StringStorageException(format_string('The string cannot be deleted because it lacks some key fields: @string', array( + '@string' => $string->getString() + ))); + } + return $this; + } + + /** + * Implements Drupal\locale\StringStorageInterface::deleteLanguage(). + */ + public function deleteStrings($conditions) { + $lids = $this->dbStringSelect($conditions, array('fields' => array('lid')))->execute()->fetchCol(); + if ($lids) { + $this->dbDelete('locales_target', array('lid' => $lids))->execute(); + $this->dbDelete('locales_source', array('lid' => $lids))->execute(); + } + } + + /** + * Implements Drupal\locale\StringStorageInterface::deleteLanguage(). + */ + public function deleteTranslations($conditions) { + $this->dbDelete('locales_target', $conditions)->execute(); + } + + /** + * Implements Drupal\locale\StringStorageInterface::createString(). + */ + public function createString($values = array()) { + return new SourceString($values + array('storage' => $this)); + } + + /** + * Implements Drupal\locale\StringStorageInterface::createTranslation(). + */ + public function createTranslation($values = array()) { + return new TranslationString($values + array( + 'storage' => $this, + 'is_new' => TRUE + )); + } + + /** + * Gets table alias for field. + * + * @param string $field + * Field name to find the table alias for. + * + * @return string + * Either 's' or 't' depending on whether the field belongs to source or + * target table. + */ + protected function dbFieldTable($field) { + return in_array($field, array('language', 'translation', 'customized')) ? 't' : 's'; + } + + /** + * Gets table name for storing string object. + * + * @param Drupal\locale\StringInterface $string + * The string object. + * + * @return string + * The table name. + */ + protected function dbStringTable($string) { + if ($string->isSource()) { + return 'locales_source'; + } + elseif ($string->isTranslation()) { + return 'locales_target'; + } + } + + /** + * Gets keys values that are in a database table. + * + * @param Drupal\locale\StringInterface $string + * The string object. + * @param string $table + * (optional) The table name. + * + * @return array + * Array with key fields if the string has all keys, or empty array if not. + */ + protected function dbStringKeys($string, $table = NULL) { + $table = $table ? $table : $this->dbStringTable($string); + if ($table && $schema = drupal_get_schema($table)) { + $keys = $schema['primary key']; + $values = $string->getValues($keys); + if (count($values) == count($keys)) { + return $values; + } + } + return NULL; + } + + /** + * Gets field values from a string object that are in the database table. + * + * @param Drupal\locale\StringInterface $string + * The string object. + * @param string $table + * (optional) The table name. + * + * @return array + * Array with field values indexed by field name. + */ + protected function dbStringValues($string, $table = NULL) { + $table = $table ? $table : $this->dbStringTable($string); + if ($table && $schema = drupal_get_schema($table)) { + $fields = array_keys($schema['fields']); + return $string->getValues($fields); + } + else { + return array(); + } + } + + /** + * Sets default values from storage. + * + * @param Drupal\locale\StringInterface $string + * The string object. + * @param string $table + * (optional) The table name. + */ + protected function dbStringDefaults($string, $table = NULL) { + $table = $table ? $table : $this->dbStringTable($string); + if ($table && $schema = drupal_get_schema($table)) { + $values = array(); + foreach ($schema['fields'] as $name => $info) { + if (isset($info['default'])) { + $values[$name] = $info['default']; + } + } + $string->setValues($values, FALSE); + } + } + + /** + * Loads multiple string objects. + * + * @param array $conditions + * Any of the conditions used by dbStringSelect(). + * @param array $options + * Any of the options used by dbStringSelect(). + * @param string $class + * Class name to use for fetching returned objects. + * + * @return array + * Array of objects of the class requested. + */ + protected function dbStringLoad(array $conditions, array $options, $class) { + $result = $this->dbStringSelect($conditions, $options)->execute(); + $result->setFetchMode(PDO::FETCH_CLASS, $class); + $strings = $result->fetchAll(); + foreach ($strings as $string) { + $string->setStorage($this); + } + return $strings; + } + + /** + * Builds a SELECT query with multiple conditions and fields. + * + * The query uses both 'locales_source' and 'locales_target' tables. + * Note that by default, as we are selecting both translated and untranslated + * strings target field's conditions will be modified to match NULL rows too. + * + * @param array $conditions + * An associative array with field => value conditions that may include + * NULL values. If a language condition is included it will be used for + * joining the 'locales_target' table. + * @param array $options + * An associative array of additional options. It may contain any of the + * options used by Drupal\locale\StringStorageInterface::getStrings() and + * these additional ones: + * - 'source', TRUE for selecting all source fields. + * - 'translation', TRUE for selecting all translation fields. + * - 'fields', Optional array of exact fields to get. Overrides the + * previous 'source' and 'translation' options. Defaults to none. + * @return SelectQuery + * Query object with all the tables, fields and conditions. + */ + protected function dbStringSelect(array $conditions, array $options = array()) { + // Check the fields we are going to select and to which table they belong. + $fields = array(); + if (isset($options['fields'])) { + foreach ($options['fields'] as $field) { + $fields[$this->dbFieldTable($field)][] = $field; + } + } + else { + if (!empty($options['source'])) { + $fields['s'] = array(); + } + if (!empty($options['translation'])) { + // If we've got translation fields, we leave out the lid field to avoid clashes. + $fields['t'] = isset($fields['s']) ? array('language', 'translation', 'customized') : array(); + } + } + + // Start building the query with source table and fields and check whether + // we need to join the target table too. + $query = $this->connection->select('locales_source', 's'); + + // Figure out how to join and translate some options into conditions. + if (isset($conditions['translated'])) { + // This is a meta-condition we need to translate into simple ones. + if ($conditions['translated']) { + // Select only translated strings. + $join = 'innerJoin'; + } + else { + // Select only untranslated strings. + $join = 'leftJoin'; + $conditions['translation'] = NULL; + } + unset($conditions['translated']); + } + else { + $join = isset($fields['t']) ? 'leftJoin' : FALSE; + } + + if ($join) { + if (isset($conditions['language'])) { + // If we've got a language condition, we use it for the join. + $query->$join('locales_target', 't', "t.lid = s.lid AND t.language = :langcode", array( + ':langcode' => $conditions['language'] + )); + unset($conditions['language']); + } + else { + // Since we don't have a language, join with locale id only. + $query->$join('locales_target', 't', "t.lid = s.lid"); + } + } + // Add fields for both tables, it may be a query without fields. + foreach ($fields as $table_alias => $table_fields) { + $query->fields($table_alias, $table_fields); + } + // Add conditions for both tables. + foreach ($conditions as $field => $value) { + $table_alias = $this->dbFieldTable($field); + $field_alias = $table_alias . '.' . $field; + if (is_null($value)) { + $query->isNull($field_alias); + } + elseif ($table_alias == 't' && $join === 'leftJoin') { + // Conditions for target fields when doing an outer join only make + // sense if we add also OR field IS NULL. + $query->condition(db_or() + ->condition($field_alias, $value) + ->isNull($field_alias) + ); + } + else { + $query->condition($field_alias, $value); + } + } + + // Process other options, string filter, query limit, etc... + if (!empty($options['filters'])) { + if (count($options['filters']) > 1) { + $filter = db_or(); + $query->condition($filter); + } + else { + // If we have a single filter, just add it to the query. + $filter = $query; + } + foreach ($options['filters'] as $field => $string) { + $filter->condition($this->dbFieldTable($field) . '.' . $field, '%' . db_like($string) . '%', 'LIKE'); + } + } + + if (!empty($options['pager limit'])) { + $query = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender')->limit($options['pager limit']); + } + + return $query; + } + + /** + * Createds a database record for a string object. + * + * @param Drupal\locale\StringInterface $string + * The string object. + * + * @return bool|int + * If the operation failed, returns FALSE. + * If it succeeded returns the last insert ID of the query, if one exists. + * + * @throws Drupal\locale\StringStorageException + * If the string is not suitable for this storage, an exception ithrown. + */ + protected function dbStringInsert($string) { + if (($table = $this->dbStringTable($string)) && ($fields = $this->dbStringValues($string, $table))) { + $this->dbStringDefaults($string, $table); + return $this->connection->insert($table, $this->options) + ->fields($fields) + ->execute(); + } + else { + throw new StringStorageException(format_string('The string cannot be saved: @string', array( + '@string' => $string->getString() + ))); + } + } + + /** + * Updates string object in the database. + * + * @param Drupal\locale\StringInterface $string + * The string object. + * + * @return bool|int + * If the record update failed, returns FALSE. If it succeeded, returns + * SAVED_NEW or SAVED_UPDATED. + * + * @throws Drupal\locale\StringStorageException + * If the string is not suitable for this storage, an exception is thrown. + */ + protected function dbStringUpdate($string) { + if (($table = $this->dbStringTable($string)) && ($keys = $this->dbStringKeys($string, $table)) && + ($fields = $this->dbStringValues($string, $table)) && ($values = array_diff_key($fields, $keys))) + { + return $this->connection->merge($table, $this->options) + ->key($keys) + ->fields($values) + ->execute(); + } + else { + throw new StringStorageException(format_string('The string cannot be updated: @string', array( + '@string' => $string->getString() + ))); + } + } + + /** + * Creates delete query. + * + * @param string $table + * The table name. + * @param array $keys + * Array with object keys indexed by field name. + * + * @return DeleteQuery + * Returns a new DeleteQuery object for the active database. + */ + protected function dbDelete($table, $keys) { + $query = $this->connection->delete($table, $this->options); + foreach ($keys as $field => $value) { + $query->condition($field, $value); + } + return $query; + } + + /** + * Executes an arbitrary SELECT query string. + */ + protected function dbExecute($query, array $args = array()) { + return $this->connection->query($query, $args, $this->options); + } +} diff --git a/core/modules/locale/lib/Drupal/locale/StringInterface.php b/core/modules/locale/lib/Drupal/locale/StringInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..c39ac7035de4399e60b6af20689cbd2360824d3d --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/StringInterface.php @@ -0,0 +1,174 @@ + 'String storage and objects', + 'description' => 'Tests the locale string storage, string objects and data API.', + 'group' => 'Locale', + ); + } + + function setUp() { + parent::setUp(); + // Add a default locale storage for all these tests. + $this->storage = locale_storage(); + // Create two languages: Spanish and German. + foreach (array('es', 'de') as $langcode) { + $language = new Language(array('langcode' => $langcode)); + $languages[$langcode] = language_save($language); + } + } + + /** + * Test CRUD API. + */ + function testStringCRUDAPI() { + // Create source string. + $source = $this->buildSourceString(); + $source->save(); + $this->assertTrue($source->lid, format_string('Successfully created string %string', array('%string' => $source->source))); + + // Load strings by lid and source. + $string1 = $this->storage->findString(array('lid' => $source->lid)); + $this->assertEqual($source, $string1, 'Successfully retrieved string by identifier.'); + $string2 = $this->storage->findString(array('source' => $source->source, 'context' => $source->context)); + $this->assertEqual($source, $string2, 'Successfully retrieved string by source and context.'); + $string3 = $this->storage->findString(array('source' => $source->source, 'context' => '')); + $this->assertFalse($string3, 'Cannot retrieve string with wrong context.'); + + // Check version handling and updating. + $this->assertEqual($source->version, 'none', 'String originally created without version.'); + $this->storage->checkVersion($source, VERSION); + $string = $this->storage->findString(array('lid' => $source->lid)); + $this->assertEqual($source->version, VERSION, 'Checked and updated string version to Drupal version.'); + + // Create translation and find it by lid and source. + $langcode = 'es'; + $translation = $this->createTranslation($source, $langcode); + $this->assertEqual($translation->customized, LOCALE_NOT_CUSTOMIZED, 'Translation created as not customized by default.'); + $string1 = $this->storage->findTranslation(array('language' => $langcode, 'lid' => $source->lid)); + $this->assertEqual($string1->translation, $translation->translation, 'Successfully loaded translation by string identifier.'); + $string2 = $this->storage->findTranslation(array('language' => $langcode, 'source' => $source->source, 'context' => $source->context)); + $this->assertEqual($string2->translation, $translation->translation, 'Successfully loaded translation by source and context.'); + $translation + ->setCustomized() + ->save(); + $translation = $this->storage->findTranslation(array('language' => $langcode, 'lid' => $source->lid)); + $this->assertEqual($translation->customized, LOCALE_CUSTOMIZED, 'Translation successfully marked as customized.'); + + // Delete translation. + $translation->delete(); + $deleted = $this->storage->findTranslation(array('language' => $langcode, 'lid' => $source->lid)); + $this->assertFalse(isset($deleted->translation), 'Successfully deleted translation string.'); + + // Create some translations and then delete string and all of its translations. + $lid = $source->lid; + $translations = $this->createAllTranslations($source); + $search = $this->storage->getTranslations(array('lid' => $source->lid)); + $this->assertEqual(count($search), 3 , 'Created and retrieved all translations for our source string.'); + + $source->delete(); + $string = $this->storage->findString(array('lid' => $lid)); + $this->assertFalse($string, 'Successfully deleted source string.'); + $deleted = $search = $this->storage->getTranslations(array('lid' => $lid)); + $this->assertFalse($deleted, 'Successfully deleted all translation strings.'); + } + + /** + * Test Search API loading multiple objects. + */ + function testStringSearchAPI() { + $language_count = 3; + // Strings 1 and 2 will have some common prefix. + // Source 1 will have all translations, not customized. + // Source 2 will have all translations, customized. + // Source 3 will have no translations. + $prefix = $this->randomName(100); + $source1 = $this->buildSourceString(array('source' => $prefix . $this->randomName(100)))->save(); + $source2 = $this->buildSourceString(array('source' => $prefix . $this->randomName(100)))->save(); + $source3 = $this->buildSourceString()->save(); + // Load all source strings. + $strings = $this->storage->getStrings(array()); + $this->assertEqual(count($strings), 3 , 'Found 3 source strings in the database.'); + // Load all source strings matching a given string + $filter_options['filters'] = array('source' => $prefix); + $strings = $this->storage->getStrings(array(), $filter_options); + $this->assertEqual(count($strings), 2 , 'Found 2 strings using some string filter.'); + + // Not customized translations. + $translate1 = $this->createAllTranslations($source1); + // Customized translations. + $translate2 = $this->createAllTranslations($source2, array('customized' => LOCALE_CUSTOMIZED)); + // Try quick search function with different field combinations. + $langcode = 'es'; + $found = locale_storage()->findTranslation(array('language' => $langcode, 'source' => $source1->source, 'context' => $source1->context)); + $this->assertTrue($found && isset($found->language) && isset($found->translation) && !$found->isNew(), 'Translation found searching by source and context.'); + $this->assertEqual($found->translation, $translate1[$langcode]->translation, 'Found the right translation.'); + // Now try a translation not found. + $found = locale_storage()->findTranslation(array('language' => $langcode, 'source' => $source3->source, 'context' => $source3->context)); + $this->assertTrue($found && $found->lid == $source3->lid && !isset($found->translation) && $found->isNew(), 'Translation not found but source string found.'); + + // Load all translations. For next queries we'll be loading only translated strings. $only_translated = array('untranslated' => FALSE); + $translations = $this->storage->getTranslations(array('translated' => TRUE)); + $this->assertEqual(count($translations), 2 * $language_count , 'Created and retrieved all translations for source strings.'); + + // Load all customized translations. + $translations = $this->storage->getTranslations(array('customized' => LOCALE_CUSTOMIZED, 'translated' => TRUE)); + $this->assertEqual(count($translations), $language_count , 'Retrieved all customized translations for source strings.'); + + // Load all Spanish customized translations + $translations = $this->storage->getTranslations(array('language' => 'es', 'customized' => LOCALE_CUSTOMIZED, 'translated' => TRUE)); + $this->assertEqual(count($translations), 1 , 'Found only Spanish and customized translations.'); + + // Load all source strings without translation (1). + $translations = $this->storage->getStrings(array('translated' => FALSE)); + $this->assertEqual(count($translations), 1 , 'Found 1 source string without translations.'); + + // Load Spanish translations using string filter. + $filter_options['filters'] = array('source' => $prefix); + $translations = $this->storage->getTranslations(array('language' => 'es'), $filter_options); + $this->assertEqual(count($strings), 2 , 'Found 2 translations using some string filter.'); + + } + + /** + * Creates random source string object. + */ + function buildSourceString($values = array()) { + return $this->storage->createString($values += array( + 'source' => $this->randomName(100), + 'context' => $this->randomName(20), + )); + } + + /** + * Creates translations for source string and all languages. + */ + function createAllTranslations($source, $values = array()) { + $list = array(); + foreach (language_list() as $language) { + $list[$language->langcode] = $this->createTranslation($source, $language->langcode, $values); + } + return $list; + } + + /** + * Creates single translation for source string. + */ + function createTranslation($source, $langcode, $values = array()) { + return $this->storage->createTranslation($values += array( + 'lid' => $source->lid, + 'language' => $langcode, + 'translation' => $this->randomName(100), + ))->save(); + } +} diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleTranslationTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleTranslationTest.php index 1c7265cbfed4792b02faeac11e8a572b3ed92bda..7ac92adfaf3ec434d9438fb0a86e4e089b67d415 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleTranslationTest.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleTranslationTest.php @@ -2,7 +2,7 @@ /** * @file - * Definition of Drupal\locale\Tests\LocaleTranslationTest. + * Definition of Drupal\locale\Tests\TranslationStringTest. */ namespace Drupal\locale\Tests; @@ -12,7 +12,7 @@ /** * Functional test for string translation and validation. */ -class LocaleTranslationTest extends WebTestBase { +class TranslationStringTest extends WebTestBase { /** * Modules to enable. diff --git a/core/modules/locale/lib/Drupal/locale/TranslationString.php b/core/modules/locale/lib/Drupal/locale/TranslationString.php new file mode 100644 index 0000000000000000000000000000000000000000..498dd663e2978492fd4eb71545839e2fed6da8d6 --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/TranslationString.php @@ -0,0 +1,128 @@ +is_new)) { + // We mark the string as not new if it is a complete translation. + // This will work when loading from database, otherwise the storage + // controller that creates the string object must handle it. + $this->is_new = !$this->isTranslation(); + } + } + + /** + * Sets the string as customized / not customized. + * + * @param bool $customized + * (optional) Whether the string is customized or not. Defaults to TRUE. + * + * @return Drupal\locale\TranslationString + * The called object. + */ + public function setCustomized($customized = TRUE) { + $this->customized = $customized ? LOCALE_CUSTOMIZED : LOCALE_NOT_CUSTOMIZED; + return $this; + } + + /** + * Implements Drupal\locale\StringInterface::isSource(). + */ + public function isSource() { + return FALSE; + } + + /** + * Implements Drupal\locale\StringInterface::isTranslation(). + */ + public function isTranslation() { + return !empty($this->lid) && !empty($this->language) && isset($this->translation); + } + + /** + * Implements Drupal\locale\StringInterface::getString(). + */ + public function getString() { + return isset($this->translation) ? $this->translation : ''; + } + + /** + * Implements Drupal\locale\StringInterface::setString(). + */ + public function setString($string) { + $this->translation = $string; + return $this; + } + + /** + * Implements Drupal\locale\StringInterface::isNew(). + */ + public function isNew() { + return $this->is_new; + } + + /** + * Implements Drupal\locale\StringInterface::save(). + */ + public function save() { + parent::save(); + $this->is_new = FALSE; + return $this; + } + + /** + * Implements Drupal\locale\StringInterface::delete(). + */ + public function delete() { + parent::delete(); + $this->is_new = TRUE; + return $this; + } + +} diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index e486441f95e40ee03c0813c771bd8c9d45c6940a..b2bfa9534984874643c03499b6ceb60dfe815335 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -12,7 +12,10 @@ use Drupal\locale\LocaleLookup; use Drupal\locale\LocaleConfigSubscriber; +use Drupal\locale\SourceString; +use Drupal\locale\StringDatabaseStorage; use Drupal\locale\TranslationsStream; +use Drupal\Core\Database\Database; /** * Regular expression pattern used to localize JavaScript strings. @@ -209,9 +212,7 @@ function locale_language_update($language) { */ function locale_language_delete($language) { // Remove translations. - db_delete('locales_target') - ->condition('language', $language->langcode) - ->execute(); + locale_storage()->deleteTranslations(array('language' => $language->langcode)); // Remove interface translation files. module_load_include('inc', 'locale', 'locale.bulk'); @@ -275,7 +276,7 @@ function locale($string = NULL, $context = NULL, $langcode = NULL) { // Strings are cached by langcode, context and roles, using instances of the // LocaleLookup class to handle string lookup and caching. if (!isset($locale_t[$langcode][$context]) && isset($language_interface)) { - $locale_t[$langcode][$context] = new LocaleLookup($langcode, $context); + $locale_t[$langcode][$context] = new LocaleLookup($langcode, $context, locale_storage()); } return ($locale_t[$langcode][$context][$string] === TRUE ? $string : $locale_t[$langcode][$context][$string]); } @@ -287,6 +288,20 @@ function locale_reset() { drupal_static_reset('locale'); } +/** + * Gets the locale storage controller class . + * + * @return Drupal\locale\StringStorageInterface + */ +function locale_storage() { + $storage = &drupal_static(__FUNCTION__); + if (!isset($storage)) { + $options = array('target' => 'default'); + $storage = new StringDatabaseStorage(Database::getConnection($options['target']), $options); + } + return $storage; +} + /** * Returns plural form index for a specific number. * @@ -513,16 +528,16 @@ function locale_library_info_alter(&$libraries, $module) { function locale_form_language_admin_overview_form_alter(&$form, &$form_state) { $languages = $form['languages']['#languages']; - $total_strings = db_query("SELECT COUNT(*) FROM {locales_source}")->fetchField(); + $total_strings = locale_storage()->countStrings(); $stats = array_fill_keys(array_keys($languages), array()); // If we have source strings, count translations and calculate progress. if (!empty($total_strings)) { - $translations = db_query("SELECT COUNT(*) AS translated, t.language FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid GROUP BY language"); - foreach ($translations as $data) { - $stats[$data->language]['translated'] = $data->translated; - if ($data->translated > 0) { - $stats[$data->language]['ratio'] = round($data->translated / $total_strings * 100, 2); + $translations = locale_storage()->countTranslations(); + foreach ($translations as $langcode => $translated) { + $stats[$langcode]['translated'] = $translated; + if ($translated > 0) { + $stats[$langcode]['ratio'] = round($translated / $total_strings * 100, 2); } } } @@ -759,7 +774,7 @@ function _locale_parse_js_file($filepath) { $string = implode('', preg_split('~(? $string, ':context' => $context))->fetchObject(); + $source = locale_storage()->findString(array('source' => $string, 'context' => $context)); if ($source) { // We already have this source string and now have to add the location // to the location column, if this file is not yet present in there. @@ -770,23 +785,17 @@ function _locale_parse_js_file($filepath) { $locations = implode('; ', $locations); // Save the new locations string to the database. - db_update('locales_source') - ->fields(array( - 'location' => $locations, - )) - ->condition('lid', $source->lid) - ->execute(); + $source->setValues(array('location' => $locations)) + ->save(); } } else { // We don't have the source string yet, thus we insert it into the database. - db_insert('locales_source') - ->fields(array( - 'location' => $filepath, - 'source' => $string, - 'context' => $context, - )) - ->execute(); + locale_storage()->createString(array( + 'location' => $filepath, + 'source' => $string, + 'context' => $context, + ))->save(); } } } @@ -845,10 +854,11 @@ function _locale_rebuild_js($langcode = NULL) { // Construct the array for JavaScript translations. // Only add strings with a translation to the translations array. - $result = db_query("SELECT s.lid, s.source, s.context, t.translation FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.location LIKE '%.js%'", array(':language' => $language->langcode)); - + $options['filters']['location'] = '.js'; + $options['fields'] = array('lid', 'context', 'source', 'translation'); + $conditions['language'] = $language->langcode; $translations = array(); - foreach ($result as $data) { + foreach (locale_storage()->getTranslations($conditions, $options) as $data) { $translations[$data->context][$data->source] = $data->translation; } diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc index 331cea4d66ff5eca4c16cd30e79d573f236dd77a..a6fb751e43e3ba3540b92a5718964befe3bb9f8d 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -5,6 +5,8 @@ * Interface translation summary, editing and deletion user interfaces. */ +use Drupal\locale\SourceString; +use Drupal\locale\TranslationString; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** @@ -20,42 +22,42 @@ function locale_translate_page() { } /** - * Build a string search query. + * Builds a string search query and returns an array of string objects. + * + * @return array + * Array of Drupal\locale\TranslationString objects. */ -function locale_translate_query() { +function locale_translate_filter_load_strings() { $filter_values = locale_translate_filter_values(); - $sql_query = db_select('locales_source', 's'); // Language is sanitized to be one of the possible options in // locale_translate_filter_values(). - $sql_query->leftJoin('locales_target', 't', "t.lid = s.lid AND t.language = :langcode", array(':langcode' => $filter_values['langcode'])); - $sql_query->fields('s', array('source', 'location', 'context', 'lid')); - $sql_query->fields('t', array('translation', 'language', 'customized')); - - if (!empty($filter_values['string'])) { - $sql_query->condition(db_or() - ->condition('s.source', '%' . db_like($filter_values['string']) . '%', 'LIKE') - ->condition('t.translation', '%' . db_like($filter_values['string']) . '%', 'LIKE') - ); - } + $conditions = array('language' => $filter_values['langcode']); + $options = array('pager limit' => 30, 'translated' => TRUE, 'untranslated' => TRUE); - // Add translation status conditions. + // Add translation status conditions and options. switch ($filter_values['translation']) { case 'translated': - $sql_query->isNotNull('t.translation'); + $conditions['translated'] = TRUE; if ($filter_values['customized'] != 'all') { - $sql_query->condition('t.customized', $filter_values['customized']); + $conditions['customized'] = $filter_values['customized']; } break; case 'untranslated': - $sql_query->isNull('t.translation'); + $conditions['translated'] = FALSE; break; } - $sql_query = $sql_query->extend('Drupal\Core\Database\Query\PagerSelectExtender')->limit(30); - return $sql_query->execute(); + if (!empty($filter_values['string'])) { + $options['filters']['source'] = $filter_values['string']; + if ($options['translated']) { + $options['filters']['translation'] = $filter_values['string']; + } + } + + return locale_storage()->getTranslations($conditions, $options); } /** @@ -276,14 +278,16 @@ function locale_translate_edit_form($form, &$form_state) { ); if (isset($langcode)) { - $strings = locale_translate_query(); + $strings = locale_translate_filter_load_strings(); $plural_formulas = variable_get('locale_translation_plurals', array()); foreach ($strings as $string) { + // Cast into source string, will do for our purposes. + $source = new SourceString($string); // Split source to work with plural values. - $source_array = explode(LOCALE_PLURAL_DELIMITER, $string->source); - $translation_array = explode(LOCALE_PLURAL_DELIMITER, $string->translation); + $source_array = $source->getPlurals(); + $translation_array = $string->getPlurals(); if (count($source_array) == 1) { // Add original string value and mark as non-plural. $form['strings'][$string->lid]['plural'] = array( @@ -400,9 +404,8 @@ function locale_translate_edit_form_validate($form, &$form_state) { function locale_translate_edit_form_submit($form, &$form_state) { $langcode = $form_state['values']['langcode']; foreach ($form_state['values']['strings'] as $lid => $translations) { - // Serialize plural variants in one string by LOCALE_PLURAL_DELIMITER. - $translation_new = implode(LOCALE_PLURAL_DELIMITER, $translations['translations']); - $translation_old = db_query("SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchField(); + // Get target string, that may be NULL if there's no translation. + $target = locale_storage()->findTranslation(array('language' => $langcode, 'lid' => $lid)); // No translation when all strings are empty. $has_translation = FALSE; foreach ($translations['translations'] as $string) { @@ -413,35 +416,15 @@ function locale_translate_edit_form_submit($form, &$form_state) { } if ($has_translation) { // Only update or insert if we have a value to use. - if (!empty($translation_old) && $translation_old != $translation_new) { - db_update('locales_target') - ->fields(array( - 'translation' => $translation_new, - 'customized' => LOCALE_CUSTOMIZED, - )) - ->condition('lid', $lid) - ->condition('language', $langcode) - ->execute(); - } - if (empty($translation_old)) { - db_insert('locales_target') - ->fields(array( - 'lid' => $lid, - 'translation' => $translation_new, - 'language' => $langcode, - 'customized' => LOCALE_CUSTOMIZED, - )) - ->execute(); - } + $target = $target && !$target->isNew() ? $target : locale_storage()->createTranslation(array('lid' => $lid, 'language' => $langcode)); + $target->setPlurals($translations['translations']) + ->setCustomized() + ->save(); } - elseif (!empty($translation_old)) { + elseif ($target) { // Empty translation entered: remove existing entry from database. - db_delete('locales_target') - ->condition('lid', $lid) - ->condition('language', $langcode) - ->execute(); + $target->delete(); } - } drupal_set_message(t('The strings have been saved.'));