diff --git a/core/modules/locale/lib/Drupal/locale/LocaleLookup.php b/core/modules/locale/lib/Drupal/locale/LocaleLookup.php
index 897eaa1..3fd4ff6 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\LocaleSource;
+use Drupal\locale\LocaleTranslation;
 
 /**
  * Extends CacheArray to allow for dynamic building of the locale cache.
@@ -44,36 +46,22 @@ 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 = LocaleTranslation::loadBySource($this->langcode, $offset, $this->context, 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();
-      }
+      $translation->checkVersion(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;
+      $source = new LocaleSource(array(
+        'source' => $offset,
+        'context' => $this->context,
+        'location' => request_uri(),
+        'version' => VERSION
+      ));
+      $source->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/LocaleSource.php b/core/modules/locale/lib/Drupal/locale/LocaleSource.php
new file mode 100644
index 0000000..c9b1a7f
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/LocaleSource.php
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\LocaleSource.
+ */
+
+namespace Drupal\locale;
+
+use Drupal\locale\LocaleString;
+
+/**
+ * Defines the locale source string object.
+ */
+class LocaleSource extends LocaleString {
+
+  /**
+   * Loads a single string object by locale identifier.
+   *
+   * @param int $lid
+   *   Locale identifier.
+   *
+   * @return Drupal\locale\LocaleSource|null
+   *   LocaleSource object if found, NULL otherwise.
+   */
+  public static function loadById($lid) {
+    $strings = self::loadMultiple(array('lid' => $lid));
+    return reset($strings);
+  }
+
+  /**
+   * Loads a single string object by context and source.
+   *
+   * @param string $source
+   *   Source string to search for.
+   * @param string $context
+   *   (optional) The tring context. Defaults to the empty context.
+   *
+   * @return Drupal\locale\LocaleSource|null
+   *   LocaleSource object if found, NULL otherwise.
+   */
+  public static function loadBySource($source, $context = '') {
+    $strings = self::loadMultiple(array('source' => $source, 'context' => $context));
+    return reset($strings);
+  }
+
+  /**
+   * Overrides Drupal\locale\LocaleString::loadMultiple().
+   */
+  public static function loadMultiple(array $conditions = array(), array $fields = array(), array $options = array()) {
+    // Invoke the parent's method with this class name to fetch objects into.
+    $options += array('fetch class' => 'Drupal\locale\LocaleSource');
+    return parent::loadMultiple($conditions, $fields, $options);
+  }
+
+  /**
+   * 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\LocaleString::insert().
+   */
+  public function insert() {
+    $this->setDefaultValues(array(
+        'version' => 'none',
+        'source' => '',
+        'context' => '',
+        'location' => ''
+    ));
+    $this->lid = db_insert('locales_source')
+      ->fields($this->getFieldValues(array('source', 'context', 'location', 'version')))
+      ->execute();
+    return $this;
+  }
+
+  /**
+   * Implements Drupal\locale\LocaleString::update().
+   */
+  public function update() {
+    db_update('locales_source')
+    ->fields($this->getFieldValues(array('source', 'context', 'location', 'version')))
+    ->condition('lid', $this->lid)
+    ->execute();
+    return $this;
+  }
+
+  /**
+   * Implements Drupal\locale\LocaleString::delete().
+   */
+  public function delete() {
+    if (!$this->isNew()) {
+      db_delete('locales_target')
+        ->condition('lid', $this->lid)
+        ->execute();
+      db_delete('locales_source')
+        ->condition('lid', $this->lid)
+        ->execute();
+      unset($this->lid);
+    }
+    return $this;
+  }
+}
diff --git a/core/modules/locale/lib/Drupal/locale/LocaleString.php b/core/modules/locale/lib/Drupal/locale/LocaleString.php
new file mode 100644
index 0000000..4024bc1
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/LocaleString.php
@@ -0,0 +1,403 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\LocaleString.
+ */
+
+namespace Drupal\locale;
+
+use PDO;
+
+/**
+ * Defines the locale string class.
+ *
+ * This is the base class for LocaleSource and LocaleTranslation.
+ */
+abstract class LocaleString {
+  /**
+   * 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;
+
+  /**
+   * Constructs a new locale source object.
+   *
+   * @param object|array $values
+   *   Object or array with initial values.
+   */
+  public function __construct($values = array()) {
+    // Set initial values.
+    $this->setValues((array)$values);
+  }
+
+  /**
+   * Sets an array of values as object properties ignoring null values.
+   *
+   * @param array $values
+   *   Array with values indexed by property name.
+   *
+   * @return Drupal\locale\LocaleString
+   *   The called object.
+   */
+  public function setValues(array $values) {
+    foreach ($values as $key => $value) {
+      if (isset($value) && !is_array($value) && property_exists($this, $key)) {
+        $this->$key = $value;
+      }
+    }
+    return $this;
+  }
+
+  /**
+   * Sets an array of values as object properties if not set before.
+   *
+   * @param array $values
+   *   Array with values indexed by property name.
+   *
+   * @return Drupal\locale\LocaleString
+   *   The called object.
+   */
+  public function setDefaultValues(array $values) {
+    foreach ($values as $key => $value) {
+      if (!isset($this->$key)) {
+        $this->$key = $value;
+      }
+    }
+    return $this;
+  }
+
+  /**
+   * Gets field values that are set for given field names.
+   *
+   * @param array $field_names
+   *   Array of field names.
+   */
+  public function getFieldValues(array $field_names) {
+    $values = array();
+    foreach ($field_names as $field) {
+      if (isset($this->$field)) {
+        $values[$field] = $this->$field;
+      }
+    }
+    return $values;
+  }
+
+  /**
+   * Checks whether this string version matches a given version, fix it if not.
+   *
+   * @param string $version
+   *   Drupal version to check against.
+   *
+   * @return Drupal\locale\LocaleString
+   *   The called object.
+   */
+  public function checkVersion($version) {
+    if (isset($this->lid) && isset($this->version) && $this->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')
+        ->condition('lid', $this->lid)
+        ->fields(array('version' => $version))
+        ->execute();
+      $this->version = $version;
+    }
+    return $this;
+  }
+
+  /**
+   * Loads multiple string objects, fast query.
+   *
+   * 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.
+   * @param array $fields
+   *   (optional) Fields to load, defaults to all.
+   * @param array $options
+   *   (optional) An associative array of additional options. Defaults to an
+   *   empty array. It may contain any of the following optional keys:
+   *   - 'fetch class' (defaults to Stdclass): Class to fetch the results into.
+   *   - Additional $options elements used by the getQuery() method.
+   *
+   * @return array
+   *   Array of string objects matching the conditions.
+   */
+  public static function loadMultiple(array $conditions = array(), array $fields = array(), array $options = array()) {
+    // We get any of the target fields to check later whether this actually has translation.
+    $result = self::getQuery($conditions, $fields, $options)->execute();
+    if (!empty($options['fetch class'])) {
+      $result->setFetchMode(PDO::FETCH_CLASS, $options['fetch class']);
+    }
+    $strings = $result->fetchAll();
+    // Set values we already know, they were used as conditions.
+    foreach ($strings as $string) {
+      $string->setValues($conditions);
+    }
+    return $strings;
+  }
+
+  /**
+   * Builds strings 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 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 $fields
+   *   (optional) Array with the fields to select. By default we select all
+   *   source fields but none of the target fields. See 'target fields' and
+   *   'source fields' options.
+   * @param array $options
+   *   (optional) An associative array of additional options. Defaults to an
+   *   empty array. 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.
+   *   - 'source fields' (defaults to TRUE): Whether to include all source
+   *     fields if no other source fields present,
+   *   - 'target fields' (defaults to FALSE): Whether to include all target
+   *     fields if no other target fields present.
+   *   - 'join': How to join the locales_target table. It can be 'innerJoin',
+   *     'leftJoin' or FALSE (for not joining it).
+   *   - 'string filter': Filter to apply to both source and target string.
+   *   - 'pager limit': Use pager and set this limit value.
+   *
+   * @return SelectQuery
+   *   Query object with all the tables, fields and conditions.
+   */
+  public static function getQuery(array $conditions, array $fields = array(), array $options = array()) {
+    // Add default options and see which kind of join we need.
+    $options += array('source fields' => TRUE, 'target fields' => FALSE, 'translated' => TRUE, 'untranslated' => TRUE);
+    $join = isset($options['join']) ? $options['join'] : FALSE;
+
+    // Translate some options into conditions.
+    if (!$options['untranslated']) {
+      // Select only translated strings.
+      $join = 'innerJoin';
+    }
+    elseif (!$options['translated']) {
+      // Select only untranslated strings.
+      $join = 'leftJoin';
+      $conditions['translation'] = NULL;
+    }
+
+    // Group fields and conditions by their table alias.
+    $table_conditions = $table_fields = array();
+    if ($options['source fields']) {
+      $table_fields['s'] = array();
+    }
+    if ($options['target fields']) {
+      $table_fields['t'] = array();
+    }
+    foreach ($conditions as $field => $value) {
+      $table_conditions[self::getFieldTableAlias($field)][$field] = $value;
+    }
+    foreach ($fields as $field) {
+      $table_fields[self::getFieldTableAlias($field)][] = $field;
+    }
+
+    // Start building the query with the source table.
+    $query = db_select('locales_source', 's');
+
+    // Check whether we need to join the target table too depending on fields
+    // and conditions.
+    if (!$join && (isset($table_conditions['t']) || isset($table_fields['t']))) {
+      $join = $options['untranslated'] ? 'leftJoin' : 'innerJoin';
+    }
+    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");
+      }
+    }
+    // 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' && $options['untranslated']) {
+          // 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);
+        }
+      }
+    }
+    // Add selection fields for each of the tables.
+    foreach ($table_fields as $alias => $alias_fields) {
+      $query->fields($alias, $alias_fields);
+    }
+
+    // Process other options, string filter, query limit, etc...
+    if (!empty($options['string filter'])) {
+      if ($join) {
+        $query->condition(db_or()
+            ->condition('s.source', '%' . db_like($options['string filter']) . '%', 'LIKE')
+            ->condition('t.translation', '%' . db_like($options['string filter']) . '%', 'LIKE')
+        );
+      }
+      else {
+        $query->condition('s.source', '%' . db_like($options['string filter']) . '%', 'LIKE');
+      }
+    }
+    if (!empty($options['pager limit'])) {
+      $query = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender')->limit($options['pager limit']);
+    }
+
+    return $query;
+  }
+
+  /**
+   * Gets table alias for field.
+   *
+   * @param string $field_name
+   *
+   * @return string
+   *   Either 's' or 't' depending on whether the field belongs to source or target table.
+   */
+  protected static function getFieldTableAlias($field_name) {
+    return in_array($field_name, array('language', 'translation', 'customized')) ? 't' : 's';
+  }
+
+  /**
+   * Checks whether the object is not saved to database yet.
+   *
+   * @return bool
+   *   TRUE if the object exists in the database, FALSE otherwise.
+   */
+  public abstract function isNew();
+
+  /**
+   * Saves string object to database.
+   *
+   * @return Drupal\locale\LocaleString
+   *   The called object.
+   */
+  public function save() {
+    if ($this->isNew()) {
+      $this->insert();
+    }
+    else {
+      $this->update();
+    }
+    return $this;
+  }
+
+  /**
+   * Creates a new record in database.
+   *
+   * @return Drupal\locale\LocaleString
+   *   The called object.
+   */
+  public abstract function insert();
+
+  /**
+   * Updates current values in database.
+   *
+   * @return Drupal\locale\LocaleString
+   *   The called object.
+   */
+  public abstract function update();
+
+  /**
+   * Deletes string object from database.
+   *
+   * @return Drupal\locale\LocaleString
+   *   The called object.
+   */
+  public abstract function delete();
+
+  /**
+   * Splits string to work with plural values.
+   *
+   * @return array
+   *   Array of strings that are plural variants.
+   */
+  public function getPlurals() {
+    return explode(LOCALE_PLURAL_DELIMITER, $this->getString());
+  }
+
+  /**
+   * 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) {
+    $this->setString(implode(LOCALE_PLURAL_DELIMITER, $plurals));
+    return $this;
+  }
+
+  /**
+   * Gets plain string contained in this object.
+   *
+   * @return string
+   *   The string contained in this object.
+   */
+  public abstract 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 abstract function setString($string);
+}
diff --git a/core/modules/locale/lib/Drupal/locale/LocaleTranslation.php b/core/modules/locale/lib/Drupal/locale/LocaleTranslation.php
new file mode 100644
index 0000000..2ce6e50
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/LocaleTranslation.php
@@ -0,0 +1,209 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\LocaleTranslation.
+ */
+
+namespace Drupal\locale;
+
+use Drupal\locale\LocaleString;
+
+/**
+ * Defines the locale translation string object.
+ */
+class LocaleTranslation extends LocaleString {
+  /**
+   * 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 target object is new.
+   *
+   * @var bool
+   */
+  protected $is_new = TRUE;
+
+  /**
+   * Loads a translation object by context and source, fast query.
+   *
+   * @param string $langcode
+   *   Language code.
+   * @param string $source
+   *   Source string.
+   * @param string $context
+   *   (optional) The tring context. Defaults to the empty context.
+   * @param array $fields
+   *   (optional) Names of the fields to load, defaults to all fields.
+   *
+   * @return Drupal\locale\LocaleTranslation|null
+   *   LocaleTranslation object if found, NULL otherwise.
+   */
+  public static function loadBySource($langcode, $source, $context = '', $fields = array()) {
+    $conditions = array('language' => $langcode, 'source' => $source, 'context' => $context);
+    $translations = self::loadMultiple($conditions, $fields);
+    return reset($translations);
+  }
+
+
+  /**
+   * Loads a translation object by string identifier, fast query.
+   *
+    * @param string $langcode
+   *   Language code.
+   * @param integer $lid
+   *   The string identifier.
+   * @param array $fields
+   *   (optional) Names of the fields to load, defaults to all fields.
+   *
+   * @return Drupal\locale\LocaleTranslation|null
+   *   LocaleTranslation object if found, NULL otherwise.
+   */
+  public static function loadById($langcode, $lid, $fields = array()) {
+    $conditions = array('language' => $langcode, 'lid' => $lid);
+    $translations = self::loadMultiple($conditions, $fields);
+    return reset($translations);
+  }
+
+
+  /**
+   * Overrides Drupal\locale\LocaleString::loadMultiple().
+   */
+  public static function loadMultiple(array $conditions = array(), array $fields = array(), array $options = array()) {
+    // Invoke the parent's method with this class name and all target fields.
+    $options += array('target fields' => TRUE, 'fetch class' => 'Drupal\locale\LocaleTranslation');
+    $translations = parent::loadMultiple($conditions, $fields, $options);
+    // We get any of the target fields to check whether this actually has translation.
+    foreach ($translations as $translation) {
+      $translation->setNew(!isset($translation->translation) && !isset($translation->customized));
+    }
+    return $translations;
+  }
+
+
+  /**
+   * Overrides Drupal\locale\LocaleString::getQuery().
+  */
+  public static function getQuery(array $conditions, array $fields = array(), array $options = array()) {
+    // Add default options and see which kind of join we need.
+    $options += array('target fields' => TRUE);
+    return parent::getQuery($conditions, $fields, $options);
+  }
+
+
+  /**
+   * Marks the string as new / not new.
+   *
+   * @param bool $is_new
+   *   (optional) Whether the string is new or not. Defatuls to TRUE.
+   *
+   * @return Drupal\locale\LocaleTranslation
+   *   The called object.
+   */
+  public function setNew($is_new = TRUE) {
+    $this->is_new = $is_new;
+    return $this;
+  }
+
+
+  /**
+   * Sets the string as customized / not customized.
+   *
+   * @param bool $customized
+   *   (optional) Whether the string is customized or not. Defaults to TRUE.
+   *
+   * @return Drupal\locale\LocaleTranslation
+   *   The called object.
+   */
+  public function setCustomized($customized = TRUE) {
+    $this->customized = $customized ? LOCALE_CUSTOMIZED : LOCALE_NOT_CUSTOMIZED;
+    return $this;
+  }
+
+
+  /**
+   * Implements Drupal\locale\LocaleString::getString().
+   */
+  public function getString() {
+    return $this->translation;
+  }
+
+
+  /**
+   * Implements Drupal\locale\LocaleString::setString().
+   *
+   * @return Drupal\locale\LocaleTranslation
+   *   The called object.
+   */
+  public function setString($string) {
+    $this->translation = $string;
+    return $this;
+  }
+
+
+  /**
+   * Implements Drupal\locale\LocaleString::isNew().
+   */
+  public function isNew() {
+    return $this->is_new;
+  }
+
+  /**
+   * Implements Drupal\locale\LocaleString::insert().
+   */
+  public function insert() {
+    $this->setDefaultValues(array(
+      'language' => '',
+      'translation' => '',
+      'customized' => LOCALE_NOT_CUSTOMIZED
+    ));
+    db_insert('locales_target')
+    ->fields($this->getFieldValues(array('lid', 'language', 'translation', 'customized')))
+    ->execute();
+    $this->setNew(FALSE);
+    return $this;
+  }
+
+  /**
+   * Implements Drupal\locale\LocaleString::update().
+   */
+  public function update() {
+    db_update('locales_target')
+    ->fields($this->getFieldValues(array('translation', 'customized')))
+    ->condition('lid', $this->lid)
+    ->condition('language', $this->language)
+    ->execute();
+    return $this;
+  }
+
+  /**
+   * Implements Drupal\locale\LocaleString::delete().
+   */
+  public function delete() {
+    if (!$this->isNew()) {
+      $query = db_delete('locales_target')
+        ->condition('lid', $this->lid)
+        ->condition('language', $this->language)
+        ->execute();
+      $this->setNew();
+    }
+    return $this;
+  }
+}
diff --git a/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php b/core/modules/locale/lib/Drupal/locale/PoDatabaseReader.php
index c0dfc2f..5cd89e8 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\LocaleTranslation;
+use PDO;
 
 /**
  * Gettext PO reader working with the locale module database.
@@ -109,47 +111,49 @@ class PoDatabaseReader implements PoReaderInterface {
   private function buildQuery() {
     $langcode = $this->_langcode;
     $options = $this->_options;
+    $fields = array('lid', 'source', 'context', 'location');
+    $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;
+      $fields[] = 'translation';
+      // 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'));
+      // If we don't want untranslated strings, use the 'untranslated' option.
+      if (!$options['not_translated']) {
+        $query_options['untranslated'] = FALSE;
+      }
+
+      $query = LocaleTranslation::getQuery($conditions, $fields, $query_options);
     }
     else {
-      $query->leftJoin('locales_target', 't', 's.lid = t.lid');
+      // If no language, we don't need any of the target fields.
+      $query = LocaleSource::getQuery($conditions, $fields, $query_options);
     }
-    $query->fields('s', array('lid', 'source', 'context', 'location'));
 
     $this->_result = $query->execute();
   }
diff --git a/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php b/core/modules/locale/lib/Drupal/locale/PoDatabaseWriter.php
index 33f05d9..0e96ca3 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\LocaleSource;
+use Drupal\locale\LocaleTranslation;
 
 /**
  * Gettext PO writer working with the locale module database.
@@ -226,12 +228,7 @@ 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 = LocaleTranslation::loadBySource($this->_langcode, $source, $context, array('lid', 'customized'));
 
     if (!empty($translation)) {
       // Skip this string unless it passes a check for dangerous code.
@@ -240,66 +237,44 @@ class PoDatabaseWriter implements PoWriterInterface {
         $this->_report['skips']++;
         return 0;
       }
-      elseif (isset($string->lid)) {
-        if (!isset($string->customized)) {
+      elseif ($string) {
+        $string->translation = $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();
+        $source_string = new LocaleSource(array('source' => $source, 'context' => $context));
+        $source_string->save();
+        $target = new LocaleTranslation($source_string);
+        $target->setValues(array(
+          'language' => $this->_langcode,
+          'translation' => $translation,
+          'customized' => $customized,
+        ));
+        $target->save();
 
         $this->_report['additions']++;
-        return $lid;
+        return $source_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();
-
+      $lid = $string->lid;
+      $string->delete();
       $this->_report['deletes']++;
-      return $string->lid;
+      return $lid;
     }
   }
 
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..cb6e9f1
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleStringTest.php
@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\locale\Tests\LocaleStringTest.
+ */
+
+namespace Drupal\locale\Tests;
+
+use Drupal\Core\Language\Language;
+use Drupal\locale\LocaleSource;
+use Drupal\locale\LocaleTranslation;
+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');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'String objects and data API',
+      'description' => 'Tests the locale string objects and data API.',
+      'group' => 'Locale',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+    // 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 = LocaleSource::loadById($source->lid);
+    $this->assertEqual($source, $string1, 'Successfully retrieved string by identifier.');
+    $string2 = LocaleSource::loadBySource($source->source, $source->context);
+    $this->assertEqual($source, $string2, 'Successfully retrieved string by source and context.');
+    $string3 = LocaleSource::loadBySource($source->source);
+    $this->assertFalse($string3, 'Cannot retrieve string with wrong context.');
+
+    // Check version handling and updating.
+    $this->assertEqual($source->version, 'none', 'String originally created without version.');
+    $source->checkVersion(VERSION);
+    $string = LocaleSource::loadById($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 = LocaleTranslation::loadById($langcode, $source->lid);
+    $this->assertEqual($string1->translation, $translation->translation, 'Successfully loaded translation by string identifier.');
+    $string2 = LocaleTranslation::loadBySource($langcode, $source->source, $source->context);
+    $this->assertEqual($string2->translation, $translation->translation, 'Successfully loaded translation by source and context.');
+    $translation
+      ->setCustomized()
+      ->save();
+    $translation = LocaleTranslation::loadById($langcode, $source->lid);
+    $this->assertEqual($translation->customized, LOCALE_CUSTOMIZED, 'Translation successfully marked as customized.');
+
+    // Delete translation.
+    $translation->delete();
+    $deleted = LocaleTranslation::loadById($langcode, $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 = LocaleTranslation::loadMultiple(array('lid' => $source->lid));
+    $this->assertEqual(count($search), 3 , 'Created and retrieved all translations for our source string.');
+
+    $source->delete();
+    $string = LocaleSource::loadById($lid);
+    $this->assertFalse($string, 'Successfully deleted source string.');
+    $deleted = $search = LocaleTranslation::loadMultiple(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 = LocaleSource::loadMultiple(array());
+    $this->assertEqual(count($strings), 3  , 'Found 3 source strings in the database.');
+    // Load all source strings matching a given string
+    $strings = LocaleSource::loadMultiple(array(), array(), array('string filter' => $prefix));
+    $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));
+    // Load all translations. For next queries we'll be loading only translated strings.
+    $only_translated = array('untranslated' => FALSE);
+    $only_untranslated = array('translated' => FALSE);
+    $translations = LocaleTranslation::loadMultiple(array(), array(), $only_translated);
+    $this->assertEqual(count($translations), 2 * $language_count  , 'Created and retrieved all translations for source strings.');
+
+    // Load all customized translations.
+    $translations = LocaleTranslation::loadMultiple(array('customized' => LOCALE_CUSTOMIZED), array(), $only_translated);
+    $this->assertEqual(count($translations), $language_count  , 'Retrieved all customized translations for source strings.');
+
+    // Load all Spanish customized translations
+    $translations = LocaleTranslation::loadMultiple(array('language' => 'es', 'customized' => LOCALE_CUSTOMIZED), array(), $only_translated);
+    $this->assertEqual(count($translations), 1  , 'Found only Spanish and customized translations.');
+
+    // Load all source strings without translation (1).
+    $translations = LocaleSource::loadMultiple(array(), array(), $only_untranslated);
+    $this->assertEqual(count($translations), 1  , 'Found 1 source string without translations.');
+
+    // Load Spanish translations using string filter.
+    $translations = LocaleTranslation::loadMultiple(array('language' => 'es'), array(), array('string filter' => $prefix));
+    $this->assertEqual(count($strings), 2  , 'Found 2 translations using some string filter.');
+
+  }
+
+  /**
+   * Creates random source string object.
+   */
+  function buildSourceString($values = array()) {
+    $values += array(
+      'source' => $this->randomName(100),
+      'context' => $this->randomName(20),
+    );
+    return new LocaleSource($values);
+  }
+
+  /**
+   * 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()) {
+    $values += array(
+      'lid' => $source->lid,
+      'language' => $langcode,
+      'translation' => $this->randomName(100),
+    );
+    $translation = new LocaleTranslation($values);
+    return $translation->save();
+  }
+}
diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc
index 5fdba76..bd42e0f 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\LocaleSource;
+use Drupal\locale\LocaleTranslation;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
@@ -18,42 +20,41 @@ 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\LocaleTranslation objects.
  */
