diff --git a/controller/tmgmt.controller.job.inc b/controller/tmgmt.controller.job.inc
new file mode 100644
index 0000000..b8c24b7
--- /dev/null
+++ b/controller/tmgmt.controller.job.inc
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @file
+ * Contains the job entity controller class.
+ */
+
+/**
+ * Controller class for the job entity.
+ *
+ * @ingroup tmgmt_job
+ */
+class TMGMTJobController extends EntityAPIController {
+
+  /**
+   * Overrides EntityAPIController::save().
+   */
+  public function save($entity, DatabaseTransaction $transaction = NULL) {
+    $entity->changed = REQUEST_TIME;
+    return parent::save($entity, $transaction);
+  }
+
+  /**
+   * Overrides EntityAPIController::delete().
+   */
+  public function delete($ids, $transaction = NULL) {
+    parent::delete($ids, $transaction);
+    // Since we are deleting one or multiple jobs here we also need to delete
+    // the attached job items and messages.
+    $query = new EntityFieldQuery();
+    $result = $query->entityCondition('entity_type', 'tmgmt_job_item')
+      ->propertyCondition('tjid', $ids)
+      ->execute();
+    if (!empty($result['tmgmt_job_item'])) {
+      $controller = entity_get_controller('tmgmt_job_item');
+      // We need to directly query the entity controller so we can pass on
+      // the transaction object.
+      $controller->delete(array_keys($result['tmgmt_job_item']), $transaction);
+    }
+    $query = new EntityFieldQuery();
+    $result = $query->entityCondition('entity_type', 'tmgmt_message')
+      ->propertyCondition('tjid', $ids)
+      ->execute();
+    if (!empty($result['tmgmt_message'])) {
+      $controller = entity_get_controller('tmgmt_message');
+      // We need to directly query the entity controller so we can pass on
+      // the transaction object.
+      $controller->delete(array_keys($result['tmgmt_message']), $transaction);
+    }
+    $query = new EntityFieldQuery();
+    $result = $query->entityCondition('entity_type', 'tmgmt_remote')
+      ->propertyCondition('tjid', $ids)
+      ->execute();
+    if (!empty($result['tmgmt_remote'])) {
+      $controller = entity_get_controller('tmgmt_remote');
+      $controller->delete(array_keys($result['tmgmt_remote']), $transaction);
+    }
+  }
+
+}
diff --git a/controller/tmgmt.controller.job_item.inc b/controller/tmgmt.controller.job_item.inc
new file mode 100644
index 0000000..02f4ffb
--- /dev/null
+++ b/controller/tmgmt.controller.job_item.inc
@@ -0,0 +1,134 @@
+<?php
+
+/**
+ * @file
+ * Contains the job item entity controller class.
+ */
+
+/**
+ * Controller class for the job item entity.
+ *
+ * @ingroup tmgmt_job
+ */
+class TMGMTJobItemController extends EntityAPIController {
+
+  /**
+   * Overrides EntityAPIController::save().
+   *
+   * @todo Eliminate the need to flatten and unflatten the JobItem data.
+   */
+  public function save($entity, DatabaseTransaction $transaction = NULL) {
+    $entity->changed = REQUEST_TIME;
+
+    // Consider everything accepted when the job item is accepted.
+    if ($entity->isAccepted()) {
+      $entity->count_pending = 0;
+      $entity->count_translated = 0;
+      $entity->count_reviewed = 0;
+      $entity->count_accepted = count(array_filter(tmgmt_flatten_data($entity->data), '_tmgmt_filter_data'));
+    }
+    // Count the data item states.
+    else {
+      // Reset counter values.
+      $entity->count_pending = 0;
+      $entity->count_translated = 0;
+      $entity->count_reviewed = 0;
+      $entity->count_accepted = 0;
+      $entity->word_count = 0;
+      $this->count($entity->data, $entity);
+    }
+    return parent::save($entity, $transaction);
+  }
+
+  /**
+   * Parse all data items recursively and sums up the counters for
+   * accepted, translated and pending items.
+   *
+   * @param $item
+   *   The current data item.
+   * @param $entity
+   *   The job item the count should be calculated.
+   */
+  protected function count(&$item, $entity) {
+    if (!empty($item['#text'])) {
+      if (_tmgmt_filter_data($item)) {
+
+        // Count words of the data item.
+        $entity->word_count += tmgmt_word_count($item['#text']);
+
+        // Set default states if no state is set.
+        if (!isset($item['#status'])) {
+          // Translation is present.
+          if (!empty($item['#translation'])) {
+            $item['#status'] = TMGMT_DATA_ITEM_STATE_TRANSLATED;
+          }
+          // No translation present.
+          else {
+            $item['#status'] = TMGMT_DATA_ITEM_STATE_PENDING;
+          }
+        }
+        switch ($item['#status']) {
+          case TMGMT_DATA_ITEM_STATE_REVIEWED:
+            $entity->count_reviewed++;
+            break;
+          case TMGMT_DATA_ITEM_STATE_TRANSLATED:
+            $entity->count_translated++;
+            break;
+          default:
+            $entity->count_pending++;
+            break;
+        }
+      }
+    }
+    else {
+      foreach (element_children($item) as $key) {
+        $this->count($item[$key], $entity);
+      }
+    }
+  }
+
+  /**
+   * Overrides EntityAPIController::delete().
+   */
+  public function delete($ids, $transaction = NULL) {
+    parent::delete($ids, $transaction);
+    // Since we are deleting one or multiple job items here we also need to
+    // delete the attached messages.
+    $query = new EntityFieldQuery();
+    $result = $query->entityCondition('entity_type', 'tmgmt_message')
+      ->propertyCondition('tjiid', $ids)
+      ->execute();
+    if (!empty($result['tmgmt_message'])) {
+      $controller = entity_get_controller('tmgmt_message');
+      // We need to directly query the entity controller so we can pass on
+      // the transaction object.
+      $controller->delete(array_keys($result['tmgmt_message']), $transaction);
+    }
+
+    $query = new EntityFieldQuery();
+    $result = $query->entityCondition('entity_type', 'tmgmt_remote')
+        ->propertyCondition('tjiid', $ids)
+        ->execute();
+    if (!empty($result['tmgmt_remote'])) {
+      $controller = entity_get_controller('tmgmt_remote');
+      $controller->delete(array_keys($result['tmgmt_remote']), $transaction);
+    }
+  }
+
+  /**
+   * Overrides EntityAPIController::invoke().
+   */
+  public function invoke($hook, $entity) {
+    // We need to check whether the state of the job is affected by this
+    // deletion.
+    if ($hook == 'delete' && $job = $entity->getJob()) {
+      // We only care for active jobs.
+      if ($job->isActive() && tmgmt_job_check_finished($job->tjid)) {
+        // Mark the job as finished.
+        $job->finished();
+      }
+    }
+    parent::invoke($hook, $entity);
+  }
+
+}
diff --git a/controller/tmgmt.controller.remote.inc b/controller/tmgmt.controller.remote.inc
new file mode 100644
index 0000000..7fa8015
--- /dev/null
+++ b/controller/tmgmt.controller.remote.inc
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * @file
+ * Contains the remote controller class.
+ */
+
+/**
+ * Controller class for the remote job mapping entity.
+ *
+ * @ingroup tmgmt_job
+ */
+class TMGMTRemoteController extends EntityAPIController {
+
+  public function load($ids = array(), $conditions = array()) {
+    $entities = parent::load($ids, $conditions);
+
+    foreach ($entities as &$entity) {
+      if (is_string($entity->remote_data)) {
+        $entity->remote_data = unserialize($entity->remote_data);
+      }
+    }
+
+    return $entities;
+  }
+
+  /**
+   * Loads remote mappings based on local data.
+   *
+   * @param int $tjid
+   *   Translation job id.
+   * @param int $tjiid
+   *   Translation job item id.
+   * @param int $data_item_key
+   *   Data item key.
+   *
+   * @return array
+   *   Array of TMGMTRemote entities.
+   */
+  function loadByLocalData($tjid = NULL, $tjiid = NULL, $data_item_key = NULL) {
+    $data_item_key = tmgmt_ensure_keys_string($data_item_key);
+
+    $query = new EntityFieldQuery();
+    $query->entityCondition('entity_type', 'tmgmt_remote');
+
+    if (!empty($tjid)) {
+      $query->propertyCondition('tjid', $tjid);
+    }
+    if (!empty($tjiid)) {
+      $query->propertyCondition('tjiid', $tjiid);
+    }
+    if (!empty($data_item_key)) {
+      $query->propertyCondition('data_item_key', $data_item_key);
+    }
+
+    $result = $query->execute();
+
+    if (isset($result['tmgmt_remote'])) {
+      return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
+    }
+
+    return array();
+  }
+
+  /**
+   * Loads remote mapping entities based on remote identifier.
+   *
+   * @param int $remote_identifier_1
+   * @param int $remote_identifier_2
+   * @param int $remote_identifier_3
+   *
+   * @return array
+   *   Array of TMGMTRemote entities.
+   */
+  function loadByRemoteIdentifier($remote_identifier_1 = NULL, $remote_identifier_2 = NULL, $remote_identifier_3 = NULL) {
+    $query = new EntityFieldQuery();
+    $query->entityCondition('entity_type', 'tmgmt_remote');
+
+    if ($remote_identifier_1 !== NULL) {
+      $query->propertyCondition('remote_identifier_1', $remote_identifier_1);
+    }
+    if ($remote_identifier_2 !== NULL) {
+      $query->propertyCondition('remote_identifier_2', $remote_identifier_2);
+    }
+    if ($remote_identifier_3 !== NULL) {
+      $query->propertyCondition('remote_identifier_3', $remote_identifier_3);
+    }
+
+    $result = $query->execute();
+
+    if (isset($result['tmgmt_remote'])) {
+      return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
+    }
+
+    return array();
+  }
+}
diff --git a/controller/tmgmt.controller.translator.inc b/controller/tmgmt.controller.translator.inc
new file mode 100644
index 0000000..c93a1fc
--- /dev/null
+++ b/controller/tmgmt.controller.translator.inc
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @file
+ * Contains the translator controller class.
+ */
+
+/**
+ * Controller class for the job entity.
+ *
+ * @ingroup tmgmt_translator
+ */
+class TMGMTTranslatorController extends EntityAPIControllerExportable {
+
+  /**
+   * Overrides EntityAPIControllerExportable::buildQuery().
+   */
+  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
+    $query = parent::buildQuery($ids, $conditions, $revision_id);
+    if ($plugins = tmgmt_translator_plugin_info()) {
+      $query->condition('plugin', array_keys($plugins));
+    }
+    else {
+      // Don't return any translators if no plugin exists.
+      $query->where('1 = 0');
+    }
+    // Sort by the weight of the translator.
+    $query->orderBy('weight');
+    return $query;
+  }
+
+  /**
+   * Overrides EntityAPIControllerExportable::delete().
+   */
+  public function delete($ids, DatabaseTransaction $transaction = NULL) {
+    $cids = array();
+    // We are never going to have many entities here, so we can risk a loop.
+    foreach ($ids as $key => $name) {
+      if (tmgmt_translator_busy($key)) {
+        // The translator can't be deleted because it is currently busy. Remove
+        // it from the ids so it wont get deleted in the parent implementation.
+        unset($ids[$key]);
+      }
+      else {
+        $cids[$key] = 'language:' . $key;
+      }
+    }
+    // Clear the language cache for the deleted translators.
+    cache_clear_all($cids, 'cache_tmgmt');
+    parent::delete($ids, $transaction);
+  }
+
+  /**
+  * Overrides EntityAPIControllerExportable::save().
+  */
+  public function save($entity, DatabaseTransaction $transaction = NULL) {
+    $return = parent::save($entity, $transaction);
+    // Clear the languages cache.
+    cache_clear_all('language:' . $entity->name, 'cache_tmgmt');
+    return $return;
+  }
+}
diff --git a/entity/tmgmt.entity.job.inc b/entity/tmgmt.entity.job.inc
new file mode 100644
index 0000000..86411d2
--- /dev/null
+++ b/entity/tmgmt.entity.job.inc
@@ -0,0 +1,770 @@
+<?php
+
+/*
+ * @file
+ * Contains job entity class.
+ */
+
+/**
+ * Entity class for the tmgmt_job entity.
+ *
+ * @ingroup tmgmt_job
+ */
+class TMGMTJob extends Entity {
+
+  /**
+   * Translation job identifier.
+   *
+   * @var integer
+   */
+  public $tjid;
+
+  /**
+   * A custom label for this job.
+   */
+  public $label;
+
+  /**
+   * Current state of the translation job
+   * @var type
+   */
+  public $state;
+
+  /**
+   * Language to be translated from.
+   *
+   * @var string
+   */
+  public $source_language;
+
+  /**
+   * Language into which the data needs to be translated.
+   *
+   * @var varchar
+   */
+  public $target_language;
+
+  /**
+   * Reference to the used translator of this job.
+   *
+   * @see TMGMTJob::getTranslatorController()
+   *
+   * @var string
+   */
+  public $translator;
+
+  /**
+   * Translator specific configuration and context information for this job.
+   *
+   * @var array
+   */
+  public $settings;
+
+  /**
+   * Remote identification of this job.
+   *
+   * @var integer
+   */
+  public $reference;
+
+  /**
+   * The time when the job was created as a timestamp.
+   *
+   * @var integer
+   */
+  public $created;
+
+  /**
+   * The time when the job was changed as a timestamp.
+   *
+   * @var integer
+   */
+  public $changed;
+
+  /**
+   * The user id of the creator of the job.
+   *
+   * @var integer
+   */
+  public $uid;
+
+  /**
+   * Overrides Entity::__construct().
+   */
+  public function __construct(array $values = array()) {
+    parent::__construct($values, 'tmgmt_job');
+    if (empty($this->tjid)) {
+      $this->created = REQUEST_TIME;
+    }
+    if (!isset($this->state)) {
+      $this->state = TMGMT_JOB_STATE_UNPROCESSED;
+    }
+  }
+
+  /**
+   * Overrides Entity::defaultLabel().
+   */
+  public function defaultLabel() {
+    // In some cases we might have a user-defined label.
+    if (!empty($this->label)) {
+      return $this->label;
+    }
+
+    $items = $this->getItems();
+    $count = count($items);
+    if ($count > 0) {
+      $t_args = array('@title' => reset($items)->getSourceLabel(), '@more' => $count - 1);
+      return format_plural($count, '@title', '@title and @more more', $t_args);
+    }
+    else {
+      $wrapper = entity_metadata_wrapper($this->entityType, $this);
+      $source = $wrapper->source_language->label();
+      if (empty($source)) {
+        $source = '?';
+      }
+      $target = $wrapper->target_language->label();
+      if (empty($target)) {
+        $target = '?';
+      }
+      return t('From @source to @target', array('@source' => $source, '@target' => $target));
+    }
+  }
+
+  /**
+   * Overrides Entity::defaultUri().
+   */
+  public function defaultUri() {
+    return array('path' => 'admin/config/regional/tmgmt/jobs/' . $this->tjid);
+  }
+
+  /**
+   * Overrides Entity::buildContent().
+   */
+  public function buildContent($view_mode = 'full', $langcode = NULL) {
+    $content = array();
+    if (module_exists('tmgmt_ui')) {
+      $content = entity_ui_get_form('tmgmt_job', $this);
+    }
+    return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
+  }
+
+  /**
+   * Adds an item to the translation job.
+   *
+   * @param $plugin
+   *   The plugin name.
+   * @param $item_type
+   *   The source item type.
+   * @param $item_id
+   *   The source item id.
+   *
+   * @return TMGMTJobItem
+   *   The job item that was added to the job or FALSE if it couldn't be saved.
+   * @throws TMGMTException
+   *   On zero item word count.
+   */
+  public function addItem($plugin, $item_type, $item_id) {
+
+    $transaction = db_transaction();
+    $is_new = FALSE;
+
+    if (empty($this->tjid)) {
+      $this->save();
+      $is_new = TRUE;
+    }
+
+    $item = tmgmt_job_item_create($plugin, $item_type, $item_id, array('tjid' => $this->tjid));
+    // Initialize job item data variable needed to determine word count
+    // in job item getWordCount().
+    $item->getData();
+    $item->save();
+
+    if ($item->getWordCount() == 0) {
+      $transaction->rollback();
+
+      // In case we got word count 0 for the first job item, NULL tjid so that
+      // if there is another addItem() call the rolled back job object will get
+      // persisted.
+      if ($is_new) {
+        $this->tjid = NULL;
+      }
+
+      throw new TMGMTException('Created job item with word count 0. Plugin: @plugin | Item type: @item_type | Item id: @item_id',
+        array('@plugin' => $plugin, '@item_type' => $item_type, '@item_id' => $item_id));
+    }
+
+    return $item;
+  }
+
+  /**
+   * Add a log message for this job.
+   *
+   * @param $message
+   *   The message to store in the log. Keep $message translatable by not
+   *   concatenating dynamic values into it! Variables in the message should be
+   *   added by using placeholder strings alongside the variables argument to
+   *   declare the value of the placeholders. See t() for documentation on how
+   *   $message and $variables interact.
+   * @param $variables
+   *   (Optional) An array of variables to replace in the message on display.
+   * @param $type
+   *   (Optional) The type of the message. Can be one of 'status', 'error',
+   *   'warning' or 'debug'. Messages of the type 'debug' will not get printed
+   *   to the screen.
+   */
+  public function addMessage($message, $variables = array(), $type = 'status') {
+    // Save the job if it hasn't yet been saved.
+    if (!empty($this->tjid) || $this->save()) {
+      $message = tmgmt_message_create($message, $variables, array(
+        'tjid' => $this->tjid,
+        'type' => $type,
+        'uid' => $GLOBALS['user']->uid,
+      ));
+      if ($message->save()) {
+        return $message;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Returns all job items attached to this job.
+   *
+   * @return array
+   *   An array of translation job items.
+   */
+  public function getItems($conditions = array()) {
+    $query = new EntityFieldQuery();
+    $query->entityCondition('entity_type', 'tmgmt_job_item');
+    $query->propertyCondition('tjid', $this->tjid);
+    foreach ($conditions as $key => $condition) {
+      if (is_array($condition)) {
+        $operator = isset($condition['operator']) ? $condition['operator'] : '=';
+        $query->propertyCondition($key, $condition['value'], $operator);
+      }
+      else {
+        $query->propertyCondition($key, $condition);
+      }
+    }
+    $results = $query->execute();
+    if (!empty($results['tmgmt_job_item'])) {
+      return entity_load('tmgmt_job_item', array_keys($results['tmgmt_job_item']));
+    }
+    return array();
+  }
+
+  /**
+   * Returns all job messages attached to this job.
+   *
+   * @return array
+   *   An array of translation job messages.
+   */
+  public function getMessages($conditions = array()) {
+    $query = new EntityFieldQuery();
+    $query->entityCondition('entity_type', 'tmgmt_message');
+    $query->propertyCondition('tjid', $this->tjid);
+    foreach ($conditions as $key => $condition) {
+      if (is_array($condition)) {
+        $operator = isset($condition['operator']) ? $condition['operator'] : '=';
+        $query->propertyCondition($key, $condition['value'], $operator);
+      }
+      else {
+        $query->propertyCondition($key, $condition);
+      }
+    }
+    $results = $query->execute();
+    if (!empty($results['tmgmt_message'])) {
+      return entity_load('tmgmt_message', array_keys($results['tmgmt_message']));
+    }
+    return array();
+  }
+
+  /**
+   * Returns all job messages attached to this job with timestamp newer than
+   * $time.
+   *
+   * @param $time
+   *   (Optional) Messages need to have a newer timestamp than $time. Defaults
+   *   to REQUEST_TIME.
+   *
+   * @return array
+   *   An array of translation job messages.
+   */
+  public function getMessagesSince($time = NULL) {
+    $time = isset($time) ? $time : REQUEST_TIME;
+    $conditions = array('created' => array('value' => $time, 'operator' => '>='));
+    return $this->getMessages($conditions);
+  }
+
+  /**
+   * Retrieves a setting value from the job settings. Pulls the default values
+   * (if defined) from the plugin controller.
+   *
+   * @param $name
+   *   The name of the setting.
+   *
+   * @return
+   *   The setting value or $default if the setting value is not set. Returns
+   *   NULL if the setting does not exist at all.
+   */
+  public function getSetting($name) {
+    if (isset($this->settings[$name])) {
+      return $this->settings[$name];
+    }
+    // The translator might provide default settings.
+    if ($translator = $this->getTranslator()) {
+      if (($setting = $translator->getSetting($name)) !== NULL) {
+        return $setting;
+      }
+    }
+    if ($controller = $this->getTranslatorController()) {
+      $defaults = $controller->defaultSettings();
+      if (isset($defaults[$name])) {
+        return $defaults[$name];
+      }
+    }
+  }
+
+  /**
+   * Returns the translator for this job.
+   *
+   * @return TMGMTTranslator
+   *   The translator entity or FALSE if there was a problem.
+   */
+  public function getTranslator() {
+    if (isset($this->translator)) {
+      return tmgmt_translator_load($this->translator);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Returns the state of the job. Can be one of the job state constants.
+   *
+   * @return integer
+   *   The state of the job or NULL if it hasn't been set yet.
+   */
+  public function getState() {
+    // We don't need to check if the state is actually set because we always set
+    // it in the constructor.
+    return $this->state;
+  }
+
+  /**
+   * Updates the state of the job.
+   *
+   * @param $state
+   *   The new state of the job. Has to be one of the job state constants.
+   * @param $message
+   *   (Optional) The log message to be saved along with the state change.
+   * @param $variables
+   *   (Optional) An array of variables to replace in the message on display.
+   *
+   * @return int
+   *   The updated state of the job if it could be set.
+   *
+   * @see TMGMTJob::addMessage()
+   */
+  public function setState($state, $message = NULL, $variables = array(), $type = 'debug') {
+    // Return TRUE if the state could be set. Return FALSE otherwise.
+    if (array_key_exists($state, tmgmt_job_states())) {
+      $this->state = $state;
+      $this->save();
+      // If a message is attached to this state change add it now.
+      if (!empty($message)) {
+        $this->addMessage($message, $variables, $type);
+      }
+    }
+    return $this->state;
+  }
+
+  /**
+   * Checks whether the passed value matches the current state.
+   *
+   * @param $state
+   *   The value to check the current state against.
+   *
+   * @return boolean
+   *   TRUE if the passed state matches the current state, FALSE otherwise.
+   */
+  public function isState($state) {
+    return $this->getState() == $state;
+  }
+
+  /**
+   * Checks whether the user described by $account is the author of this job.
+   *
+   * @param $account
+   *   (Optional) A user object. Defaults to the currently logged in user.
+   */
+  public function isAuthor($account = NULL) {
+    $account = isset($account) ? $account : $GLOBALS['user'];
+    return $this->uid == $account->uid;
+  }
+
+  /**
+   * Returns whether the state of this job is 'unprocessed'.
+   *
+   * @return boolean
+   *   TRUE if the state is 'unprocessed', FALSE otherwise.
+   */
+  public function isUnprocessed() {
+    return $this->isState(TMGMT_JOB_STATE_UNPROCESSED);
+  }
+
+  /**
+   * Returns whether the state of this job is 'cancelled'.
+   *
+   * @return boolean
+   *   TRUE if the state is 'cancelled', FALSE otherwise.
+   */
+  public function isCancelled() {
+    return $this->isState(TMGMT_JOB_STATE_CANCELLED);
+  }
+
+  /**
+   * Returns whether the state of this job is 'active'.
+   *
+   * @return boolean
+   *   TRUE if the state is 'active', FALSE otherwise.
+   */
+  public function isActive() {
+    return $this->isState(TMGMT_JOB_STATE_ACTIVE);
+  }
+
+  /**
+   * Returns whether the state of this job is 'rejected'.
+   *
+   * @return boolean
+   *   TRUE if the state is 'rejected', FALSE otherwise.
+   */
+  public function isRejected() {
+    return $this->isState(TMGMT_JOB_STATE_REJECTED);
+  }
+
+  /**
+   * Returns whether the state of this jon is 'finished'.
+   *
+   * @return boolean
+   *   TRUE if the state is 'finished', FALSE otherwise.
+   */
+  public function isFinished() {
+    return $this->isState(TMGMT_JOB_STATE_FINISHED);
+  }
+
+  /**
+   * Checks whether a job is translatable.
+   *
+   * @return boolean
+   *   TRUE if the job can be translated, FALSE otherwise.
+   */
+  public function isTranslatable() {
+    if ($translator = $this->getTranslator()) {
+      if ($translator->canTranslate($this)) {
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Checks whether a job is cancelable.
+   *
+   * @return boolean
+   *   TRUE if the job can be cancelled, FALSE otherwise.
+   */
+  public function isCancelable() {
+    // Only non-submitted translation jobs can be cancelled.
+    return $this->isActive();
+  }
+
+  /**
+   * Checks whether a job is submittable.
+   *
+   * @return boolean
+   *   TRUE if the job can be submitted, FALSE otherwise.
+   */
+  public function isSubmittable() {
+    return $this->isUnprocessed() || $this->isRejected() || $this->isCancelled();
+  }
+
+  /**
+   * Checks whether a job is deletable.
+   *
+   * @return boolean
+   *   TRUE if the job can be deleted, FALSE otherwise.
+   */
+  public function isDeletable() {
+    return !$this->isActive();
+  }
+
+  /**
+   * Set the state of the job to 'submitted'.
+   *
+   * @param $message
+   *   The log message to be saved along with the state change.
+   * @param $variables
+   *   (Optional) An array of variables to replace in the message on display.
+   *
+   * @return TMGMTJob
+   *   The job entity.
+   *
+   * @see TMGMTJob::addMessage()
+   */
+  public function submitted($message = NULL, $variables = array(), $type = 'status') {
+    if (!isset($message)) {
+      $message = 'The translation job has been submitted.';
+    }
+    $this->setState(TMGMT_JOB_STATE_ACTIVE, $message, $variables, $type);
+  }
+
+  /**
+   * Set the state of the job to 'finished'.
+   *
+   * @param $message
+   *   The log message to be saved along with the state change.
+   * @param $variables
+   *   (Optional) An array of variables to replace in the message on display.
+   *
+   * @return TMGMTJob
+   *   The job entity.
+   *
+   * @see TMGMTJob::addMessage()
+   */
+  public function finished($message = NULL, $variables = array(), $type = 'status') {
+    if (!isset($message)) {
+      $message = 'The translation job has been finished.';
+    }
+    return $this->setState(TMGMT_JOB_STATE_FINISHED, $message, $variables, $type);
+  }
+
+  /**
+   * Sets the state of the job to 'cancelled'.
+   *
+   * @param $message
+   *   The log message to be saved along with the state change.
+   * @param $variables
+   *   (Optional) An array of variables to replace in the message on display.
+   *
+   * Use TMGMTJob::cancelTranslation() to cancel a translation.
+   *
+   * @return TMGMTJob
+   *   The job entity.
+   *
+   * @see TMGMTJob::addMessage()
+   */
+  public function cancelled($message = NULL, $variables = array(), $type = 'status') {
+    if (!isset($message)) {
+      $message = 'The translation job has been cancelled.';
+    }
+    return $this->setState(TMGMT_JOB_STATE_CANCELLED, $message, $variables, $type);
+  }
+
+  /**
+   * Sets the state of the job to 'rejected'.
+   *
+   * @param $message
+   *   The log message to be saved along with the state change.
+   * @param $variables
+   *   (Optional) An array of variables to replace in the message on display.
+   *
+   * @return TMGMTJob
+   *   The job entity.
+   *
+   * @see TMGMTJob::addMessage()
+   */
+  public function rejected($message = NULL, $variables = array(), $type = 'error') {
+    if (!isset($message)) {
+      $message = 'The translation job has been rejected by the translation provider.';
+    }
+    return $this->setState(TMGMT_JOB_STATE_REJECTED, $message, $variables, $type);
+  }
+
+  /**
+   * Request the translation of a job from the translator.
+   *
+   * @return integer
+   *   The updated job status.
+   */
+  public function requestTranslation() {
+    if (!$this->isTranslatable() || !$controller = $this->getTranslatorController()) {
+      return FALSE;
+    }
+    // We don't know if the translator plugin already processed our
+    // translation request after this point. That means that the plugin has to
+    // set the 'submitted', 'needs review', etc. states on its own.
+    $controller->requestTranslation($this);
+  }
+
+  /**
+   * Attempts to cancel the translation job. Already accepted jobs can not be
+   * cancelled, submitted jobs only if supported by the translator plugin.
+   * Always use this method if you want to cancel a translation job.
+   *
+   * @return boolean
+   *   TRUE if the translation job was cancelled, FALSE otherwise.
+   */
+  public function cancelTranslation() {
+    if (!$this->isCancelable() || !$controller = $this->getTranslatorController()) {
+      return FALSE;
+    }
+    // We don't know if the translator plugin was able to cancel the translation
+    // job after this point. That means that the plugin has to set the
+    // 'cancelled' state on its own.
+    $controller->cancelTranslation($this);
+  }
+
+  /**
+   * Returns the translator plugin controller of the translator of this job.
+   *
+   * @return TMGMTTranslatorPluginControllerInterface
+   *   The controller of the translator plugin.
+   */
+  public function getTranslatorController() {
+    if ($translator = $this->getTranslator($this)) {
+      return $translator->getController();
+    }
+    return FALSE;
+  }
+
+  /**
+   * Returns the source data of all job items.
+   *
+   * @param $key
+   *   If present, only the subarray identified by key is returned.
+   * @param $index
+   *   Optional index of an attribute below $key.
+   * @return array
+   *   A nested array with the source data where the most upper key is the job
+   *   item id.
+   */
+  public function getData(array $key = array(), $index = null) {
+    $data = array();
+    if (!empty($key)) {
+      $tjiid = array_shift($key);
+      $job_item = entity_load_single('tmgmt_job_item', $tjiid);
+      if ($job_item) {
+        $data[$tjiid] = $job_item->getData($key, $index);
+      }
+    }
+    else {
+      foreach ($this->getItems() as $key => $item) {
+        $data[$key] = $item->getData();
+      }
+    }
+    return $data;
+  }
+
+  /**
+   * Sums up all pending counts of this jobs job items.
+   *
+   * @return
+   *   The sum of all pending counts
+   */
+  public function getCountPending() {
+    return tmgmt_job_statistic($this, 'count_pending');
+  }
+
+  /**
+   * Sums up all translated counts of this jobs job items.
+   *
+   * @return
+   *   The sum of all translated counts
+   */
+  public function getCountTranslated() {
+    return tmgmt_job_statistic($this, 'count_translated');
+  }
+
+  /**
+   * Sums up all accepted counts of this jobs job items.
+   *
+   * @return
+   *   The sum of all accepted data items.
+   */
+  public function getCountAccepted() {
+    return tmgmt_job_statistic($this, 'count_accepted');
+  }
+
+  /**
+   * Sums up all accepted counts of this jobs job items.
+   *
+   * @return
+   *   The sum of all accepted data items.
+   */
+  public function getCountReviewed() {
+    return tmgmt_job_statistic($this, 'count_reviewed');
+  }
+
+  /**
+   * Sums up all word counts of this jobs job items.
+   *
+   * @return
+   *   The total word count of this job.
+   */
+  public function getWordCount() {
+    return tmgmt_job_statistic($this, 'word_count');
+  }
+
+  /**
+   * Store translated data back into the items.
+   *
+   * @param $data
+   *   Partially or complete translated data, the most upper key needs to be
+   *   the translation job item id.
+   * @param $key
+   *   (Optional) Either a flattened key (a 'key1][key2][key3' string) or a nested
+   *   one, e.g. array('key1', 'key2', 'key2'). Defaults to an empty array which
+   *   means that it will replace the whole translated data array. The most
+   *   upper key entry needs to be the job id (tjiid).
+   */
+  public function addTranslatedData($data, $key = NULL) {
+    $key = tmgmt_ensure_keys_array($key);
+    $items = $this->getItems();
+    // If there is a key, get the specific item and forward the call.
+    if (!empty($key)) {
+      $item_id = array_shift($key);
+      if (isset($items[$item_id])) {
+        $items[$item_id]->addTranslatedData($data, $key);
+      }
+    }
+    else {
+      foreach ($data as $key => $value) {
+        if (isset($items[$key])) {
+          $items[$key]->addTranslatedData($value);
+        }
+      }
+    }
+  }
+
+  /**
+   * Propagates the returned job item translations to the sources.
+   *
+   * @return boolean
+   *   TRUE if we were able to propagate the translated data, FALSE otherwise.
+   */
+  public function acceptTranslation() {
+    foreach ($this->getItems() as $item) {
+      $item->acceptTranslation();
+    }
+  }
+
+  /**
+   * Gets remote mappings for current job.
+   *
+   * @return array
+   *   List of TMGMTRemote entities.
+   */
+  public function getRemoteMappings() {
+    $query = new EntityFieldQuery();
+    $query->entityCondition('entity_type', 'tmgmt_remote');
+    $query->propertyCondition('tjid', $this->tjid);
+    $result = $query->execute();
+
+    if (isset($result['tmgmt_remote'])) {
+      return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
+    }
+
+    return array();
+  }
+
+}
diff --git a/entity/tmgmt.entity.job_item.inc b/entity/tmgmt.entity.job_item.inc
new file mode 100644
index 0000000..4eeb014
--- /dev/null
+++ b/entity/tmgmt.entity.job_item.inc
@@ -0,0 +1,740 @@
+<?php
+
+/*
+ * @file
+ * Contains job item entity class.
+ */
+
+/**
+ * Entity class for the tmgmt_job entity.
+ *
+ * @ingroup tmgmt_job
+ */
+class TMGMTJobItem extends Entity {
+
+  /**
+   * The source plugin that provides the item.
+   *
+   * @var varchar
+   */
+  public $plugin;
+
+  /**
+   * The identifier of the translation job.
+   *
+   * @var integer
+   */
+  public $tjid;
+
+  /**
+   * The identifier of the translation job item.
+   *
+   * @var integer
+   */
+  public $tjiid;
+
+  /**
+   * Type of this item, used by the plugin to identify it.
+   *
+   * @var string
+   */
+  public $item_type;
+
+  /**
+   * Id of the item.
+   *
+   * @var integer
+   */
+  public $item_id;
+
+  /**
+   * The time when the job item was changed as a timestamp.
+   *
+   * @var integer
+   */
+  public $changed;
+
+  /**
+   * Can be used by the source plugin to store the data instead of creating it
+   * on demand.
+   *
+   * If additional information is added in the UI, like adding comments, it will
+   * also be saved here.
+   *
+   * Always use TMGMTJobItem::getData() to load the data, which will use
+   * this property if present and otherwise get it from the source.
+   *
+   * @var array
+   */
+  public $data = array();
+
+  /**
+   * Counter for all data items waiting for translation.
+   *
+   * @var integer
+   */
+  public $count_pending = 0;
+
+  /**
+   * Counter for all translated data items.
+   *
+   * @var integer
+   */
+  public $count_translated = 0;
+
+  /**
+   * Counter for all accepted data items.
+   *
+   * @var integer
+   */
+  public $count_accepted = 0;
+
+  /**
+   * Counter for all reviewed data items.
+   *
+   * @var integer
+   */
+  public $count_reviewed = 0;
+
+  /**
+   * Amount of words in this job item.
+   *
+   * @var integer
+   */
+  public $word_count = 0;
+
+  /**
+   * Overrides Entity::__construct().
+   */
+  public function __construct(array $values = array()) {
+    parent::__construct($values, 'tmgmt_job_item');
+    if (!isset($this->state)) {
+      $this->state = TMGMT_JOB_ITEM_STATE_ACTIVE;
+    }
+  }
+
+  /**
+   * Overrides Entity::defaultLabel()
+   */
+  public function defaultLabel() {
+    if ($controller = $this->getSourceController()) {
+      return t('Translation for @label', array('@label' => $controller->getLabel($this)));
+    }
+    return parent::defaultLabel();
+  }
+
+  /**
+   * Overrides Entity::defaultUri()
+   *
+   * @see _tmgmt_ui_breadcrumb()
+   */
+  public function defaultUri() {
+    // The path of a job item is not directly below the job that it belongs to.
+    // Having to maintain two unknowns / wildcards (job and job item) in the
+    // path is more complex than it has to be. Instead we just append the
+    // additional breadcrumb pieces manually with _tmgmt_ui_breadcrumb().
+    return array('path' => 'admin/config/regional/tmgmt/items/' . $this->tjiid);
+  }
+
+  /**
+   * Overrides Entity::buildContent().
+   */
+  public function buildContent($view_mode = 'full', $langcode = NULL) {
+    $content = array();
+    if (module_exists('tmgmt_ui')) {
+      $content = tmgmt_ui_job_item_review($this);
+    }
+    return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
+  }
+
+  /**
+   * Add a log message for this job item.
+   *
+   * @param $message
+   *   The message to store in the log. Keep $message translatable by not
+   *   concatenating dynamic values into it! Variables in the message should be
+   *   added by using placeholder strings alongside the variables argument to
+   *   declare the value of the placeholders. See t() for documentation on how
+   *   $message and $variables interact.
+   * @param $variables
+   *   (Optional) An array of variables to replace in the message on display.
+   * @param $type
+   *   (Optional) The type of the message. Can be one of 'status', 'error',
+   *   'warning' or 'debug'. Messages of the type 'debug' will not get printed
+   *   to the screen.
+   */
+  public function addMessage($message, $variables = array(), $type = 'status') {
+    // Save the job item if it hasn't yet been saved.
+    if (!empty($this->tjiid) || $this->save()) {
+      $message = tmgmt_message_create($message, $variables, array(
+        'tjid' => $this->tjid,
+        'tjiid' => $this->tjiid,
+        'uid' => $GLOBALS['user']->uid,
+        'type' => $type,
+      ));
+      if ($message->save()) {
+        return $message;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Retrieves the label of the source object via the source controller.
+   *
+   * @return
+   *   The label of the source object.
+   */
+  public function getSourceLabel() {
+    if ($controller = $this->getSourceController()) {
+      return $controller->getLabel($this);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Retrieves the path to the source object via the source controller.
+   *
+   * @return
+   *   The path to the source object.
+   */
+  public function getSourceUri() {
+    if ($controller = $this->getSourceController()) {
+      return $controller->getUri($this);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Loads the job entity that this job item is attached to.
+   *
+   * @return TMGMTJob
+   *   The job entity that this job item is attached to or FALSE if there was
+   *   a problem.
+   */
+  public function getJob() {
+    if (!empty($this->tjid)) {
+      return tmgmt_job_load($this->tjid);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Returns the translator for this job item.
+   *
+   * @return TMGMTTranslator
+   *   The translator entity or FALSE if there was a problem.
+   */
+  public function getTranslator() {
+    if ($job = $this->getJob()) {
+      return $job->getTranslator();
+    }
+    return FALSE;
+  }
+
+  /**
+   * Returns the translator plugin controller of the translator of this job item.
+   *
+   * @return TMGMTTranslatorPluginControllerInterface
+   *   The controller of the translator plugin or FALSE if there was a problem.
+   */
+  public function getTranslatorController() {
+    if ($job = $this->getJob()) {
+      return $job->getTranslatorController();
+    }
+    return FALSE;
+  }
+
+  /**
+   * Array of the data to be translated.
+   *
+   * The structure is similar to the form API in the way that it is a possibly
+   * nested array with the following properties whose presence indicate that the
+   * current element is a text that might need to be translated.
+   *
+   * - #text: The text to be translated.
+   * - #label: (Optional) The label that might be shown to the translator.
+   * - #comment: (Optional) A comment with additional information.
+   * - #translate: (Optional) If set to FALSE the text will not be translated.
+   * - #translation: The translated data. Set by the translator plugin.
+   *
+   * The key can be an alphanumeric string.
+   * @param $key
+   *   If present, only the subarray identified by key is returned.
+   * @param $index
+   *   Optional index of an attribute below $key.
+   *
+   * @return array
+   *   A structured data array.
+   */
+  public function getData(array $key = array(), $index = null) {
+    if (empty($this->data)) {
+      // Load the data from the source if it has not been set yet.
+      $this->data = $this->getSourceData();
+      $this->save();
+    }
+    if (empty($key)) {
+      return $this->data;
+    }
+    if ($index) {
+      $key = array_merge($key, array($index));
+    }
+    return drupal_array_get_nested_value($this->data, $key);
+  }
+
+  /**
+   * Loads the structured source data array from the source.
+   */
+  public function getSourceData() {
+    if ($controller = $this->getSourceController()) {
+      return $controller->getData($this);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Returns the plugin controller of the configured plugin.
+   *
+   * @return TMGMTSourcePluginControllerInterface
+   */
+  public function getSourceController() {
+    if (!empty($this->plugin)) {
+      return tmgmt_source_plugin_controller($this->plugin);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Count of all pending data items
+   *
+   * @return
+   *   Pending counts
+   */
+  public function getCountPending() {
+    return $this->count_pending;
+  }
+
+  /**
+   * Count of all translated data items.
+   *
+   * @return
+   *   Translated count
+   */
+  public function getCountTranslated() {
+    return $this->count_translated;
+  }
+
+  /**
+   * Count of all accepted data items.
+   *
+   * @return
+   *   Accepted count
+   */
+  public function getCountAccepted() {
+    return $this->count_accepted;
+  }
+
+  /**
+   * Count of all accepted data items.
+   *
+   * @return
+   *   Accepted count
+   */
+  public function getCountReviewed() {
+    return $this->count_reviewed;
+  }
+
+  /**
+   * Word count of all data items.
+   *
+   * @return
+   *   Word count
+   */
+  public function getWordCount() {
+    return (int)$this->word_count;
+  }
+
+  /**
+   * Sets the state of the job item to 'needs review'.
+   */
+  public function needsReview($message = NULL, $variables = array(), $type = 'status') {
+    if (!isset($message)) {
+      $uri = $this->getSourceUri();
+      $message = 'The translation for !source needs to be reviewed.';
+      $variables = array('!source' => l($this->getSourceLabel(), $uri['path']));
+    }
+    $return = $this->setState(TMGMT_JOB_ITEM_STATE_REVIEW, $message, $variables, $type);
+    // Auto accept the trganslation if the translator is configured for it.
+    if ($this->getTranslator()->getSetting('auto_accept')) {
+      $this->acceptTranslation();
+    }
+    return $return;
+  }
+
+  /**
+   * Sets the state of the job item to 'accepted'.
+   */
+  public function accepted($message = NULL, $variables = array(), $type = 'status') {
+    if (!isset($message)) {
+      $uri = $this->getSourceUri();
+      $message = 'The translation for !source has been accepted.';
+      $variables = array('!source' => l($this->getSourceLabel(), $uri['path']));
+    }
+    $return = $this->setState(TMGMT_JOB_ITEM_STATE_ACCEPTED, $message, $variables, $type);
+    // Check if this was the last unfinished job item in this job.
+    if (tmgmt_job_check_finished($this->tjid) && $job = $this->getJob()) {
+      // Mark the job as finished.
+      $job->finished();
+    }
+    return $return;
+  }
+
+  /**
+   * Sets the state of the job item to 'active'.
+   */
+  public function active($message = NULL, $variables = array(), $type = 'status') {
+    if (!isset($message)) {
+      $uri = $this->getSourceUri();
+      $message = 'The translation for !source is now being processed.';
+      $variables = array('!source' => l($this->getSourceLabel(), $uri['path']));
+    }
+    return $this->setState(TMGMT_JOB_ITEM_STATE_ACTIVE, $message, $variables, $type);
+  }
+
+  /**
+   * Updates the state of the job item.
+   *
+   * @param $state
+   *   The new state of the job item. Has to be one of the job state constants.
+   * @param $message
+   *   (Optional) The log message to be saved along with the state change.
+   * @param $variables
+   *   (Optional) An array of variables to replace in the message on display.
+   *
+   * @return int
+   *   The updated state of the job if it could be set.
+   *
+   * @see TMGMTJob::addMessage()
+   */
+  public function setState($state, $message = NULL, $variables = array(), $type = 'debug') {
+    // Return TRUE if the state could be set. Return FALSE otherwise.
+    if (array_key_exists($state, tmgmt_job_item_states()) && $this->state != $state) {
+      $this->state = $state;
+      $this->save();
+      // If a message is attached to this state change add it now.
+      if (!empty($message)) {
+        $this->addMessage($message, $variables, $type);
+      }
+    }
+    return $this->state;
+  }
+
+  /**
+   * Returns the state of the job item. Can be one of the job item state
+   * constants.
+   *
+   * @return integer
+   *   The state of the job item.
+   */
+  public function getState() {
+    // We don't need to check if the state is actually set because we always set
+    // it in the constructor.
+    return $this->state;
+  }
+
+  /**
+   * Checks whether the passed value matches the current state.
+   *
+   * @param $state
+   *   The value to check the current state against.
+   *
+   * @return boolean
+   *   TRUE if the passed state matches the current state, FALSE otherwise.
+   */
+  public function isState($state) {
+    return $this->getState() == $state;
+  }
+
+  /**
+   * Checks whether the state of this transaction is 'accepted'.
+   *
+   * @return boolean
+   *   TRUE if the state is 'accepted', FALSE otherwise.
+   */
+  public function isAccepted() {
+    return $this->isState(TMGMT_JOB_ITEM_STATE_ACCEPTED);
+  }
+
+  /**
+   * Checks whether the state of this transaction is 'active'.
+   *
+   * @return boolean
+   *   TRUE if the state is 'active', FALSE otherwise.
+   */
+  public function isActive() {
+    return $this->isState(TMGMT_JOB_ITEM_STATE_ACTIVE);
+  }
+
+  /**
+   * Checks whether the state of this transaction is 'needs review'.
+   *
+   * @return boolean
+   *   TRUE if the state is 'needs review', FALSE otherwise.
+   */
+  public function isNeedsReview() {
+    return $this->isState(TMGMT_JOB_ITEM_STATE_REVIEW);
+  }
+
+  /**
+   * Recursively writes translated data to the data array of a job item.
+   *
+   * While doing this the #status of each data item is set to
+   * TMGMT_DATA_ITEM_STATE_TRANSLATED.
+   *
+   * @param $translation
+   *   Nested array of translated data. Can either be a single text entry, the
+   *   whole data structure or parts of it.
+   * @param $key
+   *   (Optional) Either a flattened key (a 'key1][key2][key3' string) or a nested
+   *   one, e.g. array('key1', 'key2', 'key2'). Defaults to an empty array which
+   *   means that it will replace the whole translated data array.
+   */
+  protected function addTranslatedDataRecursive($translation, array $key = array()) {
+    if (isset($translation['#text'])) {
+      $status = $this->getData($key, '#status');
+      if (!$status || $status == TMGMT_DATA_ITEM_STATE_PENDING) {
+        $values = array(
+          '#translation' => $translation,
+          '#status' => TMGMT_DATA_ITEM_STATE_TRANSLATED,
+        );
+        $this->updateData($key, $values);
+      }
+      return;
+    }
+    foreach (element_children($translation) as $item) {
+      $this->addTranslatedDataRecursive($translation[$item], array_merge($key, array($item)));
+    }
+  }
+
+  /**
+   * Updates the values for a specific substructure in the data array.
+   *
+   * The values are either set or updated but never deleted.
+   *
+   * @param $key
+   *   Key pointing to the item the values should be applied.
+   *   The key can be either be an array containing the keys of a nested array
+   *   hierarchy path or a string with '][' or '|' as delimiter.
+   * @param $values
+   *   Nested array of values to set.
+   */
+  public function updateData($key, $values = array()) {
+    foreach ($values as $index => $value) {
+      // In order to preserve existing values, we can not aplly the values array
+      // at once. We need to apply each containing value on its own.
+      // If $value is an array we need to advance the hierarchy level.
+      if (is_array($value)) {
+        $this->updateData(array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
+      }
+      // Apply the value.
+      else {
+        drupal_array_set_nested_value($this->data, array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
+      }
+    }
+  }
+
+  /**
+   * Adds translated data to a job item.
+   *
+   * This function calls for TMGMTJobItem::addTranslatedDataRecursive() which
+   * sets the status of each added data item to TMGMT_DATA_ITEM_STATE_TRANSLATED.
+   *
+   * If all data items are translated, the status of the job item is updated to
+   * needs review.
+   *
+   * @todo
+   * To update the job item status to needs review we could take advantage of
+   * the TMGMTJobItem::getCountPending() and TMGMTJobItem::getCountTranslated().
+   * The catch is, that this counter gets updated while saveing which not yet
+   * hapened.
+   *
+   * @param $translation
+   *   Nested array of translated data. Can either be a single text entry, the
+   *   whole data structure or parts of it.
+   * @param $key
+   *   (Optional) Either a flattened key (a 'key1][key2][key3' string) or a nested
+   *   one, e.g. array('key1', 'key2', 'key2'). Defaults to an empty array which
+   *   means that it will replace the whole translated data array.
+   */
+  public function addTranslatedData($translation, $key = array()) {
+    $this->addTranslatedDataRecursive($translation, $key);
+    // Check if the job item has all the translated data that it needs now.
+    // Only attempt to change the status to needs review if it is currently
+    // active.
+    if ($this->isActive()) {
+      $data = tmgmt_flatten_data($this->getData());
+      $data = array_filter($data, '_tmgmt_filter_data');
+      $finished = TRUE;
+      foreach ($data as $item) {
+        if (empty($item['#status']) || $item['#status'] == TMGMT_DATA_ITEM_STATE_PENDING) {
+          $finished = FALSE;
+          break;
+        }
+      }
+      if ($finished) {
+        // There are no unfinished elements left.
+        $uri = $this->getSourceUri();
+        if ($this->getJob()->getTranslator()->getSetting('auto_accept')) {
+          // If the job item is going to be auto-accepted, set to review without
+          // a message.
+          $this->needsReview(FALSE);
+        }
+        else {
+          // Otherwise, create a message that contains source label, target
+          // language and links to the review form.
+          $uri = $this->uri();
+          $variables = array(
+            '!source' => l($this->getSourceLabel(), $uri['path']),
+            '@language' => entity_metadata_wrapper('tmgmt_job', $this->getJob())->target_language->label(),
+            '!review_url' => url($uri['path'], array('query' => array('destination' => current_path()))),
+          );
+          $this->needsReview('The translation of !source to @language is finished and can now be <a href="!review_url">reviewed</a>.', $variables);
+        }
+      }
+    }
+    $this->save();
+  }
+
+  /**
+   * Propagates the returned job item translations to the sources.
+   *
+   * @return boolean
+   *   TRUE if we were able to propagate the translated data and the item could
+   *   be saved, FALSE otherwise.
+   */
+  public function acceptTranslation() {
+    if (!$this->isNeedsReview() || !$controller = $this->getSourceController()) {
+      return FALSE;
+    }
+    // We don't know if the source plugin was able to save the translation after
+    // this point. That means that the plugin has to set the 'accepted' states
+    // on its own.
+    $controller->saveTranslation($this);
+  }
+
+  /**
+   * Returns all job messages attached to this job item.
+   *
+   * @return array
+   *   An array of translation job messages.
+   */
+  public function getMessages($conditions = array()) {
+    $query = new EntityFieldQuery();
+    $query->entityCondition('entity_type', 'tmgmt_message');
+    $query->propertyCondition('tjiid', $this->tjiid);
+    foreach ($conditions as $key => $condition) {
+      if (is_array($condition)) {
+        $operator = isset($condition['operator']) ? $condition['operator'] : '=';
+        $query->propertyCondition($key, $condition['value'], $operator);
+      }
+      else {
+        $query->propertyCondition($key, $condition);
+      }
+    }
+    $results = $query->execute();
+    if (!empty($results['tmgmt_message'])) {
+      return entity_load('tmgmt_message', array_keys($results['tmgmt_message']));
+    }
+    return array();
+  }
+
+  /**
+   * Retrieves all siblings of this job item.
+   *
+   * @return array
+   *   An array of job items that are the siblings of this job item.
+   */
+  public function getSiblings() {
+    $query = new EntityFieldQuery();
+    $result = $query->entityCondition('entity_type', 'tmgmt_job_item')
+      ->propertyCondition('tjiid', $this->tjiid, '<>')
+      ->propertyCondition('tjid', $this->tjid)
+      ->execute();
+    if (!empty($result['tmgmt_job_item'])) {
+      return entity_load('tmgmt_job_item', array_keys($result['tmgmt_job_item']));
+    }
+    return FALSE;
+  }
+
+  /**
+   * Returns all job messages attached to this job item with timestamp newer
+   * than $time.
+   *
+   * @param $timestamp
+   *   (Optional) Messages need to have a newer timestamp than $time. Defaults
+   *   to REQUEST_TIME.
+   *
+   * @return array
+   *   An array of translation job messages.
+   */
+  public function getMessagesSince($time = NULL) {
+    $time = isset($time) ? $time : REQUEST_TIME;
+    $conditions = array('created' => array('value' => $time, 'operator' => '>='));
+    return $this->getMessages($conditions);
+  }
+
+  /**
+   * Adds remote mapping entity to this job item.
+   *
+   * @param string $data_item_key
+   *   Job data item key.
+   * @param int $remote_identifier_1
+   *   Array of remote identifiers. In case you need to save
+   *   remote_identifier_2/3 set it into $mapping_data argument.
+   * @param array $mapping_data
+   *   Additional data to be added.
+   *
+   * @return int|bool
+   * @throws TMGMTException
+   */
+  public function addRemoteMapping($data_item_key = NULL, $remote_identifier_1 = NULL, $mapping_data = array()) {
+
+    if (empty($remote_identifier_1) && !isset($mapping_data['remote_identifier_2']) && !isset($remote_mapping['remote_identifier_3'])) {
+      throw new TMGMTException('Cannot create remote mapping without remote identifier.');
+    }
+
+    $data = array(
+      'tjid' => $this->tjid,
+      'tjiid' => $this->tjiid,
+      'data_item_key' => $data_item_key,
+      'remote_identifier_1' => $remote_identifier_1,
+    );
+
+    if (!empty($mapping_data)) {
+      $data += $mapping_data;
+    }
+
+    $remote_mapping = entity_create('tmgmt_remote', $data);
+
+    return entity_get_controller('tmgmt_remote')->save($remote_mapping);
+  }
+
+  /**
+   * Gets remote mappings for current job item.
+   *
+   * @return array
+   *   List of TMGMTRemote entities.
+   */
+  public function getRemoteMappings() {
+    $query = new EntityFieldQuery();
+    $query->entityCondition('entity_type', 'tmgmt_remote');
+    $query->propertyCondition('tjiid', $this->tjiid);
+    $result = $query->execute();
+
+    if (isset($result['tmgmt_remote'])) {
+      return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
+    }
+
+    return array();
+  }
+}
diff --git a/entity/tmgmt.entity.message.inc b/entity/tmgmt.entity.message.inc
new file mode 100644
index 0000000..3e4b5f0
--- /dev/null
+++ b/entity/tmgmt.entity.message.inc
@@ -0,0 +1,143 @@
+<?php
+
+/*
+ * @file
+ * Contains message entity class.
+ */
+
+/**
+ * Entity class for the tmgmt_message entity.
+ *
+ * @ingroup tmgmt_job
+ */
+class TMGMTMessage extends Entity {
+
+  /**
+   * The ID of the message..
+   *
+   * @var integer
+   */
+  public $mid;
+
+  /**
+   * The ID of the job.
+   *
+   * @var integer
+   */
+  public $tjid;
+
+  /**
+   * The ID of the job item.
+   *
+   * @var integer
+   */
+  public $tjiid;
+
+  /**
+   * User uid.
+   *
+   * @var integer
+   */
+  public $uid;
+
+  /**
+   * The message text.
+   *
+   * @var string
+   */
+  public $message;
+
+  /**
+   * An array of string replacement arguments as used by t().
+   *
+   * @var array
+   */
+  public $variables;
+
+  /**
+   * The time when the message object was created as a timestamp.
+   *
+   * @var integer
+   */
+  public $created;
+
+  /**
+   * Type of the message (debug, status, warning or error).
+   *
+   * @var string
+   */
+  public $type;
+
+  /**
+   * Overrides Entity::__construct().
+   */
+  public function __construct(array $values = array()) {
+    parent::__construct($values, 'tmgmt_message');
+    if (empty($this->created)) {
+      $this->created = REQUEST_TIME;
+    }
+    if (empty($this->type)) {
+      $this->type = 'status';
+    }
+  }
+
+  /**
+   * Overrides Entity::label().
+   */
+  public function defaultLabel() {
+    $created = format_date($this->created);
+    switch ($this->type) {
+      case 'error':
+        return t('Error message from @time', array('@time' => $created));
+      case 'status':
+        return t('Status message from @time', array('@time' => $created));
+      case 'warning':
+        return t('Warning message from @time', array('@time' => $created));
+      case 'debug':
+        return t('Debug message from @time', array('@time' => $created));
+    }
+  }
+
+  /**
+   * Returns the translated message.
+   *
+   * @return
+   *   The translated message.
+   */
+  public function getMessage() {
+    $text = $this->message;
+    if (is_array($this->variables) && !empty($this->variables)) {
+      $text = t($text, $this->variables);
+    }
+    return $text;
+  }
+
+  /**
+   * Loads the job entity that this job message is attached to.
+   *
+   * @return TMGMTJob
+   *   The job entity that this job message is attached to or FALSE if there was
+   *   a problem.
+   */
+  public function getJob() {
+    if (!empty($this->tjid)) {
+      return tmgmt_job_load($this->tjid);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Loads the job entity that this job message is attached to.
+   *
+   * @return TMGMTJobItem
+   *   The job item entity that this job message is attached to or FALSE if
+   *   there was a problem.
+   */
+  public function getJobItem() {
+    if (!empty($this->tjiid)) {
+      return tmgmt_job_item_load($this->tjiid);
+    }
+    return FALSE;
+  }
+
+}
diff --git a/entity/tmgmt.entity.remote.inc b/entity/tmgmt.entity.remote.inc
new file mode 100644
index 0000000..60a154e
--- /dev/null
+++ b/entity/tmgmt.entity.remote.inc
@@ -0,0 +1,141 @@
+<?php
+
+/*
+ * @file
+ * Contains remote entity class.
+ */
+
+/**
+ * Entity class for the tmgmt_remote entity.
+ *
+ * @ingroup tmgmt_job
+ */
+class TMGMTRemote extends Entity {
+
+  /**
+   * Primary key.
+   *
+   * @var int
+   */
+  public $trid;
+
+  /**
+   * TMGMTJob identifier.
+   *
+   * @var int
+   */
+  public $tjid;
+
+  /**
+   * TMGMTJobItem identifier.
+   *
+   * @var int
+   */
+  public $tjiid;
+
+  /**
+   * Translation job data item key.
+   *
+   * @var string
+   */
+  public $data_item_key;
+
+  /**
+   * Custom remote identifier 1.
+   *
+   * @var string
+   */
+  public $remote_identifier_1;
+
+  /**
+   * Custom remote identifier 2.
+   *
+   * @var string
+   */
+  public $remote_identifier_2;
+
+  /**
+   * Custom remote identifier 3.
+   *
+   * @var string
+   */
+  public $remote_identifier_3;
+
+  /**
+   * Remote job url.
+   *
+   * @var string
+   */
+  public $remote_url;
+
+  /**
+   * Word count provided by the remote service.
+   *
+   * @var int
+   */
+  public $word_count;
+
+  /**
+   * Custom remote data.
+   *
+   * @var array
+   */
+  public $remote_data;
+
+
+  /**
+   * Gets translation job.
+   *
+   * @return TMGMTJob
+   */
+  function getJob() {
+    return tmgmt_job_load($this->tjid);
+  }
+
+  /**
+   * Gets translation job item.
+   *
+   * @return TMGMTJobItem
+   */
+  function getJobItem() {
+    if (!empty($this->tjiid)) {
+      return tmgmt_job_item_load($this->tjiid);
+    }
+    return NULL;
+  }
+
+  /**
+   * Adds data to the remote_data storage.
+   *
+   * @param string $key
+   *   Key through which the data will be accessible.
+   * @param $value
+   *   Value to store.
+   */
+  function addRemoteData($key, $value) {
+    $this->remote_data[$key] = $value;
+  }
+
+  /**
+   * Gets data from remote_data storage.
+   *
+   * @param string $key
+   *   Access key for the data.
+   *
+   * @return mixed
+   *   Stored data.
+   */
+  function getRemoteData($key) {
+    return $this->remote_data[$key];
+  }
+
+  /**
+   * Removes data from remote_data storage.
+   *
+   * @param string $key
+   *   Access key for the data that are to be removed.
+   */
+  function removeRemoteData($key) {
+    unset($this->remote_data[$key]);
+  }
+}
diff --git a/entity/tmgmt.entity.translator.inc b/entity/tmgmt.entity.translator.inc
new file mode 100644
index 0000000..4051909
--- /dev/null
+++ b/entity/tmgmt.entity.translator.inc
@@ -0,0 +1,245 @@
+<?php
+
+/*
+ * @file
+ * Contains translator entity class.
+ */
+
+/**
+ * Entity class for the tmgmt_translator entity.
+ *
+ * @ingroup tmgmt_translator
+ */
+class TMGMTTranslator extends Entity {
+
+  /**
+   * The ID of the translator.
+   *
+   * @var integer
+   */
+  public $tid;
+
+  /**
+   * Machine readable name of the translator.
+   *
+   * @var string
+   */
+  public $name;
+
+  /**
+   * Label of the translator.
+   *
+   * @var string
+   */
+  public $label;
+
+  /**
+   * Description of the translator.
+   *
+   * @var string
+   */
+  public $description;
+
+  /**
+   * Weight of the translator.
+   *
+   * @var int
+   */
+  public $weight;
+
+  /**
+   * Plugin name of the translator.
+   *
+   * @type string
+   */
+  public $plugin;
+
+  /**
+   * Translator type specific settings.
+   *
+   * @var array
+   */
+  public $settings;
+
+  /**
+   * The supported target languages caches.
+   *
+   * @var array
+   */
+  protected $languageCache;
+
+  /**
+   * Whether the language cache in the database is outdated.
+   *
+   * @var boolean
+   */
+  protected $languageCacheOutdated;
+
+  /**
+   * Overrides Entity::__construct().
+   */
+  public function __construct(array $values = array()) {
+    parent::__construct($values, 'tmgmt_translator');
+  }
+
+  /**
+   * Returns the translator plugin controller of this translator.
+   *
+   * @return TMGMTTranslatorPluginControllerInterface
+   */
+  public function getController() {
+    if (!empty($this->plugin)) {
+      return tmgmt_translator_plugin_controller($this->plugin);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Returns the supported target languages for this translator.
+   *
+   * @return array
+   *   An array of supported target languages in ISO format.
+   */
+  public function getSupportedTargetLanguages($source_language) {
+    if ($controller = $this->getController()) {
+      if (isset($this->pluginInfo['cache languages']) && empty($this->pluginInfo['cache languages'])) {
+        // This plugin doesn't support language caching.
+        return $controller->getSupportedTargetLanguages($this, $source_language);
+      }
+      else {
+        // Retrieve the supported languages from the cache.
+        if (empty($this->languageCache) && $cache = cache_get('languages:' . $this->name, 'cache_tmgmt')) {
+          $this->languageCache = $cache->data;
+        }
+        // Even if we successfully queried the cache it might not have an entry
+        // for our source language yet.
+        if (!isset($this->languageCache[$source_language])) {
+          $this->languageCache[$source_language] = $controller->getSupportedTargetLanguages($this, $source_language);
+          $this->languageCacheOutdated = TRUE;
+        }
+      }
+      return $this->languageCache[$source_language];
+    }
+  }
+
+  /**
+   * Check whether this translator can handle a particular translation job.
+   *
+   * @param $job
+   *   The TMGMTJob entity that should be translated.
+   *
+   * @return boolean
+   *   TRUE if the job can be processed and translated, FALSE otherwise.
+   */
+  public function canTranslate(TMGMTJob $job) {
+    if ($controller = $this->getController()) {
+      return $controller->canTranslate($this, $job);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Checks whether a translator is available.
+   *
+   * @return boolean
+   *   TRUE if the translator plugin is available, FALSE otherwise.
+   */
+  public function isAvailable() {
+    if ($controller = $this->getController()) {
+      return $controller->isAvailable($this);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Returns if the plugin has any settings for this job.
+   */
+  public function hasCheckoutSettings(TMGMTJob $job) {
+    if ($controller = $this->getController()) {
+      return $controller->hasCheckoutSettings($job);
+    }
+    return FALSE;
+  }
+
+  /**
+   * @todo Remove this once http://drupal.org/node/1420364 is done.
+   */
+  public function getNotAvailableReason() {
+    if ($controller = $this->getController()) {
+      return $controller->getNotAvailableReason($this);
+    }
+    return FALSE;
+  }
+
+  /**
+   * @todo Remove this once http://drupal.org/node/1420364 is done.
+   */
+  public function getNotCanTranslateReason(TMGMTJob $job) {
+    if ($controller = $this->getController()) {
+      return $controller->getNotCanTranslateReason($job);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Retrieves a setting value from the translator settings. Pulls the default
+   * values (if defined) from the plugin controller.
+   *
+   * @param $name
+   *   The name of the setting.
+   *
+   * @return
+   *   The setting value or $default if the setting value is not set. Returns
+   *   NULL if the setting does not exist at all.
+   */
+  public function getSetting($name) {
+    if (isset($this->settings[$name])) {
+      return $this->settings[$name];
+    }
+    elseif ($controller = $this->getController()) {
+      $defaults = $controller->defaultSettings();
+      if (isset($defaults[$name])) {
+        return $defaults[$name];
+      }
+    }
+  }
+
+  /**
+   * Maps local language to remote language.
+   *
+   * @param $language
+   *   Local language code.
+   *
+   * @return string
+   *   Remote language code.
+   */
+  public function mapToRemoteLanguage($language) {
+    return $this->getController()->mapToRemoteLanguage($this, $language);
+  }
+
+  /**
+   * Maps remote language to local language.
+   *
+   * @param $language
+   *   Remote language code.
+   *
+   * @return string
+   *   Local language code.
+   */
+  public function mapToLocalLanguage($language) {
+    return $this->getController()->mapToLocalLanguage($this, $language);
+  }
+
+  /**
+   * Updates the language cache if it has changed.
+   */
+  public function __destruct() {
+    if ($controller = $this->getController()) {
+      $info = $controller->pluginInfo();
+      if (!isset($info['language cache']) || !empty($info['language cache']) && !empty($this->languageCacheOutdated)) {
+        cache_set('languages:' . $this->name, $this->languageCache, 'cache_tmgmt');
+      }
+    }
+  }
+
+}
diff --git a/includes/tmgmt.controller.inc b/includes/tmgmt.controller.inc
deleted file mode 100644
index 4ad7ad9..0000000
--- a/includes/tmgmt.controller.inc
+++ /dev/null
@@ -1,335 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains the controller classes.
- */
-
-/**
- * Controller class for the job entity.
- *
- * @ingroup tmgmt_job
- */
-class TMGMTJobController extends EntityAPIController {
-
-  /**
-   * Overrides EntityAPIController::save().
-   */
-  public function save($entity, DatabaseTransaction $transaction = NULL) {
-    $entity->changed = REQUEST_TIME;
-    return parent::save($entity, $transaction);
-  }
-
-  /**
-   * Overrides EntityAPIController::delete().
-   */
-  public function delete($ids, $transaction = NULL) {
-    parent::delete($ids, $transaction);
-    // Since we are deleting one or multiple jobs here we also need to delete
-    // the attached job items and messages.
-    $query = new EntityFieldQuery();
-    $result = $query->entityCondition('entity_type', 'tmgmt_job_item')
-      ->propertyCondition('tjid', $ids)
-      ->execute();
-    if (!empty($result['tmgmt_job_item'])) {
-      $controller = entity_get_controller('tmgmt_job_item');
-      // We need to directly query the entity controller so we can pass on
-      // the transaction object.
-      $controller->delete(array_keys($result['tmgmt_job_item']), $transaction);
-    }
-    $query = new EntityFieldQuery();
-    $result = $query->entityCondition('entity_type', 'tmgmt_message')
-      ->propertyCondition('tjid', $ids)
-      ->execute();
-    if (!empty($result['tmgmt_message'])) {
-      $controller = entity_get_controller('tmgmt_message');
-      // We need to directly query the entity controller so we can pass on
-      // the transaction object.
-      $controller->delete(array_keys($result['tmgmt_message']), $transaction);
-    }
-    $query = new EntityFieldQuery();
-    $result = $query->entityCondition('entity_type', 'tmgmt_remote')
-      ->propertyCondition('tjid', $ids)
-      ->execute();
-    if (!empty($result['tmgmt_remote'])) {
-      $controller = entity_get_controller('tmgmt_remote');
-      $controller->delete(array_keys($result['tmgmt_remote']), $transaction);
-    }
-  }
-
-}
-
-/**
- * Controller class for the remote job mapping entity.
- *
- * @ingroup tmgmt_job
- */
-class TMGMTRemoteController extends EntityAPIController {
-
-  public function load($ids = array(), $conditions = array()) {
-    $entities = parent::load($ids, $conditions);
-
-    foreach ($entities as &$entity) {
-      if (is_string($entity->remote_data)) {
-        $entity->remote_data = unserialize($entity->remote_data);
-      }
-    }
-
-    return $entities;
-  }
-
-  /**
-   * Loads remote mappings based on local data.
-   *
-   * @param int $tjid
-   *   Translation job id.
-   * @param int $tjiid
-   *   Translation job item id.
-   * @param int $data_item_key
-   *   Data item key.
-   *
-   * @return array
-   *   Array of TMGMTRemote entities.
-   */
-  function loadByLocalData($tjid = NULL, $tjiid = NULL, $data_item_key = NULL) {
-    $data_item_key = tmgmt_ensure_keys_string($data_item_key);
-
-    $query = new EntityFieldQuery();
-    $query->entityCondition('entity_type', 'tmgmt_remote');
-
-    if (!empty($tjid)) {
-      $query->propertyCondition('tjid', $tjid);
-    }
-    if (!empty($tjiid)) {
-      $query->propertyCondition('tjiid', $tjiid);
-    }
-    if (!empty($data_item_key)) {
-      $query->propertyCondition('data_item_key', $data_item_key);
-    }
-
-    $result = $query->execute();
-
-    if (isset($result['tmgmt_remote'])) {
-      return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
-    }
-
-    return array();
-  }
-
-  /**
-   * Loads remote mapping entities based on remote identifier.
-   *
-   * @param int $remote_identifier_1
-   * @param int $remote_identifier_2
-   * @param int $remote_identifier_3
-   *
-   * @return array
-   *   Array of TMGMTRemote entities.
-   */
-  function loadByRemoteIdentifier($remote_identifier_1 = NULL, $remote_identifier_2 = NULL, $remote_identifier_3 = NULL) {
-    $query = new EntityFieldQuery();
-    $query->entityCondition('entity_type', 'tmgmt_remote');
-
-    if ($remote_identifier_1 !== NULL) {
-      $query->propertyCondition('remote_identifier_1', $remote_identifier_1);
-    }
-    if ($remote_identifier_2 !== NULL) {
-      $query->propertyCondition('remote_identifier_2', $remote_identifier_2);
-    }
-    if ($remote_identifier_3 !== NULL) {
-      $query->propertyCondition('remote_identifier_3', $remote_identifier_3);
-    }
-
-    $result = $query->execute();
-
-    if (isset($result['tmgmt_remote'])) {
-      return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
-    }
-
-    return array();
-  }
-}
-
-/**
- * Controller class for the job item entity.
- *
- * @ingroup tmgmt_job
- */
-class TMGMTJobItemController extends EntityAPIController {
-
-  /**
-   * Overrides EntityAPIController::save().
-   *
-   * @todo Eliminate the need to flatten and unflatten the JobItem data.
-   */
-  public function save($entity, DatabaseTransaction $transaction = NULL) {
-    $entity->changed = REQUEST_TIME;
-
-    // Consider everything accepted when the job item is accepted.
-    if ($entity->isAccepted()) {
-      $entity->count_pending = 0;
-      $entity->count_translated = 0;
-      $entity->count_reviewed = 0;
-      $entity->count_accepted = count(array_filter(tmgmt_flatten_data($entity->data), '_tmgmt_filter_data'));
-    }
-    // Count the data item states.
-    else {
-      // Reset counter values.
-      $entity->count_pending = 0;
-      $entity->count_translated = 0;
-      $entity->count_reviewed = 0;
-      $entity->count_accepted = 0;
-      $entity->word_count = 0;
-      $this->count($entity->data, $entity);
-    }
-    return parent::save($entity, $transaction);
-  }
-
-  /**
-   * Parse all data items recursively and sums up the counters for
-   * accepted, translated and pending items.
-   *
-   * @param $item
-   *   The current data item.
-   * @param $entity
-   *   The job item the count should be calculated.
-   */
-  protected function count(&$item, $entity) {
-    if (!empty($item['#text'])) {
-      if (_tmgmt_filter_data($item)) {
-
-        // Count words of the data item.
-        $entity->word_count += tmgmt_word_count($item['#text']);
-
-        // Set default states if no state is set.
-        if (!isset($item['#status'])) {
-          // Translation is present.
-          if (!empty($item['#translation'])) {
-            $item['#status'] = TMGMT_DATA_ITEM_STATE_TRANSLATED;
-          }
-          // No translation present.
-          else {
-            $item['#status'] = TMGMT_DATA_ITEM_STATE_PENDING;
-          }
-        }
-        switch ($item['#status']) {
-          case TMGMT_DATA_ITEM_STATE_REVIEWED:
-            $entity->count_reviewed++;
-            break;
-          case TMGMT_DATA_ITEM_STATE_TRANSLATED:
-            $entity->count_translated++;
-            break;
-          default:
-            $entity->count_pending++;
-            break;
-        }
-      }
-    }
-    else {
-      foreach (element_children($item) as $key) {
-        $this->count($item[$key], $entity);
-      }
-    }
-  }
-
-  /**
-   * Overrides EntityAPIController::delete().
-   */
-  public function delete($ids, $transaction = NULL) {
-    parent::delete($ids, $transaction);
-    // Since we are deleting one or multiple job items here we also need to
-    // delete the attached messages.
-    $query = new EntityFieldQuery();
-    $result = $query->entityCondition('entity_type', 'tmgmt_message')
-      ->propertyCondition('tjiid', $ids)
-      ->execute();
-    if (!empty($result['tmgmt_message'])) {
-      $controller = entity_get_controller('tmgmt_message');
-      // We need to directly query the entity controller so we can pass on
-      // the transaction object.
-      $controller->delete(array_keys($result['tmgmt_message']), $transaction);
-    }
-
-    $query = new EntityFieldQuery();
-    $result = $query->entityCondition('entity_type', 'tmgmt_remote')
-        ->propertyCondition('tjiid', $ids)
-        ->execute();
-    if (!empty($result['tmgmt_remote'])) {
-      $controller = entity_get_controller('tmgmt_remote');
-      $controller->delete(array_keys($result['tmgmt_remote']), $transaction);
-    }
-  }
-
-  /**
-   * Overrides EntityAPIController::invoke().
-   */
-  public function invoke($hook, $entity) {
-    // We need to check whether the state of the job is affected by this
-    // deletion.
-    if ($hook == 'delete' && $job = $entity->getJob()) {
-      // We only care for active jobs.
-      if ($job->isActive() && tmgmt_job_check_finished($job->tjid)) {
-        // Mark the job as finished.
-        $job->finished();
-      }
-    }
-    parent::invoke($hook, $entity);
-  }
-
-}
-
-/**
- * Controller class for the job entity.
- *
- * @ingroup tmgmt_translator
- */
-class TMGMTTranslatorController extends EntityAPIControllerExportable {
-
-  /**
-   * Overrides EntityAPIControllerExportable::buildQuery().
-   */
-  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
-    $query = parent::buildQuery($ids, $conditions, $revision_id);
-    if ($plugins = tmgmt_translator_plugin_info()) {
-      $query->condition('plugin', array_keys($plugins));
-    }
-    else {
-      // Don't return any translators if no plugin exists.
-      $query->where('1 = 0');
-    }
-    // Sort by the weight of the translator.
-    $query->orderBy('weight');
-    return $query;
-  }
-
-  /**
-   * Overrides EntityAPIControllerExportable::delete().
-   */
-  public function delete($ids, DatabaseTransaction $transaction = NULL) {
-    $cids = array();
-    // We are never going to have many entities here, so we can risk a loop.
-    foreach ($ids as $key => $name) {
-      if (tmgmt_translator_busy($key)) {
-        // The translator can't be deleted because it is currently busy. Remove
-        // it from the ids so it wont get deleted in the parent implementation.
-        unset($ids[$key]);
-      }
-      else {
-        $cids[$key] = 'language:' . $key;
-      }
-    }
-    // Clear the language cache for the deleted translators.
-    cache_clear_all($cids, 'cache_tmgmt');
-    parent::delete($ids, $transaction);
-  }
-
-  /**
-  * Overrides EntityAPIControllerExportable::save().
-  */
-  public function save($entity, DatabaseTransaction $transaction = NULL) {
-    $return = parent::save($entity, $transaction);
-    // Clear the languages cache.
-    cache_clear_all('language:' . $entity->name, 'cache_tmgmt');
-    return $return;
-  }
-}
diff --git a/includes/tmgmt.entity.inc b/includes/tmgmt.entity.inc
deleted file mode 100644
index e216e64..0000000
--- a/includes/tmgmt.entity.inc
+++ /dev/null
@@ -1,2015 +0,0 @@
-<?php
-
-/*
- * @file
- * Entity classes for Translation Management entities.
- */
-
-/**
- * Entity class for the tmgmt_translator entity.
- *
- * @ingroup tmgmt_translator
- */
-class TMGMTTranslator extends Entity {
-
-  /**
-   * The ID of the translator.
-   *
-   * @var integer
-   */
-  public $tid;
-
-  /**
-   * Machine readable name of the translator.
-   *
-   * @var string
-   */
-  public $name;
-
-  /**
-   * Label of the translator.
-   *
-   * @var string
-   */
-  public $label;
-
-  /**
-   * Description of the translator.
-   *
-   * @var string
-   */
-  public $description;
-
-  /**
-   * Weight of the translator.
-   *
-   * @var int
-   */
-  public $weight;
-
-  /**
-   * Plugin name of the translator.
-   *
-   * @type string
-   */
-  public $plugin;
-
-  /**
-   * Translator type specific settings.
-   *
-   * @var array
-   */
-  public $settings;
-
-  /**
-   * The supported target languages caches.
-   *
-   * @var array
-   */
-  protected $languageCache;
-
-  /**
-   * Whether the language cache in the database is outdated.
-   *
-   * @var boolean
-   */
-  protected $languageCacheOutdated;
-
-  /**
-   * Overrides Entity::__construct().
-   */
-  public function __construct(array $values = array()) {
-    parent::__construct($values, 'tmgmt_translator');
-  }
-
-  /**
-   * Returns the translator plugin controller of this translator.
-   *
-   * @return TMGMTTranslatorPluginControllerInterface
-   */
-  public function getController() {
-    if (!empty($this->plugin)) {
-      return tmgmt_translator_plugin_controller($this->plugin);
-    }
-    return FALSE;
-  }
-
-  /**
-   * Returns the supported target languages for this translator.
-   *
-   * @return array
-   *   An array of supported target languages in ISO format.
-   */
-  public function getSupportedTargetLanguages($source_language) {
-    if ($controller = $this->getController()) {
-      if (isset($this->pluginInfo['cache languages']) && empty($this->pluginInfo['cache languages'])) {
-        // This plugin doesn't support language caching.
-        return $controller->getSupportedTargetLanguages($this, $source_language);
-      }
-      else {
-        // Retrieve the supported languages from the cache.
-        if (empty($this->languageCache) && $cache = cache_get('languages:' . $this->name, 'cache_tmgmt')) {
-          $this->languageCache = $cache->data;
-        }
-        // Even if we successfully queried the cache it might not have an entry
-        // for our source language yet.
-        if (!isset($this->languageCache[$source_language])) {
-          $this->languageCache[$source_language] = $controller->getSupportedTargetLanguages($this, $source_language);
-          $this->languageCacheOutdated = TRUE;
-        }
-      }
-      return $this->languageCache[$source_language];
-    }
-  }
-
-  /**
-   * Check whether this translator can handle a particular translation job.
-   *
-   * @param $job
-   *   The TMGMTJob entity that should be translated.
-   *
-   * @return boolean
-   *   TRUE if the job can be processed and translated, FALSE otherwise.
-   */
-  public function canTranslate(TMGMTJob $job) {
-    if ($controller = $this->getController()) {
-      return $controller->canTranslate($this, $job);
-    }
-    return FALSE;
-  }
-
-  /**
-   * Checks whether a translator is available.
-   *
-   * @return boolean
-   *   TRUE if the translator plugin is available, FALSE otherwise.
-   */
-  public function isAvailable() {
-    if ($controller = $this->getController()) {
-      return $controller->isAvailable($this);
-    }
-    return FALSE;
-  }
-
-  /**
-   * Returns if the plugin has any settings for this job.
-   */
-  public function hasCheckoutSettings(TMGMTJob $job) {
-    if ($controller = $this->getController()) {
-      return $controller->hasCheckoutSettings($job);
-    }
-    return FALSE;
-  }
-
-  /**
-   * @todo Remove this once http://drupal.org/node/1420364 is done.
-   */
-  public function getNotAvailableReason() {
-    if ($controller = $this->getController()) {
-      return $controller->getNotAvailableReason($this);
-    }
-    return FALSE;
-  }
-
-  /**
-   * @todo Remove this once http://drupal.org/node/1420364 is done.
-   */
-  public function getNotCanTranslateReason(TMGMTJob $job) {
-    if ($controller = $this->getController()) {
-      return $controller->getNotCanTranslateReason($job);
-    }
-    return FALSE;
-  }
-
-  /**
-   * Retrieves a setting value from the translator settings. Pulls the default
-   * values (if defined) from the plugin controller.
-   *
-   * @param $name
-   *   The name of the setting.
-   *
-   * @return
-   *   The setting value or $default if the setting value is not set. Returns
-   *   NULL if the setting does not exist at all.
-   */
-  public function getSetting($name) {
-    if (isset($this->settings[$name])) {
-      return $this->settings[$name];
-    }
-    elseif ($controller = $this->getController()) {
-      $defaults = $controller->defaultSettings();
-      if (isset($defaults[$name])) {
-        return $defaults[$name];
-      }
-    }
-  }
-
-  /**
-   * Maps local language to remote language.
-   *
-   * @param $language
-   *   Local language code.
-   *
-   * @return string
-   *   Remote language code.
-   */
-  public function mapToRemoteLanguage($language) {
-    return $this->getController()->mapToRemoteLanguage($this, $language);
-  }
-
-  /**
-   * Maps remote language to local language.
-   *
-   * @param $language
-   *   Remote language code.
-   *
-   * @return string
-   *   Local language code.
-   */
-  public function mapToLocalLanguage($language) {
-    return $this->getController()->mapToLocalLanguage($this, $language);
-  }
-
-  /**
-   * Updates the language cache if it has changed.
-   */
-  public function __destruct() {
-    if ($controller = $this->getController()) {
-      $info = $controller->pluginInfo();
-      if (!isset($info['language cache']) || !empty($info['language cache']) && !empty($this->languageCacheOutdated)) {
-        cache_set('languages:' . $this->name, $this->languageCache, 'cache_tmgmt');
-      }
-    }
-  }
-
-}
-
-/**
- * Entity class for the tmgmt_remote entity.
- *
- * @ingroup tmgmt_job
- */
-class TMGMTRemote extends Entity {
-
-  /**
-   * Primary key.
-   *
-   * @var int
-   */
-  public $trid;
-
-  /**
-   * TMGMTJob identifier.
-   *
-   * @var int
-   */
-  public $tjid;
-
-  /**
-   * TMGMTJobItem identifier.
-   *
-   * @var int
-   */
-  public $tjiid;
-
-  /**
-   * Translation job data item key.
-   *
-   * @var string
-   */
-  public $data_item_key;
-
-  /**
-   * Custom remote identifier 1.
-   *
-   * @var string
-   */
-  public $remote_identifier_1;
-
-  /**
-   * Custom remote identifier 2.
-   *
-   * @var string
-   */
-  public $remote_identifier_2;
-
-  /**
-   * Custom remote identifier 3.
-   *
-   * @var string
-   */
-  public $remote_identifier_3;
-
-  /**
-   * Remote job url.
-   *
-   * @var string
-   */
-  public $remote_url;
-
-  /**
-   * Word count provided by the remote service.
-   *
-   * @var int
-   */
-  public $word_count;
-
-  /**
-   * Custom remote data.
-   *
-   * @var array
-   */
-  public $remote_data;
-
-
-  /**
-   * Gets translation job.
-   *
-   * @return TMGMTJob
-   */
-  function getJob() {
-    return tmgmt_job_load($this->tjid);
-  }
-
-  /**
-   * Gets translation job item.
-   *
-   * @return TMGMTJobItem
-   */
-  function getJobItem() {
-    if (!empty($this->tjiid)) {
-      return tmgmt_job_item_load($this->tjiid);
-    }
-    return NULL;
-  }
-
-  /**
-   * Adds data to the remote_data storage.
-   *
-   * @param string $key
-   *   Key through which the data will be accessible.
-   * @param $value
-   *   Value to store.
-   */
-  function addRemoteData($key, $value) {
-    $this->remote_data[$key] = $value;
-  }
-
-  /**
-   * Gets data from remote_data storage.
-   *
-   * @param string $key
-   *   Access key for the data.
-   *
-   * @return mixed
-   *   Stored data.
-   */
-  function getRemoteData($key) {
-    return $this->remote_data[$key];
-  }
-
-  /**
-   * Removes data from remote_data storage.
-   *
-   * @param string $key
-   *   Access key for the data that are to be removed.
-   */
-  function removeRemoteData($key) {
-    unset($this->remote_data[$key]);
-  }
-}
-
-/**
- * Entity class for the tmgmt_job entity.
- *
- * @ingroup tmgmt_job
- */
-class TMGMTJobItem extends Entity {
-
-  /**
-   * The source plugin that provides the item.
-   *
-   * @var varchar
-   */
-  public $plugin;
-
-  /**
-   * The identifier of the translation job.
-   *
-   * @var integer
-   */
-  public $tjid;
-
-  /**
-   * The identifier of the translation job item.
-   *
-   * @var integer
-   */
-  public $tjiid;
-
-  /**
-   * Type of this item, used by the plugin to identify it.
-   *
-   * @var string
-   */
-  public $item_type;
-
-  /**
-   * Id of the item.
-   *
-   * @var integer
-   */
-  public $item_id;
-
-  /**
-   * The time when the job item was changed as a timestamp.
-   *
-   * @var integer
-   */
-  public $changed;
-
-  /**
-   * Can be used by the source plugin to store the data instead of creating it
-   * on demand.
-   *
-   * If additional information is added in the UI, like adding comments, it will
-   * also be saved here.
-   *
-   * Always use TMGMTJobItem::getData() to load the data, which will use
-   * this property if present and otherwise get it from the source.
-   *
-   * @var array
-   */
-  public $data = array();
-
-  /**
-   * Counter for all data items waiting for translation.
-   *
-   * @var integer
-   */
-  public $count_pending = 0;
-
-  /**
-   * Counter for all translated data items.
-   *
-   * @var integer
-   */
-  public $count_translated = 0;
-
-  /**
-   * Counter for all accepted data items.
-   *
-   * @var integer
-   */
-  public $count_accepted = 0;
-
-  /**
-   * Counter for all reviewed data items.
-   *
-   * @var integer
-   */
-  public $count_reviewed = 0;
-
-  /**
-   * Amount of words in this job item.
-   *
-   * @var integer
-   */
-  public $word_count = 0;
-
-  /**
-   * Overrides Entity::__construct().
-   */
-  public function __construct(array $values = array()) {
-    parent::__construct($values, 'tmgmt_job_item');
-    if (!isset($this->state)) {
-      $this->state = TMGMT_JOB_ITEM_STATE_ACTIVE;
-    }
-  }
-
-  /**
-   * Overrides Entity::defaultLabel()
-   */
-  public function defaultLabel() {
-    if ($controller = $this->getSourceController()) {
-      return t('Translation for @label', array('@label' => $controller->getLabel($this)));
-    }
-    return parent::defaultLabel();
-  }
-
-  /**
-   * Overrides Entity::defaultUri()
-   *
-   * @see _tmgmt_ui_breadcrumb()
-   */
-  public function defaultUri() {
-    // The path of a job item is not directly below the job that it belongs to.
-    // Having to maintain two unknowns / wildcards (job and job item) in the
-    // path is more complex than it has to be. Instead we just append the
-    // additional breadcrumb pieces manually with _tmgmt_ui_breadcrumb().
-    return array('path' => 'admin/config/regional/tmgmt/items/' . $this->tjiid);
-  }
-
-  /**
-   * Overrides Entity::buildContent().
-   */
-  public function buildContent($view_mode = 'full', $langcode = NULL) {
-    $content = array();
-    if (module_exists('tmgmt_ui')) {
-      $content = tmgmt_ui_job_item_review($this);
-    }
-    return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
-  }
-
-  /**
-   * Add a log message for this job item.
-   *
-   * @param $message
-   *   The message to store in the log. Keep $message translatable by not
-   *   concatenating dynamic values into it! Variables in the message should be
-   *   added by using placeholder strings alongside the variables argument to
-   *   declare the value of the placeholders. See t() for documentation on how
-   *   $message and $variables interact.
-   * @param $variables
-   *   (Optional) An array of variables to replace in the message on display.
-   * @param $type
-   *   (Optional) The type of the message. Can be one of 'status', 'error',
-   *   'warning' or 'debug'. Messages of the type 'debug' will not get printed
-   *   to the screen.
-   */
-  public function addMessage($message, $variables = array(), $type = 'status') {
-    // Save the job item if it hasn't yet been saved.
-    if (!empty($this->tjiid) || $this->save()) {
-      $message = tmgmt_message_create($message, $variables, array(
-        'tjid' => $this->tjid,
-        'tjiid' => $this->tjiid,
-        'uid' => $GLOBALS['user']->uid,
-        'type' => $type,
-      ));
-      if ($message->save()) {
-        return $message;
-      }
-    }
-    return FALSE;
-  }
-
-  /**
-   * Retrieves the label of the source object via the source controller.
-   *
-   * @return
-   *   The label of the source object.
-   */
-  public function getSourceLabel() {
-    if ($controller = $this->getSourceController()) {
-      return $controller->getLabel($this);
-    }
-    return FALSE;
-  }
-
-  /**
-   * Retrieves the path to the source object via the source controller.
-   *
-   * @return
-   *   The path to the source object.
-   */
-  public function getSourceUri() {
-    if ($controller = $this->getSourceController()) {
-      return $controller->getUri($this);
-    }
-    return FALSE;
-  }
-
-  /**
-   * Loads the job entity that this job item is attached to.
-   *
-   * @return TMGMTJob
-   *   The job entity that this job item is attached to or FALSE if there was
-   *   a problem.
-   */
-  public function getJob() {
-    if (!empty($this->tjid)) {
-      return tmgmt_job_load($this->tjid);
-    }
-    return FALSE;
-  }
-
-  /**
-   * Returns the translator for this job item.
-   *
-   * @return TMGMTTranslator
-   *   The translator entity or FALSE if there was a problem.
-   */
-  public function getTranslator() {
-    if ($job = $this->getJob()) {
-      return $job->getTranslator();
-    }
-    return FALSE;
-  }
-
-  /**
-   * Returns the translator plugin controller of the translator of this job item.
-   *
-   * @return TMGMTTranslatorPluginControllerInterface
-   *   The controller of the translator plugin or FALSE if there was a problem.
-   */
-  public function getTranslatorController() {
-    if ($job = $this->getJob()) {
-      return $job->getTranslatorController();
-    }
-    return FALSE;
-  }
-
-  /**
-   * Array of the data to be translated.
-   *
-   * The structure is similar to the form API in the way that it is a possibly
-   * nested array with the following properties whose presence indicate that the
-   * current element is a text that might need to be translated.
-   *
-   * - #text: The text to be translated.
-   * - #label: (Optional) The label that might be shown to the translator.
-   * - #comment: (Optional) A comment with additional information.
-   * - #translate: (Optional) If set to FALSE the text will not be translated.
-   * - #translation: The translated data. Set by the translator plugin.
-   *
-   * The key can be an alphanumeric string.
-   * @param $key
-   *   If present, only the subarray identified by key is returned.
-   * @param $index
-   *   Optional index of an attribute below $key.
-   *
-   * @return array
-   *   A structured data array.
-   */
-  public function getData(array $key = array(), $index = null) {
-    if (empty($this->data)) {
-      // Load the data from the source if it has not been set yet.
-      $this->data = $this->getSourceData();
-      $this->save();
-    }
-    if (empty($key)) {
-      return $this->data;
-    }
-    if ($index) {
-      $key = array_merge($key, array($index));
-    }
-    return drupal_array_get_nested_value($this->data, $key);
-  }
-
-  /**
-   * Loads the structured source data array from the source.
-   */
-  public function getSourceData() {
-    if ($controller = $this->getSourceController()) {
-      return $controller->getData($this);
-    }
-    return FALSE;
-  }
-
-  /**
-   * Returns the plugin controller of the configured plugin.
-   *
-   * @return TMGMTSourcePluginControllerInterface
-   */
-  public function getSourceController() {
-    if (!empty($this->plugin)) {
-      return tmgmt_source_plugin_controller($this->plugin);
-    }
-    return FALSE;
-  }
-
-  /**
-   * Count of all pending data items
-   *
-   * @return
-   *   Pending counts
-   */
-  public function getCountPending() {
-    return $this->count_pending;
-  }
-
-  /**
-   * Count of all translated data items.
-   *
-   * @return
-   *   Translated count
-   */
-  public function getCountTranslated() {
-    return $this->count_translated;
-  }
-
-  /**
-   * Count of all accepted data items.
-   *
-   * @return
-   *   Accepted count
-   */
-  public function getCountAccepted() {
-    return $this->count_accepted;
-  }
-
-  /**
-   * Count of all accepted data items.
-   *
-   * @return
-   *   Accepted count
-   */
-  public function getCountReviewed() {
-    return $this->count_reviewed;
-  }
-
-  /**
-   * Word count of all data items.
-   *
-   * @return
-   *   Word count
-   */
-  public function getWordCount() {
-    return (int)$this->word_count;
-  }
-
-  /**
-   * Sets the state of the job item to 'needs review'.
-   */
-  public function needsReview($message = NULL, $variables = array(), $type = 'status') {
-    if (!isset($message)) {
-      $uri = $this->getSourceUri();
-      $message = 'The translation for !source needs to be reviewed.';
-      $variables = array('!source' => l($this->getSourceLabel(), $uri['path']));
-    }
-    $return = $this->setState(TMGMT_JOB_ITEM_STATE_REVIEW, $message, $variables, $type);
-    // Auto accept the trganslation if the translator is configured for it.
-    if ($this->getTranslator()->getSetting('auto_accept')) {
-      $this->acceptTranslation();
-    }
-    return $return;
-  }
-
-  /**
-   * Sets the state of the job item to 'accepted'.
-   */
-  public function accepted($message = NULL, $variables = array(), $type = 'status') {
-    if (!isset($message)) {
-      $uri = $this->getSourceUri();
-      $message = 'The translation for !source has been accepted.';
-      $variables = array('!source' => l($this->getSourceLabel(), $uri['path']));
-    }
-    $return = $this->setState(TMGMT_JOB_ITEM_STATE_ACCEPTED, $message, $variables, $type);
-    // Check if this was the last unfinished job item in this job.
-    if (tmgmt_job_check_finished($this->tjid) && $job = $this->getJob()) {
-      // Mark the job as finished.
-      $job->finished();
-    }
-    return $return;
-  }
-
-  /**
-   * Sets the state of the job item to 'active'.
-   */
-  public function active($message = NULL, $variables = array(), $type = 'status') {
-    if (!isset($message)) {
-      $uri = $this->getSourceUri();
-      $message = 'The translation for !source is now being processed.';
-      $variables = array('!source' => l($this->getSourceLabel(), $uri['path']));
-    }
-    return $this->setState(TMGMT_JOB_ITEM_STATE_ACTIVE, $message, $variables, $type);
-  }
-
-  /**
-   * Updates the state of the job item.
-   *
-   * @param $state
-   *   The new state of the job item. Has to be one of the job state constants.
-   * @param $message
-   *   (Optional) The log message to be saved along with the state change.
-   * @param $variables
-   *   (Optional) An array of variables to replace in the message on display.
-   *
-   * @return int
-   *   The updated state of the job if it could be set.
-   *
-   * @see TMGMTJob::addMessage()
-   */
-  public function setState($state, $message = NULL, $variables = array(), $type = 'debug') {
-    // Return TRUE if the state could be set. Return FALSE otherwise.
-    if (array_key_exists($state, tmgmt_job_item_states()) && $this->state != $state) {
-      $this->state = $state;
-      $this->save();
-      // If a message is attached to this state change add it now.
-      if (!empty($message)) {
-        $this->addMessage($message, $variables, $type);
-      }
-    }
-    return $this->state;
-  }
-
-  /**
-   * Returns the state of the job item. Can be one of the job item state
-   * constants.
-   *
-   * @return integer
-   *   The state of the job item.
-   */
-  public function getState() {
-    // We don't need to check if the state is actually set because we always set
-    // it in the constructor.
-    return $this->state;
-  }
-
-  /**
-   * Checks whether the passed value matches the current state.
-   *
-   * @param $state
-   *   The value to check the current state against.
-   *
-   * @return boolean
-   *   TRUE if the passed state matches the current state, FALSE otherwise.
-   */
-  public function isState($state) {
-    return $this->getState() == $state;
-  }
-
-  /**
-   * Checks whether the state of this transaction is 'accepted'.
-   *
-   * @return boolean
-   *   TRUE if the state is 'accepted', FALSE otherwise.
-   */
-  public function isAccepted() {
-    return $this->isState(TMGMT_JOB_ITEM_STATE_ACCEPTED);
-  }
-
-  /**
-   * Checks whether the state of this transaction is 'active'.
-   *
-   * @return boolean
-   *   TRUE if the state is 'active', FALSE otherwise.
-   */
-  public function isActive() {
-    return $this->isState(TMGMT_JOB_ITEM_STATE_ACTIVE);
-  }
-
-  /**
-   * Checks whether the state of this transaction is 'needs review'.
-   *
-   * @return boolean
-   *   TRUE if the state is 'needs review', FALSE otherwise.
-   */
-  public function isNeedsReview() {
-    return $this->isState(TMGMT_JOB_ITEM_STATE_REVIEW);
-  }
-
-  /**
-   * Recursively writes translated data to the data array of a job item.
-   *
-   * While doing this the #status of each data item is set to
-   * TMGMT_DATA_ITEM_STATE_TRANSLATED.
-   *
-   * @param $translation
-   *   Nested array of translated data. Can either be a single text entry, the
-   *   whole data structure or parts of it.
-   * @param $key
-   *   (Optional) Either a flattened key (a 'key1][key2][key3' string) or a nested
-   *   one, e.g. array('key1', 'key2', 'key2'). Defaults to an empty array which
-   *   means that it will replace the whole translated data array.
-   */
-  protected function addTranslatedDataRecursive($translation, array $key = array()) {
-    if (isset($translation['#text'])) {
-      $status = $this->getData($key, '#status');
-      if (!$status || $status == TMGMT_DATA_ITEM_STATE_PENDING) {
-        $values = array(
-          '#translation' => $translation,
-          '#status' => TMGMT_DATA_ITEM_STATE_TRANSLATED,
-        );
-        $this->updateData($key, $values);
-      }
-      return;
-    }
-    foreach (element_children($translation) as $item) {
-      $this->addTranslatedDataRecursive($translation[$item], array_merge($key, array($item)));
-    }
-  }
-
-  /**
-   * Updates the values for a specific substructure in the data array.
-   *
-   * The values are either set or updated but never deleted.
-   *
-   * @param $key
-   *   Key pointing to the item the values should be applied.
-   *   The key can be either be an array containing the keys of a nested array
-   *   hierarchy path or a string with '][' or '|' as delimiter.
-   * @param $values
-   *   Nested array of values to set.
-   */
-  public function updateData($key, $values = array()) {
-    foreach ($values as $index => $value) {
-      // In order to preserve existing values, we can not aplly the values array
-      // at once. We need to apply each containing value on its own.
-      // If $value is an array we need to advance the hierarchy level.
-      if (is_array($value)) {
-        $this->updateData(array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
-      }
-      // Apply the value.
-      else {
-        drupal_array_set_nested_value($this->data, array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
-      }
-    }
-  }
-
-  /**
-   * Adds translated data to a job item.
-   *
-   * This function calls for TMGMTJobItem::addTranslatedDataRecursive() which
-   * sets the status of each added data item to TMGMT_DATA_ITEM_STATE_TRANSLATED.
-   *
-   * If all data items are translated, the status of the job item is updated to
-   * needs review.
-   *
-   * @todo
-   * To update the job item status to needs review we could take advantage of
-   * the TMGMTJobItem::getCountPending() and TMGMTJobItem::getCountTranslated().
-   * The catch is, that this counter gets updated while saveing which not yet
-   * hapened.
-   *
-   * @param $translation
-   *   Nested array of translated data. Can either be a single text entry, the
-   *   whole data structure or parts of it.
-   * @param $key
-   *   (Optional) Either a flattened key (a 'key1][key2][key3' string) or a nested
-   *   one, e.g. array('key1', 'key2', 'key2'). Defaults to an empty array which
-   *   means that it will replace the whole translated data array.
-   */
-  public function addTranslatedData($translation, $key = array()) {
-    $this->addTranslatedDataRecursive($translation, $key);
-    // Check if the job item has all the translated data that it needs now.
-    // Only attempt to change the status to needs review if it is currently
-    // active.
-    if ($this->isActive()) {
-      $data = tmgmt_flatten_data($this->getData());
-      $data = array_filter($data, '_tmgmt_filter_data');
-      $finished = TRUE;
-      foreach ($data as $item) {
-        if (empty($item['#status']) || $item['#status'] == TMGMT_DATA_ITEM_STATE_PENDING) {
-          $finished = FALSE;
-          break;
-        }
-      }
-      if ($finished) {
-        // There are no unfinished elements left.
-        $uri = $this->getSourceUri();
-        if ($this->getJob()->getTranslator()->getSetting('auto_accept')) {
-          // If the job item is going to be auto-accepted, set to review without
-          // a message.
-          $this->needsReview(FALSE);
-        }
-        else {
-          // Otherwise, create a message that contains source label, target
-          // language and links to the review form.
-          $uri = $this->uri();
-          $variables = array(
-            '!source' => l($this->getSourceLabel(), $uri['path']),
-            '@language' => entity_metadata_wrapper('tmgmt_job', $this->getJob())->target_language->label(),
-            '!review_url' => url($uri['path'], array('query' => array('destination' => current_path()))),
-          );
-          $this->needsReview('The translation of !source to @language is finished and can now be <a href="!review_url">reviewed</a>.', $variables);
-        }
-      }
-    }
-    $this->save();
-  }
-
-  /**
-   * Propagates the returned job item translations to the sources.
-   *
-   * @return boolean
-   *   TRUE if we were able to propagate the translated data and the item could
-   *   be saved, FALSE otherwise.
-   */
-  public function acceptTranslation() {
-    if (!$this->isNeedsReview() || !$controller = $this->getSourceController()) {
-      return FALSE;
-    }
-    // We don't know if the source plugin was able to save the translation after
-    // this point. That means that the plugin has to set the 'accepted' states
-    // on its own.
-    $controller->saveTranslation($this);
-  }
-
-  /**
-   * Returns all job messages attached to this job item.
-   *
-   * @return array
-   *   An array of translation job messages.
-   */
-  public function getMessages($conditions = array()) {
-    $query = new EntityFieldQuery();
-    $query->entityCondition('entity_type', 'tmgmt_message');
-    $query->propertyCondition('tjiid', $this->tjiid);
-    foreach ($conditions as $key => $condition) {
-      if (is_array($condition)) {
-        $operator = isset($condition['operator']) ? $condition['operator'] : '=';
-        $query->propertyCondition($key, $condition['value'], $operator);
-      }
-      else {
-        $query->propertyCondition($key, $condition);
-      }
-    }
-    $results = $query->execute();
-    if (!empty($results['tmgmt_message'])) {
-      return entity_load('tmgmt_message', array_keys($results['tmgmt_message']));
-    }
-    return array();
-  }
-
-  /**
-   * Retrieves all siblings of this job item.
-   *
-   * @return array
-   *   An array of job items that are the siblings of this job item.
-   */
-  public function getSiblings() {
-    $query = new EntityFieldQuery();
-    $result = $query->entityCondition('entity_type', 'tmgmt_job_item')
-      ->propertyCondition('tjiid', $this->tjiid, '<>')
-      ->propertyCondition('tjid', $this->tjid)
-      ->execute();
-    if (!empty($result['tmgmt_job_item'])) {
-      return entity_load('tmgmt_job_item', array_keys($result['tmgmt_job_item']));
-    }
-    return FALSE;
-  }
-
-  /**
-   * Returns all job messages attached to this job item with timestamp newer
-   * than $time.
-   *
-   * @param $timestamp
-   *   (Optional) Messages need to have a newer timestamp than $time. Defaults
-   *   to REQUEST_TIME.
-   *
-   * @return array
-   *   An array of translation job messages.
-   */
-  public function getMessagesSince($time = NULL) {
-    $time = isset($time) ? $time : REQUEST_TIME;
-    $conditions = array('created' => array('value' => $time, 'operator' => '>='));
-    return $this->getMessages($conditions);
-  }
-
-  /**
-   * Adds remote mapping entity to this job item.
-   *
-   * @param string $data_item_key
-   *   Job data item key.
-   * @param int $remote_identifier_1
-   *   Array of remote identifiers. In case you need to save
-   *   remote_identifier_2/3 set it into $mapping_data argument.
-   * @param array $mapping_data
-   *   Additional data to be added.
-   *
-   * @return int|bool
-   * @throws TMGMTException
-   */
-  public function addRemoteMapping($data_item_key = NULL, $remote_identifier_1 = NULL, $mapping_data = array()) {
-
-    if (empty($remote_identifier_1) && !isset($mapping_data['remote_identifier_2']) && !isset($remote_mapping['remote_identifier_3'])) {
-      throw new TMGMTException('Cannot create remote mapping without remote identifier.');
-    }
-
-    $data = array(
-      'tjid' => $this->tjid,
-      'tjiid' => $this->tjiid,
-      'data_item_key' => $data_item_key,
-      'remote_identifier_1' => $remote_identifier_1,
-    );
-
-    if (!empty($mapping_data)) {
-      $data += $mapping_data;
-    }
-
-    $remote_mapping = entity_create('tmgmt_remote', $data);
-
-    return entity_get_controller('tmgmt_remote')->save($remote_mapping);
-  }
-
-  /**
-   * Gets remote mappings for current job item.
-   *
-   * @return array
-   *   List of TMGMTRemote entities.
-   */
-  public function getRemoteMappings() {
-    $query = new EntityFieldQuery();
-    $query->entityCondition('entity_type', 'tmgmt_remote');
-    $query->propertyCondition('tjiid', $this->tjiid);
-    $result = $query->execute();
-
-    if (isset($result['tmgmt_remote'])) {
-      return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
-    }
-
-    return array();
-  }
-}
-
-/**
- * Entity class for the tmgmt_job entity.
- *
- * @ingroup tmgmt_job
- */
-class TMGMTJob extends Entity {
-
-  /**
-   * Translation job identifier.
-   *
-   * @var integer
-   */
-  public $tjid;
-
-  /**
-   * A custom label for this job.
-   */
-  public $label;
-
-  /**
-   * Current state of the translation job
-   * @var type
-   */
-  public $state;
-
-  /**
-   * Language to be translated from.
-   *
-   * @var string
-   */
-  public $source_language;
-
-  /**
-   * Language into which the data needs to be translated.
-   *
-   * @var varchar
-   */
-  public $target_language;
-
-  /**
-   * Reference to the used translator of this job.
-   *
-   * @see TMGMTJob::getTranslatorController()
-   *
-   * @var string
-   */
-  public $translator;
-
-  /**
-   * Translator specific configuration and context information for this job.
-   *
-   * @var array
-   */
-  public $settings;
-
-  /**
-   * Remote identification of this job.
-   *
-   * @var integer
-   */
-  public $reference;
-
-  /**
-   * The time when the job was created as a timestamp.
-   *
-   * @var integer
-   */
-  public $created;
-
-  /**
-   * The time when the job was changed as a timestamp.
-   *
-   * @var integer
-   */
-  public $changed;
-
-  /**
-   * The user id of the creator of the job.
-   *
-   * @var integer
-   */
-  public $uid;
-
-  /**
-   * Overrides Entity::__construct().
-   */
-  public function __construct(array $values = array()) {
-    parent::__construct($values, 'tmgmt_job');
-    if (empty($this->tjid)) {
-      $this->created = REQUEST_TIME;
-    }
-    if (!isset($this->state)) {
-      $this->state = TMGMT_JOB_STATE_UNPROCESSED;
-    }
-  }
-
-  /**
-   * Overrides Entity::defaultLabel().
-   */
-  public function defaultLabel() {
-    // In some cases we might have a user-defined label.
-    if (!empty($this->label)) {
-      return $this->label;
-    }
-
-    $items = $this->getItems();
-    $count = count($items);
-    if ($count > 0) {
-      $t_args = array('@title' => reset($items)->getSourceLabel(), '@more' => $count - 1);
-      return format_plural($count, '@title', '@title and @more more', $t_args);
-    }
-    else {
-      $wrapper = entity_metadata_wrapper($this->entityType, $this);
-      $source = $wrapper->source_language->label();
-      if (empty($source)) {
-        $source = '?';
-      }
-      $target = $wrapper->target_language->label();
-      if (empty($target)) {
-        $target = '?';
-      }
-      return t('From @source to @target', array('@source' => $source, '@target' => $target));
-    }
-  }
-
-  /**
-   * Overrides Entity::defaultUri().
-   */
-  public function defaultUri() {
-    return array('path' => 'admin/config/regional/tmgmt/jobs/' . $this->tjid);
-  }
-
-  /**
-   * Overrides Entity::buildContent().
-   */
-  public function buildContent($view_mode = 'full', $langcode = NULL) {
-    $content = array();
-    if (module_exists('tmgmt_ui')) {
-      $content = entity_ui_get_form('tmgmt_job', $this);
-    }
-    return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
-  }
-
-  /**
-   * Adds an item to the translation job.
-   *
-   * @param $plugin
-   *   The plugin name.
-   * @param $item_type
-   *   The source item type.
-   * @param $item_id
-   *   The source item id.
-   *
-   * @return TMGMTJobItem
-   *   The job item that was added to the job or FALSE if it couldn't be saved.
-   * @throws TMGMTException
-   *   On zero item word count.
-   */
-  public function addItem($plugin, $item_type, $item_id) {
-
-    $transaction = db_transaction();
-    $is_new = FALSE;
-
-    if (empty($this->tjid)) {
-      $this->save();
-      $is_new = TRUE;
-    }
-
-    $item = tmgmt_job_item_create($plugin, $item_type, $item_id, array('tjid' => $this->tjid));
-    // Initialize job item data variable needed to determine word count
-    // in job item getWordCount().
-    $item->getData();
-    $item->save();
-
-    if ($item->getWordCount() == 0) {
-      $transaction->rollback();
-
-      // In case we got word count 0 for the first job item, NULL tjid so that
-      // if there is another addItem() call the rolled back job object will get
-      // persisted.
-      if ($is_new) {
-        $this->tjid = NULL;
-      }
-
-      throw new TMGMTException('Created job item with word count 0. Plugin: @plugin | Item type: @item_type | Item id: @item_id',
-        array('@plugin' => $plugin, '@item_type' => $item_type, '@item_id' => $item_id));
-    }
-
-    return $item;
-  }
-
-  /**
-   * Add a log message for this job.
-   *
-   * @param $message
-   *   The message to store in the log. Keep $message translatable by not
-   *   concatenating dynamic values into it! Variables in the message should be
-   *   added by using placeholder strings alongside the variables argument to
-   *   declare the value of the placeholders. See t() for documentation on how
-   *   $message and $variables interact.
-   * @param $variables
-   *   (Optional) An array of variables to replace in the message on display.
-   * @param $type
-   *   (Optional) The type of the message. Can be one of 'status', 'error',
-   *   'warning' or 'debug'. Messages of the type 'debug' will not get printed
-   *   to the screen.
-   */
-  public function addMessage($message, $variables = array(), $type = 'status') {
-    // Save the job if it hasn't yet been saved.
-    if (!empty($this->tjid) || $this->save()) {
-      $message = tmgmt_message_create($message, $variables, array(
-        'tjid' => $this->tjid,
-        'type' => $type,
-        'uid' => $GLOBALS['user']->uid,
-      ));
-      if ($message->save()) {
-        return $message;
-      }
-    }
-    return FALSE;
-  }
-
-  /**
-   * Returns all job items attached to this job.
-   *
-   * @return array
-   *   An array of translation job items.
-   */
-  public function getItems($conditions = array()) {
-    $query = new EntityFieldQuery();
-    $query->entityCondition('entity_type', 'tmgmt_job_item');
-    $query->propertyCondition('tjid', $this->tjid);
-    foreach ($conditions as $key => $condition) {
-      if (is_array($condition)) {
-        $operator = isset($condition['operator']) ? $condition['operator'] : '=';
-        $query->propertyCondition($key, $condition['value'], $operator);
-      }
-      else {
-        $query->propertyCondition($key, $condition);
-      }
-    }
-    $results = $query->execute();
-    if (!empty($results['tmgmt_job_item'])) {
-      return entity_load('tmgmt_job_item', array_keys($results['tmgmt_job_item']));
-    }
-    return array();
-  }
-
-  /**
-   * Returns all job messages attached to this job.
-   *
-   * @return array
-   *   An array of translation job messages.
-   */
-  public function getMessages($conditions = array()) {
-    $query = new EntityFieldQuery();
-    $query->entityCondition('entity_type', 'tmgmt_message');
-    $query->propertyCondition('tjid', $this->tjid);
-    foreach ($conditions as $key => $condition) {
-      if (is_array($condition)) {
-        $operator = isset($condition['operator']) ? $condition['operator'] : '=';
-        $query->propertyCondition($key, $condition['value'], $operator);
-      }
-      else {
-        $query->propertyCondition($key, $condition);
-      }
-    }
-    $results = $query->execute();
-    if (!empty($results['tmgmt_message'])) {
-      return entity_load('tmgmt_message', array_keys($results['tmgmt_message']));
-    }
-    return array();
-  }
-
-  /**
-   * Returns all job messages attached to this job with timestamp newer than
-   * $time.
-   *
-   * @param $time
-   *   (Optional) Messages need to have a newer timestamp than $time. Defaults
-   *   to REQUEST_TIME.
-   *
-   * @return array
-   *   An array of translation job messages.
-   */
-  public function getMessagesSince($time = NULL) {
-    $time = isset($time) ? $time : REQUEST_TIME;
-    $conditions = array('created' => array('value' => $time, 'operator' => '>='));
-    return $this->getMessages($conditions);
-  }
-
-  /**
-   * Retrieves a setting value from the job settings. Pulls the default values
-   * (if defined) from the plugin controller.
-   *
-   * @param $name
-   *   The name of the setting.
-   *
-   * @return
-   *   The setting value or $default if the setting value is not set. Returns
-   *   NULL if the setting does not exist at all.
-   */
-  public function getSetting($name) {
-    if (isset($this->settings[$name])) {
-      return $this->settings[$name];
-    }
-    // The translator might provide default settings.
-    if ($translator = $this->getTranslator()) {
-      if (($setting = $translator->getSetting($name)) !== NULL) {
-        return $setting;
-      }
-    }
-    if ($controller = $this->getTranslatorController()) {
-      $defaults = $controller->defaultSettings();
-      if (isset($defaults[$name])) {
-        return $defaults[$name];
-      }
-    }
-  }
-
-  /**
-   * Returns the translator for this job.
-   *
-   * @return TMGMTTranslator
-   *   The translator entity or FALSE if there was a problem.
-   */
-  public function getTranslator() {
-    if (isset($this->translator)) {
-      return tmgmt_translator_load($this->translator);
-    }
-    return FALSE;
-  }
-
-  /**
-   * Returns the state of the job. Can be one of the job state constants.
-   *
-   * @return integer
-   *   The state of the job or NULL if it hasn't been set yet.
-   */
-  public function getState() {
-    // We don't need to check if the state is actually set because we always set
-    // it in the constructor.
-    return $this->state;
-  }
-
-  /**
-   * Updates the state of the job.
-   *
-   * @param $state
-   *   The new state of the job. Has to be one of the job state constants.
-   * @param $message
-   *   (Optional) The log message to be saved along with the state change.
-   * @param $variables
-   *   (Optional) An array of variables to replace in the message on display.
-   *
-   * @return int
-   *   The updated state of the job if it could be set.
-   *
-   * @see TMGMTJob::addMessage()
-   */
-  public function setState($state, $message = NULL, $variables = array(), $type = 'debug') {
-    // Return TRUE if the state could be set. Return FALSE otherwise.
-    if (array_key_exists($state, tmgmt_job_states())) {
-      $this->state = $state;
-      $this->save();
-      // If a message is attached to this state change add it now.
-      if (!empty($message)) {
-        $this->addMessage($message, $variables, $type);
-      }
-    }
-    return $this->state;
-  }
-
-  /**
-   * Checks whether the passed value matches the current state.
-   *
-   * @param $state
-   *   The value to check the current state against.
-   *
-   * @return boolean
-   *   TRUE if the passed state matches the current state, FALSE otherwise.
-   */
-  public function isState($state) {
-    return $this->getState() == $state;
-  }
-
-  /**
-   * Checks whether the user described by $account is the author of this job.
-   *
-   * @param $account
-   *   (Optional) A user object. Defaults to the currently logged in user.
-   */
-  public function isAuthor($account = NULL) {
-    $account = isset($account) ? $account : $GLOBALS['user'];
-    return $this->uid == $account->uid;
-  }
-
-  /**
-   * Returns whether the state of this job is 'unprocessed'.
-   *
-   * @return boolean
-   *   TRUE if the state is 'unprocessed', FALSE otherwise.
-   */
-  public function isUnprocessed() {
-    return $this->isState(TMGMT_JOB_STATE_UNPROCESSED);
-  }
-
-  /**
-   * Returns whether the state of this job is 'cancelled'.
-   *
-   * @return boolean
-   *   TRUE if the state is 'cancelled', FALSE otherwise.
-   */
-  public function isCancelled() {
-    return $this->isState(TMGMT_JOB_STATE_CANCELLED);
-  }
-
-  /**
-   * Returns whether the state of this job is 'active'.
-   *
-   * @return boolean
-   *   TRUE if the state is 'active', FALSE otherwise.
-   */
-  public function isActive() {
-    return $this->isState(TMGMT_JOB_STATE_ACTIVE);
-  }
-
-  /**
-   * Returns whether the state of this job is 'rejected'.
-   *
-   * @return boolean
-   *   TRUE if the state is 'rejected', FALSE otherwise.
-   */
-  public function isRejected() {
-    return $this->isState(TMGMT_JOB_STATE_REJECTED);
-  }
-
-  /**
-   * Returns whether the state of this jon is 'finished'.
-   *
-   * @return boolean
-   *   TRUE if the state is 'finished', FALSE otherwise.
-   */
-  public function isFinished() {
-    return $this->isState(TMGMT_JOB_STATE_FINISHED);
-  }
-
-  /**
-   * Checks whether a job is translatable.
-   *
-   * @return boolean
-   *   TRUE if the job can be translated, FALSE otherwise.
-   */
-  public function isTranslatable() {
-    if ($translator = $this->getTranslator()) {
-      if ($translator->canTranslate($this)) {
-        return TRUE;
-      }
-    }
-    return FALSE;
-  }
-
-  /**
-   * Checks whether a job is cancelable.
-   *
-   * @return boolean
-   *   TRUE if the job can be cancelled, FALSE otherwise.
-   */
-  public function isCancelable() {
-    // Only non-submitted translation jobs can be cancelled.
-    return $this->isActive();
-  }
-
-  /**
-   * Checks whether a job is submittable.
-   *
-   * @return boolean
-   *   TRUE if the job can be submitted, FALSE otherwise.
-   */
-  public function isSubmittable() {
-    return $this->isUnprocessed() || $this->isRejected() || $this->isCancelled();
-  }
-
-  /**
-   * Checks whether a job is deletable.
-   *
-   * @return boolean
-   *   TRUE if the job can be deleted, FALSE otherwise.
-   */
-  public function isDeletable() {
-    return !$this->isActive();
-  }
-
-  /**
-   * Set the state of the job to 'submitted'.
-   *
-   * @param $message
-   *   The log message to be saved along with the state change.
-   * @param $variables
-   *   (Optional) An array of variables to replace in the message on display.
-   *
-   * @return TMGMTJob
-   *   The job entity.
-   *
-   * @see TMGMTJob::addMessage()
-   */
-  public function submitted($message = NULL, $variables = array(), $type = 'status') {
-    if (!isset($message)) {
-      $message = 'The translation job has been submitted.';
-    }
-    $this->setState(TMGMT_JOB_STATE_ACTIVE, $message, $variables, $type);
-  }
-
-  /**
-   * Set the state of the job to 'finished'.
-   *
-   * @param $message
-   *   The log message to be saved along with the state change.
-   * @param $variables
-   *   (Optional) An array of variables to replace in the message on display.
-   *
-   * @return TMGMTJob
-   *   The job entity.
-   *
-   * @see TMGMTJob::addMessage()
-   */
-  public function finished($message = NULL, $variables = array(), $type = 'status') {
-    if (!isset($message)) {
-      $message = 'The translation job has been finished.';
-    }
-    return $this->setState(TMGMT_JOB_STATE_FINISHED, $message, $variables, $type);
-  }
-
-  /**
-   * Sets the state of the job to 'cancelled'.
-   *
-   * @param $message
-   *   The log message to be saved along with the state change.
-   * @param $variables
-   *   (Optional) An array of variables to replace in the message on display.
-   *
-   * Use TMGMTJob::cancelTranslation() to cancel a translation.
-   *
-   * @return TMGMTJob
-   *   The job entity.
-   *
-   * @see TMGMTJob::addMessage()
-   */
-  public function cancelled($message = NULL, $variables = array(), $type = 'status') {
-    if (!isset($message)) {
-      $message = 'The translation job has been cancelled.';
-    }
-    return $this->setState(TMGMT_JOB_STATE_CANCELLED, $message, $variables, $type);
-  }
-
-  /**
-   * Sets the state of the job to 'rejected'.
-   *
-   * @param $message
-   *   The log message to be saved along with the state change.
-   * @param $variables
-   *   (Optional) An array of variables to replace in the message on display.
-   *
-   * @return TMGMTJob
-   *   The job entity.
-   *
-   * @see TMGMTJob::addMessage()
-   */
-  public function rejected($message = NULL, $variables = array(), $type = 'error') {
-    if (!isset($message)) {
-      $message = 'The translation job has been rejected by the translation provider.';
-    }
-    return $this->setState(TMGMT_JOB_STATE_REJECTED, $message, $variables, $type);
-  }
-
-  /**
-   * Request the translation of a job from the translator.
-   *
-   * @return integer
-   *   The updated job status.
-   */
-  public function requestTranslation() {
-    if (!$this->isTranslatable() || !$controller = $this->getTranslatorController()) {
-      return FALSE;
-    }
-    // We don't know if the translator plugin already processed our
-    // translation request after this point. That means that the plugin has to
-    // set the 'submitted', 'needs review', etc. states on its own.
-    $controller->requestTranslation($this);
-  }
-
-  /**
-   * Attempts to cancel the translation job. Already accepted jobs can not be
-   * cancelled, submitted jobs only if supported by the translator plugin.
-   * Always use this method if you want to cancel a translation job.
-   *
-   * @return boolean
-   *   TRUE if the translation job was cancelled, FALSE otherwise.
-   */
-  public function cancelTranslation() {
-    if (!$this->isCancelable() || !$controller = $this->getTranslatorController()) {
-      return FALSE;
-    }
-    // We don't know if the translator plugin was able to cancel the translation
-    // job after this point. That means that the plugin has to set the
-    // 'cancelled' state on its own.
-    $controller->cancelTranslation($this);
-  }
-
-  /**
-   * Returns the translator plugin controller of the translator of this job.
-   *
-   * @return TMGMTTranslatorPluginControllerInterface
-   *   The controller of the translator plugin.
-   */
-  public function getTranslatorController() {
-    if ($translator = $this->getTranslator($this)) {
-      return $translator->getController();
-    }
-    return FALSE;
-  }
-
-  /**
-   * Returns the source data of all job items.
-   *
-   * @param $key
-   *   If present, only the subarray identified by key is returned.
-   * @param $index
-   *   Optional index of an attribute below $key.
-   * @return array
-   *   A nested array with the source data where the most upper key is the job
-   *   item id.
-   */
-  public function getData(array $key = array(), $index = null) {
-    $data = array();
-    if (!empty($key)) {
-      $tjiid = array_shift($key);
-      $job_item = entity_load_single('tmgmt_job_item', $tjiid);
-      if ($job_item) {
-        $data[$tjiid] = $job_item->getData($key, $index);
-      }
-    }
-    else {
-      foreach ($this->getItems() as $key => $item) {
-        $data[$key] = $item->getData();
-      }
-    }
-    return $data;
-  }
-
-  /**
-   * Sums up all pending counts of this jobs job items.
-   *
-   * @return
-   *   The sum of all pending counts
-   */
-  public function getCountPending() {
-    return tmgmt_job_statistic($this, 'count_pending');
-  }
-
-  /**
-   * Sums up all translated counts of this jobs job items.
-   *
-   * @return
-   *   The sum of all translated counts
-   */
-  public function getCountTranslated() {
-    return tmgmt_job_statistic($this, 'count_translated');
-  }
-
-  /**
-   * Sums up all accepted counts of this jobs job items.
-   *
-   * @return
-   *   The sum of all accepted data items.
-   */
-  public function getCountAccepted() {
-    return tmgmt_job_statistic($this, 'count_accepted');
-  }
-
-  /**
-   * Sums up all accepted counts of this jobs job items.
-   *
-   * @return
-   *   The sum of all accepted data items.
-   */
-  public function getCountReviewed() {
-    return tmgmt_job_statistic($this, 'count_reviewed');
-  }
-
-  /**
-   * Sums up all word counts of this jobs job items.
-   *
-   * @return
-   *   The total word count of this job.
-   */
-  public function getWordCount() {
-    return tmgmt_job_statistic($this, 'word_count');
-  }
-
-  /**
-   * Store translated data back into the items.
-   *
-   * @param $data
-   *   Partially or complete translated data, the most upper key needs to be
-   *   the translation job item id.
-   * @param $key
-   *   (Optional) Either a flattened key (a 'key1][key2][key3' string) or a nested
-   *   one, e.g. array('key1', 'key2', 'key2'). Defaults to an empty array which
-   *   means that it will replace the whole translated data array. The most
-   *   upper key entry needs to be the job id (tjiid).
-   */
-  public function addTranslatedData($data, $key = NULL) {
-    $key = tmgmt_ensure_keys_array($key);
-    $items = $this->getItems();
-    // If there is a key, get the specific item and forward the call.
-    if (!empty($key)) {
-      $item_id = array_shift($key);
-      if (isset($items[$item_id])) {
-        $items[$item_id]->addTranslatedData($data, $key);
-      }
-    }
-    else {
-      foreach ($data as $key => $value) {
-        if (isset($items[$key])) {
-          $items[$key]->addTranslatedData($value);
-        }
-      }
-    }
-  }
-
-  /**
-   * Propagates the returned job item translations to the sources.
-   *
-   * @return boolean
-   *   TRUE if we were able to propagate the translated data, FALSE otherwise.
-   */
-  public function acceptTranslation() {
-    foreach ($this->getItems() as $item) {
-      $item->acceptTranslation();
-    }
-  }
-
-  /**
-   * Gets remote mappings for current job.
-   *
-   * @return array
-   *   List of TMGMTRemote entities.
-   */
-  public function getRemoteMappings() {
-    $query = new EntityFieldQuery();
-    $query->entityCondition('entity_type', 'tmgmt_remote');
-    $query->propertyCondition('tjid', $this->tjid);
-    $result = $query->execute();
-
-    if (isset($result['tmgmt_remote'])) {
-      return entity_load('tmgmt_remote', array_keys($result['tmgmt_remote']));
-    }
-
-    return array();
-  }
-
-}
-
-/**
- * Entity class for the tmgmt_message entity.
- *
- * @ingroup tmgmt_job
- */
-class TMGMTMessage extends Entity {
-
-  /**
-   * The ID of the message..
-   *
-   * @var integer
-   */
-  public $mid;
-
-  /**
-   * The ID of the job.
-   *
-   * @var integer
-   */
-  public $tjid;
-
-  /**
-   * The ID of the job item.
-   *
-   * @var integer
-   */
-  public $tjiid;
-
-  /**
-   * User uid.
-   *
-   * @var integer
-   */
-  public $uid;
-
-  /**
-   * The message text.
-   *
-   * @var string
-   */
-  public $message;
-
-  /**
-   * An array of string replacement arguments as used by t().
-   *
-   * @var array
-   */
-  public $variables;
-
-  /**
-   * The time when the message object was created as a timestamp.
-   *
-   * @var integer
-   */
-  public $created;
-
-  /**
-   * Type of the message (debug, status, warning or error).
-   *
-   * @var string
-   */
-  public $type;
-
-  /**
-   * Overrides Entity::__construct().
-   */
-  public function __construct(array $values = array()) {
-    parent::__construct($values, 'tmgmt_message');
-    if (empty($this->created)) {
-      $this->created = REQUEST_TIME;
-    }
-    if (empty($this->type)) {
-      $this->type = 'status';
-    }
-  }
-
-  /**
-   * Overrides Entity::label().
-   */
-  public function defaultLabel() {
-    $created = format_date($this->created);
-    switch ($this->type) {
-      case 'error':
-        return t('Error message from @time', array('@time' => $created));
-      case 'status':
-        return t('Status message from @time', array('@time' => $created));
-      case 'warning':
-        return t('Warning message from @time', array('@time' => $created));
-      case 'debug':
-        return t('Debug message from @time', array('@time' => $created));
-    }
-  }
-
-  /**
-   * Returns the translated message.
-   *
-   * @return
-   *   The translated message.
-   */
-  public function getMessage() {
-    $text = $this->message;
-    if (is_array($this->variables) && !empty($this->variables)) {
-      $text = t($text, $this->variables);
-    }
-    return $text;
-  }
-
-  /**
-   * Loads the job entity that this job message is attached to.
-   *
-   * @return TMGMTJob
-   *   The job entity that this job message is attached to or FALSE if there was
-   *   a problem.
-   */
-  public function getJob() {
-    if (!empty($this->tjid)) {
-      return tmgmt_job_load($this->tjid);
-    }
-    return FALSE;
-  }
-
-  /**
-   * Loads the job entity that this job message is attached to.
-   *
-   * @return TMGMTJobItem
-   *   The job item entity that this job message is attached to or FALSE if
-   *   there was a problem.
-   */
-  public function getJobItem() {
-    if (!empty($this->tjiid)) {
-      return tmgmt_job_item_load($this->tjiid);
-    }
-    return FALSE;
-  }
-
-}
diff --git a/includes/tmgmt.plugin.inc b/includes/tmgmt.plugin.inc
deleted file mode 100644
index b2ac6e6..0000000
--- a/includes/tmgmt.plugin.inc
+++ /dev/null
@@ -1,568 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains the classes and interfaces for the services and source plugins.
- */
-
-/**
- * Base interface for Translation Management plugins.
- */
-interface TMGMTPluginBaseInterface {
-
-  /**
-   * Constructor.
-   *
-   * @param $type
-   *   The plugin type.
-   * @param $plugin
-   *   The machine-readable name of the plugin.
-   */
-  public function __construct($type, $plugin);
-
-  /**
-   * Returns the info of the type of the plugin.
-   *
-   * @see tmgmt_source_plugin_info()
-   */
-  public function pluginInfo();
-
-  /**
-   * Returns the type of the plugin.
-   */
-  public function pluginType();
-
-}
-
-/**
- * Base class for Translation Management plugins.
- */
-class TMGMTPluginBase implements TMGMTPluginBaseInterface {
-
-  protected $pluginType;
-  protected $pluginInfo;
-
-  /**
-   * Implements TMGMTSourcePluginControllerInterface::__construct().
-   */
-  public function __construct($type, $plugin) {
-    $this->pluginType = $plugin;
-    $this->pluginInfo = _tmgmt_plugin_info($type, $plugin);
-  }
-
-  /**
-   * Implements TMGMTSourcePluginControllerInterface::pluginInfo().
-   */
-  public function pluginInfo() {
-    return $this->pluginInfo;
-  }
-
-  /**
-   * Implements TMGMTSourcePluginControllerInterface::pluginType().
-   */
-  public function pluginType() {
-    return $this->pluginType;
-  }
-
-}
-
-/**
- * Interface for source plugin controllers.
- *
- * @ingroup tmgmt_source
- */
-interface TMGMTSourcePluginControllerInterface extends TMGMTPluginBaseInterface {
-
-  /**
-   * Returns an array with the data structured for translation.
-   *
-   * @param TMGMTJobItem $job_item
-   *   The job item entity.
-   *
-   * @see TMGMTJobItem::getData()
-   */
-  public function getData(TMGMTJobItem $job_item);
-
-  /**
-   * Saves a translation.
-   *
-   * @param TMGMTJobItem $job_item
-   *   The job item entity.
-   *
-   * @return boolean
-   *   TRUE if the translation was saved successfully, FALSE otherwise.
-   */
-  public function saveTranslation(TMGMTJobItem $job_item);
-
-  /**
-   * Return a title for this job item.
-   *
-   * @param TMGMTJobItem $job_item
-   *   The job item entity.
-   */
-  public function getLabel(TMGMTJobItem $job_item);
-
-  /**
-   * Returns the Uri for this job item.
-   *
-   * @param TMGMTJobItem $job_item
-   *   The job item entity.
-   *
-   * @see entity_uri()
-   */
-  public function getUri(TMGMTJobItem $job_item);
-
-  /**
-   * Returns an array of translatable source item types.
-   */
-  public function getItemTypes();
-
-  /**
-   * Returns the label of a source item type.
-   *
-   * @param $type
-   *   The identifier of a source item type.
-   */
-  public function getItemTypeLabel($type);
-
-}
-
-/**
- * Default controller class for source plugins.
- *
- * @ingroup tmgmt_source
- */
-abstract class TMGMTDefaultSourcePluginController extends TMGMTPluginBase implements TMGMTSourcePluginControllerInterface {
-
-  /**
-   * Implements TMGMTSourcePluginControllerInterface::getLabel().
-   */
-  public function getLabel(TMGMTJobItem $job_item) {
-    return t('@plugin item unavailable (@item)', array('@plugin' => $this->pluginInfo['label'], '@item' => $job_item->item_type . ':' . $job_item->item_id));
-  }
-
-  /**
-   * Implements TMGMTSourcePluginControllerInterface::getUri().
-   */
-  public function getUri(TMGMTJobItem $job_item) {
-    return array(
-      'path' => '',
-      'options' => array(),
-    );
-  }
-
-  /**
-   * Implements TMGMTSourcePluginControllerInterface::getItemTypes().
-   */
-  public function getItemTypes() {
-    return isset($this->pluginInfo['item types']) ? $this->pluginInfo['item types'] : array();
-  }
-
-  /**
-   * Implements TMGMTSourcePluginControllerInterface::getItemTypeLabel().
-   */
-  public function getItemTypeLabel($type) {
-    $types = $this->getItemTypes();
-    if (isset($types[$type])) {
-      return $types[$type];
-    }
-    return '';
-  }
-
-}
-
-/**
- * Interface for service plugin controllers.
- *
- * @ingroup tmgmt_translator
- */
-interface TMGMTTranslatorPluginControllerInterface extends TMGMTPluginBaseInterface {
-
-  /**
-   * Checks whether a translator is available.
-   *
-   * @param TMGMTTranslator $translator
-   *   The translator entity.
-   *
-   * @return boolean
-   *   TRUE if the translator plugin is available, FALSE otherwise.
-   */
-  public function isAvailable(TMGMTTranslator $translator);
-
-  /**
-   * Return a reason why the translator is not available.
-   *
-   * @param TMGMTTranslator $translator
-   *   The translator entity.
-   *
-   * Might be called when isAvailable() returns FALSE to get a reason that
-   * can be displayed to the user.
-   *
-   * @todo Remove this once http://drupal.org/node/1420364 is done.
-   */
-  public function getNotAvailableReason(TMGMTTranslator $translator);
-
-  /**
-   * Check whether this service can handle a particular translation job.
-   *
-   * @param TMGMTTranslator $translator
-   *   The TMGMTTranslator entity that should handle the translation.
-   * @param TMGMTJob $job
-   *   The TMGMTJob entity that should be translated.
-   *
-   * @return boolean
-   *   TRUE if the job can be processed and translated, FALSE otherwise.
-   */
-  public function canTranslate(TMGMTTranslator $translator, TMGMTJob $job);
-
-  /**
-   * Return a reason why the translator is not able to translate this job.
-   *
-   * @param TMGMTJob $job
-   *   The job entity.
-   *
-   * Might be called when canTranslate() returns FALSE to get a reason that
-   * can be displayed to the user.
-   *
-   * @todo Remove this once http://drupal.org/node/1420364 is done.
-   */
-  public function getNotCanTranslateReason(TMGMTJob $job);
-
-  /**
-   * Specifies default mappings for local to remote language codes.
-   *
-   * @return array
-   *   An array of local => remote language codes.
-   */
-  public function getDefaultRemoteLanguagesMappings();
-
-  /**
-   * Gets all supported languages of the translator.
-   *
-   * @param TMGMTTranslator $translator
-   *   Translator entity for which to get supported languages.
-   *
-   * @return array
-   *   An array of language codes which are provided by the translator
-   *   (remote language codes).
-   */
-  public function getSupportedRemoteLanguages(TMGMTTranslator $translator);
-
-  /**
-   * Gets existing remote languages mappings.
-   *
-   * @param TMGMTTranslator $translator
-   *   Translator entity for which to get mappings.
-   *
-   * @return array
-   *   An array of local => remote language codes.
-   */
-  public function getRemoteLanguagesMappings(TMGMTTranslator $translator);
-
-  /**
-   * Maps local language to remote language.
-   *
-   * @param TMGMTTranslator $translator
-   *   Translator entity for which to get remote language.
-   * @param $language
-   *   Local language code.
-   *
-   * @return string
-   *   Remote language code.
-   */
-  public function mapToRemoteLanguage(TMGMTTranslator $translator, $language);
-
-  /**
-   * Maps remote language to local language.
-   *
-   * @param TMGMTTranslator $translator
-   *   Translator entity for which to get local language.
-   * @param $language
-   *   Remote language code.
-   *
-   * @return string
-   *   Local language code.
-   */
-  public function mapToLocalLanguage(TMGMTTranslator $translator, $language);
-
-  /**
-   * Returns all available target languages that are supported by this service
-   * when given a source language.
-   *
-   * @param TMGMTTranslator $translator
-   *   The translator entity.
-   * @param $source_language
-   *   The source language.
-   *
-   * @return array
-   *   An array of remote languages in ISO format.
-   */
-  public function getSupportedTargetLanguages(TMGMTTranslator $translator, $source_language);
-
-  /**
-   * @abstract
-   *
-   * Submits the translation request and sends it to the translation provider.
-   *
-   * @param TMGMTJob $job
-   *   The job that should be submitted.
-   */
-  public function requestTranslation(TMGMTJob $job);
-
-  /**
-   * Cancels a translation job.
-   *
-   * @param TMGMTJob $job
-   *   The job that should have its translation cancelled.
-   *
-   * @return boolean
-   *   TRUE if the job could be cancelled, FALSE otherwise.
-   */
-  public function cancelTranslation(TMGMTJob $job);
-
-  /**
-   * Defines default settings.
-   *
-   * @return array
-   *   An array of default settings.
-   */
-  public function defaultSettings();
-
-  /**
-   * Returns if the translator has any settings for the passed job.
-   */
-  public function hasCheckoutSettings(TMGMTJob $job);
-
-  /**
-   * Accept a single data item.
-   *
-   * @todo Using job item breaks the current convention which uses jobs.
-   *
-   * @param $job_item
-   *   The Job item the accepted data item belongs to.
-   * @param $key
-   *   The key of the accepted data item.
-   *   The key is an array containing the keys of a nested array hierarchy path.
-   *
-   * @return
-   *   TRUE if the approving was succesfull, FALSE otherwise.
-   *   In case of an error, it is the responsibility of the translator to
-   *   provide informations about the failure by adding a message to the job
-   *   item.
-   */
-  public function acceptetDataItem(TMGMTJobItem $job_item, array $key);
-
-}
-
-/**
- * Handle reject on data item level.
- *
- * Implement this interface in a translator plugin to signal that this plugin is
- * capable of handling a reject of single data items.
- *
- * @ingroup tmgmt_translator
- */
-interface TMGMTTranslatorRejectDataItem {
-
-  /**
-   * Reject one single data item.
-   *
-   * @todo Using job item breaks the current convention which uses jobs.
-   *
-   * @param $job_item
-   *   The job item to which the rejected data item belongs.
-   * @param $key
-   *   The key of the rejected data item.
-   *   The key is an array containing the keys of a nested array hierarchy path.
-   *
-   * @return
-   *   TRUE if the reject was succesfull, else FALSE.
-   *   In case of an error, it is the responsibility of the translator to
-   *   provide informations about the faliure.
-   */
-  public function rejectDataItem(TMGMTJobItem $job_item, array $key, array $values = NULL);
-
-  /**
-   * Reject form.
-   *
-   * This method gets call by tmgmt_ui_translation_review_form_reject_confirm
-   * and allows the translator to add aditional form elements in order to
-   * collect data needed for the reject prozess.
-   *
-   * @param $form
-   *   The form array containing a confirm form.
-   *   $form['item'] holds the job item to which the to be rejected data item
-   *   belongs to.
-   *   $form['item'] holds key of the to be rejected data item as an array of
-   *   keys of a nested array hierarchy.
-   * @param $form_state
-   *   The form state.
-   *
-   * @return
-   *   The resulting form array.
-   */
-  public function rejectForm($form, &$form_state);
-}
-
-/**
- * Default controller class for service plugins.
- *
- * @ingroup tmgmt_translator
- */
-abstract class TMGMTDefaultTranslatorPluginController extends TMGMTPluginBase implements TMGMTTranslatorPluginControllerInterface {
-
-  protected $supportedRemoteLanguages = array();
-  protected $remoteLanguagesMappings = array();
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::isAvailable().
-   */
-  public function isAvailable(TMGMTTranslator $translator) {
-    // Assume that the translation service is always available.
-    return TRUE;
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::canTranslate().
-   */
-  public function canTranslate(TMGMTTranslator $translator, TMGMTJob $job) {
-    // The job is only translatable if the translator is available too.
-    if ($this->isAvailable($translator) && array_key_exists($job->target_language, $translator->getSupportedTargetLanguages($job->source_language))) {
-      // We can only translate this job if the target language of the job is in
-      // one of the supported languages.
-      return TRUE;
-    }
-    return FALSE;
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::cancelTranslation().
-   */
-  public function cancelTranslation(TMGMTJob $job) {
-    // Assume that we can cancel a translation job at any time.
-    $job->setState(TMGMT_JOB_STATE_CANCELLED);
-    return TRUE;
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::getDefaultRemoteLanguagesMappings().
-   */
-  public function getDefaultRemoteLanguagesMappings() {
-    return array();
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::getSupportedLanguages().
-   */
-  public function getSupportedRemoteLanguages(TMGMTTranslator $translator) {
-    return array();
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::getRemoteLanguagesMappings().
-   */
-  public function getRemoteLanguagesMappings(TMGMTTranslator $translator) {
-    if (!empty($this->remoteLanguagesMappings)) {
-      return $this->remoteLanguagesMappings;
-    }
-
-    foreach (language_list() as $language => $info) {
-      $this->remoteLanguagesMappings[$language] = $this->mapToRemoteLanguage($translator, $language);
-    }
-
-    return $this->remoteLanguagesMappings;
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::mapToRemoteLanguage().
-   */
-  public function mapToRemoteLanguage(TMGMTTranslator $translator, $language) {
-    if (!tmgmt_provide_remote_languages_mappings($translator)) {
-      return $language;
-    }
-
-    if (!empty($translator->settings['remote_languages_mappings'][$language])) {
-      return $translator->settings['remote_languages_mappings'][$language];
-    }
-
-    $default_mappings = $this->getDefaultRemoteLanguagesMappings();
-
-    if (isset($default_mappings[$language])) {
-      return $default_mappings[$language];
-    }
-
-    return $language;
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::mapToLocalLanguage().
-   */
-  public function mapToLocalLanguage(TMGMTTranslator $translator, $language) {
-    if (!tmgmt_provide_remote_languages_mappings($translator)) {
-      return $language;
-    }
-
-    if (isset($translator->settings['remote_languages_mappings']) && is_array($translator->settings['remote_languages_mappings'])) {
-      $mappings = $translator->settings['remote_languages_mappings'];
-    }
-    else {
-      $mappings = $this->getDefaultRemoteLanguagesMappings();
-    }
-
-    if ($remote_language = array_search($language, $mappings)) {
-      return $remote_language;
-    }
-
-    return $language;
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::getSupportedTargetLanguages().
-   */
-  public function getSupportedTargetLanguages(TMGMTTranslator $translator, $source_language) {
-    $languages = entity_metadata_language_list();
-    unset($languages[LANGUAGE_NONE], $languages[$source_language]);
-    return drupal_map_assoc(array_keys($languages));
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::getNotCanTranslateReason().
-   */
-  public function getNotCanTranslateReason(TMGMTJob $job) {
-    $wrapper = entity_metadata_wrapper('tmgmt_job', $job);
-    return t('@translator can not translate from @source to @target.', array('@translator' => $job->getTranslator()->label(), '@source' => $wrapper->source_language->label(), '@target' => $wrapper->target_language->label()));
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::getNotAvailableReason().
-   */
-  public function getNotAvailableReason(TMGMTTranslator $translator) {
-    return t('@translator is not available. Make sure it is properly !configured.', array('@translator' => $this->pluginInfo['label'], '!configured' => l(t('configured'), 'admin/config/regional/tmgmt/translators/manage/' . $translator->name)));
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::defaultSettings().
-   */
-  public function defaultSettings() {
-    $defaults = array('auto_accept' => FALSE);
-    // Check if any default settings are defined in the plugin info.
-    if (isset($this->pluginInfo['default settings'])) {
-      return array_merge($defaults, $this->pluginInfo['default settings']);
-    }
-    return $defaults;
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::checkoutInfo().
-   */
-  public function hasCheckoutSettings(TMGMTJob $job) {
-    return TRUE;
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::acceptedDataItem().
-   */
-  public function acceptetDataItem(TMGMTJobItem $job_item, array $key) {
-    return TRUE;
-  }
-}
diff --git a/includes/tmgmt.ui.inc b/includes/tmgmt.ui.inc
deleted file mode 100644
index e04e1a3..0000000
--- a/includes/tmgmt.ui.inc
+++ /dev/null
@@ -1,319 +0,0 @@
-<?php
-
-
-/**
- * Interface for source ui controllers.
- *
- * @ingroup tmgmt_source
- */
-interface TMGMTSourceUIControllerInterface extends TMGMTPluginBaseInterface {
-
-  /**
-   * Form callback for the job item review form.
-   */
-  public function reviewForm($form, &$form_state, TMGMTJobItem $item);
-
-  /**
-   * Validation callback for the job item review form.
-   */
-  public function reviewFormValidate($form, &$form_state, TMGMTJobItem $item);
-
-  /**
-   * Submit callback for the job item review form.
-   */
-  public function reviewFormSubmit($form, &$form_state, TMGMTJobItem $item);
-
-  /**
-   * Implements hook_menu().
-   *
-   * @see tmgmt_ui_menu().
-   */
-  public function hook_menu();
-
-  /**
-   * Implements hook_forms().
-   *
-   * @see tmgmt_ui_forms().
-   */
-  public function hook_forms();
-
-  /**
-   * Implements hook_views_default_views().
-   *
-   * @see tmgmt_ui_views_default_views().
-   */
-  public function hook_views_default_views();
-
-}
-
-/**
- * Default ui controller class for source plugin.
- *
- * @ingroup tmgmt_source
- */
-class TMGMTDefaultSourceUIController extends TMGMTPluginBase implements TMGMTSourceUIControllerInterface {
-
-  /**
-   * Implements TMGMTSourceUIControllerInterface::reviewForm().
-   */
-  public function reviewForm($form, &$form_state, TMGMTJobItem $item) {
-    return $form;
-  }
-
-  /**
-   * Implements TMGMTSourceUIControllerInterface::reviewFormValidate().
-   */
-  public function reviewFormValidate($form, &$form_state, TMGMTJobItem $item) {
-    // Nothing to do here by default.
-  }
-
-  /**
-   * Implements TMGMTSourceUIControllerInterface::reviewFormSubmit().
-   */
-  public function reviewFormSubmit($form, &$form_state, TMGMTJobItem $item) {
-    // Nothing to do here by default.
-  }
-
-  /**
-   * Implements TMGMTSourceUIControllerInterface::overviewForm().
-   */
-  public function overviewForm($form, &$form_state, $type) {
-    return $form;
-  }
-
-  /**
-   * Implements TMGMTSourceUIControllerInterface::overviewFormValidate().
-   */
-  public function overviewFormValidate($form, &$form_state, $type) {
-    // Nothing to do here by default.
-  }
-
-  /**
-   * Implements TMGMTSourceUIControllerInterface::overviewFormSubmit().
-   */
-  public function overviewFormSubmit($form, &$form_state, $type) {
-    // Nothing to do here by default.
-  }
-
-  /**
-   * Implements TMGMTSourceUIControllerInterface::hook_menu().
-   */
-  public function hook_menu() {
-    $items = array();
-    if ($types = tmgmt_source_translatable_item_types($this->pluginType)) {
-      $defaults = array(
-        'file' => isset($this->pluginInfo['file']) ? $this->pluginInfo['file'] : $this->pluginInfo['module'] . '.pages.inc',
-        'file path' => isset($this->pluginInfo['file path']) ? $this->pluginInfo['file path'] : drupal_get_path('module', $this->pluginInfo['module']),
-        'page callback' => 'drupal_get_form',
-        'access callback' => 'tmgmt_job_access',
-        'access arguments' => array('create'),
-      );
-      foreach ($types as $type => $name) {
-        if (empty($items['admin/config/regional/tmgmt/' . $this->pluginType])) {
-          // Make the first item type of this source the default menu tab.
-          $items['admin/config/regional/tmgmt/' . $this->pluginType] = $defaults + array(
-            'title' => t('@type sources', array('@type' => ucfirst($this->pluginInfo['label']))),
-            'page arguments' => array('tmgmt_ui_' . $this->pluginType . '_source_' . $type . '_overview_form', $this->pluginType, $type),
-            'type' => MENU_LOCAL_TASK,
-          );
-          $items['admin/config/regional/tmgmt/' . $this->pluginType . '/' . $type] = array(
-            'title' => check_plain($name),
-            'type' => MENU_DEFAULT_LOCAL_TASK,
-          );
-        }
-        else {
-          $items['admin/config/regional/tmgmt/' . $this->pluginType . '/' . $type] = $defaults + array(
-            'title' => check_plain($name),
-            'page arguments' => array('tmgmt_ui_' . $this->pluginType . '_source_' . $type . '_overview_form', $this->pluginType, $type),
-            'type' => MENU_LOCAL_TASK,
-          );
-        }
-      }
-    }
-    return $items;
-  }
-
-  /**
-   * Implements TMGMTSourceUIControllerInterface::hook_form().
-   */
-  public function hook_forms() {
-    $info = array();
-    if ($types = tmgmt_source_translatable_item_types($this->pluginType)) {
-      foreach (array_keys($types) as $type) {
-        $info['tmgmt_ui_' . $this->pluginType . '_source_' . $type . '_overview_form'] = array(
-          'callback' => 'tmgmt_ui_source_overview_form',
-          'wrapper_callback' => 'tmgmt_ui_source_overview_form_defaults',
-        );
-      }
-    }
-    return $info;
-  }
-
-  /**
-   * Implements TMGMTSourceUIControllerInterface::hook_views_default_views().
-   */
-  public function hook_views_default_views() {
-    return array();
-  }
-
-}
-
-/**
- * Interface for translator ui controllers.
- *
- * @ingroup tmgmt_translator
- */
-interface TMGMTTranslatorUIControllerInterface extends TMGMTPluginBaseInterface {
-
-  /**
-   * Form callback for the plugin settings form.
-   */
-  public function pluginSettingsForm($form, &$form_state, TMGMTTranslator $translator, $busy = FALSE);
-
-  /**
-   * Form callback for the checkout settings form.
-   */
-  public function checkoutSettingsForm($form, &$form_state, TMGMTJob $job);
-
-  /**
-   * Retrieves information about a translation job.
-   *
-   * @param TMGMTJob $job
-   *   The translation job.
-   */
-  public function checkoutInfo(TMGMTJob $job);
-
-  /**
-   * Form callback for the job item review form.
-   */
-  public function reviewForm($form, &$form_state, TMGMTJobItem $item);
-
-  /**
-   * Validation callback for the job item review form.
-   */
-  public function reviewFormValidate($form, &$form_state, TMGMTJobItem $item);
-
-  /**
-   * Submit callback for the job item review form.
-   */
-  public function reviewFormSubmit($form, &$form_state, TMGMTJobItem $item);
-
-}
-
-/**
- * Default ui controller class for translator plugins.
- *
- * @ingroup tmgmt_translator
- */
-class TMGMTDefaultTranslatorUIController extends TMGMTPluginBase implements TMGMTTranslatorUIControllerInterface {
-
-  /**
-   * Implements TMGMTTranslatorUIControllerInterface::pluginSettingsForm().
-   */
-  public function pluginSettingsForm($form, &$form_state, TMGMTTranslator $translator, $busy = FALSE) {
-
-    if (!empty($translator->plugin)) {
-      $controller = tmgmt_translator_plugin_controller($translator->plugin);
-    }
-
-    // If current translator is configured to provide remote language mapping
-    // provide the form to configure mappings, unless it does not exists yet.
-    if (!empty($controller) && tmgmt_provide_remote_languages_mappings($translator)) {
-
-      $form['remote_languages_mappings'] = array(
-        '#tree' => TRUE,
-        '#type' => 'fieldset',
-        '#title' => t('Remote languages mappings'),
-        '#description' => t('Here you can specify mappings of your local language codes to the translator language codes.'),
-        '#collapsible' => TRUE,
-        '#collapsed' => TRUE,
-      );
-
-      $options = array();
-      foreach ($controller->getSupportedRemoteLanguages($translator) as $language) {
-        $options[$language] = $language;
-      }
-
-      foreach ($controller->getRemoteLanguagesMappings($translator) as $local_language => $remote_language) {
-        $form['remote_languages_mappings'][$local_language] = array(
-          '#type' => 'textfield',
-          '#title' => tmgmt_language_label($local_language) . ' (' . $local_language . ')',
-          '#default_value' => $remote_language,
-          '#size' => 6,
-        );
-
-        if (!empty($options)) {
-          $form['remote_languages_mappings'][$local_language]['#type'] = 'select';
-          $form['remote_languages_mappings'][$local_language]['#options'] = $options;
-          $form['remote_languages_mappings'][$local_language]['#empty_option'] = ' - ';
-          unset($form['remote_languages_mappings'][$local_language]['#size']);
-        }
-      }
-    }
-
-    if (!element_children($form)) {
-      $form['#description'] = t("The @plugin plugin doesn't provide any settings.", array('@plugin' => $this->pluginInfo['label']));
-    }
-    return $form;
-  }
-
-  /**
-   * Implements TMGMTTranslatorUIControllerInterface::checkoutSettingsForm().
-   */
-  public function checkoutSettingsForm($form, &$form_state, TMGMTJob $job) {
-    if (!element_children($form)) {
-      $form['#description'] = t("The @translator translator doesn't provide any checkout settings.", array('@translator' => $job->getTranslator()->label()));
-    }
-    return $form;
-  }
-
-  /**
-   * Implements TMGMTTranslatorUIControllerInterface::checkoutInfo().
-   */
-  public function checkoutInfo(TMGMTJob $job) {
-    return array();
-  }
-
-  /**
-   * Provides a simple wrapper for the checkout info fieldset.
-   *
-   * @param TMGMTJob $job
-   *   Translation job object.
-   * @param $form
-   *   Partial form structure to be wrapped in the fieldset.
-   *
-   * @return
-   *   The provided form structure wrapped in a collapsed fieldset.
-   */
-  public function checkoutInfoWrapper(TMGMTJob $job, $form) {
-    $label = $job->getTranslator()->label();
-    $form += array(
-      '#title' => t('@translator translation job information', array('@translator' => $label)),
-      '#type' => 'fieldset',
-      '#collapsible' => TRUE,
-    );
-    return $form;
-  }
-
-  /**
-   * Implements TMGMTTranslatorUIControllerInterface::reviewForm().
-   */
-  public function reviewForm($form, &$form_state, TMGMTJobItem $item) {
-    return $form;
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::reviewFormValidate().
-   */
-  public function reviewFormValidate($form, &$form_state, TMGMTJobItem $item) {
-    // Nothing to do here by default.
-  }
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::reviewFormSubmit().
-   */
-  public function reviewFormSubmit($form, &$form_state, TMGMTJobItem $item) {
-    // Nothing to do here by default.
-  }
-
-}
diff --git a/plugin/tmgmt.plugin.base.inc b/plugin/tmgmt.plugin.base.inc
new file mode 100644
index 0000000..41b3268
--- /dev/null
+++ b/plugin/tmgmt.plugin.base.inc
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains the base plugin class.
+ */
+
+/**
+ * Base class for Translation Management plugins.
+ */
+class TMGMTPluginBase implements TMGMTPluginBaseInterface {
+
+  protected $pluginType;
+  protected $pluginInfo;
+
+  /**
+   * Implements TMGMTSourcePluginControllerInterface::__construct().
+   */
+  public function __construct($type, $plugin) {
+    $this->pluginType = $plugin;
+    $this->pluginInfo = _tmgmt_plugin_info($type, $plugin);
+  }
+
+  /**
+   * Implements TMGMTSourcePluginControllerInterface::pluginInfo().
+   */
+  public function pluginInfo() {
+    return $this->pluginInfo;
+  }
+
+  /**
+   * Implements TMGMTSourcePluginControllerInterface::pluginType().
+   */
+  public function pluginType() {
+    return $this->pluginType;
+  }
+
+}
diff --git a/plugin/tmgmt.plugin.interface.base.inc b/plugin/tmgmt.plugin.interface.base.inc
new file mode 100644
index 0000000..74c2f70
--- /dev/null
+++ b/plugin/tmgmt.plugin.interface.base.inc
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains the base plugin interface.
+ */
+
+/**
+ * Base interface for Translation Management plugins.
+ */
+interface TMGMTPluginBaseInterface {
+
+  /**
+   * Constructor.
+   *
+   * @param $type
+   *   The plugin type.
+   * @param $plugin
+   *   The machine-readable name of the plugin.
+   */
+  public function __construct($type, $plugin);
+
+  /**
+   * Returns the info of the type of the plugin.
+   *
+   * @see tmgmt_source_plugin_info()
+   */
+  public function pluginInfo();
+
+  /**
+   * Returns the type of the plugin.
+   */
+  public function pluginType();
+
+}
diff --git a/plugin/tmgmt.plugin.interface.reject.inc b/plugin/tmgmt.plugin.interface.reject.inc
new file mode 100644
index 0000000..0780add
--- /dev/null
+++ b/plugin/tmgmt.plugin.interface.reject.inc
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @file
+ * Contains the reject translator plugin interface.
+ */
+
+/**
+ * Handle reject on data item level.
+ *
+ * Implement this interface in a translator plugin to signal that this plugin is
+ * capable of handling a reject of single data items.
+ *
+ * @ingroup tmgmt_translator
+ */
+interface TMGMTTranslatorRejectDataItem {
+
+  /**
+   * Reject one single data item.
+   *
+   * @todo Using job item breaks the current convention which uses jobs.
+   *
+   * @param $job_item
+   *   The job item to which the rejected data item belongs.
+   * @param $key
+   *   The key of the rejected data item.
+   *   The key is an array containing the keys of a nested array hierarchy path.
+   *
+   * @return
+   *   TRUE if the reject was succesfull, else FALSE.
+   *   In case of an error, it is the responsibility of the translator to
+   *   provide informations about the faliure.
+   */
+  public function rejectDataItem(TMGMTJobItem $job_item, array $key, array $values = NULL);
+
+  /**
+   * Reject form.
+   *
+   * This method gets call by tmgmt_ui_translation_review_form_reject_confirm
+   * and allows the translator to add aditional form elements in order to
+   * collect data needed for the reject prozess.
+   *
+   * @param $form
+   *   The form array containing a confirm form.
+   *   $form['item'] holds the job item to which the to be rejected data item
+   *   belongs to.
+   *   $form['item'] holds key of the to be rejected data item as an array of
+   *   keys of a nested array hierarchy.
+   * @param $form_state
+   *   The form state.
+   *
+   * @return
+   *   The resulting form array.
+   */
+  public function rejectForm($form, &$form_state);
+}
diff --git a/plugin/tmgmt.plugin.interface.source.inc b/plugin/tmgmt.plugin.interface.source.inc
new file mode 100644
index 0000000..5326477
--- /dev/null
+++ b/plugin/tmgmt.plugin.interface.source.inc
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * @file
+ * Contains the source plugin interface.
+ */
+
+/**
+ * Interface for source plugin controllers.
+ *
+ * @ingroup tmgmt_source
+ */
+interface TMGMTSourcePluginControllerInterface extends TMGMTPluginBaseInterface {
+
+  /**
+   * Returns an array with the data structured for translation.
+   *
+   * @param TMGMTJobItem $job_item
+   *   The job item entity.
+   *
+   * @see TMGMTJobItem::getData()
+   */
+  public function getData(TMGMTJobItem $job_item);
+
+  /**
+   * Saves a translation.
+   *
+   * @param TMGMTJobItem $job_item
+   *   The job item entity.
+   *
+   * @return boolean
+   *   TRUE if the translation was saved successfully, FALSE otherwise.
+   */
+  public function saveTranslation(TMGMTJobItem $job_item);
+
+  /**
+   * Return a title for this job item.
+   *
+   * @param TMGMTJobItem $job_item
+   *   The job item entity.
+   */
+  public function getLabel(TMGMTJobItem $job_item);
+
+  /**
+   * Returns the Uri for this job item.
+   *
+   * @param TMGMTJobItem $job_item
+   *   The job item entity.
+   *
+   * @see entity_uri()
+   */
+  public function getUri(TMGMTJobItem $job_item);
+
+  /**
+   * Returns an array of translatable source item types.
+   */
+  public function getItemTypes();
+
+  /**
+   * Returns the label of a source item type.
+   *
+   * @param $type
+   *   The identifier of a source item type.
+   */
+  public function getItemTypeLabel($type);
+
+}
diff --git a/plugin/tmgmt.plugin.interface.translator.inc b/plugin/tmgmt.plugin.interface.translator.inc
new file mode 100644
index 0000000..f60d1ea
--- /dev/null
+++ b/plugin/tmgmt.plugin.interface.translator.inc
@@ -0,0 +1,189 @@
+<?php
+
+/**
+ * @file
+ * Contains the source plugin interface.
+ */
+
+/**
+ * Interface for service plugin controllers.
+ *
+ * @ingroup tmgmt_translator
+ */
+interface TMGMTTranslatorPluginControllerInterface extends TMGMTPluginBaseInterface {
+
+  /**
+   * Checks whether a translator is available.
+   *
+   * @param TMGMTTranslator $translator
+   *   The translator entity.
+   *
+   * @return boolean
+   *   TRUE if the translator plugin is available, FALSE otherwise.
+   */
+  public function isAvailable(TMGMTTranslator $translator);
+
+  /**
+   * Return a reason why the translator is not available.
+   *
+   * @param TMGMTTranslator $translator
+   *   The translator entity.
+   *
+   * Might be called when isAvailable() returns FALSE to get a reason that
+   * can be displayed to the user.
+   *
+   * @todo Remove this once http://drupal.org/node/1420364 is done.
+   */
+  public function getNotAvailableReason(TMGMTTranslator $translator);
+
+  /**
+   * Check whether this service can handle a particular translation job.
+   *
+   * @param TMGMTTranslator $translator
+   *   The TMGMTTranslator entity that should handle the translation.
+   * @param TMGMTJob $job
+   *   The TMGMTJob entity that should be translated.
+   *
+   * @return boolean
+   *   TRUE if the job can be processed and translated, FALSE otherwise.
+   */
+  public function canTranslate(TMGMTTranslator $translator, TMGMTJob $job);
+
+  /**
+   * Return a reason why the translator is not able to translate this job.
+   *
+   * @param TMGMTJob $job
+   *   The job entity.
+   *
+   * Might be called when canTranslate() returns FALSE to get a reason that
+   * can be displayed to the user.
+   *
+   * @todo Remove this once http://drupal.org/node/1420364 is done.
+   */
+  public function getNotCanTranslateReason(TMGMTJob $job);
+
+  /**
+   * Specifies default mappings for local to remote language codes.
+   *
+   * @return array
+   *   An array of local => remote language codes.
+   */
+  public function getDefaultRemoteLanguagesMappings();
+
+  /**
+   * Gets all supported languages of the translator.
+   *
+   * @param TMGMTTranslator $translator
+   *   Translator entity for which to get supported languages.
+   *
+   * @return array
+   *   An array of language codes which are provided by the translator
+   *   (remote language codes).
+   */
+  public function getSupportedRemoteLanguages(TMGMTTranslator $translator);
+
+  /**
+   * Gets existing remote languages mappings.
+   *
+   * @param TMGMTTranslator $translator
+   *   Translator entity for which to get mappings.
+   *
+   * @return array
+   *   An array of local => remote language codes.
+   */
+  public function getRemoteLanguagesMappings(TMGMTTranslator $translator);
+
+  /**
+   * Maps local language to remote language.
+   *
+   * @param TMGMTTranslator $translator
+   *   Translator entity for which to get remote language.
+   * @param $language
+   *   Local language code.
+   *
+   * @return string
+   *   Remote language code.
+   */
+  public function mapToRemoteLanguage(TMGMTTranslator $translator, $language);
+
+  /**
+   * Maps remote language to local language.
+   *
+   * @param TMGMTTranslator $translator
+   *   Translator entity for which to get local language.
+   * @param $language
+   *   Remote language code.
+   *
+   * @return string
+   *   Local language code.
+   */
+  public function mapToLocalLanguage(TMGMTTranslator $translator, $language);
+
+  /**
+   * Returns all available target languages that are supported by this service
+   * when given a source language.
+   *
+   * @param TMGMTTranslator $translator
+   *   The translator entity.
+   * @param $source_language
+   *   The source language.
+   *
+   * @return array
+   *   An array of remote languages in ISO format.
+   */
+  public function getSupportedTargetLanguages(TMGMTTranslator $translator, $source_language);
+
+  /**
+   * @abstract
+   *
+   * Submits the translation request and sends it to the translation provider.
+   *
+   * @param TMGMTJob $job
+   *   The job that should be submitted.
+   */
+  public function requestTranslation(TMGMTJob $job);
+
+  /**
+   * Cancels a translation job.
+   *
+   * @param TMGMTJob $job
+   *   The job that should have its translation cancelled.
+   *
+   * @return boolean
+   *   TRUE if the job could be cancelled, FALSE otherwise.
+   */
+  public function cancelTranslation(TMGMTJob $job);
+
+  /**
+   * Defines default settings.
+   *
+   * @return array
+   *   An array of default settings.
+   */
+  public function defaultSettings();
+
+  /**
+   * Returns if the translator has any settings for the passed job.
+   */
+  public function hasCheckoutSettings(TMGMTJob $job);
+
+  /**
+   * Accept a single data item.
+   *
+   * @todo Using job item breaks the current convention which uses jobs.
+   *
+   * @param $job_item
+   *   The Job item the accepted data item belongs to.
+   * @param $key
+   *   The key of the accepted data item.
+   *   The key is an array containing the keys of a nested array hierarchy path.
+   *
+   * @return
+   *   TRUE if the approving was succesfull, FALSE otherwise.
+   *   In case of an error, it is the responsibility of the translator to
+   *   provide informations about the failure by adding a message to the job
+   *   item.
+   */
+  public function acceptetDataItem(TMGMTJobItem $job_item, array $key);
+
+}
diff --git a/plugin/tmgmt.plugin.source.inc b/plugin/tmgmt.plugin.source.inc
new file mode 100644
index 0000000..028d77a
--- /dev/null
+++ b/plugin/tmgmt.plugin.source.inc
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Contains the abstract source base plugin class.
+ */
+
+/**
+ * Default controller class for source plugins.
+ *
+ * @ingroup tmgmt_source
+ */
+abstract class TMGMTDefaultSourcePluginController extends TMGMTPluginBase implements TMGMTSourcePluginControllerInterface {
+
+  /**
+   * Implements TMGMTSourcePluginControllerInterface::getLabel().
+   */
+  public function getLabel(TMGMTJobItem $job_item) {
+    return t('@plugin item unavailable (@item)', array('@plugin' => $this->pluginInfo['label'], '@item' => $job_item->item_type . ':' . $job_item->item_id));
+  }
+
+  /**
+   * Implements TMGMTSourcePluginControllerInterface::getUri().
+   */
+  public function getUri(TMGMTJobItem $job_item) {
+    return array(
+      'path' => '',
+      'options' => array(),
+    );
+  }
+
+  /**
+   * Implements TMGMTSourcePluginControllerInterface::getItemTypes().
+   */
+  public function getItemTypes() {
+    return isset($this->pluginInfo['item types']) ? $this->pluginInfo['item types'] : array();
+  }
+
+  /**
+   * Implements TMGMTSourcePluginControllerInterface::getItemTypeLabel().
+   */
+  public function getItemTypeLabel($type) {
+    $types = $this->getItemTypes();
+    if (isset($types[$type])) {
+      return $types[$type];
+    }
+    return '';
+  }
+
+}
diff --git a/plugin/tmgmt.plugin.translator.inc b/plugin/tmgmt.plugin.translator.inc
new file mode 100644
index 0000000..97328e6
--- /dev/null
+++ b/plugin/tmgmt.plugin.translator.inc
@@ -0,0 +1,169 @@
+<?php
+
+/**
+ * @file
+ * Contains the abstract translator base plugin class.
+ */
+
+/**
+ * Default controller class for service plugins.
+ *
+ * @ingroup tmgmt_translator
+ */
+abstract class TMGMTDefaultTranslatorPluginController extends TMGMTPluginBase implements TMGMTTranslatorPluginControllerInterface {
+
+  protected $supportedRemoteLanguages = array();
+  protected $remoteLanguagesMappings = array();
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::isAvailable().
+   */
+  public function isAvailable(TMGMTTranslator $translator) {
+    // Assume that the translation service is always available.
+    return TRUE;
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::canTranslate().
+   */
+  public function canTranslate(TMGMTTranslator $translator, TMGMTJob $job) {
+    // The job is only translatable if the translator is available too.
+    if ($this->isAvailable($translator) && array_key_exists($job->target_language, $translator->getSupportedTargetLanguages($job->source_language))) {
+      // We can only translate this job if the target language of the job is in
+      // one of the supported languages.
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::cancelTranslation().
+   */
+  public function cancelTranslation(TMGMTJob $job) {
+    // Assume that we can cancel a translation job at any time.
+    $job->setState(TMGMT_JOB_STATE_CANCELLED);
+    return TRUE;
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::getDefaultRemoteLanguagesMappings().
+   */
+  public function getDefaultRemoteLanguagesMappings() {
+    return array();
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::getSupportedLanguages().
+   */
+  public function getSupportedRemoteLanguages(TMGMTTranslator $translator) {
+    return array();
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::getRemoteLanguagesMappings().
+   */
+  public function getRemoteLanguagesMappings(TMGMTTranslator $translator) {
+    if (!empty($this->remoteLanguagesMappings)) {
+      return $this->remoteLanguagesMappings;
+    }
+
+    foreach (language_list() as $language => $info) {
+      $this->remoteLanguagesMappings[$language] = $this->mapToRemoteLanguage($translator, $language);
+    }
+
+    return $this->remoteLanguagesMappings;
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::mapToRemoteLanguage().
+   */
+  public function mapToRemoteLanguage(TMGMTTranslator $translator, $language) {
+    if (!tmgmt_provide_remote_languages_mappings($translator)) {
+      return $language;
+    }
+
+    if (!empty($translator->settings['remote_languages_mappings'][$language])) {
+      return $translator->settings['remote_languages_mappings'][$language];
+    }
+
+    $default_mappings = $this->getDefaultRemoteLanguagesMappings();
+
+    if (isset($default_mappings[$language])) {
+      return $default_mappings[$language];
+    }
+
+    return $language;
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::mapToLocalLanguage().
+   */
+  public function mapToLocalLanguage(TMGMTTranslator $translator, $language) {
+    if (!tmgmt_provide_remote_languages_mappings($translator)) {
+      return $language;
+    }
+
+    if (isset($translator->settings['remote_languages_mappings']) && is_array($translator->settings['remote_languages_mappings'])) {
+      $mappings = $translator->settings['remote_languages_mappings'];
+    }
+    else {
+      $mappings = $this->getDefaultRemoteLanguagesMappings();
+    }
+
+    if ($remote_language = array_search($language, $mappings)) {
+      return $remote_language;
+    }
+
+    return $language;
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::getSupportedTargetLanguages().
+   */
+  public function getSupportedTargetLanguages(TMGMTTranslator $translator, $source_language) {
+    $languages = entity_metadata_language_list();
+    unset($languages[LANGUAGE_NONE], $languages[$source_language]);
+    return drupal_map_assoc(array_keys($languages));
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::getNotCanTranslateReason().
+   */
+  public function getNotCanTranslateReason(TMGMTJob $job) {
+    $wrapper = entity_metadata_wrapper('tmgmt_job', $job);
+    return t('@translator can not translate from @source to @target.', array('@translator' => $job->getTranslator()->label(), '@source' => $wrapper->source_language->label(), '@target' => $wrapper->target_language->label()));
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::getNotAvailableReason().
+   */
+  public function getNotAvailableReason(TMGMTTranslator $translator) {
+    return t('@translator is not available. Make sure it is properly !configured.', array('@translator' => $this->pluginInfo['label'], '!configured' => l(t('configured'), 'admin/config/regional/tmgmt/translators/manage/' . $translator->name)));
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::defaultSettings().
+   */
+  public function defaultSettings() {
+    $defaults = array('auto_accept' => FALSE);
+    // Check if any default settings are defined in the plugin info.
+    if (isset($this->pluginInfo['default settings'])) {
+      return array_merge($defaults, $this->pluginInfo['default settings']);
+    }
+    return $defaults;
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::checkoutInfo().
+   */
+  public function hasCheckoutSettings(TMGMTJob $job) {
+    return TRUE;
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::acceptedDataItem().
+   */
+  public function acceptetDataItem(TMGMTJobItem $job_item, array $key) {
+    return TRUE;
+  }
+}
diff --git a/plugin/tmgmt.ui.interface.source.inc b/plugin/tmgmt.ui.interface.source.inc
new file mode 100644
index 0000000..3b9c424
--- /dev/null
+++ b/plugin/tmgmt.ui.interface.source.inc
@@ -0,0 +1,47 @@
+<?php
+
+
+/**
+ * Interface for source ui controllers.
+ *
+ * @ingroup tmgmt_source
+ */
+interface TMGMTSourceUIControllerInterface extends TMGMTPluginBaseInterface {
+
+  /**
+   * Form callback for the job item review form.
+   */
+  public function reviewForm($form, &$form_state, TMGMTJobItem $item);
+
+  /**
+   * Validation callback for the job item review form.
+   */
+  public function reviewFormValidate($form, &$form_state, TMGMTJobItem $item);
+
+  /**
+   * Submit callback for the job item review form.
+   */
+  public function reviewFormSubmit($form, &$form_state, TMGMTJobItem $item);
+
+  /**
+   * Implements hook_menu().
+   *
+   * @see tmgmt_ui_menu().
+   */
+  public function hook_menu();
+
+  /**
+   * Implements hook_forms().
+   *
+   * @see tmgmt_ui_forms().
+   */
+  public function hook_forms();
+
+  /**
+   * Implements hook_views_default_views().
+   *
+   * @see tmgmt_ui_views_default_views().
+   */
+  public function hook_views_default_views();
+
+}
diff --git a/plugin/tmgmt.ui.interface.translator.inc b/plugin/tmgmt.ui.interface.translator.inc
new file mode 100644
index 0000000..dd35dfe
--- /dev/null
+++ b/plugin/tmgmt.ui.interface.translator.inc
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * Interface for translator ui controllers.
+ *
+ * @ingroup tmgmt_translator
+ */
+interface TMGMTTranslatorUIControllerInterface extends TMGMTPluginBaseInterface {
+
+  /**
+   * Form callback for the plugin settings form.
+   */
+  public function pluginSettingsForm($form, &$form_state, TMGMTTranslator $translator, $busy = FALSE);
+
+  /**
+   * Form callback for the checkout settings form.
+   */
+  public function checkoutSettingsForm($form, &$form_state, TMGMTJob $job);
+
+  /**
+   * Retrieves information about a translation job.
+   *
+   * @param TMGMTJob $job
+   *   The translation job.
+   */
+  public function checkoutInfo(TMGMTJob $job);
+
+  /**
+   * Form callback for the job item review form.
+   */
+  public function reviewForm($form, &$form_state, TMGMTJobItem $item);
+
+  /**
+   * Validation callback for the job item review form.
+   */
+  public function reviewFormValidate($form, &$form_state, TMGMTJobItem $item);
+
+  /**
+   * Submit callback for the job item review form.
+   */
+  public function reviewFormSubmit($form, &$form_state, TMGMTJobItem $item);
+
+}
diff --git a/plugin/tmgmt.ui.source.inc b/plugin/tmgmt.ui.source.inc
new file mode 100644
index 0000000..755e456
--- /dev/null
+++ b/plugin/tmgmt.ui.source.inc
@@ -0,0 +1,114 @@
+<?php
+
+
+/**
+ * Default ui controller class for source plugin.
+ *
+ * @ingroup tmgmt_source
+ */
+class TMGMTDefaultSourceUIController extends TMGMTPluginBase implements TMGMTSourceUIControllerInterface {
+
+  /**
+   * Implements TMGMTSourceUIControllerInterface::reviewForm().
+   */
+  public function reviewForm($form, &$form_state, TMGMTJobItem $item) {
+    return $form;
+  }
+
+  /**
+   * Implements TMGMTSourceUIControllerInterface::reviewFormValidate().
+   */
+  public function reviewFormValidate($form, &$form_state, TMGMTJobItem $item) {
+    // Nothing to do here by default.
+  }
+
+  /**
+   * Implements TMGMTSourceUIControllerInterface::reviewFormSubmit().
+   */
+  public function reviewFormSubmit($form, &$form_state, TMGMTJobItem $item) {
+    // Nothing to do here by default.
+  }
+
+  /**
+   * Implements TMGMTSourceUIControllerInterface::overviewForm().
+   */
+  public function overviewForm($form, &$form_state, $type) {
+    return $form;
+  }
+
+  /**
+   * Implements TMGMTSourceUIControllerInterface::overviewFormValidate().
+   */
+  public function overviewFormValidate($form, &$form_state, $type) {
+    // Nothing to do here by default.
+  }
+
+  /**
+   * Implements TMGMTSourceUIControllerInterface::overviewFormSubmit().
+   */
+  public function overviewFormSubmit($form, &$form_state, $type) {
+    // Nothing to do here by default.
+  }
+
+  /**
+   * Implements TMGMTSourceUIControllerInterface::hook_menu().
+   */
+  public function hook_menu() {
+    $items = array();
+    if ($types = tmgmt_source_translatable_item_types($this->pluginType)) {
+      $defaults = array(
+        'file' => isset($this->pluginInfo['file']) ? $this->pluginInfo['file'] : $this->pluginInfo['module'] . '.pages.inc',
+        'file path' => isset($this->pluginInfo['file path']) ? $this->pluginInfo['file path'] : drupal_get_path('module', $this->pluginInfo['module']),
+        'page callback' => 'drupal_get_form',
+        'access callback' => 'tmgmt_job_access',
+        'access arguments' => array('create'),
+      );
+      foreach ($types as $type => $name) {
+        if (empty($items['admin/config/regional/tmgmt/' . $this->pluginType])) {
+          // Make the first item type of this source the default menu tab.
+          $items['admin/config/regional/tmgmt/' . $this->pluginType] = $defaults + array(
+            'title' => t('@type sources', array('@type' => ucfirst($this->pluginInfo['label']))),
+            'page arguments' => array('tmgmt_ui_' . $this->pluginType . '_source_' . $type . '_overview_form', $this->pluginType, $type),
+            'type' => MENU_LOCAL_TASK,
+          );
+          $items['admin/config/regional/tmgmt/' . $this->pluginType . '/' . $type] = array(
+            'title' => check_plain($name),
+            'type' => MENU_DEFAULT_LOCAL_TASK,
+          );
+        }
+        else {
+          $items['admin/config/regional/tmgmt/' . $this->pluginType . '/' . $type] = $defaults + array(
+            'title' => check_plain($name),
+            'page arguments' => array('tmgmt_ui_' . $this->pluginType . '_source_' . $type . '_overview_form', $this->pluginType, $type),
+            'type' => MENU_LOCAL_TASK,
+          );
+        }
+      }
+    }
+    return $items;
+  }
+
+  /**
+   * Implements TMGMTSourceUIControllerInterface::hook_form().
+   */
+  public function hook_forms() {
+    $info = array();
+    if ($types = tmgmt_source_translatable_item_types($this->pluginType)) {
+      foreach (array_keys($types) as $type) {
+        $info['tmgmt_ui_' . $this->pluginType . '_source_' . $type . '_overview_form'] = array(
+          'callback' => 'tmgmt_ui_source_overview_form',
+          'wrapper_callback' => 'tmgmt_ui_source_overview_form_defaults',
+        );
+      }
+    }
+    return $info;
+  }
+
+  /**
+   * Implements TMGMTSourceUIControllerInterface::hook_views_default_views().
+   */
+  public function hook_views_default_views() {
+    return array();
+  }
+
+}
diff --git a/plugin/tmgmt.ui.translator.inc b/plugin/tmgmt.ui.translator.inc
new file mode 100644
index 0000000..06e3a64
--- /dev/null
+++ b/plugin/tmgmt.ui.translator.inc
@@ -0,0 +1,119 @@
+<?php
+
+/**
+ * Default ui controller class for translator plugins.
+ *
+ * @ingroup tmgmt_translator
+ */
+class TMGMTDefaultTranslatorUIController extends TMGMTPluginBase implements TMGMTTranslatorUIControllerInterface {
+
+  /**
+   * Implements TMGMTTranslatorUIControllerInterface::pluginSettingsForm().
+   */
+  public function pluginSettingsForm($form, &$form_state, TMGMTTranslator $translator, $busy = FALSE) {
+
+    if (!empty($translator->plugin)) {
+      $controller = tmgmt_translator_plugin_controller($translator->plugin);
+    }
+
+    // If current translator is configured to provide remote language mapping
+    // provide the form to configure mappings, unless it does not exists yet.
+    if (!empty($controller) && tmgmt_provide_remote_languages_mappings($translator)) {
+
+      $form['remote_languages_mappings'] = array(
+        '#tree' => TRUE,
+        '#type' => 'fieldset',
+        '#title' => t('Remote languages mappings'),
+        '#description' => t('Here you can specify mappings of your local language codes to the translator language codes.'),
+        '#collapsible' => TRUE,
+        '#collapsed' => TRUE,
+      );
+
+      $options = array();
+      foreach ($controller->getSupportedRemoteLanguages($translator) as $language) {
+        $options[$language] = $language;
+      }
+
+      foreach ($controller->getRemoteLanguagesMappings($translator) as $local_language => $remote_language) {
+        $form['remote_languages_mappings'][$local_language] = array(
+          '#type' => 'textfield',
+          '#title' => tmgmt_language_label($local_language) . ' (' . $local_language . ')',
+          '#default_value' => $remote_language,
+          '#size' => 6,
+        );
+
+        if (!empty($options)) {
+          $form['remote_languages_mappings'][$local_language]['#type'] = 'select';
+          $form['remote_languages_mappings'][$local_language]['#options'] = $options;
+          $form['remote_languages_mappings'][$local_language]['#empty_option'] = ' - ';
+          unset($form['remote_languages_mappings'][$local_language]['#size']);
+        }
+      }
+    }
+
+    if (!element_children($form)) {
+      $form['#description'] = t("The @plugin plugin doesn't provide any settings.", array('@plugin' => $this->pluginInfo['label']));
+    }
+    return $form;
+  }
+
+  /**
+   * Implements TMGMTTranslatorUIControllerInterface::checkoutSettingsForm().
+   */
+  public function checkoutSettingsForm($form, &$form_state, TMGMTJob $job) {
+    if (!element_children($form)) {
+      $form['#description'] = t("The @translator translator doesn't provide any checkout settings.", array('@translator' => $job->getTranslator()->label()));
+    }
+    return $form;
+  }
+
+  /**
+   * Implements TMGMTTranslatorUIControllerInterface::checkoutInfo().
+   */
+  public function checkoutInfo(TMGMTJob $job) {
+    return array();
+  }
+
+  /**
+   * Provides a simple wrapper for the checkout info fieldset.
+   *
+   * @param TMGMTJob $job
+   *   Translation job object.
+   * @param $form
+   *   Partial form structure to be wrapped in the fieldset.
+   *
+   * @return
+   *   The provided form structure wrapped in a collapsed fieldset.
+   */
+  public function checkoutInfoWrapper(TMGMTJob $job, $form) {
+    $label = $job->getTranslator()->label();
+    $form += array(
+      '#title' => t('@translator translation job information', array('@translator' => $label)),
+      '#type' => 'fieldset',
+      '#collapsible' => TRUE,
+    );
+    return $form;
+  }
+
+  /**
+   * Implements TMGMTTranslatorUIControllerInterface::reviewForm().
+   */
+  public function reviewForm($form, &$form_state, TMGMTJobItem $item) {
+    return $form;
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::reviewFormValidate().
+   */
+  public function reviewFormValidate($form, &$form_state, TMGMTJobItem $item) {
+    // Nothing to do here by default.
+  }
+
+  /**
+   * Implements TMGMTTranslatorPluginControllerInterface::reviewFormSubmit().
+   */
+  public function reviewFormSubmit($form, &$form_state, TMGMTJobItem $item) {
+    // Nothing to do here by default.
+  }
+
+}
diff --git a/sources/entity/tmgmt_entity.info b/sources/entity/tmgmt_entity.info
index ff0e785..260f741 100644
--- a/sources/entity/tmgmt_entity.info
+++ b/sources/entity/tmgmt_entity.info
@@ -5,6 +5,7 @@ core = 7.x
 dependencies[] = tmgmt
 dependencies[] = tmgmt_field
 test_dependencies[] = pathauto
-files[] = tmgmt_entity.test
+files[] = tmgmt_entity.source.test
+files[] = tmgmt_entity.pathauto.test
 files[] = tmgmt_entity.plugin.inc
 files[] = tmgmt_entity.ui.inc
diff --git a/sources/entity/tmgmt_entity.pathauto.test b/sources/entity/tmgmt_entity.pathauto.test
new file mode 100644
index 0000000..801784a
--- /dev/null
+++ b/sources/entity/tmgmt_entity.pathauto.test
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * Tests integration with pathauto.
+ */
+class TMGMTEntitySourcePathAutoTestCase extends TMGMTEntityTestCaseUtility {
+
+  static function getInfo() {
+    return array(
+      'name' => 'Entity Source Pathauto tests',
+      'description' => 'Verifies that the correct aliases are generated for entity transations',
+      'group' => 'Translation Management',
+      'dependencies' => array('entity_translation', 'pathauto'),
+    );
+  }
+
+  function setUp() {
+    parent::setUp(array('tmgmt_entity', 'entity_translation', 'pathauto'));
+    $this->loginAsAdmin();
+    $this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
+  }
+
+  /**
+   * Tests that pathauto aliases are correctly created.
+   */
+  function testAliasCreation() {
+    $this->setEnvironment('de');
+
+    // Create a translation job.
+    $job = $this->createJob();
+    $job->translator = $this->default_translator->name;
+    $job->settings = array();
+    $job->save();
+
+    // Create a node.
+    $node = $this->createNode('article');
+    // Create a job item for this node and add it to the job.
+    $job->addItem('entity', 'node', $node->nid);
+
+    // Translate the job.
+    $job->requestTranslation();
+
+    // Check the translated job items.
+    foreach ($job->getItems() as $item) {
+      $item->acceptTranslation();
+    }
+
+    // Make sure that the correct url aliases were created.
+    $aliases = db_query('SELECT * FROM {url_alias} where source = :source', array(':source' => 'node/' . $node->nid))->fetchAllAssoc('language');
+    $this->assertEqual(2, count($aliases));
+    $this->assertTrue(isset($aliases['en']), 'English alias created.');
+    $this->assertTrue(isset($aliases['de']), 'German alias created.');
+  }
+
+}
diff --git a/sources/entity/tmgmt_entity.test b/sources/entity/tmgmt_entity.source.test
similarity index 79%
rename from sources/entity/tmgmt_entity.test
rename to sources/entity/tmgmt_entity.source.test
index 46f5cff..ecf18f7 100644
--- a/sources/entity/tmgmt_entity.test
+++ b/sources/entity/tmgmt_entity.source.test
@@ -185,57 +185,3 @@ class TMGMTEntitySourceTestCase extends TMGMTEntityTestCaseUtility {
     }
   }
 }
-
-/**
- * Tests integration with pathauto.
- */
-class TMGMTEntitySourcePathAutoTestCase extends TMGMTEntityTestCaseUtility {
-
-  static function getInfo() {
-    return array(
-      'name' => 'Entity Source Pathauto tests',
-      'description' => 'Verifies that the correct aliases are generated for entity transations',
-      'group' => 'Translation Management',
-      'dependencies' => array('entity_translation', 'pathauto'),
-    );
-  }
-
-  function setUp() {
-    parent::setUp(array('tmgmt_entity', 'entity_translation', 'pathauto'));
-    $this->loginAsAdmin();
-    $this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
-  }
-
-  /**
-   * Tests that pathauto aliases are correctly created.
-   */
-  function testAliasCreation() {
-    $this->setEnvironment('de');
-
-    // Create a translation job.
-    $job = $this->createJob();
-    $job->translator = $this->default_translator->name;
-    $job->settings = array();
-    $job->save();
-
-    // Create a node.
-    $node = $this->createNode('article');
-    // Create a job item for this node and add it to the job.
-    $job->addItem('entity', 'node', $node->nid);
-
-    // Translate the job.
-    $job->requestTranslation();
-
-    // Check the translated job items.
-    foreach ($job->getItems() as $item) {
-      $item->acceptTranslation();
-    }
-
-    // Make sure that the correct url aliases were created.
-    $aliases = db_query('SELECT * FROM {url_alias} where source = :source', array(':source' => 'node/' . $node->nid))->fetchAllAssoc('language');
-    $this->assertEqual(2, count($aliases));
-    $this->assertTrue(isset($aliases['en']), 'English alias created.');
-    $this->assertTrue(isset($aliases['de']), 'German alias created.');
-  }
-
-}
diff --git a/sources/entity/ui/tmgmt_entity_ui.info b/sources/entity/ui/tmgmt_entity_ui.info
index c2b4f00..3ff5f18 100644
--- a/sources/entity/ui/tmgmt_entity_ui.info
+++ b/sources/entity/ui/tmgmt_entity_ui.info
@@ -9,5 +9,6 @@ dependencies[] = views_bulk_operations
 dependencies[] = entity_translation
 
 files[] = tmgmt_entity_ui.test
+files[] = tmgmt_entity_ui.list.test
 files[] = tmgmt_entity_ui.ui.inc
 
diff --git a/sources/entity/ui/tmgmt_entity_ui.test b/sources/entity/ui/tmgmt_entity_ui.list.test
similarity index 54%
copy from sources/entity/ui/tmgmt_entity_ui.test
copy to sources/entity/ui/tmgmt_entity_ui.list.test
index 0f30afe..dd285f4 100644
--- a/sources/entity/ui/tmgmt_entity_ui.test
+++ b/sources/entity/ui/tmgmt_entity_ui.list.test
@@ -269,250 +269,3 @@ class TMGMTEntitySourceListTestCase extends TMGMTEntityTestCaseUtility {
     $this->assertText($comment->subject, t('Searching for a comment subject.'));
   }
 }
-
-/**
- * Basic Node Source tests.
- *
- */
-class TMGMTEntitySourceUITestCase extends TMGMTEntityTestCaseUtility {
-
-  /**
-   * Implements getInfo().
-   */
-  static function getInfo() {
-    return array(
-      'name' => t('Entity Source UI tests'),
-      'description' => t('Tests the user interface for entity translation sources.'),
-      'group' => t('Translation Management'),
-      'dependencies' => array('entity_translation'),
-    );
-  }
-
-  /**
-   * Overrides SimplenewsTestCase::setUp()
-   */
-  function setUp() {
-    parent::setUp(array('tmgmt_entity_ui', 'block', 'comment'));
-    variable_set('language_content_type_page', ENTITY_TRANSLATION_ENABLED);
-    variable_set('language_content_type_article', ENTITY_TRANSLATION_ENABLED);
-
-    $this->loginAsAdmin(array(
-      'create translation jobs',
-      'submit translation jobs',
-      'accept translation jobs',
-      'administer blocks',
-      'administer entity translation',
-      'toggle field translatability',
-    ));
-
-    $this->setEnvironment('de');
-    $this->setEnvironment('fr');
-    $this->setEnvironment('es');
-    $this->setEnvironment('el');
-
-    $this->createNodeType('page', st('Page'), ENTITY_TRANSLATION_ENABLED);
-    $this->createNodeType('article', st('Article'), ENTITY_TRANSLATION_ENABLED);
-
-    // Enable path locale detection.
-    $edit = array(
-      'language[enabled][locale-url]' => TRUE,
-      'language_content[enabled][locale-interface]' => TRUE,
-    );
-    $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
-
-    // @todo Re-enable this when switching to testing profile.
-    // Enable the main page content block for hook_page_alter() to work.
-    $edit = array(
-      'blocks[system_main][region]' => 'content',
-    );
-    $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
-  }
-
-  /**
-   * Test the translate tab for a single checkout.
-   */
-  function testNodeTranslateTabSingleCheckout() {
-
-    $this->loginAsTranslator(array('translate node entities'));
-
-    // Create an english source node.
-    $node = $this->createNode('page', 'en');
-
-    // Go to the translate tab.
-    $this->drupalGet('node/' . $node->nid);
-    $this->clickLink('Translate');
-
-    // Assert some basic strings on that page.
-    $this->assertText(t('Translations of @title', array('@title' => $node->title)));
-    $this->assertText(t('Pending Translations'));
-
-    // Request a translation for german.
-    $edit = array(
-      'languages[de]' => TRUE,
-    );
-    $this->drupalPost(NULL, $edit, t('Request translation'));
-
-    // Verify that we are on the translate tab.
-    $this->assertText(t('One job needs to be checked out.'));
-    $this->assertText(t('Translation for @title', array('@title' => $node->title)));
-
-    // Submit.
-    $this->drupalPost(NULL, array(), t('Submit to translator'));
-
-    // Make sure that we're back on the translate tab.
-    $this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
-    $this->assertText(t('Test translation created.'));
-    $this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('German'))));
-
-    // Verify that the pending translation is shown.
-    $this->clickLink(t('Needs review'));
-    $this->drupalPost(NULL, array(), t('Save as completed'));
-
-    $this->assertText(t('The translation for @title has been accepted.', array('@title' => $node->title)));
-
-    // German node should now be listed and be clickable.
-    // @todo Improve detection of the link, e.g. use xpath on the table or the
-    // title module to get a better title.
-    $this->clickLink('view', 1);
-    $this->assertText('de_' . $node->body['en'][0]['value']);
-  }
-
-  /**
-   * Test the translate tab for a single checkout.
-   */
-  function testNodeTranslateTabMultipeCheckout() {
-    // Allow auto-accept.
-    $default_translator = tmgmt_translator_load('test_translator');
-    $default_translator->settings = array(
-      'auto_accept' => TRUE,
-    );
-    $default_translator->save();
-
-    $this->loginAsTranslator(array('translate node entities'));
-
-    // Create an english source node.
-    $node = $this->createNode('page', 'en');
-
-    // Go to the translate tab.
-    $this->drupalGet('node/' . $node->nid);
-    $this->clickLink('Translate');
-
-    // Assert some basic strings on that page.
-    $this->assertText(t('Translations of @title', array('@title' => $node->title)));
-    $this->assertText(t('Pending Translations'));
-
-    // Request a translation for german.
-    $edit = array(
-      'languages[de]' => TRUE,
-      'languages[es]' => TRUE,
-    );
-    $this->drupalPost(NULL, $edit, t('Request translation'));
-
-    // Verify that we are on the translate tab.
-    $this->assertText(t('2 jobs need to be checked out.'));
-
-    // Submit all jobs.
-    $this->assertText(t('Translation for @title', array('@title' => $node->title)));
-    $this->drupalPost(NULL, array(), t('Submit to translator and continue'));
-    $this->assertText(t('Translation for @title', array('@title' => $node->title)));
-    $this->drupalPost(NULL, array(), t('Submit to translator'));
-
-    // Make sure that we're back on the translate tab.
-    $this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
-    $this->assertText(t('Test translation created.'));
-    $this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('Spanish'))));
-    $this->assertText(t('The translation for @title has been accepted.', array('@title' => $node->title)));
-
-    // Translated nodes should now be listed and be clickable.
-    // @todo Use links on translate tab.
-    $this->drupalGet('de/node/' . $node->nid);
-    $this->assertText('de_' . $node->body['en'][0]['value']);
-
-    $this->drupalGet('es/node/' . $node->nid);
-    $this->assertText('es_' . $node->body['en'][0]['value']);
-  }
-
-  /**
-   * Test translating comments.
-   *
-   * @todo: Disabled pending resolution of http://drupal.org/node/1760270.
-   */
-  function dtestCommentTranslateTab() {
-
-    // Login as admin to be able to submit config page.
-    $this->loginAsAdmin(array('administer entity translation'));
-    // Enable comment translation.
-    $edit = array(
-      'entity_translation_entity_types[comment]' => TRUE
-    );
-    $this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
-
-    // Change comment_body field to be translatable.
-    $comment_body = field_info_field('comment_body');
-    $comment_body['translatable'] = TRUE;
-    field_update_field($comment_body);
-
-    // Create a user that is allowed to translate comments.
-    $permissions = array('translate comment entities', 'create translation jobs', 'submit translation jobs', 'accept translation jobs', 'post comments', 'skip comment approval', 'edit own comments', 'access comments');
-    $entity_translation_permissions = entity_translation_permission();
-    // The new translation edit form of entity_translation requires a new
-    // permission that does not yet exist in older versions. Add it
-    // conditionally.
-    if (isset($entity_translation_permissions['edit original values'])) {
-      $permissions[] = 'edit original values';
-    }
-    $this->loginAsTranslator($permissions, TRUE);
-
-    // Create an english source term.
-    $node = $this->createNode('article', 'en');
-
-    // Add a comment.
-    $this->drupalGet('node/' . $node->nid);
-    $edit = array(
-      'subject' => $this->randomName(),
-      'comment_body[en][0][value]' => $this->randomName(),
-    );
-    $this->drupalPost(NULL, $edit, t('Save'));
-    $this->assertText(t('Your comment has been posted.'));
-
-    // Go to the translate tab.
-    $this->clickLink('edit');
-    $this->assertTrue(preg_match('|comment/(\d+)/edit$|', $this->getUrl(), $matches), 'Comment found');
-    $comment = comment_load($matches[1]);
-    $this->clickLink('Translate');
-
-    // Assert some basic strings on that page.
-    $this->assertText(t('Translations of @title', array('@title' => $comment->subject)));
-    $this->assertText(t('Pending Translations'));
-
-    // Request a translation for german.
-    $edit = array(
-      'languages[de]' => TRUE,
-      'languages[es]' => TRUE,
-    );
-    $this->drupalPost(NULL, $edit, t('Request translation'));
-
-    // Verify that we are on the translate tab.
-    $this->assertText(t('2 jobs need to be checked out.'));
-
-    // Submit all jobs.
-    $this->assertText(t('Translation for @title', array('@title' => $comment->subject)));
-    $this->drupalPost(NULL, array(), t('Submit to translator and continue'));
-    $this->assertText(t('Translation for @title', array('@title' => $comment->subject)));
-    $this->drupalPost(NULL, array(), t('Submit to translator'));
-
-    // Make sure that we're back on the translate tab.
-    $this->assertEqual(url('comment/' . $comment->cid . '/translate', array('absolute' => TRUE)), $this->getUrl());
-    $this->assertText(t('Test translation created.'));
-    $this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $comment->subject, '@language' => t('Spanish'))));
-    $this->assertText(t('The translation for @title has been accepted.', array('@title' => $comment->subject)));
-
-    // @todo Use links on translate tab.
-    $this->drupalGet('de/comment/' . $comment->cid);
-    $this->assertText('de_' . $comment->comment_body['en'][0]['value']);
-
-    // @todo Use links on translate tab.
-    $this->drupalGet('es/node/' . $comment->cid);
-    $this->assertText('es_' . $comment->comment_body['en'][0]['value']);
-  }
-}
diff --git a/sources/entity/ui/tmgmt_entity_ui.test b/sources/entity/ui/tmgmt_entity_ui.test
index 0f30afe..2865ed2 100644
--- a/sources/entity/ui/tmgmt_entity_ui.test
+++ b/sources/entity/ui/tmgmt_entity_ui.test
@@ -1,275 +1,5 @@
 <?php
 
-
-class TMGMTEntitySourceListTestCase extends TMGMTEntityTestCaseUtility {
-
-  protected $nodes = array();
-
-  /**
-   * Implements getInfo().
-   */
-  static function getInfo() {
-    return array(
-      'name' => t('Entity Source List tests'),
-      'description' => t('Tests the user interface for entity translation lists.'),
-      'group' => t('Translation Management'),
-    );
-  }
-
-  function setUp() {
-    parent::setUp(array('tmgmt_entity_ui', 'translation', 'comment', 'taxonomy'));
-    $this->loginAsAdmin(array('administer entity translation'));
-
-    $this->setEnvironment('de');
-    $this->setEnvironment('fr');
-
-    // Enable entity translations for nodes and comments.
-    $edit = array();
-    $edit['entity_translation_entity_types[comment]'] = 1;
-    $edit['entity_translation_entity_types[node]'] = 1;
-    $edit['entity_translation_entity_types[taxonomy_term]'] = 1;
-    $this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
-
-    $this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
-    $this->createNodeType('page', 'Page', TRANSLATION_ENABLED);
-
-    // Create nodes that will be used during tests.
-    // NOTE that the order matters as results are read by xpath based on
-    // position in the list.
-    $this->nodes['page']['en'][] = $this->createNode('page');
-    $this->nodes['article']['de'][0] = $this->createNode('article', 'de');
-    $this->nodes['article']['fr'][0] = $this->createNode('article', 'fr');
-    $this->nodes['article']['en'][3] = $this->createNode('article', 'en');
-    $this->nodes['article']['en'][2] = $this->createNode('article', 'en');
-    $this->nodes['article']['en'][1] = $this->createNode('article', 'en');
-    $this->nodes['article']['en'][0] = $this->createNode('article', 'en');
-  }
-
-  /**
-   * Tests that the term bundle filter works.
-   */
-  function testTermBundleFilter() {
-
-    $vocabulary1 = entity_create('taxonomy_vocabulary', array(
-      'machine_name' => 'vocab1',
-      'name' => $this->randomName(),
-    ));
-    taxonomy_vocabulary_save($vocabulary1);
-
-    $term1 = entity_create('taxonomy_term', array(
-      'name' => $this->randomName(),
-      'vid' => $vocabulary1->vid,
-    ));
-    taxonomy_term_save($term1);
-
-    $vocabulary2 = (object) array(
-      'machine_name' => 'vocab2',
-      'name' => $this->randomName(),
-    );
-    taxonomy_vocabulary_save($vocabulary2);
-
-    $term2 = entity_create('taxonomy_term', array(
-      'name' => $this->randomName(),
-      'vid' => $vocabulary2->vid,
-    ));
-    taxonomy_term_save($term2);
-
-    $this->drupalGet('admin/config/regional/tmgmt/entity/taxonomy_term');
-    // Both terms should be displayed with their bundle.
-    $this->assertText($term1->name);
-    $this->assertText($term2->name);
-    $this->assertTrue($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary1->name)));
-    $this->assertTrue($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary2->name)));
-
-    // Limit to the first vocabulary.
-    $edit = array();
-    $edit['search[vocabulary_machine_name]'] = $vocabulary1->machine_name;
-    $this->drupalPost(NULL, $edit, t('Search'));
-    // Only term 1 should be displayed now.
-    $this->assertText($term1->name);
-    $this->assertNoText($term2->name);
-    $this->assertTrue($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary1->name)));
-    $this->assertFalse($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary2->name)));
-
-  }
-
-  function testAvailabilityOfEntityLists() {
-
-    $this->drupalGet('admin/config/regional/tmgmt/entity/comment');
-    // Check if we are at comments page.
-    $this->assertText(t('Entity overview (Comment)'));
-    // No comments yet - empty message is expected.
-    $this->assertText(t('No entities matching given criteria have been found.'));
-
-    $this->drupalGet('admin/config/regional/tmgmt/entity/node');
-    // Check if we are at nodes page.
-    $this->assertText(t('Entity overview (Node)'));
-    // We expect article title as article node type is entity translatable.
-    $this->assertText($this->nodes['article']['en'][0]->title);
-    // Page node type should not be listed as it is not entity translatable.
-    $this->assertNoText($this->nodes['page']['en'][0]->title);
-  }
-
-  function testTranslationStatuses() {
-
-    // Test statuses: Source, Missing.
-    $this->drupalGet('admin/config/regional/tmgmt/entity/node');
-    $langstatus_en = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-en"]');
-    $langstatus_de = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-de"]');
-
-    $this->assertEqual($langstatus_en[0]->div['title'], t('Source language'));
-    $this->assertEqual($langstatus_de[0]->div['title'], t('Not translated'));
-
-    // Test status: Active job item.
-    $job = $this->createJob('en', 'de');
-    $job->translator = $this->default_translator->name;
-    $job->settings = array();
-    $job->save();
-
-    $job->addItem('entity', 'node', $this->nodes['article']['en'][0]->nid);
-    $job->requestTranslation();
-
-    $this->drupalGet('admin/config/regional/tmgmt/entity/node');
-    $langstatus_de = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-de"]/a');
-
-    $items = $job->getItems();
-    $wrapper = entity_metadata_wrapper('tmgmt_job_item', array_shift($items));
-    $label = t('Active job item: @state', array('@state' => $wrapper->state->label()));
-
-    $this->assertEqual($langstatus_de[0]->div['title'], $label);
-
-    // Test status: Current
-    foreach ($job->getItems() as $job_item) {
-      $job_item->acceptTranslation();
-    }
-
-    $this->drupalGet('admin/config/regional/tmgmt/entity/node');
-    $langstatus_de = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-de"]');
-
-    $this->assertEqual($langstatus_de[0]->div['title'], t('Translation up to date'));
-  }
-
-  function testTranslationSubmissions() {
-
-    // Simple submission.
-    $nid = $this->nodes['article']['en'][0]->nid;
-    $edit = array();
-    $edit["items[$nid]"] = 1;
-    $this->drupalPost('admin/config/regional/tmgmt/entity/node', $edit, t('Request translation'));
-    $this->assertText(t('One job needs to be checked out.'));
-
-    // Submission of two entities of the same source language.
-    $nid1 = $this->nodes['article']['en'][0]->nid;
-    $nid2 = $this->nodes['article']['en'][1]->nid;
-    $edit = array();
-    $edit["items[$nid1]"] = 1;
-    $edit["items[$nid2]"] = 1;
-    $this->drupalPost('admin/config/regional/tmgmt/entity/node', $edit, t('Request translation'));
-    $this->assertText(t('One job needs to be checked out.'));
-
-    // Submission of several entities of different source languages.
-    $nid1 = $this->nodes['article']['en'][0]->nid;
-    $nid2 = $this->nodes['article']['en'][1]->nid;
-    $nid3 = $this->nodes['article']['en'][2]->nid;
-    $nid4 = $this->nodes['article']['en'][3]->nid;
-    $nid5 = $this->nodes['article']['de'][0]->nid;
-    $nid6 = $this->nodes['article']['fr'][0]->nid;
-    $edit = array();
-    $edit["items[$nid1]"] = 1;
-    $edit["items[$nid2]"] = 1;
-    $edit["items[$nid3]"] = 1;
-    $edit["items[$nid4]"] = 1;
-    $edit["items[$nid5]"] = 1;
-    $edit["items[$nid6]"] = 1;
-    $this->drupalPost('admin/config/regional/tmgmt/entity/node', $edit, t('Request translation'));
-    $this->assertText(t('@count jobs need to be checked out.', array('@count' => '3')));
-  }
-
-  function testNodeEntityListings() {
-
-    // Turn off the entity translation.
-    $edit = array();
-    $edit['language_content_type'] = TRANSLATION_ENABLED;
-    $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
-
-    // Check if we have appropriate message in case there are no entity
-    // translatable content types.
-    $this->drupalGet('admin/config/regional/tmgmt/entity/node');
-    $this->assertText(t('Entity translation is not enabled for any of existing content types. To use this functionality go to Content types administration and enable entity translation for desired content types.'));
-
-    // Turn on the entity translation for both - article and page - to test
-    // search form.
-    $edit = array();
-    $edit['language_content_type'] = ENTITY_TRANSLATION_ENABLED;
-    $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
-    $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type'));
-    // Create page node after entity translation is enabled.
-    $page_node_translatable = $this->createNode('page');
-
-    $this->drupalGet('admin/config/regional/tmgmt/entity/node');
-    // We have both listed - one of articles and page.
-    $this->assertText($this->nodes['article']['en'][0]->title);
-    $this->assertText($page_node_translatable->title);
-
-    // Try the search by content type.
-    $edit = array();
-    $edit['search[type]'] = 'article';
-    $this->drupalPost('admin/config/regional/tmgmt/entity/node', $edit, t('Search'));
-    // There should be article present.
-    $this->assertText($this->nodes['article']['en'][0]->title);
-    // The page node should not be listed.
-    $this->assertNoText($page_node_translatable->title);
-
-    // Try cancel button - despite we do post content type search value
-    // we should get nodes of botch content types.
-    $this->drupalPost('admin/config/regional/tmgmt/entity/node', $edit, t('Cancel'));
-    $this->assertText($this->nodes['article']['en'][0]->title);
-    $this->assertText($page_node_translatable->title);
-  }
-
-  function testEntitySourceListSearch() {
-
-    // We need a node with title composed of several words to test
-    // "any words" search.
-    $title_part_1 = $this->randomName('4');
-    $title_part_2 = $this->randomName('4');
-    $title_part_3 = $this->randomName('4');
-
-    $this->nodes['article']['en'][0]->title = "$title_part_1 $title_part_2 $title_part_3";
-    node_save($this->nodes['article']['en'][0]);
-
-    // Submit partial node title and see if we have a result.
-    $edit = array();
-    $edit['search[title]'] = "$title_part_1 $title_part_3";
-    $this->drupalPost('admin/config/regional/tmgmt/entity/node', $edit, t('Search'));
-    $this->assertText("$title_part_1 $title_part_2 $title_part_3", t('Searching on partial node title must return the result.'));
-
-    // Check if there is only one result in the list.
-    $search_result_rows = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr');
-    $this->assert(count($search_result_rows) == 1, t('The search result must return only one row.'));
-
-    // To test if other entity types work go for simple comment search.
-    $comment = new stdClass();
-    $comment->comment_body[LANGUAGE_NONE][0]['value'] = $this->randomName();
-    $comment->subject = $this->randomName();
-    // We need to associate the comment with entity translatable node object.
-    $comment->nid = $this->nodes['article']['en'][0]->nid;
-    // Set defaults - without these we will get Undefined property notices.
-    $comment->is_anonymous = TRUE;
-    $comment->cid = 0;
-    $comment->pid = 0;
-    $comment->uid = 0;
-    // Will add further comment variables.
-    $comment = comment_submit($comment);
-    comment_save($comment);
-    // Do search for the comment.
-    $edit = array();
-    $edit['search[subject]'] = $comment->subject;
-    $this->drupalPost('admin/config/regional/tmgmt/entity/comment', $edit, t('Search'));
-    $this->assertText($comment->subject, t('Searching for a comment subject.'));
-  }
-}
-
 /**
  * Basic Node Source tests.
  *
diff --git a/sources/node/ui/tmgmt_node_ui.info b/sources/node/ui/tmgmt_node_ui.info
index 7fd2235..222b4a8 100644
--- a/sources/node/ui/tmgmt_node_ui.info
+++ b/sources/node/ui/tmgmt_node_ui.info
@@ -8,6 +8,7 @@ dependencies[] = tmgmt_ui
 dependencies[] = views_bulk_operations
 
 files[] = tmgmt_node_ui.test
+files[] = tmgmt_node_ui.overview.test
 
 ; Views handlers
 files[] = views/tmgmt_node_ui_handler_filter_node_translatable_types.inc
diff --git a/sources/node/ui/tmgmt_node_ui.overview.test b/sources/node/ui/tmgmt_node_ui.overview.test
new file mode 100644
index 0000000..c62a6a7
--- /dev/null
+++ b/sources/node/ui/tmgmt_node_ui.overview.test
@@ -0,0 +1,118 @@
+<?php
+
+/**
+ * Content Overview Tests
+ */
+class TMGMTNodeSourceUIOverviewTestCase extends TMGMTBaseTestCase {
+
+  /**
+   * Implements getInfo().
+   */
+  static function getInfo() {
+    return array(
+      'name' => t('Node Source UI Overview tests'),
+      'description' => t('Tests the user interface for node overviews.'),
+      'group' => t('Translation Management'),
+      'dependencies' => array('rules'),
+    );
+  }
+
+  /**
+   * Overrides SimplenewsTestCase::setUp()
+   */
+  function setUp() {
+    parent::setUp(array('tmgmt_node_ui'));
+    variable_set('language_content_type_page', TRANSLATION_ENABLED);
+
+    $this->loginAsAdmin();
+
+    $this->setEnvironment('de');
+    $this->setEnvironment('fr');
+    $this->setEnvironment('es');
+    $this->setEnvironment('el');
+
+    // Copied from standard.install
+    $type = array(
+      'type' => 'page',
+      'name' => st('Basic page'),
+      'base' => 'node_content',
+      'description' => st("Use <em>basic pages</em> for your static content, such as an 'About us' page."),
+      'custom' => 1,
+      'modified' => 1,
+      'locked' => 0,
+    );
+    $type = node_type_set_defaults($type);
+    node_type_save($type);
+    node_add_body_field($type);
+    node_types_rebuild();
+
+    $this->checkPermissions(array(), TRUE);
+
+    // Allow auto-accept.
+    $default_translator = tmgmt_translator_load('test_translator');
+    $default_translator->settings = array(
+      'auto_accept' => TRUE,
+    );
+    $default_translator->save();
+  }
+
+  /**
+   * Tests translating through the content source overview.
+   */
+  function testNodeSourceOverview() {
+
+    // Login as translator to translate nodes.
+    $this->loginAsTranslator(array(
+      'translate content',
+      'edit any page content',
+      'create page content',
+    ));
+
+    // Create a bunch of english nodes.
+    $node1 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
+    $node2 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
+    $node3 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
+    $node4 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
+
+    // Go to the overview page and make sure the nodes are there.
+    $this->drupalGet('admin/config/regional/tmgmt/node');
+
+    $this->assertText($node1->title);
+    $this->assertText($node2->title);
+    $this->assertText($node3->title);
+    $this->assertText($node4->title);
+
+    // Now translate them.
+    $edit = array(
+      'views_bulk_operations[0]' => TRUE,
+      'views_bulk_operations[1]' => TRUE,
+      'views_bulk_operations[2]' => TRUE,
+    );
+    $this->drupalPost(NULL, $edit, t('Request translations'));
+
+    // Some assertions on the submit form.
+    $this->assertText(t('@title and 2 more (English to ?, Unprocessed)', array('@title' => $node1->title)));
+    $this->assertText(t('Translation for @title', array('@title' => $node1->title)));
+    $this->assertText(t('Translation for @title', array('@title' => $node2->title)));
+    $this->assertText(t('Translation for @title', array('@title' => $node3->title)));
+    $this->assertNoText(t('Translation for @title', array('@title' => $node4->title)));
+
+    // Translate
+    $edit = array(
+      'target_language' => 'de',
+    );
+    $this->drupalPost(NULL, $edit, t('Submit to translator'));
+    $this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node1->title, '@language' => t('German'))));
+    $this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
+    $this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node2->title, '@language' => t('German'))));
+    $this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
+    $this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node3->title, '@language' => t('German'))));
+    $this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
+
+    // Check the translated node.
+    $this->clickLink($node1->title);
+    $this->clickLink(t('Translate'));
+    $this->assertText('de_' . $node1->title);
+  }
+}
+
diff --git a/sources/node/ui/tmgmt_node_ui.test b/sources/node/ui/tmgmt_node_ui.test
index d77ffef..0410bca 100644
--- a/sources/node/ui/tmgmt_node_ui.test
+++ b/sources/node/ui/tmgmt_node_ui.test
@@ -368,120 +368,3 @@ class TMGMTNodeSourceUITestCase extends TMGMTBaseTestCase {
     $this->clickLink('de_' . $node->title);
   }
 }
-
-/**
- * Content Overview Tests
- */
-class TMGMTNodeSourceUIOverviewTestCase extends TMGMTBaseTestCase {
-
-  /**
-   * Implements getInfo().
-   */
-  static function getInfo() {
-    return array(
-      'name' => t('Node Source UI Overview tests'),
-      'description' => t('Tests the user interface for node overviews.'),
-      'group' => t('Translation Management'),
-      'dependencies' => array('rules'),
-    );
-  }
-
-  /**
-   * Overrides SimplenewsTestCase::setUp()
-   */
-  function setUp() {
-    parent::setUp(array('tmgmt_node_ui'));
-    variable_set('language_content_type_page', TRANSLATION_ENABLED);
-
-    $this->loginAsAdmin();
-
-    $this->setEnvironment('de');
-    $this->setEnvironment('fr');
-    $this->setEnvironment('es');
-    $this->setEnvironment('el');
-
-    // Copied from standard.install
-    $type = array(
-      'type' => 'page',
-      'name' => st('Basic page'),
-      'base' => 'node_content',
-      'description' => st("Use <em>basic pages</em> for your static content, such as an 'About us' page."),
-      'custom' => 1,
-      'modified' => 1,
-      'locked' => 0,
-    );
-    $type = node_type_set_defaults($type);
-    node_type_save($type);
-    node_add_body_field($type);
-    node_types_rebuild();
-
-    $this->checkPermissions(array(), TRUE);
-
-    // Allow auto-accept.
-    $default_translator = tmgmt_translator_load('test_translator');
-    $default_translator->settings = array(
-      'auto_accept' => TRUE,
-    );
-    $default_translator->save();
-  }
-
-  /**
-   * Tests translating through the content source overview.
-   */
-  function testNodeSourceOverview() {
-
-    // Login as translator to translate nodes.
-    $this->loginAsTranslator(array(
-      'translate content',
-      'edit any page content',
-      'create page content',
-    ));
-
-    // Create a bunch of english nodes.
-    $node1 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
-    $node2 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
-    $node3 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
-    $node4 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
-
-    // Go to the overview page and make sure the nodes are there.
-    $this->drupalGet('admin/config/regional/tmgmt/node');
-
-    $this->assertText($node1->title);
-    $this->assertText($node2->title);
-    $this->assertText($node3->title);
-    $this->assertText($node4->title);
-
-    // Now translate them.
-    $edit = array(
-      'views_bulk_operations[0]' => TRUE,
-      'views_bulk_operations[1]' => TRUE,
-      'views_bulk_operations[2]' => TRUE,
-    );
-    $this->drupalPost(NULL, $edit, t('Request translations'));
-
-    // Some assertions on the submit form.
-    $this->assertText(t('@title and 2 more (English to ?, Unprocessed)', array('@title' => $node1->title)));
-    $this->assertText(t('Translation for @title', array('@title' => $node1->title)));
-    $this->assertText(t('Translation for @title', array('@title' => $node2->title)));
-    $this->assertText(t('Translation for @title', array('@title' => $node3->title)));
-    $this->assertNoText(t('Translation for @title', array('@title' => $node4->title)));
-
-    // Translate
-    $edit = array(
-      'target_language' => 'de',
-    );
-    $this->drupalPost(NULL, $edit, t('Submit to translator'));
-    $this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node1->title, '@language' => t('German'))));
-    $this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
-    $this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node2->title, '@language' => t('German'))));
-    $this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
-    $this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node3->title, '@language' => t('German'))));
-    $this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
-
-    // Check the translated node.
-    $this->clickLink($node1->title);
-    $this->clickLink(t('Translate'));
-    $this->assertText('de_' . $node1->title);
-  }
-}
-
diff --git a/tests/tmgmt.base.entity.test b/tests/tmgmt.base.entity.test
new file mode 100644
index 0000000..4a2a5a4
--- /dev/null
+++ b/tests/tmgmt.base.entity.test
@@ -0,0 +1,222 @@
+<?php
+
+/*
+ * @file
+ * Contains tests for Translation management
+ */
+
+/**
+ * Utility test case class with helper methods to create entities and their
+ * fields with populated translatable content. Extend this class if you create
+ * tests in which you need Drupal entities and/or fields.
+ */
+abstract class TMGMTEntityTestCaseUtility extends TMGMTBaseTestCase {
+
+  public $field_names = array();
+
+  /**
+   * Creates node type with several text fields with different cardinality.
+   *
+   * Internally it calls TMGMTEntityTestCaseUtility::attachFields() to create
+   * and attach fields to newly created bundle. You can than use
+   * $this->field_names['node']['YOUR_BUNDLE_NAME'] to access them.
+   *
+   * @param string $machine_name
+   *   Machine name of the node type.
+   * @param string $human_name
+   *   Human readable name of the node type.
+   * @param int $language_content_type
+   *   Flag of how the translation should be handled.
+   */
+  function createNodeType($machine_name, $human_name, $language_content_type = 0) {
+
+    // Create new bundle.
+    $type = array(
+      'type' => $machine_name,
+      'name' => $human_name,
+      'base' => 'node_content',
+      'description' => '',
+      'custom' => 1,
+      'modified' => 1,
+      'locked' => 0,
+    );
+    $type = node_type_set_defaults($type);
+    node_type_save($type);
+    node_add_body_field($type);
+    node_types_rebuild();
+
+    // Set content type to be translatable as specified by
+    // $language_content_type.
+    $edit = array();
+    $edit['language_content_type'] = $language_content_type;
+    $this->drupalPost('admin/structure/types/manage/' . $machine_name, $edit, t('Save content type'));
+
+    $translatable = FALSE;
+    if (defined('ENTITY_TRANSLATION_ENABLED') && $language_content_type == ENTITY_TRANSLATION_ENABLED) {
+      $translatable = TRUE;
+    }
+
+    // Push in also the body field.
+    $this->field_names['node'][$machine_name][] = 'body';
+
+    $this->attachFields('node', $machine_name, $translatable);
+
+    // Change body field to be translatable.
+    $body = field_info_field('body');
+    $body['translatable'] = $translatable;
+    field_update_field($body);
+  }
+
+  /**
+   * Creates taxonomy vocabulary with custom fields.
+   *
+   * To create and attach fields it internally calls
+   * TMGMTEntityTestCaseUtility::attachFields(). You can than access these
+   * fields calling $this->field_names['node']['YOUR_BUNDLE_NAME'].
+   *
+   * @param string $machine_name
+   *   Vocabulary machine name.
+   * @param string $human_name
+   *   Vocabulary human readable name.
+   * @param bool|array $fields_translatable
+   *   Flag or definition array to determine which or all fields should be
+   *   translatable.
+   *
+   * @return stdClass
+   *   Created vocabulary object.
+   */
+  function createTaxonomyVocab($machine_name, $human_name, $fields_translatable = TRUE) {
+    $vocabulary = new stdClass();
+    $vocabulary->name = $human_name;
+    $vocabulary->machine_name = $machine_name;
+    taxonomy_vocabulary_save($vocabulary);
+
+    $this->attachFields('taxonomy_term', $vocabulary->machine_name, $fields_translatable);
+
+    return $vocabulary;
+  }
+
+  /**
+   * Creates fields of type text and text_with_summary of different cardinality.
+   *
+   * It will attach created fields to provided entity name and bundle.
+   *
+   * Field names will be stored in $this->field_names['entity']['bundle']
+   * through which you can access them.
+   *
+   * @param string $entity_name
+   *   Entity name to which fields should be attached.
+   * @param string $bundle
+   *   Bundle name to which fields should be attached.
+   * @param bool|array $translatable
+   *   Flag or definition array to determine which or all fields should be
+   *   translatable.
+   */
+  function attachFields($entity_name, $bundle, $translatable = TRUE) {
+    // Create several text fields.
+    $field_types = array('text', 'text_with_summary');
+
+    for ($i = 0 ; $i <= 5; $i++) {
+      $field_type = $field_types[array_rand($field_types, 1)];
+      $field_name = drupal_strtolower($this->randomName());
+
+      // Create a field.
+      $field = array(
+        'field_name' => $field_name,
+        'type' => $field_type,
+        'cardinality' => mt_rand(1, 5),
+        'translatable' => is_array($translatable) && isset($translatable[$i]) ? $translatable[$i] : (boolean) $translatable,
+      );
+      field_create_field($field);
+
+      // Create an instance of the previously created field.
+      $instance = array(
+        'field_name' => $field_name,
+        'entity_type' => $entity_name,
+        'bundle' => $bundle,
+        'label' => $this->randomName(10),
+        'description' => $this->randomString(30),
+        'widget' => array(
+          'type' => $field_type == 'text' ? 'text_textfield' : 'text_textarea_with_summary',
+          'label' => $this->randomString(10),
+        ),
+      );
+      field_create_instance($instance);
+
+      // Store field names in case there are needed outside this method.
+      $this->field_names[$entity_name][$bundle][] = $field_name;
+    }
+  }
+
+  /**
+   * Creates a node of a given bundle.
+   *
+   * It uses $this->field_names to populate content of attached fields.
+   *
+   * @param string $bundle
+   *   Node type name.
+   * @param string $sourcelang
+   *   Source lang of the node to be created.
+   *
+   * @return object
+   *   Newly created node object.
+   */
+  function createNode($bundle, $sourcelang = 'en') {
+    $node = array(
+      'type' => $bundle,
+      'language' => $sourcelang,
+    );
+
+    foreach ($this->field_names['node'][$bundle] as $field_name) {
+      $field_info = field_info_field($field_name);
+      $cardinality = $field_info['cardinality'] == FIELD_CARDINALITY_UNLIMITED ? 1 : $field_info['cardinality'];
+
+      // Create two deltas for each field.
+      for ($delta = 0; $delta <= $cardinality; $delta++) {
+        $node[$field_name][$sourcelang][$delta]['value'] = $this->randomName(20);
+        if ($field_info['type'] == 'text_with_summary') {
+          $node[$field_name][$sourcelang][$delta]['summary'] = $this->randomName(10);
+        }
+      }
+    }
+
+    return $this->drupalCreateNode($node);
+  }
+
+  /**
+   * Creates a taxonomy term of a given vocabulary.
+   *
+   * It uses $this->field_names to populate content of attached fields. You can
+   * access fields values using
+   * $this->field_names['taxonomy_term'][$vocabulary->machine_name].
+   *
+   * @param object $vocabulary
+   *   Vocabulary object for which the term should be created.
+   *
+   * @return object
+   *   Newly created node object.
+   */
+  function createTaxonomyTerm($vocabulary) {
+    $term = new stdClass();
+    $term->name = $this->randomName();
+    $term->description = $this->randomName();
+    $term->vid = $vocabulary->vid;
+
+    foreach ($this->field_names['taxonomy_term'][$vocabulary->machine_name] as $field_name) {
+      $field_info = field_info_field($field_name);
+      $cardinality = $field_info['cardinality'] == FIELD_CARDINALITY_UNLIMITED ? 1 : $field_info['cardinality'];
+      $field_lang = $field_info['translatable'] ? 'en' : LANGUAGE_NONE;
+
+      // Create two deltas for each field.
+      for ($delta = 0; $delta <= $cardinality; $delta++) {
+        $term->{$field_name}[$field_lang][$delta]['value'] = $this->randomName(20);
+        if ($field_info['type'] == 'text_with_summary') {
+          $term->{$field_name}[$field_lang][$delta]['summary'] = $this->randomName(10);
+        }
+      }
+    }
+
+    taxonomy_term_save($term);
+    return taxonomy_term_load($term->tid);
+  }
+}
diff --git a/tests/tmgmt.base.test b/tests/tmgmt.base.test
new file mode 100644
index 0000000..6ea7b96
--- /dev/null
+++ b/tests/tmgmt.base.test
@@ -0,0 +1,181 @@
+<?php
+
+/*
+ * @file
+ * Contains tests for Translation management
+ */
+
+/**
+ * Base class for tests.
+ */
+class TMGMTBaseTestCase extends DrupalWebTestCase {
+  protected $profile = 'testing';
+
+  /**
+   * A default translator using the test translator.
+   *
+   * @var TMGMTTranslator
+   */
+  protected $default_translator;
+
+  /**
+   * List of permissions used by loginAsAdmin().
+   *
+   * @var array
+   */
+  protected $admin_permissions = array();
+
+  /**
+   * Drupal user object created by loginAsAdmin().
+   *
+   * @var object
+   */
+  protected $admin_user = NULL;
+
+  /**
+   * List of permissions used by loginAsTranslator().
+   *
+   * @var array
+   */
+  protected $translator_permissions = array();
+
+  /**
+   * Drupal user object created by loginAsTranslator().
+   *
+   * @var object
+   */
+  protected $translator_user = NULL;
+
+  /**
+   * Overrides DrupalWebTestCase::setUp()
+   */
+  function setUp() {
+    $modules = func_get_args();
+    if (isset($modules[0]) && is_array($modules[0])) {
+      $modules = $modules[0];
+    }
+    $modules = array_merge(array('entity', 'tmgmt', 'tmgmt_test'), $modules);
+    parent::setUp($modules);
+    $this->default_translator = tmgmt_translator_load('test_translator');
+
+    // Load default admin permissions.
+    $this->admin_permissions = array(
+      'administer languages',
+      'access administration pages',
+      'administer content types',
+      'administer tmgmt',
+    );
+
+    // Load default translator user permissions.
+    $this->translator_permissions = array(
+      'create translation jobs',
+      'submit translation jobs',
+      'accept translation jobs',
+    );
+  }
+
+  /**
+   * Will create a user with admin permissions and log it in.
+   *
+   * @param array $additional_permissions
+   *   Additional permissions that will be granted to admin user.
+   * @param boolean $reset_permissions
+   *   Flag to determine if default admin permissions will be replaced by
+   *   $additional_permissions.
+   *
+   * @return object
+   *   Newly created and logged in user object.
+   */
+  function loginAsAdmin($additional_permissions = array(), $reset_permissions = FALSE) {
+    $permissions = $this->admin_permissions;
+
+    if ($reset_permissions) {
+      $permissions = $additional_permissions;
+    }
+    elseif (!empty($additional_permissions)) {
+      $permissions = array_merge($permissions, $additional_permissions);
+    }
+
+    $this->admin_user = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($this->admin_user);
+    return $this->admin_user;
+  }
+
+  /**
+   * Will create a user with translator permissions and log it in.
+   *
+   * @param array $additional_permissions
+   *   Additional permissions that will be granted to admin user.
+   * @param boolean $reset_permissions
+   *   Flag to determine if default admin permissions will be replaced by
+   *   $additional_permissions.
+   *
+   * @return object
+   *   Newly created and logged in user object.
+   */
+  function loginAsTranslator($additional_permissions = array(), $reset_permissions = FALSE) {
+    $permissions = $this->translator_permissions;
+
+    if ($reset_permissions) {
+      $permissions = $additional_permissions;
+    }
+    elseif (!empty($additional_permissions)) {
+      $permissions = array_merge($permissions, $additional_permissions);
+    }
+
+    $this->translator_user = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($this->translator_user);
+    return $this->translator_user;
+  }
+
+  /**
+   * Creates, saves and returns a translator.
+   *
+   * @return TMGMTTranslator
+   */
+  function createTranslator() {
+    $translator = new TMGMTTranslator();
+    $translator->name = strtolower($this->randomName());
+    $translator->label = $this->randomName();
+    $translator->plugin = 'test_translator';
+    $translator->settings = array(
+      'key' => $this->randomName(),
+      'another_key' => $this->randomName(),
+    );
+    $this->assertEqual(SAVED_NEW, $translator->save());
+
+    // Assert that the translator was assigned a tid.
+    $this->assertTrue($translator->tid > 0);
+    return $translator;
+  }
+
+  /**
+   * Creates, saves and returns a translation job.
+   *
+   * @return TMGMTJob
+   */
+  function createJob($source = 'en', $target = 'de', $uid = 1)  {
+    $job = tmgmt_job_create($source, $target, $uid);
+    $this->assertEqual(SAVED_NEW, $job->save());
+
+    // Assert that the translator was assigned a tid.
+    $this->assertTrue($job->tjid > 0);
+    return $job;
+  }
+
+
+  /**
+   * Sets the proper environment.
+   *
+   * Currently just adds a new language.
+   *
+   * @param string $langcode
+   *   The language code.
+   */
+  function setEnvironment($langcode) {
+    // Add the language.
+    locale_add_language($langcode);
+  }
+
+}
+
diff --git a/tests/tmgmt.crud.test b/tests/tmgmt.crud.test
new file mode 100644
index 0000000..c48fe63
--- /dev/null
+++ b/tests/tmgmt.crud.test
@@ -0,0 +1,381 @@
+<?php
+
+/*
+ * @file
+ * Contains tests for Translation management
+ */
+
+/**
+ * Basic CRUD tests.
+ */
+class TMGMTCRUDTestCase extends TMGMTBaseTestCase {
+
+  /**
+   * Implements getInfo().
+   */
+  static function getInfo() {
+    return array(
+      'name' => t('CRUD tests'),
+      'description' => t('Basic crud operations for jobs and translators'),
+      'group' => t('Translation Management'),
+    );
+  }
+
+  /**
+   * Test crud operations of translators.
+   */
+  function testTranslators() {
+    $translator = $this->createTranslator();
+
+    $loaded_translator = tmgmt_translator_load($translator->tid);
+
+    $this->assertEqual($translator->name, $loaded_translator->name);
+    $this->assertEqual($translator->label, $loaded_translator->label);
+    $this->assertEqual($translator->settings, $loaded_translator->settings);
+
+    // Update the settings.
+    $translator->settings['new_key'] = $this->randomString();
+    $this->assertEqual(SAVED_UPDATED, $translator->save());
+
+    $loaded_translator = tmgmt_translator_load($translator->tid);
+
+    $this->assertEqual($translator->name, $loaded_translator->name);
+    $this->assertEqual($translator->label, $loaded_translator->label);
+    $this->assertEqual($translator->settings, $loaded_translator->settings);
+
+    // Delete the translator, make sure the translator is gone.
+    $translator->delete();
+    $this->assertFalse(tmgmt_translator_load($translator->tid));
+  }
+
+  /**
+   * Test crud operations of jobs.
+   */
+  function testJobs() {
+    $job = $this->createJob();
+
+    $loaded_job = tmgmt_job_load($job->tjid);
+
+    $this->assertEqual($job->source_language, $loaded_job->source_language);
+    $this->assertEqual($job->target_language, $loaded_job->target_language);
+
+    // Assert that the created and changed information has been set to the
+    // default value.
+    $this->assertTrue($loaded_job->created > 0);
+    $this->assertTrue($loaded_job->changed > 0);
+    $this->assertEqual(0, $loaded_job->state);
+
+    // Update the settings.
+    $job->reference = 7;
+    $this->assertEqual(SAVED_UPDATED, $job->save());
+
+    $loaded_job = tmgmt_job_load($job->tjid);
+
+    $this->assertEqual($job->reference, $loaded_job->reference);
+
+    // Test the job items.
+    $item1 = $job->addItem('test_source', 'type', 5);
+    $item2 = $job->addItem('test_source', 'type', 4);
+
+    // Load and compare the items.
+    $items = $job->getItems();
+    $this->assertEqual(2, count($items));
+
+    $this->assertEqual($item1->plugin, $items[$item1->tjiid]->plugin);
+    $this->assertEqual($item1->item_type, $items[$item1->tjiid]->item_type);
+    $this->assertEqual($item1->item_id, $items[$item1->tjiid]->item_id);
+    $this->assertEqual($item2->plugin, $items[$item2->tjiid]->plugin);
+    $this->assertEqual($item2->item_type, $items[$item2->tjiid]->item_type);
+    $this->assertEqual($item2->item_id, $items[$item2->tjiid]->item_id);
+
+    // Delete the translator, make sure the translator is gone.
+    $job->delete();
+    $this->assertFalse(tmgmt_job_load($job->tjid));
+  }
+
+  function testRemoteMappings() {
+
+    $data_key = '5][test_source][type';
+
+    $translator = $this->createTranslator();
+    $job = $this->createJob();
+    $job->translator = $translator->name;
+    $job->save();
+    $item1 = $job->addItem('test_source', 'type', 5);
+    $item2 = $job->addItem('test_source', 'type', 4);
+
+    $result = $item1->addRemoteMapping($data_key, 'id11', array('remote_identifier_2' => 'id12', 'remote_identifier_3' => 'id13'));
+    $this->assertEqual($result, SAVED_NEW);
+
+    $job_mappings = $job->getRemoteMappings();
+    $item_mappings = $item1->getRemoteMappings();
+
+    $job_mapping = array_shift($job_mappings);
+    $item_mapping = array_shift($item_mappings);
+
+    $_job = $job_mapping->getJob();
+    $this->assertEqual($job->tjid, $_job->tjid);
+
+    $_job = $item_mapping->getJob();
+    $this->assertEqual($job->tjid, $_job->tjid);
+
+    $_item1 = $item_mapping->getJobItem();
+    $this->assertEqual($item1->tjiid, $_item1->tjiid);
+
+    /**
+     * @var TMGMTRemoteController $remote_mapping_controller
+     */
+    $remote_mapping_controller = entity_get_controller('tmgmt_remote');
+    $remote_mappings = $remote_mapping_controller->loadByRemoteIdentifier('id11', 'id12', 'id13');
+    $remote_mapping = array_shift($remote_mappings);
+    $this->assertEqual($item1->tjiid, $remote_mapping->tjiid);
+
+    $this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier('id11')), 1);
+    $this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier('id11', '')), 0);
+    $this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier('id11', NULL, '')), 0);
+    $this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier(NULL, NULL, 'id13')), 1);
+
+    // Test remote data.
+    $item_mapping->addRemoteData('test_data', 'test_value');
+    entity_save('tmgmt_remote', $item_mapping);
+    $item_mapping = entity_load_single('tmgmt_remote', $item_mapping->trid);
+    $this->assertEqual($item_mapping->getRemoteData('test_data'), 'test_value');
+
+    // Add mapping to the other job item as well.
+    $item2->addRemoteMapping($data_key, 'id21', array('remote_identifier_2' => 'id22', 'remote_identifier_3' => 'id23'));
+
+    // Test deleting.
+
+    // Delete item1.
+    entity_get_controller('tmgmt_job_item')->delete(array($item1->tjiid));
+    // Test if mapping for item1 has been removed as well.
+
+    $this->assertEqual(count($remote_mapping_controller->loadByLocalData(NULL, $item1->tjiid)), 0);
+
+    // We still should have mapping for item2.
+    $this->assertEqual(count($remote_mapping_controller->loadByLocalData(NULL, $item2->tjiid)), 1);
+
+    // Now delete the job and see if remaining mappings were removed as well.
+    entity_get_controller('tmgmt_job')->delete(array($job->tjid));
+    $this->assertEqual(count($remote_mapping_controller->loadByLocalData(NULL, $item2->tjiid)), 0);
+  }
+
+  /**
+   * Test crud operations of job items.
+   */
+  function testJobItems() {
+    $job = $this->createJob();
+
+    // Add some test items.
+    $item1 = $job->addItem('test_source', 'type', 5);
+    $item2 = $job->addItem('test_source', 'type', 4);
+
+    // Test single load callback.
+    $item = tmgmt_job_item_load($item1->tjiid);
+    $this->assertEqual($item1->plugin, $item->plugin);
+    $this->assertEqual($item1->item_type, $item->item_type);
+    $this->assertEqual($item1->item_id, $item->item_id);
+
+    // Test multiple load callback.
+    $items = tmgmt_job_item_load_multiple(array($item1->tjiid, $item2->tjiid));
+
+    $this->assertEqual(2, count($items));
+
+    $this->assertEqual($item1->plugin, $items[$item1->tjiid]->plugin);
+    $this->assertEqual($item1->item_type, $items[$item1->tjiid]->item_type);
+    $this->assertEqual($item1->item_id, $items[$item1->tjiid]->item_id);
+    $this->assertEqual($item2->plugin, $items[$item2->tjiid]->plugin);
+    $this->assertEqual($item2->item_type, $items[$item2->tjiid]->item_type);
+    $this->assertEqual($item2->item_id, $items[$item2->tjiid]->item_id);
+  }
+
+  /**
+   * Test the calculations of the counters.
+   */
+  function testJobItemsCounters() {
+    $job = $this->createJob();
+
+    // Some test data items.
+    $data1 = array(
+      '#text' => 'The text to be translated.',
+    );
+    $data2 = array(
+      '#text' => 'The text to be translated.',
+      '#translation' => '',
+    );
+    $data3 = array(
+      '#text' => 'The text to be translated.',
+      '#translation' => 'The translated data. Set by the translator plugin.',
+    );
+    $data4 = array(
+      '#text' => 'Another, longer text to be translated.',
+      '#translation' => 'The translated data. Set by the translator plugin.',
+      '#status' => TMGMT_DATA_ITEM_STATE_REVIEWED,
+    );
+    $data5 = array(
+      '#label' => 'label',
+      'data1' => $data1,
+      'data4' => $data4,
+    );
+
+    // No data items.
+    $this->assertEqual(0, $job->getCountPending());
+    $this->assertEqual(0, $job->getCountTranslated());
+    $this->assertEqual(0, $job->getCountReviewed());
+    $this->assertEqual(0, $job->getCountAccepted());
+    $this->assertEqual(0, $job->getWordCount());
+
+    // Add a test items.
+    $job_item1 = tmgmt_job_item_create('plugin', 'type', 4, array('tjid' => $job->tjid));
+    $job_item1->save();
+
+    // No pending, translated and confirmed data items.
+    $job = entity_load_single('tmgmt_job', $job->tjid);
+    $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
+    drupal_static_reset('tmgmt_job_statistics_load');
+    $this->assertEqual(0, $job_item1->getCountPending());
+    $this->assertEqual(0, $job_item1->getCountTranslated());
+    $this->assertEqual(0, $job_item1->getCountReviewed());
+    $this->assertEqual(0, $job_item1->getCountAccepted());
+    $this->assertEqual(0, $job->getCountPending());
+    $this->assertEqual(0, $job->getCountTranslated());
+    $this->assertEqual(0, $job->getCountReviewed());
+    $this->assertEqual(0, $job->getCountAccepted());
+
+    // Add an untranslated data item.
+    $job_item1->data['data_item1'] = $data1;
+    $job_item1->save();
+
+    // One pending data items.
+    $job = entity_load_single('tmgmt_job', $job->tjid);
+    $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
+    drupal_static_reset('tmgmt_job_statistics_load');
+    $this->assertEqual(1, $job_item1->getCountPending());
+    $this->assertEqual(0, $job_item1->getCountTranslated());
+    $this->assertEqual(0, $job_item1->getCountReviewed());
+    $this->assertEqual(5, $job_item1->getWordCount());
+    $this->assertEqual(1, $job->getCountPending());
+    $this->assertEqual(0, $job->getCountReviewed());
+    $this->assertEqual(0, $job->getCountTranslated());
+    $this->assertEqual(5, $job->getWordCount());
+
+
+    // Add another untranslated data item.
+    // Test with an empty translation set.
+    $job_item1->data['data_item1'] = $data2;
+    $job_item1->save();
+
+    // One pending data items.
+    $job = entity_load_single('tmgmt_job', $job->tjid);
+    $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
+    drupal_static_reset('tmgmt_job_statistics_load');
+    $this->assertEqual(1, $job_item1->getCountPending());
+    $this->assertEqual(0, $job_item1->getCountTranslated());
+    $this->assertEqual(0, $job_item1->getCountReviewed());
+    $this->assertEqual(5, $job_item1->getWordCount());
+    $this->assertEqual(1, $job->getCountPending());
+    $this->assertEqual(0, $job->getCountTranslated());
+    $this->assertEqual(0, $job->getCountReviewed());
+    $this->assertEqual(5, $job->getWordCount());
+
+    // Add a translated data item.
+    $job_item1->data['data_item1'] = $data3;
+    $job_item1->save();
+
+    // One translated data items.
+    drupal_static_reset('tmgmt_job_statistics_load');
+    $this->assertEqual(0, $job_item1->getCountPending());
+    $this->assertEqual(1, $job_item1->getCountTranslated());
+    $this->assertEqual(0, $job_item1->getCountReviewed());
+    $this->assertEqual(0, $job->getCountPending());
+    $this->assertEqual(0, $job->getCountReviewed());
+    $this->assertEqual(1, $job->getCountTranslated());
+
+    // Add a confirmed data item.
+    $job_item1->data['data_item1'] = $data4;
+    $job_item1->save();
+
+    // One reviewed data item.
+    drupal_static_reset('tmgmt_job_statistics_load');
+    $this->assertEqual(1, $job_item1->getCountReviewed());
+    $this->assertEqual(1, $job->getCountReviewed());
+
+    // Add a translated and an untranslated and a confirmed data item
+    $job = entity_load_single('tmgmt_job', $job->tjid);
+    $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
+    $job_item1->data['data_item1'] = $data1;
+    $job_item1->data['data_item2'] = $data3;
+    $job_item1->data['data_item3'] = $data4;
+    $job_item1->save();
+
+    // One pending and translated data items each.
+    drupal_static_reset('tmgmt_job_statistics_load');
+    $this->assertEqual(1, $job->getCountPending());
+    $this->assertEqual(1, $job->getCountTranslated());
+    $this->assertEqual(1, $job->getCountReviewed());
+    $this->assertEqual(16, $job->getWordCount());
+
+    // Add nested data items.
+    $job_item1->data['data_item1'] = $data5;
+    $job_item1->save();
+
+    // One pending data items.
+    $job = entity_load_single('tmgmt_job', $job->tjid);
+    $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
+    $this->assertEqual('label', $job_item1->data['data_item1']['#label']);
+    $this->assertEqual(3, count($job_item1->data['data_item1']));
+
+    // Add a greater number of data items
+    for ($index = 1; $index <= 3; $index++) {
+      $job_item1->data['data_item' . $index] = $data1;
+    }
+    for ($index = 4; $index <= 10; $index++) {
+      $job_item1->data['data_item' . $index] = $data3;
+    }
+    for ($index = 11; $index <= 15; $index++) {
+      $job_item1->data['data_item' . $index] = $data4;
+    }
+    $job_item1->save();
+
+    // 3 pending and 7 translated data items each.
+    $job = entity_load_single('tmgmt_job', $job->tjid);
+    drupal_static_reset('tmgmt_job_statistics_load');
+    $this->assertEqual(3, $job->getCountPending());
+    $this->assertEqual(7, $job->getCountTranslated());
+    $this->assertEqual(5, $job->getCountReviewed());
+
+    // Add several job items
+    $job_item2 = tmgmt_job_item_create('plugin', 'type', 5, array('tjid' => $job->tjid));
+    for ($index = 1; $index <= 4; $index++) {
+      $job_item2->data['data_item' . $index] = $data1;
+    }
+    for ($index = 5; $index <= 12; $index++) {
+      $job_item2->data['data_item' . $index] = $data3;
+    }
+    for ($index = 13; $index <= 16; $index++) {
+      $job_item2->data['data_item' . $index] = $data4;
+    }
+    $job_item2->save();
+
+    // 3 pending and 7 translated data items each.
+    $job = entity_load_single('tmgmt_job', $job->tjid);
+    drupal_static_reset('tmgmt_job_statistics_load');
+    $this->assertEqual(7, $job->getCountPending());
+    $this->assertEqual(15, $job->getCountTranslated());
+    $this->assertEqual(9, $job->getCountReviewed());
+
+    // Accept the job items.
+    foreach ($job->getItems() as $item) {
+      // Set the state directly to avoid triggering translator and source
+      // controllers that do not exist.
+      $item->setState(TMGMT_JOB_ITEM_STATE_ACCEPTED);
+      $item->save();
+    }
+    drupal_static_reset('tmgmt_job_statistics_load');
+    $this->assertEqual(0, $job->getCountPending());
+    $this->assertEqual(0, $job->getCountTranslated());
+    $this->assertEqual(0, $job->getCountReviewed());
+    $this->assertEqual(31, $job->getCountAccepted());
+  }
+
+}
diff --git a/tests/tmgmt.helper.test b/tests/tmgmt.helper.test
new file mode 100644
index 0000000..60b4067
--- /dev/null
+++ b/tests/tmgmt.helper.test
@@ -0,0 +1,95 @@
+<?php
+
+/*
+ * @file
+ * Contains tests for Translation management
+ */
+
+/**
+ * Test the helper functions in tmgmt.module.
+ */
+class TMGMTHelperTestCase extends TMGMTBaseTestCase {
+
+  /**
+   * Implements getInfo().
+   */
+  static function getInfo() {
+    return array(
+      'name' => t('Helper functions Test case'),
+      'description' => t('Helper functions for other modules'),
+      'group' => t('Translation Management'),
+    );
+  }
+
+  /**
+   * Tests tmgmt_job_match_item()
+   *
+   * @see tmgmt_job_match_item
+   */
+  function testTMGTJobMatchItem() {
+    $this->loginAsAdmin();
+    $this->setEnvironment('fr');
+    $this->setEnvironment('es');
+
+    // Add a job from en to fr and en to sp.
+    $job_en_fr = $this->createJob('en', 'fr');
+    $job_en_sp = $this->createJob('en', 'es');
+
+    // Add a job which has existing source-target combinations.
+    $this->assertEqual($job_en_fr->tjid, tmgmt_job_match_item('en', 'fr')->tjid);
+    $this->assertEqual($job_en_sp->tjid, tmgmt_job_match_item('en', 'es')->tjid);
+
+    // Add a job which has no existing source-target combination.
+    $this->assertTrue(tmgmt_job_match_item('fr', 'es'));
+  }
+
+  /**
+   * Tests the tmgmt_data_item_label() function.
+   *
+   * @todo: Move into a unit test case once available.
+   */
+  function testDataIemLabel() {
+    $no_label = array(
+      '#text' => 'No label',
+    );
+    $this->assertEqual(tmgmt_data_item_label($no_label), 'No label');
+    $label = array(
+      '#parent_label' => array(),
+      '#label' => 'A label',
+    );
+    $this->assertEqual(tmgmt_data_item_label($label), 'A label');
+    $parent_label = array(
+      '#parent_label' => array('Parent label', 'Sub label'),
+      '#label' => 'A label',
+    );
+    $this->assertEqual(tmgmt_data_item_label($parent_label), 'Parent label > Sub label');
+  }
+
+  function testWordCount() {
+    $unit_tests = array(
+      'empty' => array(
+        'text' => '',
+        'count' => 0,
+      ),
+      'latin' => array(
+        'text' => 'Drupal is the best!',
+        'count' => 4,
+      ),
+      'non-latin' => array(
+        'text' => 'Друпал лучший!',
+        'count' => 2,
+      ),
+      'complex punctuation' => array(
+        'text' => '<[({-!ReAd@*;: ,?+MoRe...})]>\\|/',
+        'count' => 2,
+      ),
+      'repeat' => array(
+        'text' => 'repeat repeat',
+        'count' => 2,
+      ),
+    );
+    foreach ($unit_tests as $id => $test_data) {
+      $this->assertEqual($real_count = tmgmt_word_count($test_data['text']), $desirable_count = $test_data['count'], t('!test_id: Real count (=!real_count) should be equal to desirable (=!desirable_count)', array('!test_id' => $id, '!real_count' => $real_count, '!desirable_count' => $desirable_count)));
+    }
+  }
+}
diff --git a/tests/tmgmt.plugin.test b/tests/tmgmt.plugin.test
new file mode 100644
index 0000000..3e4ebe9
--- /dev/null
+++ b/tests/tmgmt.plugin.test
@@ -0,0 +1,171 @@
+<?php
+
+/*
+ * @file
+ * Contains tests for Translation management
+ */
+
+/**
+ * Tests interaction between core and the plugins.
+ */
+class TMGMTPluginsTestCase extends TMGMTBaseTestCase {
+
+  /**
+   * Implements getInfo().
+   */
+  static function getInfo() {
+    return array(
+      'name' => t('Plugin tests'),
+      'description' => t('Verifies basic functionality of source and translator plugins'),
+      'group' => t('Translation Management'),
+    );
+  }
+
+  function createJob($source = 'en', $target = 'de', $uid = 1) {
+    $job = parent::createJob();
+
+    for ($i = 1; $i < 3; $i++) {
+      if ($i == 3) {
+        // Explicitly define the data for the third item.
+        $data['data'] = array(
+          'dummy' => array(
+            'deep_nesting' => array(
+              '#text' => 'Stored data',
+            ),
+          ),
+        );
+        $job->addItem('test_source', 'test', $i, array($data));
+      }
+      $job->addItem('test_source', 'test', $i);
+    }
+
+    // Manually specify the translator for now.
+    $job->translator = $this->default_translator->name;
+
+    return $job;
+  }
+
+  function testBasicWorkflow() {
+    // Submit a translation job.
+    $submit_job = $this->createJob();
+    $submit_job->settings = array('action' => 'submit');
+    $submit_job->requestTranslation();
+    $submit_job = tmgmt_job_load($submit_job->tjid);
+    $this->assertTrue($submit_job->isActive());
+    $messages = $submit_job->getMessages();
+    $last_message = end($messages);
+    $this->assertEqual('Test submit.', $last_message->message);
+
+    // Translate a job.
+    $translate_job = $this->createJob();
+    $translate_job->settings = array('action' => 'translate');
+    $translate_job->requestTranslation();
+    $translate_job = tmgmt_job_load($translate_job->tjid);
+    foreach ($translate_job->getItems() as $job_item) {
+      $this->assertTrue($job_item->isNeedsReview());
+    }
+
+    $messages = $translate_job->getMessages();
+    // array_values() results in numeric keys, which is necessary for list.
+    list($debug, $translated, $needs_review) = array_values($messages);
+    $this->assertEqual('Test translator called.', $debug->message);
+    $this->assertEqual('debug', $debug->type);
+    $this->assertEqual('Test translation created.', $translated->message);
+    $this->assertEqual('status', $translated->type);
+
+    // The third message is specific to a job item and has different state
+    // constants.
+    $this->assertEqual('The translation of !source to @language is finished and can now be <a href="!review_url">reviewed</a>.', $needs_review->message);
+    $this->assertEqual('status', $needs_review->type);
+
+    $i = 1;
+    foreach ($translate_job->getItems() as $item) {
+      // Check the translated text.
+      if ($i != 3) {
+        $expected_text = 'de_Text for job item with type ' . $item->item_type . ' and id ' . $item->item_id . '.';
+      }
+      else {
+        // The third item has an explicitly stored data value.
+        $expected_text = 'de_Stored data';
+      }
+      $item_data = $item->getData();
+      $this->assertEqual($expected_text, $item_data['dummy']['deep_nesting']['#translation']['#text']);
+      $i++;
+    }
+
+    foreach ($translate_job->getItems() as $job_item) {
+      $job_item->acceptTranslation();
+    }
+
+    // @todo Accepting does not result in messages on the job anymore.
+    // Update once there are job item messages.
+    /*
+    $messages = $translate_job->getMessages();
+    $last_message = end($messages);
+    $this->assertEqual('Job accepted', $last_message->message);
+    $this->assertEqual('status', $last_message->type);*/
+
+    // Check if the translations have been "saved".
+    foreach ($translate_job->getItems() as $item) {
+      $this->assertTrue(variable_get('tmgmt_test_saved_translation_' . $item->item_type . '_' . $item->item_id, FALSE));
+    }
+
+    // A rejected job.
+    $reject_job = $this->createJob();
+    $reject_job->settings = array('action' => 'reject');
+    $reject_job->requestTranslation();
+    // Still rejected.
+    $this->assertTrue($reject_job->isRejected());
+
+    $messages = $reject_job->getMessages();
+    $last_message = end($messages);
+    $this->assertEqual('This is not supported.', $last_message->message);
+    $this->assertEqual('error', $last_message->type);
+
+    // A failing job.
+    $failing_job = $this->createJob();
+    $failing_job->settings = array('action' => 'fail');
+    $failing_job->requestTranslation();
+    // Still new.
+    $this->assertTrue($failing_job->isUnprocessed());
+
+    $messages = $failing_job->getMessages();
+    $last_message = end($messages);
+    $this->assertEqual('Service not reachable.', $last_message->message);
+    $this->assertEqual('error', $last_message->type);
+  }
+
+  /**
+   * Tests remote languages mappings support in the tmgmt core.
+   */
+  function testRemoteLanguagesMappings() {
+    $this->loginAsAdmin();
+    $this->setEnvironment('de');
+    $controller = $this->default_translator->getController();
+
+    $mappings = $controller->getRemoteLanguagesMappings($this->default_translator);
+    $this->assertEqual($mappings, array(
+      'en' => 'en-us',
+      'de' => 'de-ch',
+    ));
+
+    $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'en'), 'en-us');
+    $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'de'), 'de-ch');
+    $this->assertEqual($controller->mapToLocalLanguage($this->default_translator, 'en-us'), 'en');
+    $this->assertEqual($controller->mapToLocalLanguage($this->default_translator, 'de-ch'), 'de');
+
+    $this->default_translator->settings['remote_languages_mappings']['de'] = 'de-de';
+    $this->default_translator->settings['remote_languages_mappings']['en'] = 'en-uk';
+    $this->default_translator->save();
+
+    $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'en'), 'en-uk');
+    $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'de'), 'de-de');
+
+    // Test the fallback.
+    $info = &drupal_static('_tmgmt_plugin_info');
+    $info['translator']['test_translator']['map remote languages'] = FALSE;
+
+    $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'en'), 'en');
+    $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'de'), 'de');
+  }
+}
diff --git a/tests/tmgmt.upgrade.alpha1.test b/tests/tmgmt.upgrade.alpha1.test
new file mode 100644
index 0000000..ceb3507
--- /dev/null
+++ b/tests/tmgmt.upgrade.alpha1.test
@@ -0,0 +1,159 @@
+<?php
+
+/*
+ * @file
+ * Contains tests for Translation management
+ */
+
+/**
+ * Upgrade tests.
+ */
+class TMGMTUpgradeAlpha1TestCase extends DrupalWebTestCase {
+
+  protected $profile = 'testing';
+
+  static function getInfo() {
+    return array(
+      'name' => t('Upgrade tests Alpha1'),
+      'description' => t('Tests the upgrade path from 7.x-1.0-alpha1'),
+      'group' => t('Translation Management'),
+    );
+  }
+
+  function setUp() {
+    // Enable all dependencies.
+    parent::setUp(array('entity', 'views', 'translation', 'locale'));
+
+    // Create the tmgmt tables and fill them.
+    module_load_include('inc', 'tmgmt', 'tests/tmgmt_alpha1_dump.sql');
+
+    // @todo: Figure out why this is necessary.
+    $enabled_modules = db_query("SELECT name FROM {system} where status = 1 and type = 'module'")->fetchCol();
+    foreach ($enabled_modules as $enabled_module) {
+      module_load_install($enabled_module);
+      // Set the schema version to the number of the last update provided
+      // by the module.
+      $versions = drupal_get_schema_versions($enabled_module);
+      $version = $versions ? max($versions) : SCHEMA_INSTALLED;
+      db_update('system')
+        ->condition('name', $enabled_module)
+        ->fields(array('schema_version' => $version))
+        ->execute();
+    }
+
+    // Set schema version to 0 and then install the tmgmt modules, to simulate
+    // an enabling.
+    db_update('system')
+      ->condition('name', array('tmgmt', 'tmgmt_ui', 'tmgmt_field', 'tmgmt_node', 'tmgmt_test', 'tmgmt_node_ui'))
+      ->fields(array(
+        'schema_version' => 0,
+      ))
+      ->execute();
+    module_enable(array('tmgmt', 'tmgmt_ui', 'tmgmt_field', 'tmgmt_node', 'tmgmt_test', 'tmgmt_node_ui'));
+
+    // Log in as a user that can run update.php
+    $admin = $this->drupalCreateUser(array('administer software updates'));
+    $this->drupalLogin($admin);
+
+    $this->performUpgrade();
+  }
+
+  /**
+   * Verifies that the data has been migrated properly
+   */
+  function testUpgradePath() {
+    // Log in as a user with enough permissions.
+    $translator = $this->drupalCreateUser(array('administer tmgmt'));
+    $this->drupalLogin($translator);
+    // Go to a job and check the review form.
+    $this->drupalGet('admin/config/regional/tmgmt/jobs/1');
+    // Make sure the #status values have been set accordingly.
+    $this->assertRaw(t('Accepted: @accepted, reviewed: @reviewed, translated: @translated, pending: @pending.', array('@accepted' => 0, '@reviewed' => 0, '@translated' => 2, '@pending' => 0)));
+    // Extract the word count field and make sure it's correct.
+    $word_count = $this->xpath('//td[contains(@class, :class)]', array(':class' => 'views-field-word-count-1'));
+    $this->assertEqual(6, trim((string)reset($word_count)));
+
+    $this->clickLink(t('review'));
+    // Needs review icon.
+    $this->assertRaw('tmgmt-ui-icon-yellow tmgmt-ui-state-translated');
+    // Translated values.
+    $this->assertRaw('de_Test content');
+    $this->assertRaw('de_This is the body.');
+    // Reject button.
+    $this->assertRaw('✗');
+
+    // Check that accepted count has been updated correctly.
+    $this->drupalGet('admin/config/regional/tmgmt/jobs/2');
+    // Make sure the #status values have been set accordingly.
+    $this->assertRaw(t('Accepted: @accepted, reviewed: @reviewed, translated: @translated, pending: @pending.', array('@accepted' => 2, '@reviewed' => 0, '@translated' => 0, '@pending' => 0)));
+
+
+  }
+
+
+  /**
+   * Perform the upgrade.
+   *
+   * Copied and adapted from UpgradePathTestCase::performUpgrade().
+   *
+   * @param $register_errors
+   *   Register the errors during the upgrade process as failures.
+   * @return
+   *   TRUE if the upgrade succeeded, FALSE otherwise.
+   */
+  protected function performUpgrade($register_errors = TRUE) {
+    $update_url = $GLOBALS['base_url'] . '/update.php';
+
+    // Load the first update screen.
+    $this->drupalGet($update_url, array('external' => TRUE));
+    if (!$this->assertResponse(200)) {
+      return FALSE;
+    }
+
+    // Continue.
+    $this->drupalPost(NULL, array(), t('Continue'));
+    if (!$this->assertResponse(200)) {
+      return FALSE;
+    }
+
+    // The test should pass if there are no pending updates.
+    $content = $this->drupalGetContent();
+    if (strpos($content, t('No pending updates.')) !== FALSE) {
+      $this->pass(t('No pending updates and therefore no upgrade process to test.'));
+      $this->pendingUpdates = FALSE;
+      return TRUE;
+    }
+
+    // Go!
+    $this->drupalPost(NULL, array(), t('Apply pending updates'));
+    if (!$this->assertResponse(200)) {
+      return FALSE;
+    }
+
+    // Check for errors during the update process.
+    foreach ($this->xpath('//li[@class=:class]', array(':class' => 'failure')) as $element) {
+      $message = strip_tags($element->asXML());
+      $this->upgradeErrors[] = $message;
+      if ($register_errors) {
+        $this->fail($message);
+      }
+    }
+
+    if (!empty($this->upgradeErrors)) {
+      // Upgrade failed, the installation might be in an inconsistent state,
+      // don't process.
+      return FALSE;
+    }
+
+    // Check if there still are pending updates.
+    $this->drupalGet($update_url, array('external' => TRUE));
+    $this->drupalPost(NULL, array(), t('Continue'));
+    if (!$this->assertText(t('No pending updates.'), t('No pending updates at the end of the update process.'))) {
+      return FALSE;
+    }
+
+    // Clear caches.
+    $this->checkPermissions(array(), TRUE);
+  }
+
+}
diff --git a/tests/tmgmt_test.info b/tests/tmgmt_test.info
index b376d4c..b6ec301 100644
--- a/tests/tmgmt_test.info
+++ b/tests/tmgmt_test.info
@@ -3,4 +3,6 @@ package = Translation Management
 core = 7.x
 hidden = TRUE
 dependencies[] = tmgmt
-files[] = tmgmt_test.plugin.inc
+files[] = tmgmt_test.plugin.source.inc
+files[] = tmgmt_test.plugin.translator.inc
+files[] = tmgmt_test.ui.translator.inc
diff --git a/tests/tmgmt_test.plugin.source.inc b/tests/tmgmt_test.plugin.source.inc
new file mode 100644
index 0000000..7f64b5e
--- /dev/null
+++ b/tests/tmgmt_test.plugin.source.inc
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Contains the test source plugin.
+ */
+
+class TMGMTTestSourcePluginController extends TMGMTDefaultSourcePluginController {
+
+  /**
+   * Overrides TMGMTDefaultSourcePluginController::getLabel().
+   */
+  public function getLabel(TMGMTJobItem $job_item) {
+    return $this->pluginType . ':' . $job_item->item_type . ':' . $job_item->item_id;
+  }
+
+  /**
+   * Implements TMGMTSourcePluginControllerInterface::getData().
+   */
+  public function getData(TMGMTJobItem $job_item) {
+    return array(
+      'dummy' => array(
+        'deep_nesting' => array(
+          '#text' => 'Text for job item with type ' . $job_item->item_type . ' and id ' . $job_item->item_id . '.',
+          '#label' => 'Label for job item with type ' . $job_item->item_type . ' and id ' . $job_item->item_id . '.',
+        ),
+      ),
+    );
+  }
+
+  /**
+   * Implements TMGMTSourcePluginControllerInterface::saveTranslation().
+   */
+  public function saveTranslation(TMGMTJobItem $job_item) {
+    // Set a variable that can be checked later for a given job item.
+    variable_set('tmgmt_test_saved_translation_' . $job_item->item_type . '_' . $job_item->item_id, TRUE);
+    $job_item->accepted();
+  }
+}
diff --git a/tests/tmgmt_test.plugin.inc b/tests/tmgmt_test.plugin.translator.inc
similarity index 53%
rename from tests/tmgmt_test.plugin.inc
rename to tests/tmgmt_test.plugin.translator.inc
index 36bdaca..db88218 100644
--- a/tests/tmgmt_test.plugin.inc
+++ b/tests/tmgmt_test.plugin.translator.inc
@@ -2,58 +2,9 @@
 
 /**
  * @file
- * Provides the user translator plugin controller.
+ * Cotains the test translator plugin.
  */
 
-class TMGMTTestTranslatorUIController extends TMGMTDefaultTranslatorUIController {
-
-  /**
-   * Overrides TMGMTDefaultTranslatorUIController::pluginSettingsForm().
-   */
-  public function pluginSettingsForm($form, &$form_state, TMGMTTranslator $translator, $busy = FALSE) {
-    $form['expose_settings'] = array(
-      '#type' => 'checkbox',
-      '#title' => t('Display settings'),
-      '#default_value' => TRUE,
-    );
-
-    $form['action'] = array(
-      '#type' => 'select',
-      '#title' => t('Default action'),
-      '#options' => array(
-        'translate' => t('Translate'),
-        'submit' => t('Submit'),
-        'reject' => t('Reject'),
-        'fail' => t('Fail'),
-        'not_translatable' => t('Not translatable'),
-      ),
-    );
-    return parent::pluginSettingsForm($form, $form_state, $translator, $busy);
-  }
-
-  /**
-   * Overrides TMGMTDefaultTranslatorPluginController::checkoutSettingsForm().
-   */
-  public function checkoutSettingsForm($form, &$form_state, TMGMTJob $job) {
-    if ($job->getTranslator()->getSetting('expose_settings')) {
-      $form['action'] = array(
-        '#type' => 'select',
-        '#title' => t('Action'),
-        '#options' => array(
-          'translate' => t('Translate'),
-          'submit' => t('Submit'),
-          'reject' => t('Reject'),
-          'fail' => t('Fail'),
-          'not_translatable' => t('Not translatable'),
-        ),
-        '#default_value' => $job->getTranslator()->getSetting('action'),
-      );
-    }
-    return $form;
-  }
-
-}
-
 class TMGMTTestTranslatorPluginController extends TMGMTDefaultTranslatorPluginController implements TMGMTTranslatorRejectDataItem {
 
   /**
@@ -145,36 +96,3 @@ class TMGMTTestTranslatorPluginController extends TMGMTDefaultTranslatorPluginCo
     return $form;
   }
 }
-
-class TMGMTTestSourcePluginController extends TMGMTDefaultSourcePluginController {
-
-  /**
-   * Overrides TMGMTDefaultSourcePluginController::getLabel().
-   */
-  public function getLabel(TMGMTJobItem $job_item) {
-    return $this->pluginType . ':' . $job_item->item_type . ':' . $job_item->item_id;
-  }
-
-  /**
-   * Implements TMGMTSourcePluginControllerInterface::getData().
-   */
-  public function getData(TMGMTJobItem $job_item) {
-    return array(
-      'dummy' => array(
-        'deep_nesting' => array(
-          '#text' => 'Text for job item with type ' . $job_item->item_type . ' and id ' . $job_item->item_id . '.',
-          '#label' => 'Label for job item with type ' . $job_item->item_type . ' and id ' . $job_item->item_id . '.',
-        ),
-      ),
-    );
-  }
-
-  /**
-   * Implements TMGMTSourcePluginControllerInterface::saveTranslation().
-   */
-  public function saveTranslation(TMGMTJobItem $job_item) {
-    // Set a variable that can be checked later for a given job item.
-    variable_set('tmgmt_test_saved_translation_' . $job_item->item_type . '_' . $job_item->item_id, TRUE);
-    $job_item->accepted();
-  }
-}
diff --git a/tests/tmgmt_test.ui.translator.inc b/tests/tmgmt_test.ui.translator.inc
new file mode 100644
index 0000000..9e150ca
--- /dev/null
+++ b/tests/tmgmt_test.ui.translator.inc
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Contains the test translator UI plugin.
+ */
+
+class TMGMTTestTranslatorUIController extends TMGMTDefaultTranslatorUIController {
+
+  /**
+   * Overrides TMGMTDefaultTranslatorUIController::pluginSettingsForm().
+   */
+  public function pluginSettingsForm($form, &$form_state, TMGMTTranslator $translator, $busy = FALSE) {
+    $form['expose_settings'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Display settings'),
+      '#default_value' => TRUE,
+    );
+
+    $form['action'] = array(
+      '#type' => 'select',
+      '#title' => t('Default action'),
+      '#options' => array(
+        'translate' => t('Translate'),
+        'submit' => t('Submit'),
+        'reject' => t('Reject'),
+        'fail' => t('Fail'),
+        'not_translatable' => t('Not translatable'),
+      ),
+    );
+    return parent::pluginSettingsForm($form, $form_state, $translator, $busy);
+  }
+
+  /**
+   * Overrides TMGMTDefaultTranslatorPluginController::checkoutSettingsForm().
+   */
+  public function checkoutSettingsForm($form, &$form_state, TMGMTJob $job) {
+    if ($job->getTranslator()->getSetting('expose_settings')) {
+      $form['action'] = array(
+        '#type' => 'select',
+        '#title' => t('Action'),
+        '#options' => array(
+          'translate' => t('Translate'),
+          'submit' => t('Submit'),
+          'reject' => t('Reject'),
+          'fail' => t('Fail'),
+          'not_translatable' => t('Not translatable'),
+        ),
+        '#default_value' => $job->getTranslator()->getSetting('action'),
+      );
+    }
+    return $form;
+  }
+
+}
diff --git a/tmgmt.info b/tmgmt.info
index 8a45aaa..27769a4 100644
--- a/tmgmt.info
+++ b/tmgmt.info
@@ -8,12 +8,33 @@ dependencies[] = locale
 dependencies[] = views
 
 files[] = includes/tmgmt.exception.inc
-files[] = includes/tmgmt.controller.inc
-files[] = includes/tmgmt.entity.inc
-files[] = includes/tmgmt.plugin.inc
-files[] = includes/tmgmt.ui.inc
+files[] = controller/tmgmt.controller.job.inc
+files[] = controller/tmgmt.controller.job_item.inc
+files[] = controller/tmgmt.controller.remote.inc
+files[] = controller/tmgmt.controller.translator.inc
+files[] = entity/tmgmt.entity.job.inc
+files[] = entity/tmgmt.entity.job_item.inc
+files[] = entity/tmgmt.entity.message.inc
+files[] = entity/tmgmt.entity.remote.inc
+files[] = entity/tmgmt.entity.translate.inc
+files[] = plugin/tmgmt.plugin.base.inc
+files[] = plugin/tmgmt.plugin.interface.base.inc
+files[] = plugin/tmgmt.plugin.interface.reject.inc
+files[] = plugin/tmgmt.plugin.interface.source.inc
+files[] = plugin/tmgmt.plugin.interface.translator.inc
+files[] = plugin/tmgmt.plugin.source.inc
+files[] = plugin/tmgmt.plugin.translator.inc
+files[] = plugin/tmgmt.ui.interface.source.inc
+files[] = plugin/tmgmt.ui.interface.translator.inc
+files[] = plugin/tmgmt.ui.source.inc
+files[] = plugin/tmgmt.ui.translator.inc
 files[] = includes/tmgmt.info.inc
-files[] = tmgmt.test
+files[] = tests/tmgmt.base.test
+files[] = tests/tmgmt.base.entity.test
+files[] = tests/tmgmt.crud.test
+files[] = tests/tmgmt.plugin.test
+files[] = tests/tmgmt.helper.test
+files[] = tests/tmgmt.upgrade.alpha1.test
 
 ; Views integration and handlers
 files[] = views/tmgmt.views.inc
diff --git a/tmgmt.test b/tmgmt.test
deleted file mode 100644
index 36354ec..0000000
--- a/tmgmt.test
+++ /dev/null
@@ -1,1179 +0,0 @@
-<?php
-
-/*
- * @file
- * Contains tests for Translation management
- */
-
-/**
- * Base class for tests.
- */
-class TMGMTBaseTestCase extends DrupalWebTestCase {
-  protected $profile = 'testing';
-
-  /**
-   * A default translator using the test translator.
-   *
-   * @var TMGMTTranslator
-   */
-  protected $default_translator;
-
-  /**
-   * List of permissions used by loginAsAdmin().
-   *
-   * @var array
-   */
-  protected $admin_permissions = array();
-
-  /**
-   * Drupal user object created by loginAsAdmin().
-   *
-   * @var object
-   */
-  protected $admin_user = NULL;
-
-  /**
-   * List of permissions used by loginAsTranslator().
-   *
-   * @var array
-   */
-  protected $translator_permissions = array();
-
-  /**
-   * Drupal user object created by loginAsTranslator().
-   *
-   * @var object
-   */
-  protected $translator_user = NULL;
-
-  /**
-   * Overrides DrupalWebTestCase::setUp()
-   */
-  function setUp() {
-    $modules = func_get_args();
-    if (isset($modules[0]) && is_array($modules[0])) {
-      $modules = $modules[0];
-    }
-    $modules = array_merge(array('entity', 'tmgmt', 'tmgmt_test'), $modules);
-    parent::setUp($modules);
-    $this->default_translator = tmgmt_translator_load('test_translator');
-
-    // Load default admin permissions.
-    $this->admin_permissions = array(
-      'administer languages',
-      'access administration pages',
-      'administer content types',
-      'administer tmgmt',
-    );
-
-    // Load default translator user permissions.
-    $this->translator_permissions = array(
-      'create translation jobs',
-      'submit translation jobs',
-      'accept translation jobs',
-    );
-  }
-
-  /**
-   * Will create a user with admin permissions and log it in.
-   *
-   * @param array $additional_permissions
-   *   Additional permissions that will be granted to admin user.
-   * @param boolean $reset_permissions
-   *   Flag to determine if default admin permissions will be replaced by
-   *   $additional_permissions.
-   *
-   * @return object
-   *   Newly created and logged in user object.
-   */
-  function loginAsAdmin($additional_permissions = array(), $reset_permissions = FALSE) {
-    $permissions = $this->admin_permissions;
-
-    if ($reset_permissions) {
-      $permissions = $additional_permissions;
-    }
-    elseif (!empty($additional_permissions)) {
-      $permissions = array_merge($permissions, $additional_permissions);
-    }
-
-    $this->admin_user = $this->drupalCreateUser($permissions);
-    $this->drupalLogin($this->admin_user);
-    return $this->admin_user;
-  }
-
-  /**
-   * Will create a user with translator permissions and log it in.
-   *
-   * @param array $additional_permissions
-   *   Additional permissions that will be granted to admin user.
-   * @param boolean $reset_permissions
-   *   Flag to determine if default admin permissions will be replaced by
-   *   $additional_permissions.
-   *
-   * @return object
-   *   Newly created and logged in user object.
-   */
-  function loginAsTranslator($additional_permissions = array(), $reset_permissions = FALSE) {
-    $permissions = $this->translator_permissions;
-
-    if ($reset_permissions) {
-      $permissions = $additional_permissions;
-    }
-    elseif (!empty($additional_permissions)) {
-      $permissions = array_merge($permissions, $additional_permissions);
-    }
-
-    $this->translator_user = $this->drupalCreateUser($permissions);
-    $this->drupalLogin($this->translator_user);
-    return $this->translator_user;
-  }
-
-  /**
-   * Creates, saves and returns a translator.
-   *
-   * @return TMGMTTranslator
-   */
-  function createTranslator() {
-    $translator = new TMGMTTranslator();
-    $translator->name = strtolower($this->randomName());
-    $translator->label = $this->randomName();
-    $translator->plugin = 'test_translator';
-    $translator->settings = array(
-      'key' => $this->randomName(),
-      'another_key' => $this->randomName(),
-    );
-    $this->assertEqual(SAVED_NEW, $translator->save());
-
-    // Assert that the translator was assigned a tid.
-    $this->assertTrue($translator->tid > 0);
-    return $translator;
-  }
-
-  /**
-   * Creates, saves and returns a translation job.
-   *
-   * @return TMGMTJob
-   */
-  function createJob($source = 'en', $target = 'de', $uid = 1)  {
-    $job = tmgmt_job_create($source, $target, $uid);
-    $this->assertEqual(SAVED_NEW, $job->save());
-
-    // Assert that the translator was assigned a tid.
-    $this->assertTrue($job->tjid > 0);
-    return $job;
-  }
-
-
-  /**
-   * Sets the proper environment.
-   *
-   * Currently just adds a new language.
-   *
-   * @param string $langcode
-   *   The language code.
-   */
-  function setEnvironment($langcode) {
-    // Add the language.
-    locale_add_language($langcode);
-  }
-
-}
-
-/**
- * Utility test case class with helper methods to create entities and their
- * fields with populated translatable content. Extend this class if you create
- * tests in which you need Drupal entities and/or fields.
- */
-abstract class TMGMTEntityTestCaseUtility extends TMGMTBaseTestCase {
-
-  public $field_names = array();
-
-  /**
-   * Creates node type with several text fields with different cardinality.
-   *
-   * Internally it calls TMGMTEntityTestCaseUtility::attachFields() to create
-   * and attach fields to newly created bundle. You can than use
-   * $this->field_names['node']['YOUR_BUNDLE_NAME'] to access them.
-   *
-   * @param string $machine_name
-   *   Machine name of the node type.
-   * @param string $human_name
-   *   Human readable name of the node type.
-   * @param int $language_content_type
-   *   Flag of how the translation should be handled.
-   */
-  function createNodeType($machine_name, $human_name, $language_content_type = 0) {
-
-    // Create new bundle.
-    $type = array(
-      'type' => $machine_name,
-      'name' => $human_name,
-      'base' => 'node_content',
-      'description' => '',
-      'custom' => 1,
-      'modified' => 1,
-      'locked' => 0,
-    );
-    $type = node_type_set_defaults($type);
-    node_type_save($type);
-    node_add_body_field($type);
-    node_types_rebuild();
-
-    // Set content type to be translatable as specified by
-    // $language_content_type.
-    $edit = array();
-    $edit['language_content_type'] = $language_content_type;
-    $this->drupalPost('admin/structure/types/manage/' . $machine_name, $edit, t('Save content type'));
-
-    $translatable = FALSE;
-    if (defined('ENTITY_TRANSLATION_ENABLED') && $language_content_type == ENTITY_TRANSLATION_ENABLED) {
-      $translatable = TRUE;
-    }
-
-    // Push in also the body field.
-    $this->field_names['node'][$machine_name][] = 'body';
-
-    $this->attachFields('node', $machine_name, $translatable);
-
-    // Change body field to be translatable.
-    $body = field_info_field('body');
-    $body['translatable'] = $translatable;
-    field_update_field($body);
-  }
-
-  /**
-   * Creates taxonomy vocabulary with custom fields.
-   *
-   * To create and attach fields it internally calls
-   * TMGMTEntityTestCaseUtility::attachFields(). You can than access these
-   * fields calling $this->field_names['node']['YOUR_BUNDLE_NAME'].
-   *
-   * @param string $machine_name
-   *   Vocabulary machine name.
-   * @param string $human_name
-   *   Vocabulary human readable name.
-   * @param bool|array $fields_translatable
-   *   Flag or definition array to determine which or all fields should be
-   *   translatable.
-   *
-   * @return stdClass
-   *   Created vocabulary object.
-   */
-  function createTaxonomyVocab($machine_name, $human_name, $fields_translatable = TRUE) {
-    $vocabulary = new stdClass();
-    $vocabulary->name = $human_name;
-    $vocabulary->machine_name = $machine_name;
-    taxonomy_vocabulary_save($vocabulary);
-
-    $this->attachFields('taxonomy_term', $vocabulary->machine_name, $fields_translatable);
-
-    return $vocabulary;
-  }
-
-  /**
-   * Creates fields of type text and text_with_summary of different cardinality.
-   *
-   * It will attach created fields to provided entity name and bundle.
-   *
-   * Field names will be stored in $this->field_names['entity']['bundle']
-   * through which you can access them.
-   *
-   * @param string $entity_name
-   *   Entity name to which fields should be attached.
-   * @param string $bundle
-   *   Bundle name to which fields should be attached.
-   * @param bool|array $translatable
-   *   Flag or definition array to determine which or all fields should be
-   *   translatable.
-   */
-  function attachFields($entity_name, $bundle, $translatable = TRUE) {
-    // Create several text fields.
-    $field_types = array('text', 'text_with_summary');
-
-    for ($i = 0 ; $i <= 5; $i++) {
-      $field_type = $field_types[array_rand($field_types, 1)];
-      $field_name = drupal_strtolower($this->randomName());
-
-      // Create a field.
-      $field = array(
-        'field_name' => $field_name,
-        'type' => $field_type,
-        'cardinality' => mt_rand(1, 5),
-        'translatable' => is_array($translatable) && isset($translatable[$i]) ? $translatable[$i] : (boolean) $translatable,
-      );
-      field_create_field($field);
-
-      // Create an instance of the previously created field.
-      $instance = array(
-        'field_name' => $field_name,
-        'entity_type' => $entity_name,
-        'bundle' => $bundle,
-        'label' => $this->randomName(10),
-        'description' => $this->randomString(30),
-        'widget' => array(
-          'type' => $field_type == 'text' ? 'text_textfield' : 'text_textarea_with_summary',
-          'label' => $this->randomString(10),
-        ),
-      );
-      field_create_instance($instance);
-
-      // Store field names in case there are needed outside this method.
-      $this->field_names[$entity_name][$bundle][] = $field_name;
-    }
-  }
-
-  /**
-   * Creates a node of a given bundle.
-   *
-   * It uses $this->field_names to populate content of attached fields.
-   *
-   * @param string $bundle
-   *   Node type name.
-   * @param string $sourcelang
-   *   Source lang of the node to be created.
-   *
-   * @return object
-   *   Newly created node object.
-   */
-  function createNode($bundle, $sourcelang = 'en') {
-    $node = array(
-      'type' => $bundle,
-      'language' => $sourcelang,
-    );
-
-    foreach ($this->field_names['node'][$bundle] as $field_name) {
-      $field_info = field_info_field($field_name);
-      $cardinality = $field_info['cardinality'] == FIELD_CARDINALITY_UNLIMITED ? 1 : $field_info['cardinality'];
-
-      // Create two deltas for each field.
-      for ($delta = 0; $delta <= $cardinality; $delta++) {
-        $node[$field_name][$sourcelang][$delta]['value'] = $this->randomName(20);
-        if ($field_info['type'] == 'text_with_summary') {
-          $node[$field_name][$sourcelang][$delta]['summary'] = $this->randomName(10);
-        }
-      }
-    }
-
-    return $this->drupalCreateNode($node);
-  }
-
-  /**
-   * Creates a taxonomy term of a given vocabulary.
-   *
-   * It uses $this->field_names to populate content of attached fields. You can
-   * access fields values using
-   * $this->field_names['taxonomy_term'][$vocabulary->machine_name].
-   *
-   * @param object $vocabulary
-   *   Vocabulary object for which the term should be created.
-   *
-   * @return object
-   *   Newly created node object.
-   */
-  function createTaxonomyTerm($vocabulary) {
-    $term = new stdClass();
-    $term->name = $this->randomName();
-    $term->description = $this->randomName();
-    $term->vid = $vocabulary->vid;
-
-    foreach ($this->field_names['taxonomy_term'][$vocabulary->machine_name] as $field_name) {
-      $field_info = field_info_field($field_name);
-      $cardinality = $field_info['cardinality'] == FIELD_CARDINALITY_UNLIMITED ? 1 : $field_info['cardinality'];
-      $field_lang = $field_info['translatable'] ? 'en' : LANGUAGE_NONE;
-
-      // Create two deltas for each field.
-      for ($delta = 0; $delta <= $cardinality; $delta++) {
-        $term->{$field_name}[$field_lang][$delta]['value'] = $this->randomName(20);
-        if ($field_info['type'] == 'text_with_summary') {
-          $term->{$field_name}[$field_lang][$delta]['summary'] = $this->randomName(10);
-        }
-      }
-    }
-
-    taxonomy_term_save($term);
-    return taxonomy_term_load($term->tid);
-  }
-}
-
-/**
- * Basic CRUD tests.
- */
-class TMGMTCRUDTestCase extends TMGMTBaseTestCase {
-
-  /**
-   * Implements getInfo().
-   */
-  static function getInfo() {
-    return array(
-      'name' => t('CRUD tests'),
-      'description' => t('Basic crud operations for jobs and translators'),
-      'group' => t('Translation Management'),
-    );
-  }
-
-  /**
-   * Test crud operations of translators.
-   */
-  function testTranslators() {
-    $translator = $this->createTranslator();
-
-    $loaded_translator = tmgmt_translator_load($translator->tid);
-
-    $this->assertEqual($translator->name, $loaded_translator->name);
-    $this->assertEqual($translator->label, $loaded_translator->label);
-    $this->assertEqual($translator->settings, $loaded_translator->settings);
-
-    // Update the settings.
-    $translator->settings['new_key'] = $this->randomString();
-    $this->assertEqual(SAVED_UPDATED, $translator->save());
-
-    $loaded_translator = tmgmt_translator_load($translator->tid);
-
-    $this->assertEqual($translator->name, $loaded_translator->name);
-    $this->assertEqual($translator->label, $loaded_translator->label);
-    $this->assertEqual($translator->settings, $loaded_translator->settings);
-
-    // Delete the translator, make sure the translator is gone.
-    $translator->delete();
-    $this->assertFalse(tmgmt_translator_load($translator->tid));
-  }
-
-  /**
-   * Test crud operations of jobs.
-   */
-  function testJobs() {
-    $job = $this->createJob();
-
-    $loaded_job = tmgmt_job_load($job->tjid);
-
-    $this->assertEqual($job->source_language, $loaded_job->source_language);
-    $this->assertEqual($job->target_language, $loaded_job->target_language);
-
-    // Assert that the created and changed information has been set to the
-    // default value.
-    $this->assertTrue($loaded_job->created > 0);
-    $this->assertTrue($loaded_job->changed > 0);
-    $this->assertEqual(0, $loaded_job->state);
-
-    // Update the settings.
-    $job->reference = 7;
-    $this->assertEqual(SAVED_UPDATED, $job->save());
-
-    $loaded_job = tmgmt_job_load($job->tjid);
-
-    $this->assertEqual($job->reference, $loaded_job->reference);
-
-    // Test the job items.
-    $item1 = $job->addItem('test_source', 'type', 5);
-    $item2 = $job->addItem('test_source', 'type', 4);
-
-    // Load and compare the items.
-    $items = $job->getItems();
-    $this->assertEqual(2, count($items));
-
-    $this->assertEqual($item1->plugin, $items[$item1->tjiid]->plugin);
-    $this->assertEqual($item1->item_type, $items[$item1->tjiid]->item_type);
-    $this->assertEqual($item1->item_id, $items[$item1->tjiid]->item_id);
-    $this->assertEqual($item2->plugin, $items[$item2->tjiid]->plugin);
-    $this->assertEqual($item2->item_type, $items[$item2->tjiid]->item_type);
-    $this->assertEqual($item2->item_id, $items[$item2->tjiid]->item_id);
-
-    // Delete the translator, make sure the translator is gone.
-    $job->delete();
-    $this->assertFalse(tmgmt_job_load($job->tjid));
-  }
-
-  function testRemoteMappings() {
-
-    $data_key = '5][test_source][type';
-
-    $translator = $this->createTranslator();
-    $job = $this->createJob();
-    $job->translator = $translator->name;
-    $job->save();
-    $item1 = $job->addItem('test_source', 'type', 5);
-    $item2 = $job->addItem('test_source', 'type', 4);
-
-    $result = $item1->addRemoteMapping($data_key, 'id11', array('remote_identifier_2' => 'id12', 'remote_identifier_3' => 'id13'));
-    $this->assertEqual($result, SAVED_NEW);
-
-    $job_mappings = $job->getRemoteMappings();
-    $item_mappings = $item1->getRemoteMappings();
-
-    $job_mapping = array_shift($job_mappings);
-    $item_mapping = array_shift($item_mappings);
-
-    $_job = $job_mapping->getJob();
-    $this->assertEqual($job->tjid, $_job->tjid);
-
-    $_job = $item_mapping->getJob();
-    $this->assertEqual($job->tjid, $_job->tjid);
-
-    $_item1 = $item_mapping->getJobItem();
-    $this->assertEqual($item1->tjiid, $_item1->tjiid);
-
-    /**
-     * @var TMGMTRemoteController $remote_mapping_controller
-     */
-    $remote_mapping_controller = entity_get_controller('tmgmt_remote');
-    $remote_mappings = $remote_mapping_controller->loadByRemoteIdentifier('id11', 'id12', 'id13');
-    $remote_mapping = array_shift($remote_mappings);
-    $this->assertEqual($item1->tjiid, $remote_mapping->tjiid);
-
-    $this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier('id11')), 1);
-    $this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier('id11', '')), 0);
-    $this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier('id11', NULL, '')), 0);
-    $this->assertEqual(count($remote_mapping_controller->loadByRemoteIdentifier(NULL, NULL, 'id13')), 1);
-
-    // Test remote data.
-    $item_mapping->addRemoteData('test_data', 'test_value');
-    entity_save('tmgmt_remote', $item_mapping);
-    $item_mapping = entity_load_single('tmgmt_remote', $item_mapping->trid);
-    $this->assertEqual($item_mapping->getRemoteData('test_data'), 'test_value');
-
-    // Add mapping to the other job item as well.
-    $item2->addRemoteMapping($data_key, 'id21', array('remote_identifier_2' => 'id22', 'remote_identifier_3' => 'id23'));
-
-    // Test deleting.
-
-    // Delete item1.
-    entity_get_controller('tmgmt_job_item')->delete(array($item1->tjiid));
-    // Test if mapping for item1 has been removed as well.
-
-    $this->assertEqual(count($remote_mapping_controller->loadByLocalData(NULL, $item1->tjiid)), 0);
-
-    // We still should have mapping for item2.
-    $this->assertEqual(count($remote_mapping_controller->loadByLocalData(NULL, $item2->tjiid)), 1);
-
-    // Now delete the job and see if remaining mappings were removed as well.
-    entity_get_controller('tmgmt_job')->delete(array($job->tjid));
-    $this->assertEqual(count($remote_mapping_controller->loadByLocalData(NULL, $item2->tjiid)), 0);
-  }
-
-  /**
-   * Test crud operations of job items.
-   */
-  function testJobItems() {
-    $job = $this->createJob();
-
-    // Add some test items.
-    $item1 = $job->addItem('test_source', 'type', 5);
-    $item2 = $job->addItem('test_source', 'type', 4);
-
-    // Test single load callback.
-    $item = tmgmt_job_item_load($item1->tjiid);
-    $this->assertEqual($item1->plugin, $item->plugin);
-    $this->assertEqual($item1->item_type, $item->item_type);
-    $this->assertEqual($item1->item_id, $item->item_id);
-
-    // Test multiple load callback.
-    $items = tmgmt_job_item_load_multiple(array($item1->tjiid, $item2->tjiid));
-
-    $this->assertEqual(2, count($items));
-
-    $this->assertEqual($item1->plugin, $items[$item1->tjiid]->plugin);
-    $this->assertEqual($item1->item_type, $items[$item1->tjiid]->item_type);
-    $this->assertEqual($item1->item_id, $items[$item1->tjiid]->item_id);
-    $this->assertEqual($item2->plugin, $items[$item2->tjiid]->plugin);
-    $this->assertEqual($item2->item_type, $items[$item2->tjiid]->item_type);
-    $this->assertEqual($item2->item_id, $items[$item2->tjiid]->item_id);
-  }
-
-  /**
-   * Test the calculations of the counters.
-   */
-  function testJobItemsCounters() {
-    $job = $this->createJob();
-
-    // Some test data items.
-    $data1 = array(
-      '#text' => 'The text to be translated.',
-    );
-    $data2 = array(
-      '#text' => 'The text to be translated.',
-      '#translation' => '',
-    );
-    $data3 = array(
-      '#text' => 'The text to be translated.',
-      '#translation' => 'The translated data. Set by the translator plugin.',
-    );
-    $data4 = array(
-      '#text' => 'Another, longer text to be translated.',
-      '#translation' => 'The translated data. Set by the translator plugin.',
-      '#status' => TMGMT_DATA_ITEM_STATE_REVIEWED,
-    );
-    $data5 = array(
-      '#label' => 'label',
-      'data1' => $data1,
-      'data4' => $data4,
-    );
-
-    // No data items.
-    $this->assertEqual(0, $job->getCountPending());
-    $this->assertEqual(0, $job->getCountTranslated());
-    $this->assertEqual(0, $job->getCountReviewed());
-    $this->assertEqual(0, $job->getCountAccepted());
-    $this->assertEqual(0, $job->getWordCount());
-
-    // Add a test items.
-    $job_item1 = tmgmt_job_item_create('plugin', 'type', 4, array('tjid' => $job->tjid));
-    $job_item1->save();
-
-    // No pending, translated and confirmed data items.
-    $job = entity_load_single('tmgmt_job', $job->tjid);
-    $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
-    drupal_static_reset('tmgmt_job_statistics_load');
-    $this->assertEqual(0, $job_item1->getCountPending());
-    $this->assertEqual(0, $job_item1->getCountTranslated());
-    $this->assertEqual(0, $job_item1->getCountReviewed());
-    $this->assertEqual(0, $job_item1->getCountAccepted());
-    $this->assertEqual(0, $job->getCountPending());
-    $this->assertEqual(0, $job->getCountTranslated());
-    $this->assertEqual(0, $job->getCountReviewed());
-    $this->assertEqual(0, $job->getCountAccepted());
-
-    // Add an untranslated data item.
-    $job_item1->data['data_item1'] = $data1;
-    $job_item1->save();
-
-    // One pending data items.
-    $job = entity_load_single('tmgmt_job', $job->tjid);
-    $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
-    drupal_static_reset('tmgmt_job_statistics_load');
-    $this->assertEqual(1, $job_item1->getCountPending());
-    $this->assertEqual(0, $job_item1->getCountTranslated());
-    $this->assertEqual(0, $job_item1->getCountReviewed());
-    $this->assertEqual(5, $job_item1->getWordCount());
-    $this->assertEqual(1, $job->getCountPending());
-    $this->assertEqual(0, $job->getCountReviewed());
-    $this->assertEqual(0, $job->getCountTranslated());
-    $this->assertEqual(5, $job->getWordCount());
-
-
-    // Add another untranslated data item.
-    // Test with an empty translation set.
-    $job_item1->data['data_item1'] = $data2;
-    $job_item1->save();
-
-    // One pending data items.
-    $job = entity_load_single('tmgmt_job', $job->tjid);
-    $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
-    drupal_static_reset('tmgmt_job_statistics_load');
-    $this->assertEqual(1, $job_item1->getCountPending());
-    $this->assertEqual(0, $job_item1->getCountTranslated());
-    $this->assertEqual(0, $job_item1->getCountReviewed());
-    $this->assertEqual(5, $job_item1->getWordCount());
-    $this->assertEqual(1, $job->getCountPending());
-    $this->assertEqual(0, $job->getCountTranslated());
-    $this->assertEqual(0, $job->getCountReviewed());
-    $this->assertEqual(5, $job->getWordCount());
-
-    // Add a translated data item.
-    $job_item1->data['data_item1'] = $data3;
-    $job_item1->save();
-
-    // One translated data items.
-    drupal_static_reset('tmgmt_job_statistics_load');
-    $this->assertEqual(0, $job_item1->getCountPending());
-    $this->assertEqual(1, $job_item1->getCountTranslated());
-    $this->assertEqual(0, $job_item1->getCountReviewed());
-    $this->assertEqual(0, $job->getCountPending());
-    $this->assertEqual(0, $job->getCountReviewed());
-    $this->assertEqual(1, $job->getCountTranslated());
-
-    // Add a confirmed data item.
-    $job_item1->data['data_item1'] = $data4;
-    $job_item1->save();
-
-    // One reviewed data item.
-    drupal_static_reset('tmgmt_job_statistics_load');
-    $this->assertEqual(1, $job_item1->getCountReviewed());
-    $this->assertEqual(1, $job->getCountReviewed());
-
-    // Add a translated and an untranslated and a confirmed data item
-    $job = entity_load_single('tmgmt_job', $job->tjid);
-    $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
-    $job_item1->data['data_item1'] = $data1;
-    $job_item1->data['data_item2'] = $data3;
-    $job_item1->data['data_item3'] = $data4;
-    $job_item1->save();
-
-    // One pending and translated data items each.
-    drupal_static_reset('tmgmt_job_statistics_load');
-    $this->assertEqual(1, $job->getCountPending());
-    $this->assertEqual(1, $job->getCountTranslated());
-    $this->assertEqual(1, $job->getCountReviewed());
-    $this->assertEqual(16, $job->getWordCount());
-
-    // Add nested data items.
-    $job_item1->data['data_item1'] = $data5;
-    $job_item1->save();
-
-    // One pending data items.
-    $job = entity_load_single('tmgmt_job', $job->tjid);
-    $job_item1 = entity_load_single('tmgmt_job_item', $job_item1->tjiid);
-    $this->assertEqual('label', $job_item1->data['data_item1']['#label']);
-    $this->assertEqual(3, count($job_item1->data['data_item1']));
-
-    // Add a greater number of data items
-    for ($index = 1; $index <= 3; $index++) {
-      $job_item1->data['data_item' . $index] = $data1;
-    }
-    for ($index = 4; $index <= 10; $index++) {
-      $job_item1->data['data_item' . $index] = $data3;
-    }
-    for ($index = 11; $index <= 15; $index++) {
-      $job_item1->data['data_item' . $index] = $data4;
-    }
-    $job_item1->save();
-
-    // 3 pending and 7 translated data items each.
-    $job = entity_load_single('tmgmt_job', $job->tjid);
-    drupal_static_reset('tmgmt_job_statistics_load');
-    $this->assertEqual(3, $job->getCountPending());
-    $this->assertEqual(7, $job->getCountTranslated());
-    $this->assertEqual(5, $job->getCountReviewed());
-
-    // Add several job items
-    $job_item2 = tmgmt_job_item_create('plugin', 'type', 5, array('tjid' => $job->tjid));
-    for ($index = 1; $index <= 4; $index++) {
-      $job_item2->data['data_item' . $index] = $data1;
-    }
-    for ($index = 5; $index <= 12; $index++) {
-      $job_item2->data['data_item' . $index] = $data3;
-    }
-    for ($index = 13; $index <= 16; $index++) {
-      $job_item2->data['data_item' . $index] = $data4;
-    }
-    $job_item2->save();
-
-    // 3 pending and 7 translated data items each.
-    $job = entity_load_single('tmgmt_job', $job->tjid);
-    drupal_static_reset('tmgmt_job_statistics_load');
-    $this->assertEqual(7, $job->getCountPending());
-    $this->assertEqual(15, $job->getCountTranslated());
-    $this->assertEqual(9, $job->getCountReviewed());
-
-    // Accept the job items.
-    foreach ($job->getItems() as $item) {
-      // Set the state directly to avoid triggering translator and source
-      // controllers that do not exist.
-      $item->setState(TMGMT_JOB_ITEM_STATE_ACCEPTED);
-      $item->save();
-    }
-    drupal_static_reset('tmgmt_job_statistics_load');
-    $this->assertEqual(0, $job->getCountPending());
-    $this->assertEqual(0, $job->getCountTranslated());
-    $this->assertEqual(0, $job->getCountReviewed());
-    $this->assertEqual(31, $job->getCountAccepted());
-  }
-
-}
-
-/**
- * Tests interaction between core and the plugins.
- */
-class TMGMTPluginsTestCase extends TMGMTBaseTestCase {
-
-  /**
-   * Implements getInfo().
-   */
-  static function getInfo() {
-    return array(
-      'name' => t('Plugin tests'),
-      'description' => t('Verifies basic functionality of source and translator plugins'),
-      'group' => t('Translation Management'),
-    );
-  }
-
-  function createJob($source = 'en', $target = 'de', $uid = 1) {
-    $job = parent::createJob();
-
-    for ($i = 1; $i < 3; $i++) {
-      if ($i == 3) {
-        // Explicitly define the data for the third item.
-        $data['data'] = array(
-          'dummy' => array(
-            'deep_nesting' => array(
-              '#text' => 'Stored data',
-            ),
-          ),
-        );
-        $job->addItem('test_source', 'test', $i, array($data));
-      }
-      $job->addItem('test_source', 'test', $i);
-    }
-
-    // Manually specify the translator for now.
-    $job->translator = $this->default_translator->name;
-
-    return $job;
-  }
-
-  function testBasicWorkflow() {
-    // Submit a translation job.
-    $submit_job = $this->createJob();
-    $submit_job->settings = array('action' => 'submit');
-    $submit_job->requestTranslation();
-    $submit_job = tmgmt_job_load($submit_job->tjid);
-    $this->assertTrue($submit_job->isActive());
-    $messages = $submit_job->getMessages();
-    $last_message = end($messages);
-    $this->assertEqual('Test submit.', $last_message->message);
-
-    // Translate a job.
-    $translate_job = $this->createJob();
-    $translate_job->settings = array('action' => 'translate');
-    $translate_job->requestTranslation();
-    $translate_job = tmgmt_job_load($translate_job->tjid);
-    foreach ($translate_job->getItems() as $job_item) {
-      $this->assertTrue($job_item->isNeedsReview());
-    }
-
-    $messages = $translate_job->getMessages();
-    // array_values() results in numeric keys, which is necessary for list.
-    list($debug, $translated, $needs_review) = array_values($messages);
-    $this->assertEqual('Test translator called.', $debug->message);
-    $this->assertEqual('debug', $debug->type);
-    $this->assertEqual('Test translation created.', $translated->message);
-    $this->assertEqual('status', $translated->type);
-
-    // The third message is specific to a job item and has different state
-    // constants.
-    $this->assertEqual('The translation of !source to @language is finished and can now be <a href="!review_url">reviewed</a>.', $needs_review->message);
-    $this->assertEqual('status', $needs_review->type);
-
-    $i = 1;
-    foreach ($translate_job->getItems() as $item) {
-      // Check the translated text.
-      if ($i != 3) {
-        $expected_text = 'de_Text for job item with type ' . $item->item_type . ' and id ' . $item->item_id . '.';
-      }
-      else {
-        // The third item has an explicitly stored data value.
-        $expected_text = 'de_Stored data';
-      }
-      $item_data = $item->getData();
-      $this->assertEqual($expected_text, $item_data['dummy']['deep_nesting']['#translation']['#text']);
-      $i++;
-    }
-
-    foreach ($translate_job->getItems() as $job_item) {
-      $job_item->acceptTranslation();
-    }
-
-    // @todo Accepting does not result in messages on the job anymore.
-    // Update once there are job item messages.
-    /*
-    $messages = $translate_job->getMessages();
-    $last_message = end($messages);
-    $this->assertEqual('Job accepted', $last_message->message);
-    $this->assertEqual('status', $last_message->type);*/
-
-    // Check if the translations have been "saved".
-    foreach ($translate_job->getItems() as $item) {
-      $this->assertTrue(variable_get('tmgmt_test_saved_translation_' . $item->item_type . '_' . $item->item_id, FALSE));
-    }
-
-    // A rejected job.
-    $reject_job = $this->createJob();
-    $reject_job->settings = array('action' => 'reject');
-    $reject_job->requestTranslation();
-    // Still rejected.
-    $this->assertTrue($reject_job->isRejected());
-
-    $messages = $reject_job->getMessages();
-    $last_message = end($messages);
-    $this->assertEqual('This is not supported.', $last_message->message);
-    $this->assertEqual('error', $last_message->type);
-
-    // A failing job.
-    $failing_job = $this->createJob();
-    $failing_job->settings = array('action' => 'fail');
-    $failing_job->requestTranslation();
-    // Still new.
-    $this->assertTrue($failing_job->isUnprocessed());
-
-    $messages = $failing_job->getMessages();
-    $last_message = end($messages);
-    $this->assertEqual('Service not reachable.', $last_message->message);
-    $this->assertEqual('error', $last_message->type);
-  }
-
-  /**
-   * Tests remote languages mappings support in the tmgmt core.
-   */
-  function testRemoteLanguagesMappings() {
-    $this->loginAsAdmin();
-    $this->setEnvironment('de');
-    $controller = $this->default_translator->getController();
-
-    $mappings = $controller->getRemoteLanguagesMappings($this->default_translator);
-    $this->assertEqual($mappings, array(
-      'en' => 'en-us',
-      'de' => 'de-ch',
-    ));
-
-    $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'en'), 'en-us');
-    $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'de'), 'de-ch');
-    $this->assertEqual($controller->mapToLocalLanguage($this->default_translator, 'en-us'), 'en');
-    $this->assertEqual($controller->mapToLocalLanguage($this->default_translator, 'de-ch'), 'de');
-
-    $this->default_translator->settings['remote_languages_mappings']['de'] = 'de-de';
-    $this->default_translator->settings['remote_languages_mappings']['en'] = 'en-uk';
-    $this->default_translator->save();
-
-    $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'en'), 'en-uk');
-    $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'de'), 'de-de');
-
-    // Test the fallback.
-    $info = &drupal_static('_tmgmt_plugin_info');
-    $info['translator']['test_translator']['map remote languages'] = FALSE;
-
-    $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'en'), 'en');
-    $this->assertEqual($controller->mapToRemoteLanguage($this->default_translator, 'de'), 'de');
-  }
-}
-
-
-/**
- * Test the helper functions in tmgmt.module.
- */
-class TMGMTHelperTestCase extends TMGMTBaseTestCase {
-
-  /**
-   * Implements getInfo().
-   */
-  static function getInfo() {
-    return array(
-      'name' => t('Helper functions Test case'),
-      'description' => t('Helper functions for other modules'),
-      'group' => t('Translation Management'),
-    );
-  }
-
-  /**
-   * Tests tmgmt_job_match_item()
-   *
-   * @see tmgmt_job_match_item
-   */
-  function testTMGTJobMatchItem() {
-    $this->loginAsAdmin();
-    $this->setEnvironment('fr');
-    $this->setEnvironment('es');
-
-    // Add a job from en to fr and en to sp.
-    $job_en_fr = $this->createJob('en', 'fr');
-    $job_en_sp = $this->createJob('en', 'es');
-
-    // Add a job which has existing source-target combinations.
-    $this->assertEqual($job_en_fr->tjid, tmgmt_job_match_item('en', 'fr')->tjid);
-    $this->assertEqual($job_en_sp->tjid, tmgmt_job_match_item('en', 'es')->tjid);
-
-    // Add a job which has no existing source-target combination.
-    $this->assertTrue(tmgmt_job_match_item('fr', 'es'));
-  }
-
-  /**
-   * Tests the tmgmt_data_item_label() function.
-   *
-   * @todo: Move into a unit test case once available.
-   */
-  function testDataIemLabel() {
-    $no_label = array(
-      '#text' => 'No label',
-    );
-    $this->assertEqual(tmgmt_data_item_label($no_label), 'No label');
-    $label = array(
-      '#parent_label' => array(),
-      '#label' => 'A label',
-    );
-    $this->assertEqual(tmgmt_data_item_label($label), 'A label');
-    $parent_label = array(
-      '#parent_label' => array('Parent label', 'Sub label'),
-      '#label' => 'A label',
-    );
-    $this->assertEqual(tmgmt_data_item_label($parent_label), 'Parent label > Sub label');
-  }
-
-  function testWordCount() {
-    $unit_tests = array(
-      'empty' => array(
-        'text' => '',
-        'count' => 0,
-      ),
-      'latin' => array(
-        'text' => 'Drupal is the best!',
-        'count' => 4,
-      ),
-      'non-latin' => array(
-        'text' => 'Друпал лучший!',
-        'count' => 2,
-      ),
-      'complex punctuation' => array(
-        'text' => '<[({-!ReAd@*;: ,?+MoRe...})]>\\|/',
-        'count' => 2,
-      ),
-      'repeat' => array(
-        'text' => 'repeat repeat',
-        'count' => 2,
-      ),
-    );
-    foreach ($unit_tests as $id => $test_data) {
-      $this->assertEqual($real_count = tmgmt_word_count($test_data['text']), $desirable_count = $test_data['count'], t('!test_id: Real count (=!real_count) should be equal to desirable (=!desirable_count)', array('!test_id' => $id, '!real_count' => $real_count, '!desirable_count' => $desirable_count)));
-    }
-  }
-}
-
-/**
- * Upgrade tests.
- */
-class TMGMTUpgradeAlpha1TestCase extends DrupalWebTestCase {
-
-  protected $profile = 'testing';
-
-  static function getInfo() {
-    return array(
-      'name' => t('Upgrade tests Alpha1'),
-      'description' => t('Tests the upgrade path from 7.x-1.0-alpha1'),
-      'group' => t('Translation Management'),
-    );
-  }
-
-  function setUp() {
-    // Enable all dependencies.
-    parent::setUp(array('entity', 'views', 'translation', 'locale'));
-
-    // Create the tmgmt tables and fill them.
-    module_load_include('inc', 'tmgmt', 'tests/tmgmt_alpha1_dump.sql');
-
-    // @todo: Figure out why this is necessary.
-    $enabled_modules = db_query("SELECT name FROM {system} where status = 1 and type = 'module'")->fetchCol();
-    foreach ($enabled_modules as $enabled_module) {
-      module_load_install($enabled_module);
-      // Set the schema version to the number of the last update provided
-      // by the module.
-      $versions = drupal_get_schema_versions($enabled_module);
-      $version = $versions ? max($versions) : SCHEMA_INSTALLED;
-      db_update('system')
-        ->condition('name', $enabled_module)
-        ->fields(array('schema_version' => $version))
-        ->execute();
-    }
-
-    // Set schema version to 0 and then install the tmgmt modules, to simulate
-    // an enabling.
-    db_update('system')
-      ->condition('name', array('tmgmt', 'tmgmt_ui', 'tmgmt_field', 'tmgmt_node', 'tmgmt_test', 'tmgmt_node_ui'))
-      ->fields(array(
-        'schema_version' => 0,
-      ))
-      ->execute();
-    module_enable(array('tmgmt', 'tmgmt_ui', 'tmgmt_field', 'tmgmt_node', 'tmgmt_test', 'tmgmt_node_ui'));
-
-    // Log in as a user that can run update.php
-    $admin = $this->drupalCreateUser(array('administer software updates'));
-    $this->drupalLogin($admin);
-
-    $this->performUpgrade();
-  }
-
-  /**
-   * Verifies that the data has been migrated properly
-   */
-  function testUpgradePath() {
-    // Log in as a user with enough permissions.
-    $translator = $this->drupalCreateUser(array('administer tmgmt'));
-    $this->drupalLogin($translator);
-    // Go to a job and check the review form.
-    $this->drupalGet('admin/config/regional/tmgmt/jobs/1');
-    // Make sure the #status values have been set accordingly.
-    $this->assertRaw(t('Accepted: @accepted, reviewed: @reviewed, translated: @translated, pending: @pending.', array('@accepted' => 0, '@reviewed' => 0, '@translated' => 2, '@pending' => 0)));
-    // Extract the word count field and make sure it's correct.
-    $word_count = $this->xpath('//td[contains(@class, :class)]', array(':class' => 'views-field-word-count-1'));
-    $this->assertEqual(6, trim((string)reset($word_count)));
-
-    $this->clickLink(t('review'));
-    // Needs review icon.
-    $this->assertRaw('tmgmt-ui-icon-yellow tmgmt-ui-state-translated');
-    // Translated values.
-    $this->assertRaw('de_Test content');
-    $this->assertRaw('de_This is the body.');
-    // Reject button.
-    $this->assertRaw('✗');
-
-    // Check that accepted count has been updated correctly.
-    $this->drupalGet('admin/config/regional/tmgmt/jobs/2');
-    // Make sure the #status values have been set accordingly.
-    $this->assertRaw(t('Accepted: @accepted, reviewed: @reviewed, translated: @translated, pending: @pending.', array('@accepted' => 2, '@reviewed' => 0, '@translated' => 0, '@pending' => 0)));
-
-
-  }
-
-
-  /**
-   * Perform the upgrade.
-   *
-   * Copied and adapted from UpgradePathTestCase::performUpgrade().
-   *
-   * @param $register_errors
-   *   Register the errors during the upgrade process as failures.
-   * @return
-   *   TRUE if the upgrade succeeded, FALSE otherwise.
-   */
-  protected function performUpgrade($register_errors = TRUE) {
-    $update_url = $GLOBALS['base_url'] . '/update.php';
-
-    // Load the first update screen.
-    $this->drupalGet($update_url, array('external' => TRUE));
-    if (!$this->assertResponse(200)) {
-      return FALSE;
-    }
-
-    // Continue.
-    $this->drupalPost(NULL, array(), t('Continue'));
-    if (!$this->assertResponse(200)) {
-      return FALSE;
-    }
-
-    // The test should pass if there are no pending updates.
-    $content = $this->drupalGetContent();
-    if (strpos($content, t('No pending updates.')) !== FALSE) {
-      $this->pass(t('No pending updates and therefore no upgrade process to test.'));
-      $this->pendingUpdates = FALSE;
-      return TRUE;
-    }
-
-    // Go!
-    $this->drupalPost(NULL, array(), t('Apply pending updates'));
-    if (!$this->assertResponse(200)) {
-      return FALSE;
-    }
-
-    // Check for errors during the update process.
-    foreach ($this->xpath('//li[@class=:class]', array(':class' => 'failure')) as $element) {
-      $message = strip_tags($element->asXML());
-      $this->upgradeErrors[] = $message;
-      if ($register_errors) {
-        $this->fail($message);
-      }
-    }
-
-    if (!empty($this->upgradeErrors)) {
-      // Upgrade failed, the installation might be in an inconsistent state,
-      // don't process.
-      return FALSE;
-    }
-
-    // Check if there still are pending updates.
-    $this->drupalGet($update_url, array('external' => TRUE));
-    $this->drupalPost(NULL, array(), t('Continue'));
-    if (!$this->assertText(t('No pending updates.'), t('No pending updates at the end of the update process.'))) {
-      return FALSE;
-    }
-
-    // Clear caches.
-    $this->checkPermissions(array(), TRUE);
-  }
-
-}
diff --git a/translators/file/tmgmt_file.format.html.inc b/translators/file/tmgmt_file.format.html.inc
new file mode 100644
index 0000000..7c4d377
--- /dev/null
+++ b/translators/file/tmgmt_file.format.html.inc
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * Export into HTML.
+ */
+class TMGMTFileFormatHTML implements TMGMTFileFormatInterface {
+
+  /**
+   * Returns base64 encoded data that is safe for use in xml ids.
+   */
+  protected function encodeIdSafeBase64($data) {
+    // Prefix with a b to enforce that the first character is a letter.
+    return 'b' . rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
+  }
+
+  /**
+   * Returns decoded id safe base64 data.
+   */
+  protected function decodeIdSafeBase64($data) {
+    // Remove prefixed b.
+    $data = substr($data, 1);
+    return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
+  }
+
+  /**
+   * Implements TMGMTFileExportInterface::export().
+   */
+  public function export(TMGMTJob $job) {
+
+    $items = array();
+    foreach ($job->getItems() as $item) {
+      $data = array_filter(tmgmt_flatten_data($item->getData()), '_tmgmt_filter_data');
+      foreach ($data as $key => $value) {
+        $items[$item->tjiid][$this->encodeIdSafeBase64($item->tjiid . '][' . $key)] = $value;
+      }
+    }
+    return theme('tmgmt_file_html_template', array(
+      'tjid' => $job->tjid,
+      'source_language' => $job->getTranslator()->mapToRemoteLanguage($job->source_language),
+      'target_language' => $job->getTranslator()->mapToRemoteLanguage($job->target_language),
+      'items' => $items,
+    ));
+  }
+
+  /**
+   * Implements TMGMTFileExportInterface::import().
+   */
+  public function import($imported_file) {
+    $dom = new DOMDocument();
+    $dom->loadHTMLFile($imported_file);
+    $xml = simplexml_import_dom($dom);
+
+    $data = array();
+    foreach ($xml->xpath("//div[@class='atom']") as $atom) {
+      // Assets are our strings (eq fields in nodes).
+      $key = $this->decodeIdSafeBase64((string) $atom['id']);
+      $data[$key]['#text'] = (string) $atom;
+    }
+    return tmgmt_unflatten_data($data);
+  }
+
+  /**
+   *
+   * @param type $imported_file
+   */
+  public function validateImport($imported_file) {
+    $dom = new DOMDocument();
+    if (!$dom->loadHTMLFile($imported_file)) {
+      return FALSE;
+    }
+    $xml = simplexml_import_dom($dom);
+
+    // Collect meta information.
+    $meta_tags = $xml->xpath('//meta');
+    $meta = array();
+    foreach ($meta_tags as $meta_tag) {
+      $meta[(string) $meta_tag['name']] = (string) $meta_tag['content'];
+    }
+
+    // Check required meta tags.
+    foreach (array('JobID', 'languageSource', 'languageTarget') as $name) {
+      if (!isset($meta[$name])) {
+        return FALSE;
+      }
+    }
+
+    // Attempt to load job.
+    if (!$job = tmgmt_job_load($meta['JobID'])) {
+      return FALSE;
+    }
+
+
+    // Check language.
+    if ($meta['languageSource'] != $job->getTranslator()->mapToRemoteLanguage($job->source_language) ||
+        $meta['languageTarget'] != $job->getTranslator()->mapToRemoteLanguage($job->target_language)) {
+      return FALSE;
+    }
+
+    // Validation successful.
+    return $job;
+  }
+
+}
diff --git a/translators/file/tmgmt_file.format.interface.inc b/translators/file/tmgmt_file.format.interface.inc
new file mode 100644
index 0000000..a2e8839
--- /dev/null
+++ b/translators/file/tmgmt_file.format.interface.inc
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * Interface for exporting to a given file format.
+ */
+interface TMGMTFileFormatInterface {
+
+  /**
+   * Return the file content for the job data.
+   *
+   * @param $job
+   *   The translation job object to be exported.
+   *
+   * @return
+   *   String with the file content.
+   */
+  function export(TMGMTJob $job);
+
+  /**
+   * Validates that the given file is valid and can be imported.
+   *
+   * @param $imported_file
+   *   File path to the file to be imported.
+   *
+   * @return TMGMTJob
+   *   Returns the corresponding translation job entity if the import file is
+   *   valid, FALSE otherwise.
+   */
+  function validateImport($imported_file);
+
+  /**
+   * Converts an exported file content back to the translated data.
+   *
+   * @return
+   *   Translated data array.
+   */
+  function import($imported_file);
+}
diff --git a/translators/file/tmgmt_file.format.inc b/translators/file/tmgmt_file.format.xliff.inc
similarity index 61%
rename from translators/file/tmgmt_file.format.inc
rename to translators/file/tmgmt_file.format.xliff.inc
index 3eadc92..7a8ff2f 100644
--- a/translators/file/tmgmt_file.format.inc
+++ b/translators/file/tmgmt_file.format.xliff.inc
@@ -1,43 +1,6 @@
 <?php
 
 /**
- * Interface for exporting to a given file format.
- */
-interface TMGMTFileFormatInterface {
-
-  /**
-   * Return the file content for the job data.
-   *
-   * @param $job
-   *   The translation job object to be exported.
-   *
-   * @return
-   *   String with the file content.
-   */
-  function export(TMGMTJob $job);
-
-  /**
-   * Validates that the given file is valid and can be imported.
-   *
-   * @param $imported_file
-   *   File path to the file to be imported.
-   *
-   * @return TMGMTJob
-   *   Returns the corresponding translation job entity if the import file is
-   *   valid, FALSE otherwise.
-   */
-  function validateImport($imported_file);
-
-  /**
-   * Converts an exported file content back to the translated data.
-   *
-   * @return
-   *   Translated data array.
-   */
-  function import($imported_file);
-}
-
-/**
  * Export to XLIFF format.
  */
 class TMGMTFileformatXLIFF extends XMLWriter implements TMGMTFileFormatInterface {
@@ -223,105 +186,3 @@ class TMGMTFileformatXLIFF extends XMLWriter implements TMGMTFileFormatInterface
   }
 
 }
-
-/**
- * Export into HTML.
- */
-class TMGMTFileFormatHTML implements TMGMTFileFormatInterface {
-
-  /**
-   * Returns base64 encoded data that is safe for use in xml ids.
-   */
-  protected function encodeIdSafeBase64($data) {
-    // Prefix with a b to enforce that the first character is a letter.
-    return 'b' . rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
-  }
-
-  /**
-   * Returns decoded id safe base64 data.
-   */
-  protected function decodeIdSafeBase64($data) {
-    // Remove prefixed b.
-    $data = substr($data, 1);
-    return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
-  }
-
-  /**
-   * Implements TMGMTFileExportInterface::export().
-   */
-  public function export(TMGMTJob $job) {
-
-    $items = array();
-    foreach ($job->getItems() as $item) {
-      $data = array_filter(tmgmt_flatten_data($item->getData()), '_tmgmt_filter_data');
-      foreach ($data as $key => $value) {
-        $items[$item->tjiid][$this->encodeIdSafeBase64($item->tjiid . '][' . $key)] = $value;
-      }
-    }
-    return theme('tmgmt_file_html_template', array(
-      'tjid' => $job->tjid,
-      'source_language' => $job->getTranslator()->mapToRemoteLanguage($job->source_language),
-      'target_language' => $job->getTranslator()->mapToRemoteLanguage($job->target_language),
-      'items' => $items,
-    ));
-  }
-
-  /**
-   * Implements TMGMTFileExportInterface::import().
-   */
-  public function import($imported_file) {
-    $dom = new DOMDocument();
-    $dom->loadHTMLFile($imported_file);
-    $xml = simplexml_import_dom($dom);
-
-    $data = array();
-    foreach ($xml->xpath("//div[@class='atom']") as $atom) {
-      // Assets are our strings (eq fields in nodes).
-      $key = $this->decodeIdSafeBase64((string) $atom['id']);
-      $data[$key]['#text'] = (string) $atom;
-    }
-    return tmgmt_unflatten_data($data);
-  }
-
-  /**
-   *
-   * @param type $imported_file
-   */
-  public function validateImport($imported_file) {
-    $dom = new DOMDocument();
-    if (!$dom->loadHTMLFile($imported_file)) {
-      return FALSE;
-    }
-    $xml = simplexml_import_dom($dom);
-
-    // Collect meta information.
-    $meta_tags = $xml->xpath('//meta');
-    $meta = array();
-    foreach ($meta_tags as $meta_tag) {
-      $meta[(string) $meta_tag['name']] = (string) $meta_tag['content'];
-    }
-
-    // Check required meta tags.
-    foreach (array('JobID', 'languageSource', 'languageTarget') as $name) {
-      if (!isset($meta[$name])) {
-        return FALSE;
-      }
-    }
-
-    // Attempt to load job.
-    if (!$job = tmgmt_job_load($meta['JobID'])) {
-      return FALSE;
-    }
-
-
-    // Check language.
-    if ($meta['languageSource'] != $job->getTranslator()->mapToRemoteLanguage($job->source_language) ||
-        $meta['languageTarget'] != $job->getTranslator()->mapToRemoteLanguage($job->target_language)) {
-      return FALSE;
-    }
-
-    // Validation successful.
-    return $job;
-  }
-
-}
diff --git a/translators/file/tmgmt_file.info b/translators/file/tmgmt_file.info
index b8f0a6e..07b9eed 100644
--- a/translators/file/tmgmt_file.info
+++ b/translators/file/tmgmt_file.info
@@ -5,5 +5,7 @@ core = 7.x
 dependencies[] = tmgmt
 files[] = tmgmt_file.plugin.inc
 files[] = tmgmt_file.ui.inc
-files[] = tmgmt_file.format.inc
+files[] = tmgmt_file.format.interface.inc
+files[] = tmgmt_file.format.xliff.inc
+files[] = tmgmt_file.format.html.inc
 files[] = tmgmt_file.test
diff --git a/translators/tmgmt_local/controller/tmgmt_local.controller.task.inc b/translators/tmgmt_local/controller/tmgmt_local.controller.task.inc
new file mode 100644
index 0000000..3c348a1
--- /dev/null
+++ b/translators/tmgmt_local/controller/tmgmt_local.controller.task.inc
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Contains the task controller.
+ */
+
+/**
+ * Controller class for the local task entity.
+ *
+ * @ingroup tmgmt_local_task
+ */
+class TMGMTLocalTaskController extends EntityAPIController {
+
+  /**
+   * Overrides EntityAPIController::save().
+   */
+  public function save($entity, DatabaseTransaction $transaction = NULL) {
+    $entity->changed = REQUEST_TIME;
+    return parent::save($entity, $transaction);
+  }
+
+  /**
+   * Overrides EntityAPIController::delete().
+   */
+  public function delete($ids, $transaction = NULL) {
+    parent::delete($ids, $transaction);
+
+    $query = new EntityFieldQuery();
+    $result = $query
+      ->entityCondition('entity_type', 'tmgmt_local_task_item')
+      ->propertyCondition('tltid', $ids)
+      ->execute();
+    if (!empty($result['tmgmt_local_task_item'])) {
+      entity_delete_multiple('tmgmt_local_task_item', array_keys($result['tmgmt_local_task_item']));
+    }
+  }
+
+}
diff --git a/translators/tmgmt_local/includes/tmgmt_local.controller.inc b/translators/tmgmt_local/controller/tmgmt_local.controller.task_item.inc
similarity index 71%
rename from translators/tmgmt_local/includes/tmgmt_local.controller.inc
rename to translators/tmgmt_local/controller/tmgmt_local.controller.task_item.inc
index 4622b4e..e7018bd 100644
--- a/translators/tmgmt_local/includes/tmgmt_local.controller.inc
+++ b/translators/tmgmt_local/controller/tmgmt_local.controller.task_item.inc
@@ -2,7 +2,7 @@
 
 /**
  * @file
- * UI controller classes.
+ * Contains the task item controller.
  */
 
 /**
@@ -10,39 +10,6 @@
  *
  * @ingroup tmgmt_local_task
  */
-class TMGMTLocalTaskController extends EntityAPIController {
-
-  /**
-   * Overrides EntityAPIController::save().
-   */
-  public function save($entity, DatabaseTransaction $transaction = NULL) {
-    $entity->changed = REQUEST_TIME;
-    return parent::save($entity, $transaction);
-  }
-
-  /**
-   * Overrides EntityAPIController::delete().
-   */
-  public function delete($ids, $transaction = NULL) {
-    parent::delete($ids, $transaction);
-
-    $query = new EntityFieldQuery();
-    $result = $query
-      ->entityCondition('entity_type', 'tmgmt_local_task_item')
-      ->propertyCondition('tltid', $ids)
-      ->execute();
-    if (!empty($result['tmgmt_local_task_item'])) {
-      entity_delete_multiple('tmgmt_local_task_item', array_keys($result['tmgmt_local_task_item']));
-    }
-  }
-
-}
-
-/**
- * Controller class for the local task entity.
- *
- * @ingroup tmgmt_local_task
- */
 class TMGMTLocalTaskItemController extends EntityAPIController {
 
   /**
diff --git a/translators/tmgmt_local/includes/tmgmt_local_ui.controller.inc b/translators/tmgmt_local/controller/tmgmt_local.ui_controller.task.inc
similarity index 76%
copy from translators/tmgmt_local/includes/tmgmt_local_ui.controller.inc
copy to translators/tmgmt_local/controller/tmgmt_local.ui_controller.task.inc
index d22c790..eccc206 100644
--- a/translators/tmgmt_local/includes/tmgmt_local_ui.controller.inc
+++ b/translators/tmgmt_local/controller/tmgmt_local.ui_controller.task.inc
@@ -74,29 +74,3 @@ class TMGMTLocalTaskUIController extends EntityDefaultUIController {
   }
 
 }
-
-/**
- * Entity UI controller for the local task item entity.
- */
-class TMGMTLocalTaskItemUIController extends EntityDefaultUIController {
-
-  /**
-   * Overrides EntityDefaultUIController::hook_menu().
-   */
-  public function hook_menu() {
-    $id_count = count(explode('/', $this->path));
-    $items[$this->path . '/%tmgmt_local_task/item/%tmgmt_local_task_item'] = array(
-      'title callback' => 'entity_label',
-      'title arguments' => array($this->entityType, $id_count + 2),
-      'page callback' => 'tmgmt_local_task_item_view',
-      'page arguments' => array($id_count + 2),
-      'load arguments' => array($this->entityType),
-      'access callback' => 'entity_access',
-      'access arguments' => array('view', $this->entityType, $id_count + 2),
-      'file' => 'tmgmt_local.pages.inc',
-      'file path' => drupal_get_path('module', 'tmgmt_local') . '/includes',
-    );
-    return $items;
-  }
-
-}
diff --git a/translators/tmgmt_local/controller/tmgmt_local.ui_controller.task_item.inc b/translators/tmgmt_local/controller/tmgmt_local.ui_controller.task_item.inc
new file mode 100644
index 0000000..94f3ae0
--- /dev/null
+++ b/translators/tmgmt_local/controller/tmgmt_local.ui_controller.task_item.inc
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * Entity UI controller for the local task item entity.
+ */
+class TMGMTLocalTaskItemUIController extends EntityDefaultUIController {
+
+  /**
+   * Overrides EntityDefaultUIController::hook_menu().
+   */
+  public function hook_menu() {
+    $id_count = count(explode('/', $this->path));
+    $items[$this->path . '/%tmgmt_local_task/item/%tmgmt_local_task_item'] = array(
+      'title callback' => 'entity_label',
+      'title arguments' => array($this->entityType, $id_count + 2),
+      'page callback' => 'tmgmt_local_task_item_view',
+      'page arguments' => array($id_count + 2),
+      'load arguments' => array($this->entityType),
+      'access callback' => 'entity_access',
+      'access arguments' => array('view', $this->entityType, $id_count + 2),
+      'file' => 'tmgmt_local.pages.inc',
+      'file path' => drupal_get_path('module', 'tmgmt_local') . '/includes',
+    );
+    return $items;
+  }
+
+}
diff --git a/translators/tmgmt_local/includes/tmgmt_local.entity.inc b/translators/tmgmt_local/entity/tmgmt_local.entity.task.inc
similarity index 62%
rename from translators/tmgmt_local/includes/tmgmt_local.entity.inc
rename to translators/tmgmt_local/entity/tmgmt_local.entity.task.inc
index ab836e8..2666b1e 100644
--- a/translators/tmgmt_local/includes/tmgmt_local.entity.inc
+++ b/translators/tmgmt_local/entity/tmgmt_local.entity.task.inc
@@ -385,249 +385,3 @@ class TMGMTLocalTask extends Entity {
   }
 
 }
-
-/**
- * Entity class for the local task item entity.
- *
- * @ingroup tmgmt_local_task
- */
-class TMGMTLocalTaskItem extends Entity {
-
-  /**
-   * Translation local task item identifier.
-   *
-   * @var int
-   */
-  public $tltiid;
-
-  /**
-   * The task identifier.
-   *
-   * @var int
-   */
-  public $tltid;
-
-  /**
-   * Translation job item.
-   *
-   * @var int
-   */
-  public $tjiid;
-
-  /**
-   * Current status of the task.
-   *
-   * @var int
-   */
-  public $status;
-
-  /**
-   * Translated data and data item status.
-   *
-   * @var array
-   */
-  public $data = array();
-
-  /**
-   * Counter for all untranslated data items.
-   *
-   * @var integer
-   */
-  public $count_untranslated = 0;
-
-  /**
-   * Counter for all translated data items.
-   *
-   * @var integer
-   */
-  public $count_translated = 0;
-
-  /**
-   * Counter for all completed data items.
-   *
-   * @var integer
-   */
-  public $count_completed = 0;
-
-  /**
-   * Overrides Entity::__construct().
-   */
-  public function __construct(array $values = array(), $entity_type = 'tmgmt_local_task_item') {
-    parent::__construct($values, $entity_type);
-  }
-
-  /*
-   * Overrides Entity::defaultUri().
-   */
-  public function defaultUri() {
-    return array('path' => 'translate/' . $this->tltid . '/item/' . $this->tltiid);
-  }
-
-  /**
-   * Overrides Entity::defaultLabel().
-   */
-  protected function defaultLabel() {
-    if ($job_item = $this->getJobItem()) {
-      return $job_item->label();
-    }
-    return t('Missing job item');
-  }
-
-  /**
-   * Returns the translation task.
-   *
-   * @return TMGMTLocalTask
-   */
-  public function getTask() {
-    return entity_load_single('tmgmt_local_task', $this->tltid);
-  }
-
-  /**
-   * Returns the translation job item.
-   *
-   * @return TMGMTJobItem
-   */
-  public function getJobItem() {
-    return entity_load_single('tmgmt_job_item', $this->tjiid);
-  }
-
-  /**
-   * Overrides Entity::buildContent().
-   */
-  public function buildContent($view_mode = 'full', $langcode = NULL) {
-    $content = drupal_get_form('tmgmt_local_translation_form', $this);
-    return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
-  }
-
-  /**
-   * Returns TRUE if the local task is pending.
-   *
-   * @return bool
-   *   TRUE if the local task item is untranslated.
-   */
-  public function isPending() {
-    return $this->status == TMGMT_LOCAL_TASK_ITEM_STATUS_PENDING;
-  }
-
-  /**
-   * Returns TRUE if the local task is translated (fully translated).
-   *
-   * @return bool
-   *   TRUE if the local task item is translated.
-   */
-  public function isCompleted() {
-    return $this->status == TMGMT_LOCAL_TASK_ITEM_STATUS_COMPLETED;
-  }
-
-  /**
-   * Rreturns TRUE if the local task is closed (translated and accepted).
-   *
-   * @return bool
-   *   TRUE if the local task item is translated and accepted.
-   */
-  public function isClosed() {
-    return $this->status == TMGMT_LOCAL_TASK_ITEM_STATUS_CLOSED;
-  }
-
-  /**
-   * Sets the task item status to completed.
-   */
-  public function completed() {
-    $this->status = TMGMT_LOCAL_TASK_ITEM_STATUS_COMPLETED;
-  }
-
-  /**
-   * Sets the task item status to closed.
-   */
-  public function closed() {
-    $this->status = TMGMT_LOCAL_TASK_ITEM_STATUS_CLOSED;
-  }
-
-  /**
-   * Updates the values for a specific substructure in the data array.
-   *
-   * The values are either set or updated but never deleted.
-   *
-   * @param $key
-   *   Key pointing to the item the values should be applied.
-   *   The key can be either be an array containing the keys of a nested array
-   *   hierarchy path or a string with '][' or '|' as delimiter.
-   * @param $values
-   *   Nested array of values to set.
-   */
-  public function updateData($key, $values = array()) {
-    foreach ($values as $index => $value) {
-      // In order to preserve existing values, we can not aplly the values array
-      // at once. We need to apply each containing value on its own.
-      // If $value is an array we need to advance the hierarchy level.
-      if (is_array($value)) {
-        $this->updateData(array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
-      }
-      // Apply the value.
-      else {
-        drupal_array_set_nested_value($this->data, array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
-      }
-    }
-  }
-
-  /**
-   * Array of translations.
-   *
-   * The structure is similar to the form API in the way that it is a possibly
-   * nested array with the following properties whose presence indicate that the
-   * current element is a text that might need to be translated.
-   *
-   * - #text: The translated text of the corresponding entry in the job item.
-   * - #status: The status of the translation.
-   *
-   * The key can be an alphanumeric string.
-   *
-   * @param array $key
-   *   If present, only the subarray identified by key is returned.
-   * @param string $index
-   *   Optional index of an attribute below $key.
-   *
-   * @return array
-   *   A structured data array.
-   */
-  public function getData(array $key = array(), $index = NULL) {
-    if (empty($key)) {
-      return $this->data;
-    }
-    if ($index) {
-      $key = array_merge($key, array($index));
-    }
-    return drupal_array_get_nested_value($this->data, $key);
-  }
-
-  /**
-   * Count of all translated data items.
-   *
-   * @return
-   *   Translated count
-   */
-  public function getCountTranslated() {
-    return $this->count_translated;
-  }
-
-  /**
-   * Count of all untranslated data items.
-   *
-   * @return
-   *   Translated count
-   */
-  public function getCountUntranslated() {
-    return $this->count_untranslated;
-  }
-
-  /**
-   * Count of all completed data items.
-   *
-   * @return
-   *   Translated count
-   */
-  public function getCountCompleted() {
-    return $this->count_completed;
-  }
-
-}
diff --git a/translators/tmgmt_local/entity/tmgmt_local.entity.task_item.inc b/translators/tmgmt_local/entity/tmgmt_local.entity.task_item.inc
new file mode 100644
index 0000000..a277285
--- /dev/null
+++ b/translators/tmgmt_local/entity/tmgmt_local.entity.task_item.inc
@@ -0,0 +1,252 @@
+<?php
+
+/*
+ * @file
+ * Entity class.
+ */
+
+/**
+ * Entity class for the local task item entity.
+ *
+ * @ingroup tmgmt_local_task
+ */
+class TMGMTLocalTaskItem extends Entity {
+
+  /**
+   * Translation local task item identifier.
+   *
+   * @var int
+   */
+  public $tltiid;
+
+  /**
+   * The task identifier.
+   *
+   * @var int
+   */
+  public $tltid;
+
+  /**
+   * Translation job item.
+   *
+   * @var int
+   */
+  public $tjiid;
+
+  /**
+   * Current status of the task.
+   *
+   * @var int
+   */
+  public $status;
+
+  /**
+   * Translated data and data item status.
+   *
+   * @var array
+   */
+  public $data = array();
+
+  /**
+   * Counter for all untranslated data items.
+   *
+   * @var integer
+   */
+  public $count_untranslated = 0;
+
+  /**
+   * Counter for all translated data items.
+   *
+   * @var integer
+   */
+  public $count_translated = 0;
+
+  /**
+   * Counter for all completed data items.
+   *
+   * @var integer
+   */
+  public $count_completed = 0;
+
+  /**
+   * Overrides Entity::__construct().
+   */
+  public function __construct(array $values = array(), $entity_type = 'tmgmt_local_task_item') {
+    parent::__construct($values, $entity_type);
+  }
+
+  /*
+   * Overrides Entity::defaultUri().
+   */
+  public function defaultUri() {
+    return array('path' => 'translate/' . $this->tltid . '/item/' . $this->tltiid);
+  }
+
+  /**
+   * Overrides Entity::defaultLabel().
+   */
+  protected function defaultLabel() {
+    if ($job_item = $this->getJobItem()) {
+      return $job_item->label();
+    }
+    return t('Missing job item');
+  }
+
+  /**
+   * Returns the translation task.
+   *
+   * @return TMGMTLocalTask
+   */
+  public function getTask() {
+    return entity_load_single('tmgmt_local_task', $this->tltid);
+  }
+
+  /**
+   * Returns the translation job item.
+   *
+   * @return TMGMTJobItem
+   */
+  public function getJobItem() {
+    return entity_load_single('tmgmt_job_item', $this->tjiid);
+  }
+
+  /**
+   * Overrides Entity::buildContent().
+   */
+  public function buildContent($view_mode = 'full', $langcode = NULL) {
+    $content = drupal_get_form('tmgmt_local_translation_form', $this);
+    return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
+  }
+
+  /**
+   * Returns TRUE if the local task is pending.
+   *
+   * @return bool
+   *   TRUE if the local task item is untranslated.
+   */
+  public function isPending() {
+    return $this->status == TMGMT_LOCAL_TASK_ITEM_STATUS_PENDING;
+  }
+
+  /**
+   * Returns TRUE if the local task is translated (fully translated).
+   *
+   * @return bool
+   *   TRUE if the local task item is translated.
+   */
+  public function isCompleted() {
+    return $this->status == TMGMT_LOCAL_TASK_ITEM_STATUS_COMPLETED;
+  }
+
+  /**
+   * Rreturns TRUE if the local task is closed (translated and accepted).
+   *
+   * @return bool
+   *   TRUE if the local task item is translated and accepted.
+   */
+  public function isClosed() {
+    return $this->status == TMGMT_LOCAL_TASK_ITEM_STATUS_CLOSED;
+  }
+
+  /**
+   * Sets the task item status to completed.
+   */
+  public function completed() {
+    $this->status = TMGMT_LOCAL_TASK_ITEM_STATUS_COMPLETED;
+  }
+
+  /**
+   * Sets the task item status to closed.
+   */
+  public function closed() {
+    $this->status = TMGMT_LOCAL_TASK_ITEM_STATUS_CLOSED;
+  }
+
+  /**
+   * Updates the values for a specific substructure in the data array.
+   *
+   * The values are either set or updated but never deleted.
+   *
+   * @param $key
+   *   Key pointing to the item the values should be applied.
+   *   The key can be either be an array containing the keys of a nested array
+   *   hierarchy path or a string with '][' or '|' as delimiter.
+   * @param $values
+   *   Nested array of values to set.
+   */
+  public function updateData($key, $values = array()) {
+    foreach ($values as $index => $value) {
+      // In order to preserve existing values, we can not aplly the values array
+      // at once. We need to apply each containing value on its own.
+      // If $value is an array we need to advance the hierarchy level.
+      if (is_array($value)) {
+        $this->updateData(array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
+      }
+      // Apply the value.
+      else {
+        drupal_array_set_nested_value($this->data, array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
+      }
+    }
+  }
+
+  /**
+   * Array of translations.
+   *
+   * The structure is similar to the form API in the way that it is a possibly
+   * nested array with the following properties whose presence indicate that the
+   * current element is a text that might need to be translated.
+   *
+   * - #text: The translated text of the corresponding entry in the job item.
+   * - #status: The status of the translation.
+   *
+   * The key can be an alphanumeric string.
+   *
+   * @param array $key
+   *   If present, only the subarray identified by key is returned.
+   * @param string $index
+   *   Optional index of an attribute below $key.
+   *
+   * @return array
+   *   A structured data array.
+   */
+  public function getData(array $key = array(), $index = NULL) {
+    if (empty($key)) {
+      return $this->data;
+    }
+    if ($index) {
+      $key = array_merge($key, array($index));
+    }
+    return drupal_array_get_nested_value($this->data, $key);
+  }
+
+  /**
+   * Count of all translated data items.
+   *
+   * @return
+   *   Translated count
+   */
+  public function getCountTranslated() {
+    return $this->count_translated;
+  }
+
+  /**
+   * Count of all untranslated data items.
+   *
+   * @return
+   *   Translated count
+   */
+  public function getCountUntranslated() {
+    return $this->count_untranslated;
+  }
+
+  /**
+   * Count of all completed data items.
+   *
+   * @return
+   *   Translated count
+   */
+  public function getCountCompleted() {
+    return $this->count_completed;
+  }
+
+}
diff --git a/translators/tmgmt_local/includes/tmgmt_local.plugin.inc b/translators/tmgmt_local/includes/tmgmt_local.plugin.inc
index 4c66497..38adb3e 100644
--- a/translators/tmgmt_local/includes/tmgmt_local.plugin.inc
+++ b/translators/tmgmt_local/includes/tmgmt_local.plugin.inc
@@ -50,75 +50,3 @@ class TMGMTLocalTranslatorPluginController extends TMGMTDefaultTranslatorPluginC
   }
 
 }
-
-/**
- * Local translator plugin UI controller.
- */
-class TMGMTLocalTranslatorUIController extends TMGMTDefaultTranslatorUIController {
-
-  /**
-   * Overrides TMGMTDefaultTranslatorPluginController::checkoutSettingsForm().
-   */
-  public function checkoutSettingsForm($form, &$form_state, TMGMTJob $job) {
-    if ($translators = tmgmt_local_translators($job->source_language, $job->target_language)) {
-      $form['translator'] = array(
-        '#title' => t('Select translator for this job'),
-        '#type' => 'select',
-        '#options' => array('' => t('Select user')) + $translators,
-        '#default_value' => $job->getSetting('translator'),
-      );
-    }
-    else {
-      $form['message'] = array(
-        '#markup' => t('There are no translators available.'),
-      );
-    }
-
-    return $form;
-  }
-
-  /**
-   * Overrides TMGMTTranslatorUIControllerInterface::checkoutInfo().
-   */
-  public function checkoutInfo(TMGMTJob $job) {
-    $label = $job->getTranslator()->label();
-    $form['#title'] = t('@translator translation job information', array('@translator' => $label));
-    $form['#type'] = 'fieldset';
-
-    $tuid = $job->getSetting('translator');
-    if ($tuid && $translator = user_load($tuid)) {
-      $form['job_status'] = array(
-        '#type' => 'item',
-        '#title' => t('Job status'),
-        '#markup' => t('Translation job is assigned to %name.', array('%name' => entity_label('user', $translator))),
-      );
-    }
-    else {
-      $form['job_status'] = array(
-        '#type' => 'item',
-        '#title' => t('Job status'),
-        '#markup' => t('Translation job is not assigned to any translator.'),
-      );
-    }
-
-    if ($job->getSetting('job_comment')) {
-      $form['job_comment'] = array(
-        '#type' => 'item',
-        '#title' => t('Job comment'),
-        '#markup' => check_plain($job->getSetting('job_comment')),
-      );
-    }
-
-    return $form;
-  }
-
-  public function pluginSettingsForm($form, &$form_state, TMGMTTranslator $translator, $busy = FALSE) {
-    $form['allow_all'] = array(
-      '#title' => t('Allow translations for enabled languages even if no translator has the necessary capabilities'),
-      '#type' => 'checkbox',
-      '#default_value' => $translator->getSetting('allow_all'),
-    );
-    return $form;
-  }
-
-}
diff --git a/translators/tmgmt_local/includes/tmgmt_local.plugin.inc b/translators/tmgmt_local/includes/tmgmt_local.plugin.ui.inc
similarity index 61%
copy from translators/tmgmt_local/includes/tmgmt_local.plugin.inc
copy to translators/tmgmt_local/includes/tmgmt_local.plugin.ui.inc
index 4c66497..d03a090 100644
--- a/translators/tmgmt_local/includes/tmgmt_local.plugin.inc
+++ b/translators/tmgmt_local/includes/tmgmt_local.plugin.ui.inc
@@ -2,56 +2,10 @@
 
 /**
  * @file
- * Provides the user translator plugin controller.
+ * Provides the user translator UI plugin controller.
  */
 
 /**
- * Local translator plugin controller.
- */
-class TMGMTLocalTranslatorPluginController extends TMGMTDefaultTranslatorPluginController {
-
-  /**
-   * Implements TMGMTTranslatorPluginControllerInterface::requestTranslation().
-   */
-  public function requestTranslation(TMGMTJob $job) {
-    $tuid = $job->getSetting('translator');
-
-    // Create local task for this job.
-    $local_task = tmgmt_local_task_create(array(
-      'uid' => $job->uid,
-      'tuid' => $tuid,
-      'tjid' => $job->tjid,
-      'title' => t('Task for !label', array('!label' => $job->defaultLabel())),
-    ));
-    // If we have translator then switch to pending state.
-    if ($tuid) {
-      $local_task->status = TMGMT_LOCAL_TASK_STATUS_PENDING;
-    }
-    $local_task->save();
-
-    // Create task items.
-    foreach ($job->getItems() as $item) {
-      $local_task->addTaskItem($item);
-    }
-
-    // The translation job has been successfully submitted.
-    $job->submitted();
-  }
-
-  /**
-   * Overrides TMGMTDefaultTranslatorPluginController::getSupportedTargetLanguages().
-   */
-  public function getSupportedTargetLanguages(TMGMTTranslator $translator, $source_language) {
-    $languages = drupal_map_assoc(tmgmt_local_translation_capabilities($source_language));
-    if ($translator->getSetting('allow_all')) {
-      $languages += parent::getSupportedTargetLanguages($translator, $source_language);
-    }
-    return $languages;
-  }
-
-}
-
-/**
  * Local translator plugin UI controller.
  */
 class TMGMTLocalTranslatorUIController extends TMGMTDefaultTranslatorUIController {
diff --git a/translators/tmgmt_local/tmgmt_local.info b/translators/tmgmt_local/tmgmt_local.info
index 29da59c..390ba38 100644
--- a/translators/tmgmt_local/tmgmt_local.info
+++ b/translators/tmgmt_local/tmgmt_local.info
@@ -5,11 +5,15 @@ core = 7.x
 
 dependencies[] = tmgmt
 
-files[] = includes/tmgmt_local.controller.inc
-files[] = includes/tmgmt_local.entity.inc
+files[] = controller/tmgmt_local.controller.task.inc
+files[] = controller/tmgmt_local.controller.task_item.inc
+files[] = entity/tmgmt_local.entity.task.inc
+files[] = entity/tmgmt_local.entity.task_item.inc
 files[] = includes/tmgmt_local.info.inc
 files[] = includes/tmgmt_local.plugin.inc
-files[] = includes/tmgmt_local_ui.controller.inc
+files[] = includes/tmgmt_local.plugin.ui.inc
+files[] = controller/tmgmt_local.ui_controller.task.inc
+files[] = controller/tmgmt_local.ui_controller.task_item.inc
 files[] = tmgmt_local.test
 
 ; Views integration and handlers
diff --git a/ui/includes/tmgmt_ui.controller.inc b/ui/includes/tmgmt_ui.controller.inc
deleted file mode 100644
index e5e3ed2..0000000
--- a/ui/includes/tmgmt_ui.controller.inc
+++ /dev/null
@@ -1,382 +0,0 @@
-<?php
-
-/**
- * @file
- * Please supply a file description.
- */
-
-/**
- * Entity UI controller for the Translator Entity.
- */
-class TMGMTTranslatorUIController extends EntityDefaultUIController {
-
-  /**
-   * Overrides EntityDefaultUIController::hook_menu().
-   */
-  public function hook_menu() {
-    $items = parent::hook_menu();
-    // We don't need the entire entity label here.
-    $items[$this->path]['title'] = 'Translators';
-    $items[$this->path]['type'] = MENU_LOCAL_TASK;
-    $items[$this->path . '/add']['title'] = 'Add Translator';
-    unset($items[$this->path . '/add']['title callback']);
-    unset($items[$this->path . '/add']['title arguments']);
-    if (!empty($this->entityInfo['exportable'])) {
-      $items[$this->path . '/import']['title'] = 'Import Translator';
-      unset($items[$this->path . '/import']['title callback']);
-      unset($items[$this->path . '/import']['title arguments']);
-    }
-    return $items;
-  }
-
-  /**
-   * Overrides EntityDefaultUIController::overviewForm().
-   */
-  public function overviewForm($form, &$form_state) {
-    $form['translators']['#tree'] = TRUE;
-    $form['translators']['#theme'] = 'tmgmt_ui_translator_overview_form';
-    $form['translators']['#entity_info'] = $this->entityInfo;
-    // Load all translator entities.
-    $translators = tmgmt_translator_load_multiple(FALSE);
-    foreach ($translators as $key => $translator) {
-      $form['translators'][$key] = $this->overviewFormRow(array(), $form_state, $translator, $key);
-      $form['translators'][$key]['#translator'] = $translator;
-    }
-    $form['actions']['#type'] = 'actions';
-    $form['actions']['submit'] = array(
-      '#type' => 'submit',
-      '#value' => t('Save'),
-    );
-    return $form;
-  }
-
-  /**
-   * Overrides EntityDefaultUIController::overviewFormSubmit().
-   */
-  public function overviewFormSubmit($form, &$form_state) {
-    // Update image effect weights.
-    if (!empty($form_state['values']['translators'])) {
-      $translators = tmgmt_translator_load_multiple(array_keys($form_state['values']['translators']));
-      foreach ($form_state['values']['translators'] as $key => $item) {
-        if (isset($translators[$key])) {
-          $translators[$key]->weight = $item['weight'];
-          entity_save($this->entityType, $translators[$key]);
-        }
-      }
-    }
-  }
-
-  /**
-   * Helper method for building a row in the overview form.
-   */
-  protected function overviewFormRow($form, &$form_state, $entity, $id) {
-    $form['#weight'] = isset($form_state['input']['translators']) ? $form_state['input']['translators'][$id]['weight'] : NULL;
-    $form['label'] = array(
-      '#theme' => 'tmgmt_ui_translator_overview_item',
-      '#attached' => array('css' => array(drupal_get_path('module', 'tmgmt_ui') . '/css/tmgmt_ui.admin.css')),
-      '#label' => $entity->label(),
-      '#name' => !empty($this->entityInfo['exportable']) ? entity_id($this->entityType, $entity) : FALSE,
-      '#url' => FALSE,
-      '#description' => $entity->description,
-      '#entity_type' => $this->entityType,
-    );
-    // Add a row for the exportable status.
-    if (!empty($this->entityInfo['exportable'])) {
-      $form['status'] = array(
-        '#theme' => 'entity_status',
-        '#status' => $entity->{$this->statusKey},
-      );
-    }
-    $wrapper = entity_metadata_wrapper($this->entityType, $entity);
-    // Add a column to show the translator plugin via the metadata wrapper.
-    $form['plugin'] = array(
-      '#markup' => $wrapper->plugin->label(),
-      '#status' => $entity->{$this->statusKey},
-    );
-    $controller = $entity->getController();
-    $form['configured'] = array(
-      '#markup' => $controller->isAvailable($entity) ? t('Yes') : t('No'),
-    );
-    $form['weight'] = array(
-      '#type' => 'weight',
-      '#delta' => 30,
-      '#default_value' => $entity->weight,
-    );
-    // Add operations depending on the status.
-    if (entity_has_status($this->entityType, $entity, ENTITY_FIXED)) {
-      $form['operations']['clone'] = array(
-        '#type' => 'link',
-        '#title' => t('clone'),
-        '#href' => $this->path . '/manage/' . $id . '/clone',
-      );
-    }
-    else {
-      $form['operations']['edit'] = array(
-        '#type' => 'link',
-        '#title' => t('edit'),
-        '#href' => $this->path . '/manage/' . $id,
-      );
-      if (!empty($this->entityInfo['exportable'])) {
-        $form['operations']['clone'] = array(
-          '#type' => 'link',
-          '#title' => t('clone'),
-          '#href' => $this->path . '/manage/' . $id . '/clone',
-        );
-      }
-      if (empty($this->entityInfo['exportable']) || !entity_has_status($this->entityType, $entity, ENTITY_IN_CODE)) {
-        $form['operations']['delete'] = array(
-          '#type' => 'link',
-          '#title' => t('delete'),
-          '#href' => $this->path . '/manage/' . $id . '/delete',
-          '#options' => array('query' => drupal_get_destination()),
-        );
-      }
-      elseif (entity_has_status($this->entityType, $entity, ENTITY_OVERRIDDEN)) {
-        $form['operations']['revert'] = array(
-          '#type' => 'link',
-          '#title' => t('revert'),
-          '#href' => $this->path . '/manage/' . $id . '/revert',
-          '#options' => array('query' => drupal_get_destination()),
-        );
-      }
-      else {
-        $row[] = '';
-      }
-    }
-    if (!empty($this->entityInfo['exportable'])) {
-      $form['operations']['export'] = array(
-        '#type' => 'link',
-        '#title' => t('export'),
-        '#href' => $this->path . '/manage/' . $id . '/export',
-      );
-    }
-    return $form;
-  }
-
-  /**
-   * Overrides EntityDefaultUIController::applyOperation().
-   */
-  public function applyOperation($op, $entity) {
-    if ($op == 'delete' && tmgmt_translator_busy($entity->name)) {
-      drupal_set_message(t("The translator %translator could not be deleted because it is currently being used by at least one active translation job.", array('%translator' => $entity->label())), 'error');
-      return FALSE;
-    }
-    return parent::applyOperation($op, $entity);
-  }
-
-}
-
-
-/**
- * Entity UI controller for the Job Entity.
- */
-class TMGMTJobUIController extends EntityDefaultUIController {
-
-  /**
-   * Overrides EntityDefaultUIController::hook_menu().
-   */
-  public function hook_menu() {
-    $id_count = count(explode('/', $this->path));
-    $wildcard = isset($this->entityInfo['admin ui']['menu wildcard']) ? $this->entityInfo['admin ui']['menu wildcard'] : '%entity_object';
-    $items[$this->path . '/' . $wildcard] = array(
-      'title callback' => 'entity_label',
-      'title arguments' => array($this->entityType, $id_count),
-      'page callback' => 'tmgmt_ui_job_view',
-      'page arguments' => array($id_count),
-      'load arguments' => array($this->entityType),
-      'access callback' => 'entity_access',
-      'access arguments' => array('view', $this->entityType, $id_count),
-      'file' => $this->entityInfo['admin ui']['file'],
-      'file path' => $this->entityInfo['admin ui']['file path'],
-    );
-    $items[$this->path . '/' . $wildcard . '/cancel'] = array(
-      'page callback' => 'drupal_get_form',
-      'page arguments' => array($this->entityType . '_operation_form', $this->entityType, $id_count, $id_count + 1),
-      'load arguments' => array($this->entityType),
-      'access callback' => 'entity_access',
-      'access arguments' => array('submit', $this->entityType, $id_count),
-      'type' => MENU_CALLBACK,
-    );
-    $items[$this->path . '/' . $wildcard . '/delete'] = array(
-      'page callback' => 'drupal_get_form',
-      'page arguments' => array($this->entityType . '_operation_form', $this->entityType, $id_count, $id_count + 1),
-      'load arguments' => array($this->entityType),
-      'access callback' => 'entity_access',
-      'access arguments' => array('delete', $this->entityType, $id_count),
-      'type' => MENU_CALLBACK,
-    );
-    return $items;
-  }
-
-  /**
-   * Overrides EntityDefaultUIController::operationForm().
-   */
-  public function operationForm($form, &$form_state, $entity, $op) {
-    switch ($op) {
-      case 'cancel':
-        $confirm_question = t('Are you sure you want to cancel the translation job %label?', array('%label' => $entity->label()));
-        return confirm_form($form, $confirm_question, $this->path);
-      case 'delete':
-        $confirm_question = t('Are you sure you want to delete the translation job %label?', array('%label' => $entity->label()));
-        return confirm_form($form, $confirm_question, $this->path);
-    }
-    drupal_not_found();
-    exit;
-  }
-
-  /**
-   * Overrides EntityDefaultUIController::applyOperation().
-   */
-  public function applyOperation($op, $entity) {
-    switch ($op) {
-      case 'delete':
-        $entity->delete();
-        return t('Deleted the translation job %label.', array('%label' => $entity->label()));
-      case 'cancel':
-        $entity->cancelTranslation();
-        return t('Cancelled the translation job %label.', array('%label' => $entity->label()));
-    }
-    return FALSE;
-  }
-
-}
-
-/**
- * Entity UI controller for the Job Entity.
- */
-class TMGMTJobItemUIController extends EntityDefaultUIController {
-
-  /**
-   * Overrides EntityDefaultUIController::hook_menu().
-   */
-  public function hook_menu() {
-    $id_count = count(explode('/', $this->path));
-    $wildcard = isset($this->entityInfo['admin ui']['menu wildcard']) ? $this->entityInfo['admin ui']['menu wildcard'] : '%entity_object';
-    $items[$this->path . '/' . $wildcard] = array(
-      'title callback' => 'entity_label',
-      'title arguments' => array($this->entityType, $id_count),
-      'page callback' => 'tmgmt_ui_job_item_view',
-      'page arguments' => array($id_count),
-      'load arguments' => array($this->entityType),
-      'access callback' => 'entity_access',
-      'access arguments' => array('view', 'tmgmt_job_item', $id_count),
-      'file' => $this->entityInfo['admin ui']['file'],
-      'file path' => $this->entityInfo['admin ui']['file path'],
-    );
-    $items[$this->path . '/' . $wildcard . '/view'] = array(
-      'title' => 'View',
-      'load arguments' => array($this->entityType),
-      'type' => MENU_DEFAULT_LOCAL_TASK,
-      'weight' => -10,
-    );
-    $items[$this->path . '/' . $wildcard . '/reject/%'] = array(
-      'title' => 'Reject',
-      'page callback' => 'drupal_get_form',
-      'page arguments' => array('tmgmt_ui_translation_review_form_reject_confirm', $id_count, $id_count + 2),
-      'load arguments' => array($this->entityType),
-      'access callback' => 'entity_access',
-      'access arguments' => array('accept', $this->entityType, $id_count),
-      'type' => MENU_VISIBLE_IN_BREADCRUMB,
-      'file' => $this->entityInfo['admin ui']['file'],
-      'file path' => $this->entityInfo['admin ui']['file path'],
-    );
-    $items[$this->path . '/' . $wildcard . '/delete'] = array(
-      'page callback' => 'drupal_get_form',
-      'page arguments' => array($this->entityType . '_operation_form', $this->entityType, $id_count, $id_count + 1),
-      'load arguments' => array($this->entityType),
-      'access callback' => 'entity_access',
-      'access arguments' => array('delete', $this->entityType, $id_count),
-      'type' => MENU_CALLBACK,
-    );
-    $items[$this->path . '/' . $wildcard . '/accept'] = array(
-      'page callback' => 'drupal_get_form',
-      'page arguments' => array($this->entityType . '_operation_form', $this->entityType, $id_count, $id_count + 1),
-      'load arguments' => array($this->entityType),
-      'access callback' => 'entity_access',
-      'access arguments' => array('accept', $this->entityType, $id_count),
-      'type' => MENU_CALLBACK,
-    );
-    return $items;
-  }
-
-  /**
-   * Overrides EntityDefaultUIController::operationForm().
-   */
-  public function operationForm($form, &$form_state, $entity, $op) {
-    $controller = $entity->getSourceController();
-    $info = $controller->pluginInfo();
-    switch ($op) {
-      case 'delete':
-        $confirm_question = t('Are you sure you want to delete the %plugin translation job item for %label?', array('%plugin' => $info['label'], '%label' => $entity->label()));
-        return confirm_form($form, $confirm_question, $this->path);
-      case 'accept':
-        $confirm_question = t('Are you sure you want to accept the %plugin translation job item for %label?', array('%plugin' => $info['label'], '%label' => $entity->label()));
-        return confirm_form($form, $confirm_question, $this->path);
-    }
-    drupal_not_found();
-    exit;
-  }
-
-  /**
-   * Overrides EntityDefaultUIController::applyOperation().
-   */
-  public function applyOperation($op, $entity) {
-    switch ($op) {
-      case 'delete':
-        $entity->delete();
-        return t('The translation job item %label has been deleted.', array('%label' => $entity->label()));
-      case 'accept':
-        $entity->accepted('The translation job item has been accepted by !user.', array('!user' => theme('username', array('account' => $GLOBALS['user']))));
-        return t('The translation job item %label has been accepted.', array('%label' => $entity->label()));
-    }
-    return FALSE;
-  }
-
-}
-
-/**
- * Adds a description to the translator entity on the entity overview form.
- *
- * @see theme_entity_ui_overview_item()
- */
-function theme_tmgmt_ui_translator_overview_item($variables) {
-  $output = theme('entity_ui_overview_item', $variables);
-  if (!empty($variables['description'])) {
-    $output = '<div class="tmgmt-ui-translator-label-wrapper">' . $output . '<div class="description">' . $variables['description'] . '</div></div>';
-  }
-  return $output;
-}
-
-/**
- * Theme callback for adding the tabledrag to the translator entity overview
- * form.
- */
-function theme_tmgmt_ui_translator_overview_form($variables) {
-  $form = $variables['form'];
-  $colspan = !empty($form['#entity_info']['exportable']) ? 4 : 3;
-  $rows = array();
-  $header = array(
-    t('Label'),
-    t('Plugin'),
-    t('Configured'),
-    t('Status'),
-    array('data' => t('Operations'), 'colspan' => $colspan),
-    t('Weight'),
-  );
-  foreach (element_children($form) as $key) {
-    $row = array();
-    $form[$key]['weight']['#attributes']['class'] = array('tmgmt-ui-translator-weight');
-    $row[] = drupal_render($form[$key]['label']);
-    $row[] = drupal_render($form[$key]['plugin']);
-    $row[] = drupal_render($form[$key]['configured']);
-    $row[] = drupal_render($form[$key]['status']);
-    $operations = element_children($form[$key]['operations']);
-    foreach ($operations as $op) {
-      $row[] = array('data' => $form[$key]['operations'][$op]);
-    }
-    $row[] = drupal_render($form[$key]['weight']);
-    $rows[] = array('data' => $row, 'class' => array('draggable'));
-  }
-  drupal_add_tabledrag('tmgmt-ui-translator-overview', 'order', 'sibling', 'tmgmt-ui-translator-weight');
-  return theme('table', array('header' => $header, 'rows' => $rows, 'empty' => t('None.'), 'attributes' => array('id' => 'tmgmt-ui-translator-overview')));
-}
diff --git a/translators/tmgmt_local/includes/tmgmt_local_ui.controller.inc b/ui/includes/tmgmt_ui.controller.job.inc
similarity index 53%
copy from translators/tmgmt_local/includes/tmgmt_local_ui.controller.inc
copy to ui/includes/tmgmt_ui.controller.job.inc
index d22c790..1cf5014 100644
--- a/translators/tmgmt_local/includes/tmgmt_local_ui.controller.inc
+++ b/ui/includes/tmgmt_ui.controller.job.inc
@@ -1,9 +1,14 @@
 <?php
 
 /**
- * Entity UI controller for the local task entity.
+ * @file
+ * Contains the job UI controller.
  */
-class TMGMTLocalTaskUIController extends EntityDefaultUIController {
+
+/**
+ * Entity UI controller for the Job Entity.
+ */
+class TMGMTJobUIController extends EntityDefaultUIController {
 
   /**
    * Overrides EntityDefaultUIController::hook_menu().
@@ -14,28 +19,28 @@ class TMGMTLocalTaskUIController extends EntityDefaultUIController {
     $items[$this->path . '/' . $wildcard] = array(
       'title callback' => 'entity_label',
       'title arguments' => array($this->entityType, $id_count),
-      'page callback' => 'tmgmt_local_task_view',
+      'page callback' => 'tmgmt_ui_job_view',
       'page arguments' => array($id_count),
       'load arguments' => array($this->entityType),
       'access callback' => 'entity_access',
       'access arguments' => array('view', $this->entityType, $id_count),
-      'file' => 'tmgmt_local.pages.inc',
-      'file path' => drupal_get_path('module', 'tmgmt_local') . '/includes',
+      'file' => $this->entityInfo['admin ui']['file'],
+      'file path' => $this->entityInfo['admin ui']['file path'],
     );
-    $items[$this->path . '/' . $wildcard . '/delete'] = array(
+    $items[$this->path . '/' . $wildcard . '/cancel'] = array(
       'page callback' => 'drupal_get_form',
       'page arguments' => array($this->entityType . '_operation_form', $this->entityType, $id_count, $id_count + 1),
       'load arguments' => array($this->entityType),
       'access callback' => 'entity_access',
-      'access arguments' => array('delete', $this->entityType, $id_count),
+      'access arguments' => array('submit', $this->entityType, $id_count),
       'type' => MENU_CALLBACK,
     );
-    $items[$this->path . '/' . $wildcard . '/unassign'] = array(
+    $items[$this->path . '/' . $wildcard . '/delete'] = array(
       'page callback' => 'drupal_get_form',
       'page arguments' => array($this->entityType . '_operation_form', $this->entityType, $id_count, $id_count + 1),
       'load arguments' => array($this->entityType),
       'access callback' => 'entity_access',
-      'access arguments' => array('unassign', $this->entityType, $id_count),
+      'access arguments' => array('delete', $this->entityType, $id_count),
       'type' => MENU_CALLBACK,
     );
     return $items;
@@ -46,11 +51,11 @@ class TMGMTLocalTaskUIController extends EntityDefaultUIController {
    */
   public function operationForm($form, &$form_state, $entity, $op) {
     switch ($op) {
-      case 'delete':
-        $confirm_question = t('Are you sure you want to delete the translation task %label?', array('%label' => $entity->label()));
+      case 'cancel':
+        $confirm_question = t('Are you sure you want to cancel the translation job %label?', array('%label' => $entity->label()));
         return confirm_form($form, $confirm_question, $this->path);
-      case 'unassign':
-        $confirm_question = t('Are you sure you want to unassign from the translation task %label?', array('%label' => $entity->label()));
+      case 'delete':
+        $confirm_question = t('Are you sure you want to delete the translation job %label?', array('%label' => $entity->label()));
         return confirm_form($form, $confirm_question, $this->path);
     }
     drupal_not_found();
@@ -64,39 +69,12 @@ class TMGMTLocalTaskUIController extends EntityDefaultUIController {
     switch ($op) {
       case 'delete':
         $entity->delete();
-        return t('Deleted the translation local task %label.', array('%label' => $entity->label()));
-      case 'unassign':
-        $entity->unassign();
-        $entity->save();
-        return t('Unassigned from translation local task %label.', array('%label' => $entity->label()));
+        return t('Deleted the translation job %label.', array('%label' => $entity->label()));
+      case 'cancel':
+        $entity->cancelTranslation();
+        return t('Cancelled the translation job %label.', array('%label' => $entity->label()));
     }
     return FALSE;
   }
 
 }
-
-/**
- * Entity UI controller for the local task item entity.
- */
-class TMGMTLocalTaskItemUIController extends EntityDefaultUIController {
-
-  /**
-   * Overrides EntityDefaultUIController::hook_menu().
-   */
-  public function hook_menu() {
-    $id_count = count(explode('/', $this->path));
-    $items[$this->path . '/%tmgmt_local_task/item/%tmgmt_local_task_item'] = array(
-      'title callback' => 'entity_label',
-      'title arguments' => array($this->entityType, $id_count + 2),
-      'page callback' => 'tmgmt_local_task_item_view',
-      'page arguments' => array($id_count + 2),
-      'load arguments' => array($this->entityType),
-      'access callback' => 'entity_access',
-      'access arguments' => array('view', $this->entityType, $id_count + 2),
-      'file' => 'tmgmt_local.pages.inc',
-      'file path' => drupal_get_path('module', 'tmgmt_local') . '/includes',
-    );
-    return $items;
-  }
-
-}
diff --git a/translators/tmgmt_local/includes/tmgmt_local_ui.controller.inc b/ui/includes/tmgmt_ui.controller.job_item.inc
similarity index 51%
rename from translators/tmgmt_local/includes/tmgmt_local_ui.controller.inc
rename to ui/includes/tmgmt_ui.controller.job_item.inc
index d22c790..42b66ae 100644
--- a/translators/tmgmt_local/includes/tmgmt_local_ui.controller.inc
+++ b/ui/includes/tmgmt_ui.controller.job_item.inc
@@ -1,9 +1,14 @@
 <?php
 
 /**
- * Entity UI controller for the local task entity.
+ * @file
+ * Contains the job item UI controller.
  */
-class TMGMTLocalTaskUIController extends EntityDefaultUIController {
+
+/**
+ * Entity UI controller for the Job Entity.
+ */
+class TMGMTJobItemUIController extends EntityDefaultUIController {
 
   /**
    * Overrides EntityDefaultUIController::hook_menu().
@@ -14,13 +19,30 @@ class TMGMTLocalTaskUIController extends EntityDefaultUIController {
     $items[$this->path . '/' . $wildcard] = array(
       'title callback' => 'entity_label',
       'title arguments' => array($this->entityType, $id_count),
-      'page callback' => 'tmgmt_local_task_view',
+      'page callback' => 'tmgmt_ui_job_item_view',
       'page arguments' => array($id_count),
       'load arguments' => array($this->entityType),
       'access callback' => 'entity_access',
-      'access arguments' => array('view', $this->entityType, $id_count),
-      'file' => 'tmgmt_local.pages.inc',
-      'file path' => drupal_get_path('module', 'tmgmt_local') . '/includes',
+      'access arguments' => array('view', 'tmgmt_job_item', $id_count),
+      'file' => $this->entityInfo['admin ui']['file'],
+      'file path' => $this->entityInfo['admin ui']['file path'],
+    );
+    $items[$this->path . '/' . $wildcard . '/view'] = array(
+      'title' => 'View',
+      'load arguments' => array($this->entityType),
+      'type' => MENU_DEFAULT_LOCAL_TASK,
+      'weight' => -10,
+    );
+    $items[$this->path . '/' . $wildcard . '/reject/%'] = array(
+      'title' => 'Reject',
+      'page callback' => 'drupal_get_form',
+      'page arguments' => array('tmgmt_ui_translation_review_form_reject_confirm', $id_count, $id_count + 2),
+      'load arguments' => array($this->entityType),
+      'access callback' => 'entity_access',
+      'access arguments' => array('accept', $this->entityType, $id_count),
+      'type' => MENU_VISIBLE_IN_BREADCRUMB,
+      'file' => $this->entityInfo['admin ui']['file'],
+      'file path' => $this->entityInfo['admin ui']['file path'],
     );
     $items[$this->path . '/' . $wildcard . '/delete'] = array(
       'page callback' => 'drupal_get_form',
@@ -30,12 +52,12 @@ class TMGMTLocalTaskUIController extends EntityDefaultUIController {
       'access arguments' => array('delete', $this->entityType, $id_count),
       'type' => MENU_CALLBACK,
     );
-    $items[$this->path . '/' . $wildcard . '/unassign'] = array(
+    $items[$this->path . '/' . $wildcard . '/accept'] = array(
       'page callback' => 'drupal_get_form',
       'page arguments' => array($this->entityType . '_operation_form', $this->entityType, $id_count, $id_count + 1),
       'load arguments' => array($this->entityType),
       'access callback' => 'entity_access',
-      'access arguments' => array('unassign', $this->entityType, $id_count),
+      'access arguments' => array('accept', $this->entityType, $id_count),
       'type' => MENU_CALLBACK,
     );
     return $items;
@@ -45,12 +67,14 @@ class TMGMTLocalTaskUIController extends EntityDefaultUIController {
    * Overrides EntityDefaultUIController::operationForm().
    */
   public function operationForm($form, &$form_state, $entity, $op) {
+    $controller = $entity->getSourceController();
+    $info = $controller->pluginInfo();
     switch ($op) {
       case 'delete':
-        $confirm_question = t('Are you sure you want to delete the translation task %label?', array('%label' => $entity->label()));
+        $confirm_question = t('Are you sure you want to delete the %plugin translation job item for %label?', array('%plugin' => $info['label'], '%label' => $entity->label()));
         return confirm_form($form, $confirm_question, $this->path);
-      case 'unassign':
-        $confirm_question = t('Are you sure you want to unassign from the translation task %label?', array('%label' => $entity->label()));
+      case 'accept':
+        $confirm_question = t('Are you sure you want to accept the %plugin translation job item for %label?', array('%plugin' => $info['label'], '%label' => $entity->label()));
         return confirm_form($form, $confirm_question, $this->path);
     }
     drupal_not_found();
@@ -64,39 +88,12 @@ class TMGMTLocalTaskUIController extends EntityDefaultUIController {
     switch ($op) {
       case 'delete':
         $entity->delete();
-        return t('Deleted the translation local task %label.', array('%label' => $entity->label()));
-      case 'unassign':
-        $entity->unassign();
-        $entity->save();
-        return t('Unassigned from translation local task %label.', array('%label' => $entity->label()));
+        return t('The translation job item %label has been deleted.', array('%label' => $entity->label()));
+      case 'accept':
+        $entity->accepted('The translation job item has been accepted by !user.', array('!user' => theme('username', array('account' => $GLOBALS['user']))));
+        return t('The translation job item %label has been accepted.', array('%label' => $entity->label()));
     }
     return FALSE;
   }
 
 }
-
-/**
- * Entity UI controller for the local task item entity.
- */
-class TMGMTLocalTaskItemUIController extends EntityDefaultUIController {
-
-  /**
-   * Overrides EntityDefaultUIController::hook_menu().
-   */
-  public function hook_menu() {
-    $id_count = count(explode('/', $this->path));
-    $items[$this->path . '/%tmgmt_local_task/item/%tmgmt_local_task_item'] = array(
-      'title callback' => 'entity_label',
-      'title arguments' => array($this->entityType, $id_count + 2),
-      'page callback' => 'tmgmt_local_task_item_view',
-      'page arguments' => array($id_count + 2),
-      'load arguments' => array($this->entityType),
-      'access callback' => 'entity_access',
-      'access arguments' => array('view', $this->entityType, $id_count + 2),
-      'file' => 'tmgmt_local.pages.inc',
-      'file path' => drupal_get_path('module', 'tmgmt_local') . '/includes',
-    );
-    return $items;
-  }
-
-}
diff --git a/ui/includes/tmgmt_ui.controller.translator.inc b/ui/includes/tmgmt_ui.controller.translator.inc
new file mode 100644
index 0000000..3b41a53
--- /dev/null
+++ b/ui/includes/tmgmt_ui.controller.translator.inc
@@ -0,0 +1,167 @@
+<?php
+
+/**
+ * @file
+ * Contains the translator UI controller.
+ */
+
+/**
+ * Entity UI controller for the Translator Entity.
+ */
+class TMGMTTranslatorUIController extends EntityDefaultUIController {
+
+  /**
+   * Overrides EntityDefaultUIController::hook_menu().
+   */
+  public function hook_menu() {
+    $items = parent::hook_menu();
+    // We don't need the entire entity label here.
+    $items[$this->path]['title'] = 'Translators';
+    $items[$this->path]['type'] = MENU_LOCAL_TASK;
+    $items[$this->path . '/add']['title'] = 'Add Translator';
+    unset($items[$this->path . '/add']['title callback']);
+    unset($items[$this->path . '/add']['title arguments']);
+    if (!empty($this->entityInfo['exportable'])) {
+      $items[$this->path . '/import']['title'] = 'Import Translator';
+      unset($items[$this->path . '/import']['title callback']);
+      unset($items[$this->path . '/import']['title arguments']);
+    }
+    return $items;
+  }
+
+  /**
+   * Overrides EntityDefaultUIController::overviewForm().
+   */
+  public function overviewForm($form, &$form_state) {
+    $form['translators']['#tree'] = TRUE;
+    $form['translators']['#theme'] = 'tmgmt_ui_translator_overview_form';
+    $form['translators']['#entity_info'] = $this->entityInfo;
+    // Load all translator entities.
+    $translators = tmgmt_translator_load_multiple(FALSE);
+    foreach ($translators as $key => $translator) {
+      $form['translators'][$key] = $this->overviewFormRow(array(), $form_state, $translator, $key);
+      $form['translators'][$key]['#translator'] = $translator;
+    }
+    $form['actions']['#type'] = 'actions';
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Save'),
+    );
+    return $form;
+  }
+
+  /**
+   * Overrides EntityDefaultUIController::overviewFormSubmit().
+   */
+  public function overviewFormSubmit($form, &$form_state) {
+    // Update image effect weights.
+    if (!empty($form_state['values']['translators'])) {
+      $translators = tmgmt_translator_load_multiple(array_keys($form_state['values']['translators']));
+      foreach ($form_state['values']['translators'] as $key => $item) {
+        if (isset($translators[$key])) {
+          $translators[$key]->weight = $item['weight'];
+          entity_save($this->entityType, $translators[$key]);
+        }
+      }
+    }
+  }
+
+  /**
+   * Helper method for building a row in the overview form.
+   */
+  protected function overviewFormRow($form, &$form_state, $entity, $id) {
+    $form['#weight'] = isset($form_state['input']['translators']) ? $form_state['input']['translators'][$id]['weight'] : NULL;
+    $form['label'] = array(
+      '#theme' => 'tmgmt_ui_translator_overview_item',
+      '#attached' => array('css' => array(drupal_get_path('module', 'tmgmt_ui') . '/css/tmgmt_ui.admin.css')),
+      '#label' => $entity->label(),
+      '#name' => !empty($this->entityInfo['exportable']) ? entity_id($this->entityType, $entity) : FALSE,
+      '#url' => FALSE,
+      '#description' => $entity->description,
+      '#entity_type' => $this->entityType,
+    );
+    // Add a row for the exportable status.
+    if (!empty($this->entityInfo['exportable'])) {
+      $form['status'] = array(
+        '#theme' => 'entity_status',
+        '#status' => $entity->{$this->statusKey},
+      );
+    }
+    $wrapper = entity_metadata_wrapper($this->entityType, $entity);
+    // Add a column to show the translator plugin via the metadata wrapper.
+    $form['plugin'] = array(
+      '#markup' => $wrapper->plugin->label(),
+      '#status' => $entity->{$this->statusKey},
+    );
+    $controller = $entity->getController();
+    $form['configured'] = array(
+      '#markup' => $controller->isAvailable($entity) ? t('Yes') : t('No'),
+    );
+    $form['weight'] = array(
+      '#type' => 'weight',
+      '#delta' => 30,
+      '#default_value' => $entity->weight,
+    );
+    // Add operations depending on the status.
+    if (entity_has_status($this->entityType, $entity, ENTITY_FIXED)) {
+      $form['operations']['clone'] = array(
+        '#type' => 'link',
+        '#title' => t('clone'),
+        '#href' => $this->path . '/manage/' . $id . '/clone',
+      );
+    }
+    else {
+      $form['operations']['edit'] = array(
+        '#type' => 'link',
+        '#title' => t('edit'),
+        '#href' => $this->path . '/manage/' . $id,
+      );
+      if (!empty($this->entityInfo['exportable'])) {
+        $form['operations']['clone'] = array(
+          '#type' => 'link',
+          '#title' => t('clone'),
+          '#href' => $this->path . '/manage/' . $id . '/clone',
+        );
+      }
+      if (empty($this->entityInfo['exportable']) || !entity_has_status($this->entityType, $entity, ENTITY_IN_CODE)) {
+        $form['operations']['delete'] = array(
+          '#type' => 'link',
+          '#title' => t('delete'),
+          '#href' => $this->path . '/manage/' . $id . '/delete',
+          '#options' => array('query' => drupal_get_destination()),
+        );
+      }
+      elseif (entity_has_status($this->entityType, $entity, ENTITY_OVERRIDDEN)) {
+        $form['operations']['revert'] = array(
+          '#type' => 'link',
+          '#title' => t('revert'),
+          '#href' => $this->path . '/manage/' . $id . '/revert',
+          '#options' => array('query' => drupal_get_destination()),
+        );
+      }
+      else {
+        $row[] = '';
+      }
+    }
+    if (!empty($this->entityInfo['exportable'])) {
+      $form['operations']['export'] = array(
+        '#type' => 'link',
+        '#title' => t('export'),
+        '#href' => $this->path . '/manage/' . $id . '/export',
+      );
+    }
+    return $form;
+  }
+
+  /**
+   * Overrides EntityDefaultUIController::applyOperation().
+   */
+  public function applyOperation($op, $entity) {
+    if ($op == 'delete' && tmgmt_translator_busy($entity->name)) {
+      drupal_set_message(t("The translator %translator could not be deleted because it is currently being used by at least one active translation job.", array('%translator' => $entity->label())), 'error');
+      return FALSE;
+    }
+    return parent::applyOperation($op, $entity);
+  }
+
+}
diff --git a/ui/includes/tmgmt_ui.theme.inc b/ui/includes/tmgmt_ui.theme.inc
index 753e4c5..c2c60b5 100644
--- a/ui/includes/tmgmt_ui.theme.inc
+++ b/ui/includes/tmgmt_ui.theme.inc
@@ -248,3 +248,50 @@ function theme_tmgmt_ui_translator_review_form($variables) {
   }
   return '<div id="tmgmt-status-messages-' . strtolower($parent_label) . '"></div><table class="tmgmt-ui-review"><colgroup width="100" /><colgroup width="*" span="2" /><colgroup width="100" />' . $result . '</table>';
 }
+
+/**
+ * Adds a description to the translator entity on the entity overview form.
+ *
+ * @see theme_entity_ui_overview_item()
+ */
+function theme_tmgmt_ui_translator_overview_item($variables) {
+  $output = theme('entity_ui_overview_item', $variables);
+  if (!empty($variables['description'])) {
+    $output = '<div class="tmgmt-ui-translator-label-wrapper">' . $output . '<div class="description">' . $variables['description'] . '</div></div>';
+  }
+  return $output;
+}
+
+/**
+ * Theme callback for adding the tabledrag to the translator entity overview
+ * form.
+ */
+function theme_tmgmt_ui_translator_overview_form($variables) {
+  $form = $variables['form'];
+  $colspan = !empty($form['#entity_info']['exportable']) ? 4 : 3;
+  $rows = array();
+  $header = array(
+    t('Label'),
+    t('Plugin'),
+    t('Configured'),
+    t('Status'),
+    array('data' => t('Operations'), 'colspan' => $colspan),
+    t('Weight'),
+  );
+  foreach (element_children($form) as $key) {
+    $row = array();
+    $form[$key]['weight']['#attributes']['class'] = array('tmgmt-ui-translator-weight');
+    $row[] = drupal_render($form[$key]['label']);
+    $row[] = drupal_render($form[$key]['plugin']);
+    $row[] = drupal_render($form[$key]['configured']);
+    $row[] = drupal_render($form[$key]['status']);
+    $operations = element_children($form[$key]['operations']);
+    foreach ($operations as $op) {
+      $row[] = array('data' => $form[$key]['operations'][$op]);
+    }
+    $row[] = drupal_render($form[$key]['weight']);
+    $rows[] = array('data' => $row, 'class' => array('draggable'));
+  }
+  drupal_add_tabledrag('tmgmt-ui-translator-overview', 'order', 'sibling', 'tmgmt-ui-translator-weight');
+  return theme('table', array('header' => $header, 'rows' => $rows, 'empty' => t('None.'), 'attributes' => array('id' => 'tmgmt-ui-translator-overview')));
+}
diff --git a/ui/tmgmt_ui.info b/ui/tmgmt_ui.info
index 3909897..52a6c2c 100644
--- a/ui/tmgmt_ui.info
+++ b/ui/tmgmt_ui.info
@@ -7,5 +7,7 @@ dependencies[] = tmgmt
 dependencies[] = views_bulk_operations
 dependencies[] = rules
 
-files[] = includes/tmgmt_ui.controller.inc
+files[] = includes/tmgmt_ui.controller.job.inc
+files[] = includes/tmgmt_ui.controller.job_item.inc
+files[] = includes/tmgmt_ui.controller.translator.inc
 files[] = tmgmt_ui.test
diff --git a/ui/tmgmt_ui.module b/ui/tmgmt_ui.module
index b7a185b..405f198 100644
--- a/ui/tmgmt_ui.module
+++ b/ui/tmgmt_ui.module
@@ -74,11 +74,11 @@ function tmgmt_ui_theme() {
       // We also have the entity_type here because we are still populating the
       // defaults via the entity api so we just need to add the description.
       'variables' => array('label' => NULL, 'entity_type' => NULL, 'url' => FALSE, 'name' => FALSE, 'description' => FALSE),
-      'file' => 'includes/tmgmt_ui.controller.inc',
+      'file' => 'includes/tmgmt_ui.theme.inc',
     ),
     'tmgmt_ui_translator_overview_form' => array(
       'render element' => 'form',
-      'file' => 'includes/tmgmt_ui.controller.inc',
+      'file' => 'includes/tmgmt_ui.theme.inc',
     ),
     'tmgmt_ui_translator_review_form' => array(
       'render element' => 'element',
