diff --git a/core/modules/locale/lib/Drupal/locale/LocaleLookup.php b/core/modules/locale/lib/Drupal/locale/LocaleLookup.php
index 77aa4dd..c949b5e 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,26 @@ 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(
+      'language' => $this->langcode,
+      'source' => $offset,
+      'context' => $this->context
+    ), array('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..46cad32 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,66 @@ 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 = $query_options = 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');
+          $query_options['translated'] = FALSE;
         }
       }
-      $query->fields('t', array('translation'));
+      // Load untranslated strings or not depending on 'not_translated' option.
+      $query_options['untranslated'] = $options['not_translated'];
+
+      return locale_storage()->getTranslations($conditions, $query_options);
     }
     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_options);
     }
-    $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..57b1a1b 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
+    ), array('customized'));
 
     if (!empty($translation)) {
       // Skip this string unless it passes a check for dangerous code.
@@ -240,64 +241,40 @@ 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->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..ba8b78d
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/SourceString.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\SourceString.
+ */
+
+namespace Drupal\locale;
+
+use Drupal\locale\LocaleString;
+use PDO;
+
+/**
+ * Defines the locale source string object.
+ */
+class SourceString extends StringBase {
+  /**
+   * Implements Drupal\locale\StringInterface::isSource().
+   */
+  public function isSource() {
+    return TRUE;
+  }
+
+  /**
+   * Implements Drupal\locale\StringInterface::isTranslation().
+   */
+  public function isTranslation() {
+    return FALSE;
+  }
+
+  /**
+   * Implements Drupal\locale\LocaleString::getString().
+   */
+  public function getString() {
+    return $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);
+  }
+
+  /**
+   * Implements Drupal\locale\StringInterface::setDefaults().
+   */
+  public function setDefaults() {
+    $this->setValues(array(
+        'version' => 'none',
+        'source' => '',
+        'context' => '',
+        'location' => ''
+    ), FALSE);
+    return $this;
+  }
+
+}
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..077ccd2
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/StringBase.php
@@ -0,0 +1,162 @@
+<?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->storage->save($this);
+    return $this;
+  }
+
+  /**
+   * Implements Drupal\locale\LocaleString::delete().
+   */
+  public function delete() {
+    if (!$this->isNew()) {
+      $this->storage->delete($this);
+    }
+    return $this;
+  }
+
+}
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..a886819
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php
@@ -0,0 +1,467 @@
+<?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::findString().
+   */
+  public function findString(array $conditions, array $fields = array()) {
+    if ($fields && !in_array('lid', $fields)) {
+      $fields[] = 'lid';
+    }
+    $strings = $this->prepareStrings($this->getStrings($conditions, array(), $fields), $conditions);
+    return reset($strings);
+  }
+
+  /**
+   * Implements Drupal\locale\StringStorageInterface::findTranslation().
+   */
+  public function findTranslation(array $conditions, array $fields = array()) {
+    if (!$fields) {
+      $fields = array('*', 'translation', 'language', 'customized');
+    }
+    elseif (!in_array('lid', $fields)) {
+      $fields[] = 'lid';
+    }
+    $args = array();
+    $values = $conditions;
+    // Prefix fields with table alias and eliminate duplicates.
+    foreach (array_unique($fields) as $index => $field) {
+      $fields[$index] = $this->dbPrefixField($field);
+    }
+    $sql = 'SELECT ' . implode(', ', $fields) . ' FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid';
+    if (isset($conditions['language'])) {
+      $sql .= ' AND t.language = :langcode';
+      $args[':langcode'] = $conditions['language'];
+      unset($conditions['language']);
+    }
+    if ($conditions) {
+      foreach ($conditions as $field => $value) {
+        $conditions[$field] = $this->dbPrefixField($field) .  ' = :' . $field;
+        $args[':' . $field] =  $value;
+      }
+      $sql .= ' WHERE ' . implode(' AND ', $conditions);
+    }
+    $string = $this->dbExecute($sql, $args)->fetchObject('Drupal\locale\TranslationString');
+    return $this->prepareStrings($string, $values);
+  }
+
+  /**
+   * 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 ($table = $this->dbStringTable($string)) {
+      if ($string->isNew()) {
+        $string->setDefaults();
+        $fields = $this->dbStringValues($string, $table);
+        $result = $this->connection->insert($table, $this->options)
+          ->fields($fields)
+          ->execute();
+        if ($string->isSource() && $result) {
+          // Only for source strings, we set the locale identifier.
+          $string->setId($result);
+        }
+      }
+      else {
+        $fields = $this->dbStringValues($string, $table);
+        if ($keys = $this->dbStringKeys($string, $table)) {
+          $values = array_diff_key($fields, $keys);
+          $this->connection->merge($table, $this->options)
+            ->key($keys)
+            ->fields($values)
+            ->execute();
+        }
+      }
+    }
+    // The operation failed.
+    return $this;
+  }
+
+  /**
+   * Implements Drupal\locale\StringStorageInterface::delete().
+   */
+  public function delete($string) {
+    if (($table = $this->dbStringTable($string)) && ($keys = $this->dbStringKeys($string, $table))) {
+      $this->dbDelete($table, $keys);
+      if ($string->isSource()) {
+        $string->setId(NULL);
+        $this->dbDelete('locales_target', $keys);
+      }
+    }
+    return $this;
+  }
+
+  /**
+   * Implements Drupal\locale\StringStorageInterface::deleteLanguage().
+   */
+  public function deleteLanguage($langcode) {
+    $this->dbDelete('locales_target', array('language' => $langcode));
+  }
+
+  /**
+   * Implements Drupal\locale\StringStorageInterface::getStrings().
+   */
+  public function getStrings(array $conditions = array(), array $options = array(), array $fields = array()) {
+    $result = $this->dbSelect($conditions, $options, $fields)->execute();
+    $result->setFetchMode(PDO::FETCH_CLASS, 'Drupal\locale\SourceString');
+    return $this->prepareStrings($result->fetchAll());
+  }
+
+  /**
+   * Implements Drupal\locale\StringStorageInterface::loadTranslations().
+   */
+  public function getTranslations(array $conditions = array(), array $options = array(), array $fields = array()) {
+    $options += array('untranslated' => FALSE);
+    $result = $this->dbSelect($conditions, $options, $fields)->execute();
+    $result->setFetchMode(PDO::FETCH_CLASS, 'Drupal\locale\TranslationString');
+    return $this->prepareStrings($result->fetchAll());
+  }
+
+  /**
+   * 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));
+  }
+
+  /**
+   * Prepares string or array of strings for being returned.
+   *
+   * Adds this storage and some optional values to all of them. The returned
+   * strings are bound to this storage object so they can use it for their
+   * save() or delete() methods.
+   *
+   * @param array|object $strings
+   *   Drupal\locale\StringInterface object or array of them.
+   * @param array $values
+   *   (optional) Array of field values to add to the string objects. Defatuls
+   *   to empty array.
+   *
+   * @return array|object
+   *   Drupal\locale\StringInterface object or array of them.
+   */
+  protected function prepareStrings($strings, array $values = array()) {
+    if (!$strings) {
+      return $strings;
+    }
+    else {
+      $values += array('storage' => $this);
+      $is_array = is_array($strings);
+      $strings = $is_array ? $strings : array($strings);
+      foreach ($strings as $string) {
+        $string->setValues($values, FALSE);
+      }
+      return $is_array ? $strings : reset($strings);
+    }
+  }
+
+  /**
+   * 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 static function dbFieldAlias($field) {
+    return in_array($field, array('language', 'translation', 'customized')) ? 't' : 's';
+  }
+
+  /**
+   * Prefixes field name with table alias.
+   *
+    * @param string $field
+   *   Field name to find the table alias for.
+   *
+   * @return string
+   *   Field name prefixed with table alias.
+   */
+  protected static function dbPrefixField($field) {
+    return self::dbFieldAlias($field) . '.' . $field;
+  }
+
+  /**
+   * Gets table name for storing string object.
+   *
+   * @param Drupal\locale\StringInterface $string
+   *   The string object.
+   *
+   * @return string
+   *   The table name.
+   */
+  protected static function dbStringTable($string) {
+    if ($string->isSource()) {
+      return 'locales_source';
+    }
+    elseif ($string->isTranslation()) {
+      return 'locales_target';
+    }
+  }
+
+  /**
+   * Gets string keys that are in a database table from the table schema.
+   *
+   * @param Drupal\locale\StringInterface $string
+   *   The string object.
+   * @param string $table
+   *   The table name.
+   *
+   * @return array|null
+   *   Array with key fields if the string has all keys, NULL otherwise.
+   */
+  protected static function dbStringKeys($string, $table) {
+    if ($schema = drupal_get_schema($table)) {
+      $keys = $schema['primary key'];
+      $values = $string->getValues($keys);
+      if (count($values) == count($keys)) {
+        return $values;
+      }
+    }
+    return NULL;
+  }
+
+  /**
+   * Gets string fields that are in a database table from the table schema.
+   *
+   * @param Drupal\locale\StringInterface $string
+   *   The string object.
+   * @param string $table
+   *   The table name.
+   */
+  protected static function dbStringValues($string, $table) {
+    if ($schema = drupal_get_schema($table)) {
+      $fields = array_keys($schema['fields']);
+      return $string->getValues($fields);
+    }
+  }
+
+  /**
+   * 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():
+   * @param array $fields
+   *   (optional) Fields to load. Defaults to all fields from involved tables.
+   *
+   * @return SelectQuery
+   *   Query object with all the tables, fields and conditions.
+   */
+  protected function dbSelect(array $conditions, array $options, array $fields = array()) {
+    // Add default options and see which kind of join we need.
+    $options += array('translated' => TRUE, 'untranslated' => TRUE);
+
+    // Group conditions and fields by their table alias.
+    $table_conditions = $table_fields = array();
+    foreach ($conditions as $field => $value) {
+      $table_conditions[$this->dbFieldAlias($field)][$field] = $value;
+    }
+    // Group fields by their table alias eliminating duplicates.
+    if ($fields) {
+      foreach ($fields as $name) {
+        $table_fields[$this->dbFieldAlias($name)][$name] = $name;
+      }
+    }
+    else {
+      // We don't have fields option, start with all source fields,
+      $table_fields['s'] = array();
+    }
+
+    // Translate some options into conditions.
+    if (!$options['translated']) {
+      // Select only untranslated strings.
+      $join = 'leftJoin';
+      $table_conditions['t']['translation'] = NULL;
+    }
+    elseif (!$options['untranslated']) {
+      // Select only translated strings.
+      $join = 'innerJoin';
+    }
+    elseif (isset($table_conditions['t']) || isset($table_fields['t'])) {
+      // If we have conditions or fields in target table we need to join it.
+      // Outer join because we are loading translated and untranslated.
+      $join = 'leftJoin';
+    }
+    else {
+      $join = FALSE;
+    }
+
+    // Start building the query with source table and fields.
+    $query = $this->connection->select('locales_source', 's', $this->options);
+    if (isset($table_fields['s'])) {
+      $query->fields('s', $table_fields['s']);
+    }
+    // We may need to join the target table too.
+    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($table_conditions['t']['language']);
+      }
+      else {
+        // Since we don't have a language, join with locale id only.
+        $query->$join('locales_target', 't', "t.lid = s.lid");
+      }
+      // Since we have joined the table, add all fields if we don't have any,
+      // but not 'lid' that is in the sources table.
+      if (!$fields) {
+        $table_fields['t'] = array('language', 'translation', 'customized');
+      }
+      if (isset($table_fields['t'])) {
+        $query->fields('t', $table_fields['t']);
+      }
+    }
+
+    // Add conditions for all tables, handling NULL conditions too.
+    foreach ($table_conditions as $table_alias => $alias_conditions) {
+      foreach ($alias_conditions as $field => $value) {
+        $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->dbPrefixField($field), '%' . db_like($string) . '%', 'LIKE');
+      }
+    }
+
+    if (!empty($options['pager limit'])) {
+      $query = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender')->limit($options['pager limit']);
+    }
+
+    return $query;
+  }
+
+  /**
+   * Deletes record from database.
+   *
+   * @param string $table
+   *   The table name.
+   * @param array $keys
+   *   Array with object keys indexed by field name.
+   */
+  Protected function dbDelete($table, $keys) {
+    $query = $this->connection->delete($table, $this->options);
+    foreach ($keys as $field => $value) {
+      $query->condition($field, $value);
+    }
+    $query->execute();
+  }
+
+  /**
+   * 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..d42bcfc
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/StringInterface.php
@@ -0,0 +1,176 @@
+<?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);
+
+  /**
+   * Sets default values as object properties if not set before.
+   *
+   * @return Drupal\locale\LocaleString
+   *   The called object.
+   */
+  public function setDefaults();
+
+  /**
+   * Saves string object to storage.
+   *
+   * @return Drupal\locale\LocaleString
+   *   The called object.
+   */
+  public function save();
+
+  /**
+   * Deletes string object from storage.
+   *
+   * @return Drupal\locale\LocaleString
+   *   The called object.
+   */
+  public function delete();
+
+}
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..55c1fe0
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/StringStorageInterface.php
@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\StringStorageInterface.
+ */
+
+namespace Drupal\locale;
+
+/**
+ * Defines the locale string storage interface that will be used to store and
+ * retrieve strings and translations.
+ */
+interface StringStorageInterface {
+
+  /**
+   * Loads a minimal string source object, fast query.
+   *
+   * Note the resulting value may not be suitable for CRUD operations because
+   * it may be missing some important fields depending on the ones selected.
+   *
+   * @param array $conditions
+   *   Array with simple field conditions.
+   * @param array $fields
+   *   (optional) List of fields to load. Defaults to all fields available for
+   *   the source string.
+   *
+   * @return Drupal\locale\SourceString|null
+   *   Minimal TranslationString object if found, NULL otherwise.
+   */
+  public function findString(array $conditions, array $fields = array());
+
+  /**
+   * Loads a minimal string translation object, fast query.
+   *
+   * Note the resulting value may not be suitable for CRUD operations because
+   * it may be missing some important fields depending on the ones selected.
+   *
+   * These queries run as part of a critical path to look up translations and
+   * may run many times per page request, so the implementation should be as
+   * fast as possible. For a database storage benchmarks found using db_select()
+   * would be significantly slower to build the query, so using a direct
+   * db_query() instead would be preferred.
+   *
+   * @param array $conditions
+   *   Array with simple field conditions.
+   * @param array $fields
+   *   (optional) List of fields to load. Defaults to all fields available for
+   *   the string translation.
+   *
+   * @return Drupal\locale\TranslationString|null
+   *   Minimal TranslationString object if found, NULL otherwise.
+   */
+  public function findTranslation(array $conditions, array $fields = array());
+
+  /**
+   * Loads multiple source string objects.
+   *
+   * In order to produce a query as fast as possible we must pass the exact
+   * fields we need to load only that ones.
+   *
+   * @param array $conditions
+   *   (optional) Array with simple field conditions. Defaults to no conditions
+   *   which means that it will load all source strings.
+   * @param array $options
+   *   (optional) An associative array of additional options. It may contain any of the
+   *   following optional keys:
+   *   - 'translated' (defaults to TRUE): Whether to include translated
+   *     strings.
+   *   - 'untranslated' (defaults to TRUE): Whether to include untranslated
+   *     strings.
+   *   - 'filters': Array of string filters indexed by field name.
+   *   - 'pager limit': Use pager and set this limit value.
+   * @param array $fields
+   *   (optional) List of field names to retrieve. Defaults to all fields.
+   *
+   * @return array
+   *   Array of Drupal\locale\StringInterface objects matching the conditions.
+   */
+  public function getStrings(array $conditions = array(), array $options = array(), array $fields = array());
+
+  /**
+   * Loads multiple string translation objects.
+   *
+   * In order to produce a query as fast as possible we must pass the exact
+   * fields we need to load only that ones.
+   *
+   * @see Drupal\locale\StringStorageInterface::getStrings()
+   *
+   * @param array $conditions
+   *   (optional) Array with simple field conditions. Defaults to no conditions
+   *   which means that it will load all source strings.
+   * @param array $options
+   *   (optional) An associative array of additional options. It may contain
+   *   any of the options defined by getStrings().
+   * @param array $fields
+   *   (optional) List of field names to retrieve. Defaults to all fields.
+   *
+   * @return array
+   *   Array of Drupal\locale\StringInterface objects matching the conditions.
+  */
+  public function getTranslations(array $conditions = array(), array $options = array(), array $fields = 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.
+   */
+  public function save($string);
+
+  /**
+   * Delete string from storage.
+   *
+   * @param Drupal\locale\StringInterface $string
+   *   The string object.
+   *
+   * @return Drupal\locale\StringStorageInterface
+   *   The called object.
+   */
+  public function delete($string);
+
+  /**
+   * Delete all translations for a language.
+   *
+   * @param string $langcode
+   *   Language code.
+   */
+  public function deleteLanguage($langcode);
+
+  /**
+   * 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..856d5e1
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleStringTest.php
@@ -0,0 +1,201 @@
+<?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), array('translation'));
+    $this->assertTrue($found && isset($found->language) && !isset($found->customized) && !$found->isNew(), 'Translation found with only the right fields.');
+    $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), array('translation'));
+    $this->assertTrue($found && $found->lid == $source3->lid && !isset($found->translation) && $found->isNew(), 'Missing translation found with only the right fields.');
+
+    // Load all translations. For next queries we'll be loading only translated strings.
+    $only_translated = array('untranslated' => FALSE);
+    $only_untranslated = array('translated' => FALSE);
+    $translations = $this->storage->getTranslations(array(), $only_translated);
+    $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), $only_translated);
+    $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), $only_translated);
+    $this->assertEqual(count($translations), 1  , 'Found only Spanish and customized translations.');
+
+    // Load all source strings without translation (1).
+    $translations = $this->storage->getStrings(array(), $only_untranslated);
+    $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 @@
 /**
  * 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..127245a
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/TranslationString.php
@@ -0,0 +1,135 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\TranslationString.
+ */
+
+namespace Drupal\locale;
+
+/**
+ * Defines the locale translation string object.
+ */
+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 has any of the translation fields.
+      // This will work when loading from database, otherwise the storage
+      // controller that creates the string object must handle it.
+      $this->is_new = !isset($this->language) && !isset($this->translation) && !isset($this->customized);
+    }
+  }
+
+  /**
+   * 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 TRUE;
+  }
+
+  /**
+   * Implements Drupal\locale\StringInterface::getString().
+   */
+  public function getString() {
+    return $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;
+  }
+
+  /**
+   * Implements Drupal\locale\StringInterface::setDefaults().
+   */
+  public function setDefaults() {
+    $this->setValues(array(
+      'language' => '',
+      'translation' => '',
+      'customized' => LOCALE_NOT_CUSTOMIZED
+    ), FALSE);
+    return $this;
+  }
+
+}
diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module
index a32d47d..b21fc34 100644
--- a/core/modules/locale/locale.module
+++ b/core/modules/locale/locale.module
@@ -13,7 +13,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.
@@ -213,9 +216,7 @@ function locale_language_update($language) {
  */
 function locale_language_delete($language) {
   // Remove translations.
-  db_delete('locales_target')
-    ->condition('language', $language->langcode)
-    ->execute();
+  locale_storage()->deleteLanguage($language->langcode);
 
   // Remove interface translation files.
   module_load_include('inc', 'locale', 'locale.bulk');
@@ -281,7 +282,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]);
 }
@@ -294,6 +295,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.
@@ -519,16 +534,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);
       }
     }
   }
@@ -760,7 +775,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), array('location'));
     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.
@@ -771,23 +786,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();
     }
   }
 }
@@ -846,10 +855,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';
+  $fields = array('source', 'context', 'translation');
+  $conditions['language'] = $language->langcode;
   $translations = array();
-  foreach ($result as $data) {
+  foreach (locale_storage()->getTranslations($conditions, $options, $fields) 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 312aa94..c1ce0c9 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;
 
 /**
@@ -18,42 +20,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');
+      $options['untranslated'] = FALSE;
       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');
+      $options['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);
 }
 
 /**
@@ -270,14 +272,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(
@@ -390,9 +394,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) {
@@ -403,35 +406,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 : 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.'));