-function locale_translate_query() {
+function locale_translate_query_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'));
+  $fields = array('source', 'location', 'context', 'lid', 'translation', 'language', 'customized');
+  $conditions = array('language' => $filter_values['langcode']);
+  $options = array('pager limit' => 30);
 
   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')
-    );
+    $options['string filter'] = $filter_values['string'];
   }
 
-  // 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;
+      $conditions['translation'] = NULL;
       break;
 
   }
 
-  $sql_query = $sql_query->extend('Drupal\Core\Database\Query\PagerSelectExtender')->limit(30);
-  return $sql_query->execute();
+  return LocaleTranslation::loadMultiple($conditions, $fields, $options);
 }
 
 /**
@@ -270,14 +271,16 @@ function locale_translate_edit_form($form, &$form_state) {
   );
 
   if (isset($langcode)) {
-    $strings = locale_translate_query();
+    $strings = locale_translate_query_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 LocaleSource($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 +393,9 @@ 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 empty if there's no translation.
+    $target = LocaleTranslation::loadById($langcode, $lid, array('translation'));
+
     // 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
+        ->setPlurals($translations['translations'])
+        ->setCustomized()
+        ->save();
     }
-    elseif (!empty($translation_old)) {
+    elseif (!$target->isNew()) {
       // 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.'));
