summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwebchick2012-10-08 18:10:13 (GMT)
committerwebchick2012-10-08 18:10:13 (GMT)
commit662c42a418317de0c9e228289418f5809ed09b4e (patch)
tree8e3ba769cd24b7a334fb3f01a1829c1f81898c10
parente7787c9cca63a416701b20b90793b4bc5f0def24 (diff)
Issue #1785086 by Jose Reyero, Gábor Hojtsy, catch, loganfsmyth, nedjo, stella: Added Introduce a generic API for interface translation strings.
-rw-r--r--core/modules/locale/lib/Drupal/locale/LocaleLookup.php54
-rw-r--r--core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php48
-rw-r--r--core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php76
-rw-r--r--core/modules/locale/lib/Drupal/locale/SourceString.php57
-rw-r--r--core/modules/locale/lib/Drupal/locale/StringBase.php179
-rw-r--r--core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php514
-rw-r--r--core/modules/locale/lib/Drupal/locale/StringInterface.php174
-rw-r--r--core/modules/locale/lib/Drupal/locale/StringStorageException.php15
-rw-r--r--core/modules/locale/lib/Drupal/locale/StringStorageInterface.php179
-rw-r--r--core/modules/locale/lib/Drupal/locale/Tests/LocaleStringTest.php199
-rw-r--r--core/modules/locale/lib/Drupal/locale/Tests/LocaleTranslationTest.php4
-rw-r--r--core/modules/locale/lib/Drupal/locale/TranslationString.php128
-rw-r--r--core/modules/locale/locale.module64
-rw-r--r--core/modules/locale/locale.pages.inc85
14 files changed, 1598 insertions, 178 deletions
diff --git a/core/modules/locale/lib/Drupal/locale/LocaleLookup.php b/core/modules/locale/lib/Drupal/locale/LocaleLookup.php
index 77aa4dd..30d12ec 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.
@@ -27,11 +29,19 @@ 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 @@ class LocaleLookup extends CacheArray {
* 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 c0dfc2f..74cf40d 100644
--- a/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php
+++ b/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php
@@ -10,6 +10,8 @@ namespace Drupal\locale;
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 @@ class PoDatabaseReader implements PoReaderInterface {
/**
* 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 33f05d9..7afa1db 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\PoHeader;
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 @@ class PoDatabaseWriter implements PoWriterInterface {
// 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 @@ class PoDatabaseWriter implements PoWriterInterface {
$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 0000000..40254f4
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/SourceString.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\SourceString.
+ */
+
+namespace Drupal\locale;
+
+use Drupal\locale\LocaleString;
+use PDO;
+
+/**
+ * Defines the locale source string object.
+ *
+ * This class represents a module-defined string value that is to be translated.
+ * This string must at least contain a 'source' field, which is the raw source
+ * value, and is assumed to be in English language.
+ */
+class SourceString extends StringBase {
+ /**
+ * Implements Drupal\locale\StringInterface::isSource().
+ */
+ public function isSource() {
+ return isset($this->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 0000000..fdb35e8
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/StringBase.php
@@ -0,0 +1,179 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\StringBase.
+ */
+
+namespace Drupal\locale;
+
+/**
+ * Defines the locale string base class.
+ */
+abstract class StringBase implements StringInterface {
+ /**
+ * The string identifier.
+ *
+ * @var integer
+ */
+ public $lid;
+
+ /**
+ * The string location.
+ *
+ * @var string
+ */
+ public $location;
+
+ /**
+ * The source string.
+ *
+ * @var string
+ */
+ public $source;
+
+ /**
+ * The string context.
+ *
+ * @var string
+ */
+ public $context;
+
+ /**
+ * The string version.
+ *
+ * @var string
+ */
+ public $version;
+
+ /**
+ * The locale storage this string comes from or is to be saved to.
+ *
+ * @var Drupal\locale\StringStorageInterface
+ */
+ protected $storage;
+
+ /**
+ * Constructs a new locale string object.
+ *
+ * @param object|array $values
+ * Object or array with initial values.
+ */
+ public function __construct($values = array()) {
+ $this->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 0000000..01927c7
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php
@@ -0,0 +1,514 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\StringDatabaseStorage.
+ */
+
+namespace Drupal\locale;
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\Connection;
+use PDO;
+
+/**
+ * Defines the locale string class.
+ *
+ * This is the base class for SourceString and TranslationString.
+ */
+class StringDatabaseStorage implements StringStorageInterface {
+
+ /**
+ * The database connection.
+ *
+ * @var Drupal\Core\Database\Connection
+ */
+ protected $connection;
+
+ /**
+ * Additional database connection options to use in queries.
+ *
+ * @var array
+ */
+ protected $options = array();
+
+ /**
+ * Constructs a new StringStorage controller.
+ *
+ * @param Drupal\Core\Database\Connection $connection
+ * A Database connection to use for reading and writing configuration data.
+ * @param array $options
+ * (optional) Any additional database connection options to use in queries.
+ */
+ public function __construct(Connection $connection, array $options = array()) {
+ $this->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 0000000..c39ac70
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/StringInterface.php
@@ -0,0 +1,174 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\StringInterface.
+ */
+
+namespace Drupal\locale;
+
+/**
+ * Defines the locale string interface.
+ */
+interface StringInterface {
+
+ /**
+ * Gets the string unique identifier.
+ *
+ * @return int
+ * The string identifier.
+ */
+ public function getId();
+
+ /**
+ * Sets the string unique identifier.
+ *
+ * @param int $id
+ * The string identifier.
+ *
+ * @return Drupal\locale\LocaleString
+ * The called object.
+ */
+ public function setId($id);
+
+ /**
+ * Gets the string version.
+ *
+ * @return string
+ * Version identifier.
+ */
+ public function getVersion();
+
+ /**
+ * Sets the string version.
+ *
+ * @param string $version
+ * Version identifier.
+ *
+ * @return Drupal\locale\LocaleString
+ * The called object.
+ */
+ public function setVersion($version);
+
+ /**
+ * Gets plain string contained in this object.
+ *
+ * @return string
+ * The string contained in this object.
+ */
+ public function getString();
+
+ /**
+ * Sets the string contained in this object.
+ *
+ * @param string $string
+ * String to set as value.
+ *
+ * @return Drupal\locale\LocaleString
+ * The called object.
+ */
+ public function setString($string);
+
+ /**
+ * Splits string to work with plural values.
+ *
+ * @return array
+ * Array of strings that are plural variants.
+ */
+ public function getPlurals();
+
+ /**
+ * Sets this string using array of plural values.
+ *
+ * Serializes plural variants in one string glued by LOCALE_PLURAL_DELIMITER.
+ *
+ * @param array $plurals
+ * Array of strings with plural variants.
+ *
+ * @return Drupal\locale\LocaleString
+ * The called object.
+ */
+ public function setPlurals($plurals);
+
+ /**
+ * Sets the string storage.
+ *
+ * @param Drupal\locale\StringStorageInterface $storage
+ * The storage to use for this string.
+ *
+ * @return Drupal\locale\LocaleString
+ * The called object.
+ */
+ public function setStorage($storage);
+
+ /**
+ * Checks whether the object is not saved to storage yet.
+ *
+ * @return bool
+ * TRUE if the object exists in the storage, FALSE otherwise.
+ */
+ public function isNew();
+
+ /**
+ * Checks whether the object is a source string.
+ *
+ * @return bool
+ * TRUE if the object is a source string, FALSE otherwise.
+ */
+ public function isSource();
+
+ /**
+ * Checks whether the object is a translation string.
+ *
+ * @return bool
+ * TRUE if the object is a translation string, FALSE otherwise.
+ */
+ public function isTranslation();
+
+ /**
+ * Sets an array of values as object properties.
+ *
+ * @param array $values
+ * Array with values indexed by property name,
+ * @param bool $override
+ * (optional) Whether to override already set fields, defaults to TRUE.
+ *
+ * @return Drupal\locale\LocaleString
+ * The called object.
+ */
+ public function setValues(array $values, $override = TRUE);
+
+ /**
+ * Gets field values that are set for given field names.
+ *
+ * @param array $fields
+ * Array of field names.
+ *
+ * @return array
+ * Array of field values indexed by field name.
+ */
+ public function getValues(array $fields);
+
+ /**
+ * Saves string object to storage.
+ *
+ * @return Drupal\locale\LocaleString
+ * The called object.
+ *
+ * @throws Drupal\locale\StringStorageException
+ * In case of failures, an exception is thrown.
+ */
+ public function save();
+
+ /**
+ * Deletes string object from storage.
+ *
+ * @return Drupal\locale\LocaleString
+ * The called object.
+ *
+ * @throws Drupal\locale\StringStorageException
+ * In case of failures, an exception is thrown.
+ */
+ public function delete();
+
+}
diff --git a/core/modules/locale/lib/Drupal/locale/StringStorageException.php b/core/modules/locale/lib/Drupal/locale/StringStorageException.php
new file mode 100644
index 0000000..c6f1b40
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/StringStorageException.php
@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Entity\StringStorageException.
+ */
+
+namespace Drupal\locale;
+
+use Exception;
+
+/**
+ * Defines an exception thrown when storage operations fail.
+ */
+class StringStorageException extends Exception { }
diff --git a/core/modules/locale/lib/Drupal/locale/StringStorageInterface.php b/core/modules/locale/lib/Drupal/locale/StringStorageInterface.php
new file mode 100644
index 0000000..20dfca3
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/StringStorageInterface.php
@@ -0,0 +1,179 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\StringStorageInterface.
+ */
+
+namespace Drupal\locale;
+
+/**
+ * Defines the locale string storage interface.
+ */
+interface StringStorageInterface {
+
+ /**
+ * Loads multiple source string objects.
+ *
+ * @param array $conditions
+ * (optional) Array with conditions that will be used to filter the strings
+ * returned and may include any of the following elements:
+ * - Any simple field value indexed by field name.
+ * - 'translated', TRUE to get only translated strings or FALSE to get only
+ * untranslated strings. If not set it returns both translated and
+ * untranslated strings that fit the other conditions.
+ * Defaults to no conditions which means that it will load all strings.
+ * @param array $options
+ * (optional) An associative array of additional options. It may contain
+ * any of the following optional keys:
+ * - 'filters': Array of string filters indexed by field name.
+ * - 'pager limit': Use pager and set this limit value.
+ * - 'fields', Array with the exact fields to load. Defaults to all.
+ *
+ * @return array
+ * Array of Drupal\locale\StringInterface objects matching the conditions.
+ */
+ public function getStrings(array $conditions = array(), array $options = array());
+
+ /**
+ * Loads multiple string translation objects.
+ *
+ * @see Drupal\locale\StringStorageInterface::getStrings()
+ *
+ * @param array $conditions
+ * (optional) Array with conditions that will be used to filter the strings
+ * returned and may include all of the conditions defined by getStrings().
+ * @param array $options
+ * (optional) An associative array of additional options. It may contain
+ * any of the options defined by getStrings().
+ *
+ * @return array
+ * Array of Drupal\locale\StringInterface objects matching the conditions.
+ */
+ public function getTranslations(array $conditions = array(), array $options = array());
+ /**
+ * Loads a string source object, fast query.
+ *
+ * These 'fast query' methods are the ones in the critical path and their
+ * implementation must be optimized for speed, as they may run many times
+ * in a single page request.
+ *
+ * @param array $conditions
+ * (optional) Array with conditions that will be used to filter the strings
+ * returned and may include all of the conditions defined by getStrings().
+ * @param array $options
+ * (optional) An associative array of additional options. It may contain
+ * any of the options defined by getStrings().
+ *
+ * @return Drupal\locale\SourceString|null
+ * Minimal TranslationString object if found, NULL otherwise.
+ */
+ public function findString(array $conditions, array $options = array());
+
+ /**
+ * Loads a string translation object, fast query.
+ *
+ * @param array $conditions
+ * (optional) Array with conditions that will be used to filter the strings
+ * returned and may include all of the conditions defined by getStrings().
+ * @param array $options
+ * (optional) An associative array of additional options. It may contain
+ * any of the options defined by getStrings().
+ *
+ * @return Drupal\locale\TranslationString|null
+ * Minimal TranslationString object if found, NULL otherwise.
+ */
+ public function findTranslation(array $conditions, array $options = array());
+
+ /**
+ * Checks whether the string version matches a given version, fix it if not.
+ *
+ * @param Drupal\locale\StringInterface $string
+ * The string object.
+ * @param string $version
+ * Drupal version to check against.
+ */
+ public function checkVersion($string, $version);
+
+ /**
+ * Save string object to storage.
+ *
+ * @param Drupal\locale\StringInterface $string
+ * The string object.
+ *
+ * @return Drupal\locale\StringStorageInterface
+ * The called object.
+ *
+ * @throws Drupal\locale\StringStorageException
+ * In case of failures, an exception is thrown.
+ */
+ public function save($string);
+
+ /**
+ * Delete string from storage.
+ *
+ * @param Drupal\locale\StringInterface $string
+ * The string object.
+ *
+ * @return Drupal\locale\StringStorageInterface
+ * The called object.
+ *
+ * @throws Drupal\locale\StringStorageException
+ * In case of failures, an exception is thrown.
+ */
+ public function delete($string);
+
+ /**
+ * Deletes source strings and translations using conditions.
+ *
+ * @param array $conditions
+ * Array with simple field conditions for source strings.
+ */
+ public function deleteStrings($conditions);
+
+ /**
+ * Deletes translations using conditions.
+ *
+ * @param array $conditions
+ * Array with simple field conditions for string translations.
+ */
+ public function deleteTranslations($conditions);
+
+ /**
+ * Counts source strings.
+ *
+ * @return int
+ * The number of source strings contained in the storage.
+ */
+ public function countStrings();
+
+ /**
+ * Counts translations.
+ *
+ * @return array
+ * The number of translations for each language indexed by language code.
+ */
+ public function countTranslations();
+
+ /**
+ * Creates a source string object bound to this storage but not saved.
+ *
+ * @param array $values
+ * (optional) Array with initial values. Defaults to empty array.
+ *
+ * @return Drupal\locale\SourceString
+ * New source string object.
+ */
+ public function createString($values = array());
+
+ /**
+ * Creates a string translation object bound to this storage but not saved.
+ *
+ * @param array $values
+ * (optional) Array with initial values. Defaults to empty array.
+ *
+ * @return Drupal\locale\TranslationString
+ * New string translation object.
+ */
+ public function createTranslation($values = array());
+}
diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleStringTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleStringTest.php
new file mode 100644
index 0000000..4666962
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleStringTest.php
@@ -0,0 +1,199 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\Tests\LocaleStringTest.
+ */
+
+namespace Drupal\locale\Tests;
+
+use Drupal\Core\Language\Language;
+use Drupal\locale\SourceString;
+use Drupal\locale\TranslationString;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests for the locale string data API.
+ */
+class LocaleStringTest extends WebTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = array('locale');
+
+ /**
+ * The locale storage.
+ *
+ * @var Drupal\locale\StringStorageInterface
+ */
+ protected $storage;
+
+ /**
+ * @return multitype:string
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => '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 1c7265c..7ac92ad 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 @@ use Drupal\simpletest\WebTestBase;
/**
* 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 0000000..498dd66
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/TranslationString.php
@@ -0,0 +1,128 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\TranslationString.
+ */
+
+namespace Drupal\locale;
+
+/**
+ * Defines the locale translation string object.
+ *
+ * This class represents a translation of a source string to a given language,
+ * thus it must have at least a 'language' which is the language code and a
+ * 'translation' property which is the translated text of the the source string
+ * in the specified language.
+ */
+class TranslationString extends StringBase {
+ /**
+ * The language code.
+ *
+ * @var string
+ */
+ public $language;
+
+ /**
+ * The string translation.
+ *
+ * @var string
+ */
+ public $translation;
+
+ /**
+ * Integer indicating whether this string is customized.
+ *
+ * @var int
+ */
+ public $customized;
+
+ /**
+ * Boolean indicating whether the string object is new.
+ *
+ * @var bool
+ */
+ protected $is_new;
+
+ /**
+ * Overrides Drupal\locale\StringBase::__construct().
+ */
+ public function __construct($values = array()) {
+ parent::__construct($values);
+ if (!isset($this->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 e486441..b2bfa95 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]);
}
@@ -288,6 +289,20 @@ function locale_reset() {
}
/**
+ * 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.
*
* The index is computed from the formula of this language.
@@ -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('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', substr($match['string'], 1, -1)));
$context = implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', substr($match['context'], 1, -1)));
- $source = db_query("SELECT lid, location FROM {locales_source} WHERE source = :source AND context = :context", array(':source' => $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 331cea4..a6fb751 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.'));