diff --git a/tests/title.test b/tests/title.test
index a3addd2..a67c47c 100644
--- a/tests/title.test
+++ b/tests/title.test
@@ -5,3 +5,145 @@
  * Tests for the Title module.
  */
 
+/**
+ * Tests for legacy field replacement.
+ */
+class TitleFieldReplacementTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Field replacement',
+      'description' => 'Test field replacement.',
+      'group' => 'Title',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('entity', 'field_test', 'title', 'title_test');
+  }
+
+  /**
+   * Test field replacement API and workflow.
+   */
+  function testFieldReplacementWorkflow() {
+    $info = entity_get_info('test_entity');
+    $label_key = $info['entity keys']['label'];
+    $field_name = $label_key . '_field';
+
+    // Enable field replacement for the test entity.
+    title_field_replacement_toggle('test_entity', 'test_bundle', $label_key);
+
+    $i = 0;
+    $entity = field_test_create_stub_entity(FALSE, FALSE);
+
+    while ($i++ <= 1) {
+      // The first time the entity gets created the second time gets updated.
+      title_test_entity_save($entity);
+
+      // Check that the replacing field value has been synchronized on save.
+      $query = db_select('test_entity', 'te');
+      $query->addJoin('INNER', 'field_data_' . $field_name, 'f', 'te.ftid = f.entity_id');
+      $record = $query
+        ->fields('te')
+        ->fields('f')
+        ->condition('ftid', $entity->ftid)
+        ->execute()
+        ->fetch();
+
+      $phase = $entity->is_new ? 'insert' : 'update';
+      $this->assertIdentical($record->{$label_key}, $record->{$field_name . '_value'}, t('Field synchronization is correctly performed on %phase.', array('%phase' => $phase)));
+      unset($entity->is_new);
+    }
+
+    // Store a dummy value in the legacy field.
+    while (($label = $this->randomName()) == $entity->{$label_key});
+
+    db_update('test_entity')
+      ->fields(array($label_key => $label))
+      ->execute();
+
+    $record = db_select('test_entity', 'te')
+      ->fields('te')
+      ->condition('ftid', $entity->ftid)
+      ->execute()
+      ->fetch();
+
+    $this->assertNotIdentical($record->{$label_key}, $entity->{$label_key}, t('Entity label has been changed.'));
+
+    // Clear field cache so we synchrboization can be performed on field attach
+    // load.
+    cache_clear_all('*', 'cache_field');
+
+    // Check that the replacing field value is correctly synchronized on load
+    // and view.
+    $entity = title_test_entity_test_load($entity);
+    title_test_phase_check('after_load', $entity);
+    $build = entity_view('test_entity', array($entity->ftid => $entity));
+
+    foreach (title_test_phase_store() as $phase => $value) {
+      $this->assertTrue($value, t('Field synchronization is correctly performed on %phase.', array('%phase' => $phase)));
+    }
+
+    // Change the value stored into the label field to check entity_label().
+    if (isset($info['label callback'])) {
+      $label = $this->randomName();
+      $entity->{$field_name}[LANGUAGE_NONE][0]['value'] = $label;
+      $this->assertIdentical(entity_label('test_entity', $entity), $label, t('entity_label() returns the expected value.'));
+    }
+  }
+
+  /**
+   * Test field replacement UI.
+   */
+  function testFieldReplacementUI() {
+    $admin_user = $this->drupalCreateUser(array('access administration pages', 'view the administration theme', 'administer content types', 'administer taxonomy', 'administer comments'));
+    $this->drupalLogin($admin_user);
+
+    foreach (entity_get_info() as $entity_type => $entity_info) {
+      if (!empty($entity_info['field replacement'])) {
+        foreach ($entity_info['bundles'] as $bundle => $bundle_info) {
+          if (isset($bundle_info['admin']['path'])) {
+            $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle) . '/fields';
+
+            foreach ($entity_info['field replacement'] as $legacy_field => $info) {
+              $path = $admin_path . '/replace/' . $legacy_field;
+              $xpath = '//a[@href=:url and text()=:label]';
+              $args = array(':url' => url($path), ':label' => t('replace'));
+              $targs = array('%legacy_field' => $legacy_field, '%entity_type' => $entity_type, '%bundle' => $bundle);
+              $field_name = $info['field']['field_name'];
+
+              // Check that the current legacy field has a "replace" operation.
+              $this->drupalGet($admin_path);
+              $link = $this->xpath($xpath, $args);
+              $this->assertEqual(count($link), 1, t('Replace link found for the field %legacy_field of the bundle %bundle of the entity %entity_type.', $targs));
+
+              // Check that the legacy field has correctly been replaced through
+              // field replacement UI.
+              $this->drupalPost($path, array('enabled' => TRUE), t('Save settings'));
+              _field_info_collate_fields(TRUE);
+              $link = $this->xpath($xpath, $args);
+              $this->assertTrue(empty($link) && title_field_replacement_enabled($entity_type, $bundle, $legacy_field), t('%legacy_field successfully replaced for the bundle %bundle of the entity %entity_type.', $targs));
+
+              // Check that the enabled status cannot be changed unless the
+              // field instance is removed.
+              $this->drupalGet($path);
+              $this->assertFieldByXPath('//form//input[@name="enabled" and @checked="checked" and @disabled="disabled"]', NULL, t('Field replacement for %legacy_field cannot be disabled unless the replacing field instance is deleted.', array('%legacy_field' => $legacy_field)));
+              $this->drupalPost($path, array(), t('Save settings'));
+              _field_info_collate_fields(TRUE);
+              $this->assertTrue(title_field_replacement_enabled($entity_type, $bundle, $legacy_field), t('Submitting the form does not alter field replacement settings.'));
+
+              // Delete the field instance and check that the "replace"
+              // operation is available again.
+              $this->drupalPost($admin_path . '/' . $field_name . '/delete', array(), t('Delete'));
+              $link = $this->xpath($xpath, $args);
+              $this->assertEqual(count($link), 1, t('Replace link found for the field %legacy_field of the bundle %bundle of the entity %entity_type.', $targs));
+
+              // Check that field replacement can be enabled again.
+              $this->drupalGet($path);
+              $this->assertFieldByXPath('//form//input[@name="enabled" and not(@checked) and not(@disabled)]', NULL, t('Field replacement for %legacy_field cannot be disabled unless the replacing field instance is deleted.', array('%legacy_field' => $legacy_field)));
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/tests/title_test.module b/tests/title_test.module
index 3268077..24ce4e7 100644
--- a/tests/title_test.module
+++ b/tests/title_test.module
@@ -5,3 +5,127 @@
  * Testing functionality for Title module.
  */
 
+/**
+ * Implements hook_entity_info().
+ */
+function title_test_entity_info() {
+  $info = array();
+
+  $field = array(
+    'type' => 'text',
+    'cardinality' => 1,
+    'translatable' => TRUE,
+  );
+
+  $instance = array(
+    'required' => TRUE,
+    'settings' => array(
+      'text_processing' => 0,
+    ),
+    'widget' => array(
+      'weight' => -5,
+    ),
+  );
+
+  $info['test_entity'] = array(
+    'label' => t('Test entity'),
+    'entity keys' => array(
+      'label' => 'ftlabel',
+    ),
+    'field replacement' => array(
+      'ftlabel' => array(
+        'field' => $field,
+        'instance' => array(
+          'label' => t('Title'),
+          'description' => t('A field replacing node title.'),
+        ) + $instance,
+      ),
+    ),
+    'controller class' => 'EntityAPIController',
+  );
+
+  return $info;
+}
+
+/**
+ * Save the given test entity.
+ */
+function title_test_entity_save($entity) {
+  // field_test_entity_save does not invoke hook_entity_presave().
+  module_invoke_all('entity_presave', $entity, 'test_entity');
+  field_test_entity_save($entity);
+  // field_test_entity_save does not invoke hook_entity_insert().
+  $hook = $entity->is_new ? 'entity_insert' : 'entity_update';
+  module_invoke_all($hook, $entity, 'test_entity');
+}
+
+/**
+ * Load the given test entity.
+ */
+function title_test_entity_test_load($entity) {
+  $entity = field_test_entity_test_load($entity->ftid);
+  // field_test_entity_load does not invoke hook_entity_load().
+  module_invoke_all('entity_load', array($entity), 'test_entity');
+  return $entity;
+}
+
+/**
+ * Store a value for the given phase.
+ */
+function title_test_phase_store($phase = NULL, $value = NULL) {
+  $store = &drupal_static(__FUNCTION__, array());
+  if (isset($phase)) {
+    $store[$phase] = $value;
+  }
+  return $store;
+}
+
+/**
+ * Check the entity label at a give phase.
+ */
+function title_test_phase_check($phase, $entity) {
+  $info = entity_get_info('test_entity');
+  $label_key = $info['entity keys']['label'];
+  $field_name = $label_key . '_field';
+  $value = $entity->{$label_key} == $entity->{$field_name}[LANGUAGE_NONE][0]['value'];
+  title_test_phase_store($phase, $value);
+  return $value;
+}
+
+/**
+ * Implements hook_entity_presave().
+ */
+function title_test_entity_presave($entity, $type) {
+  if ($type == 'test_entity') {
+    $info = entity_get_info('test_entity');
+    $label_key = $info['entity keys']['label'];
+    $entity->{$label_key} = DrupalWebTestCase::randomName();
+  }
+}
+
+/**
+ * Implements hook_field_attach_load().
+ */
+function title_test_field_attach_load($entity_type, $entities, $age, $options) {
+  if ($entity_type == 'test_entity') {
+    title_test_phase_check('field_attach_load', current($entities));
+  }
+}
+
+/**
+ * Implements hook_entity_load().
+ */
+function title_test_entity_load($entities, $type) {
+  if ($type == 'test_entity') {
+    title_test_phase_check('entity_load', current($entities));
+  }
+}
+
+/**
+ * Implements hook_entity_prepare_view().
+ */
+function title_test_entity_prepare_view($entities, $type, $langcode = NULL) {
+  if ($type == 'test_entity') {
+    title_test_phase_check('entity_prepare_view', current($entities));
+  }
+}
diff --git a/title.admin.inc b/title.admin.inc
new file mode 100644
index 0000000..05e23db
--- /dev/null
+++ b/title.admin.inc
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * @file
+ * Admin page callbacks for the Title module.
+ */
+
+/**
+ * Provide settings to enable title field.
+ */
+function title_form_field_ui_overview(&$form, &$form_state) {
+  $entity_info = entity_get_info($form['#entity_type']);
+
+  if (!empty($entity_info['field replacement'])) {
+    $field_replacement_info = $entity_info['field replacement'];
+    $admin_path = _field_ui_bundle_admin_path($form['#entity_type'], $form['#bundle']);
+    $form['fields']['#header'][6]['colspan'] += 1;
+
+    foreach (element_children($form['fields']) as $field_name) {
+      if (isset($field_replacement_info[$field_name])) {
+        $form['fields'][$field_name]['field_replacement'] = array(
+          '#type' => 'link',
+          '#title' => t('replace'),
+          '#href' => $admin_path . '/fields/replace/' . $field_name,
+          '#options' => array('attributes' => array('title' => t('Replace %field with a field instance.', array('%field' => $field_name)))),
+        );
+      }
+      else {
+        $form['fields'][$field_name]['field_replacement'] = array();
+      }
+    }
+  }
+}
+
+/**
+ * Generate a field replacement form.
+ */
+function title_field_replacement_form($form, $form_state, $entity_type, $bundle, $field_name) {
+  $bundle_name = field_extract_bundle($entity_type, $bundle);
+  $entity_info = entity_get_info($entity_type);
+  $info = $entity_info['field replacement'][$field_name];
+  $instance = field_info_instance($entity_type, $info['field']['field_name'], $bundle_name);
+  $enabled = !empty($instance);
+
+  $form['#entity_type'] = $entity_type;
+  $form['#bundle'] = $bundle_name;
+  $form['#field_name'] = $field_name;
+
+  $form['enabled'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Replace %field with a field instance.', array('%field' => $field_name)),
+    '#description' => t('If this is enabled the %field legacy field will be replaced with a regular field and will disappear from the <em>Manage fields</em> page. It will get back if the replacing field instance is deleted.', array('%field' => $field_name)),
+    '#default_value' => $enabled,
+    '#disabled' => $enabled,
+  );
+
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save settings'));
+  return $form;
+}
+
+/**
+ * Process field replacement form subissions.
+ */
+function title_field_replacement_form_submit($form, &$form_state) {
+  if ($form_state['values']['enabled'] != $form['enabled']['#default_value']) {
+    if (title_field_replacement_toggle($form['#entity_type'], $form['#bundle'], $form['#field_name'])) {
+      drupal_set_message(t('%field replaced with a field instance.', array('%field' => $form['#field_name'])));
+    }
+    else {
+      drupal_set_message(t('Field replacement removed.'));
+    }
+  }
+  $form_state['redirect'] = _field_ui_bundle_admin_path($form['#entity_type'], $form['#bundle']) . '/fields';
+}
diff --git a/title.core.inc b/title.core.inc
new file mode 100644
index 0000000..50461ae
--- /dev/null
+++ b/title.core.inc
@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * @file
+ * Provide field replacement information for core entities and type specific
+ * callbacks.
+ */
+
+/**
+ * Implements hook_entity_info().
+ */
+function title_entity_info() {
+  $info = array();
+
+  $field = array(
+    'type' => 'text',
+    'cardinality' => 1,
+    'translatable' => TRUE,
+  );
+
+  $instance = array(
+    'required' => TRUE,
+    'settings' => array(
+      'text_processing' => 0,
+    ),
+    'widget' => array(
+      'weight' => -5,
+    ),
+  );
+
+  $info['node'] = array(
+    'field replacement' => array(
+      'title' => array(
+        'field' => $field,
+        'instance' => array(
+          'label' => t('Title'),
+          'description' => t('A field replacing node title.'),
+        ) + $instance,
+      ),
+    ),
+  );
+
+  $info['taxonomy_term'] = array(
+    'field replacement' => array(
+      'name' => array(
+        'field' => $field,
+        'instance' => array(
+          'label' => t('Name'),
+          'description' => t('A field replacing taxonomy term name.'),
+        ) + $instance,
+      ),
+      'description' => array(
+        'field' => array(
+          'type' => 'text_with_summary',
+        ) + $field,
+        'instance' => array(
+          'required' => FALSE,
+          'label' => t('Description'),
+          'description' => t('A field replacing taxonomy term description.'),
+          'settings' => array(
+            'text_processing' => 1,
+          ),
+        ) + $instance,
+        'callbacks' => array(
+          'submit' => 'title_field_term_description_submit',
+        ),
+        'additional keys' => array(
+          'format' => 'format',
+        ),
+      ),
+    ),
+  );
+
+  $info['comment'] = array(
+    'field replacement' => array(
+      'subject' => array(
+        'field' => $field,
+        'instance' => array(
+          'label' => t('Subject'),
+          'description' => t('A field replacing comment subject.'),
+        ) + $instance,
+      ),
+    ),
+  );
+
+  return $info;
+}
+
+/**
+ * Submit callback for the taxonomy term description.
+ */
+function title_field_term_description_submit(&$values, $legacy_field, $info, $langcode) {
+  $values['description'] = array();
+  foreach (array('value', 'format') as $key) {
+    $values['description'][$key] = $values[$info['field']['field_name']][$langcode][0][$key];
+  }
+}
+
+/**
+ * Sync callback for the text field type.
+ */
+function title_field_text_sync_get($entity_type, $entity, $legacy_field, $info, $langcode) {
+  $wrapper = entity_metadata_wrapper($entity_type, $entity);
+  $wrapper->language($langcode);
+  return $wrapper->{$info['field']['field_name']}->raw();
+}
+
+/**
+ * Sync back callback for the text field type.
+ */
+function title_field_text_sync_set($entity_type, $entity, $legacy_field, $info, $langcode) {
+  $wrapper = entity_metadata_wrapper($entity_type, $entity);
+  $wrapper->language($langcode);
+  $wrapper->{$info['field']['field_name']}->set($entity->{$legacy_field});
+}
+
+/**
+ * Sync callback for the text with summary field type.
+ */
+function title_field_text_with_summary_sync_get($entity_type, $entity, $legacy_field, $info, $langcode) {
+  $format_key = $info['additional keys']['format'];
+  $wrapper = entity_metadata_wrapper($entity_type, $entity);
+  $wrapper->language($langcode);
+  $field_name = $info['field']['field_name'];
+  $entity->{$format_key} = $wrapper->{$field_name}->format->raw();
+  return $wrapper->{$field_name}->value->raw();
+}
+
+/**
+ * Sync back callback for the text with summary field type.
+ */
+function title_field_text_with_summary_sync_set($entity_type, $entity, $legacy_field, $info, $langcode) {
+  $format_key = $info['additional keys']['format'];
+  $value = array('value' => $entity->{$legacy_field}, 'format' => $entity->{$format_key});
+  $wrapper = entity_metadata_wrapper($entity_type, $entity);
+  $wrapper->language($langcode);
+  $wrapper->{$info['field']['field_name']}->set($value);
+}
diff --git a/title.info b/title.info
index 8f642a7..e7f43e1 100644
--- a/title.info
+++ b/title.info
@@ -1,7 +1,6 @@
 name = Title
-description = Allows entity titles/labels to be translated.
-package = Multilingual
+description = Replaces entity legacy fields with regular fields.
 core = 7.x
-dependencies[] = locale
 files[] = title.module
 files[] = tests/title.test
+dependencies[] = entity
diff --git a/title.install b/title.install
index 35cabb8..b60b8c5 100644
--- a/title.install
+++ b/title.install
@@ -2,6 +2,16 @@
 
 /**
  * @file
- * Installation functions for Title module.
+ * Installation functions for the Title module.
  */
 
+/**
+ * Implements hook_install().
+ */
+function title_install() {
+  // Make sure fields are properly handled before any other module access them.
+  db_update('system')
+    ->fields(array('weight' => -100))
+    ->condition('name', 'title')
+    ->execute();
+}
diff --git a/title.module b/title.module
index 2c20b56..709edcc 100644
--- a/title.module
+++ b/title.module
@@ -2,6 +2,463 @@
 
 /**
  * @file
- * Translatable entity title/label functionality.
+ * Replaces entity legacy fields with regular fields.
+ *
+ * Provides an API and a basic UI to replace legacy pseudo-fields with regular
+ * fields. The API only offers synchronization between the two data storage
+ * systems and data replacement on entity load/save. Field definitions have to
+ * be provided by the modules exploiting the API.
+ *
+ * Title implements its own entity description API to describe core legacy
+ * pseudo-fields:
+ * - Node: title
+ * - Taxonomy Term: name, description
+ * - Comment: subject
+ *
+ * @todo: API PHPdocs
  */
 
+module_load_include('inc', 'title', 'title.core');
+
+/**
+ * Implements hook_entity_info_alter().
+ */
+function title_entity_info_alter(&$info) {
+  foreach ($info as $entity_type => $entity_info) {
+    if ($entity_info['fieldable'] && !empty($info[$entity_type]['field replacement'])) {
+      foreach ($info[$entity_type]['field replacement'] as $legacy_field => $data) {
+        // Provide defaults for the replacing field name.
+        $fr_info = &$info[$entity_type]['field replacement'][$legacy_field];
+        if (empty($fr_info['field']['field_name'])) {
+          $fr_info['field']['field_name'] = $legacy_field . '_field';
+        }
+        $fr_info['instance']['field_name'] = $fr_info['field']['field_name'];
+
+        // Provide defaults for the sync callbacks.
+        $type = $fr_info['field']['type'];
+        if (empty($fr_info['callbacks'])) {
+          $fr_info['callbacks'] = array();
+        }
+        $fr_info['callbacks'] += array(
+          'sync_get' => "title_field_{$type}_sync_get",
+          'sync_set' => "title_field_{$type}_sync_set",
+        );
+
+        // Support add explicit support for entity_label().
+        // @todo Currently core does not pass the entity type to the label
+        // callback thus rendering impossible a generalized handling of the
+        // entity label.
+        // @see http://drupal.org/node/1096446
+        if (false && isset($entity_info['entity keys']['label']) && $entity_info['entity keys']['label'] == $legacy_field) {
+          $info[$entity_type]['label callback'] = 'title_entity_label';
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Return field replacement specific information.
+ */
+function title_field_replacement_info($entity_type, $legacy_field = NULL) {
+  $info = entity_get_info($entity_type);
+  if (empty($info['field replacement'])) {
+    return FALSE;
+  }
+  return isset($legacy_field) ? $info['field replacement'][$legacy_field] : $info['field replacement'];
+}
+
+/**
+ * Return an entity label value.
+ *
+ * @param $entity
+ *   The entity whose label has to be displayed.
+ * @param $type
+ *   The entity type.
+ * @param $langcode
+ *   (Optional) The language the entity label has to be displayed in.
+ *
+ * @return
+ *   The entity label as a string value.
+ */
+function title_entity_label($entity, $type, $langcode = NULL) {
+  $entity_info = entity_get_info($type);
+  $legacy_field = $entity_info['entity keys']['label'];
+  $info = $entity_info['field replacement'][$legacy_field];
+  $langcode = field_language($type, $entity, $info['field']['field_name'], $langcode);
+  return $info['callbacks']['sync_get']($type, $entity, $legacy_field, $info, $langcode);
+}
+
+/**
+ * Implements hook_entity_presave().
+ */
+function title_entity_presave($entity, $type) {
+  title_entity_sync($type, $entity, NULL, TRUE);
+  // Store a copy of the synchronized values to check if they have been altered
+  // before saving.
+  $entity->field_replacement = clone($entity);
+}
+
+/**
+ * Implements hook_entity_insert().
+ */
+function title_entity_insert($entity, $type) {
+  title_entity_update($entity, $type);
+}
+
+/**
+ * Implements hook_entity_update().
+ *
+ * Since Title is supposed to act as the first module on hook invocation, legacy
+ * field values might be altered by subsequent hook implementations after
+ * reverse synchronization has happened. If this happens the field values must
+ * be synchronized again and the updated versions must be saved.
+ */
+function title_entity_update($entity, $type) {
+  $fr_info = title_field_replacement_info($type);
+
+  if ($fr_info) {
+    $update = FALSE;
+    list(, , $bundle) = entity_extract_ids($type, $entity);
+
+    foreach ($fr_info as $legacy_field => $info) {
+      if ($entity->{$legacy_field} !== $entity->field_replacement->{$legacy_field} && title_field_replacement_enabled($type, $bundle, $legacy_field)) {
+        title_field_sync_set($type, $entity, $legacy_field, $info);
+        $update = TRUE;
+      }
+    }
+
+    if ($update) {
+      // Save updated field values.
+      field_attach_update($type, $entity);
+    }
+  }
+}
+
+/**
+ * Implements hook_field_attach_load().
+ *
+ * Synchronization must be performed as early as possible to prevent other code
+ * from accessing replaced fields before they get their actual value.
+ *
+ * @see title_entity_load()
+ */
+function title_field_attach_load($entity_type, $entities, $age, $options) {
+  // @todo: Do we need to handle revisions here?
+  title_entity_load($entities, $entity_type);
+}
+
+/**
+ * Implements hook_entity_load().
+ *
+ * Since the result of field_attach_load() is cached, synchronization must be
+ * performed also here to ensure that there is always the correct value in the
+ * replaced fields.
+ */
+function title_entity_load($entities, $type) {
+  foreach ($entities as &$entity) {
+    title_entity_sync($type, $entity);
+  }
+}
+
+/**
+ * Implements hook_entitycache_load().
+ *
+ * Entity cache might cache the entire $entity object, in which case
+ * synchronization will not be performed on entity load.
+ */
+function title_entitycache_load($entities, $type) {
+  foreach ($entities as &$entity) {
+    title_entity_sync($type, $entity);
+  }
+}
+
+/**
+ * Implements hook_entity_prepare_view().
+ *
+ * On load synchronization is performed using the current display language. A
+ * different language might be specified while viewing the entity in which case
+ * synchronization must be performed again.
+ *
+ * @todo $langcode is not passed along by entity_prepare_view() currently. We
+ * will need to remove the default NULL value once this is fixed.
+ * @see http://drupal.org/node/1089174
+ */
+function title_entity_prepare_view($entities, $type, $langcode = NULL) {
+  foreach ($entities as &$entity) {
+    title_entity_sync($type, $entity, $langcode);
+  }
+}
+
+/**
+ * Check whether field replacement is enabled for the given field.
+ *
+ * @param $entity_type
+ *   The type of $entity.
+ * @param $bundle
+ *   The bundle the legacy field belongs to.
+ * @param $legacy_field
+ *   The name of the legacy field to be replaced.
+ *
+ * @return
+ *   TRUE if field replacement is enabled for the given field, FALSE otherwise.
+ */
+function title_field_replacement_enabled($entity_type, $bundle, $legacy_field) {
+  $info = title_field_replacement_info($entity_type, $legacy_field);
+  $instance = field_info_instance($entity_type, $info['field']['field_name'], $bundle);
+  return !empty($instance);
+}
+
+/**
+ * Toggle field replacement for the given field.
+ *
+ * @param $entity_type
+ *   The name of the entity type.
+ * @param $bundle
+ *   The bundle the legacy field belongs to.
+ * @param $legacy_field
+ *   The name of the legacy field to be replaced.
+ */
+function title_field_replacement_toggle($entity_type, $bundle, $legacy_field) {
+  $info = title_field_replacement_info($entity_type, $legacy_field);
+
+  if (!$info) {
+    return;
+  }
+
+  $field_name = $info['field']['field_name'];
+  $instance = field_info_instance($entity_type, $field_name, $bundle);
+
+  if (empty($instance)) {
+    $field = field_info_field($field_name);
+    if (empty($field)) {
+      field_create_field($info['field']);
+    }
+    $info['instance']['entity_type'] = $entity_type;
+    $info['instance']['bundle'] = $bundle;
+    field_create_instance($info['instance']);
+    return TRUE;
+  }
+  else {
+    field_delete_instance($instance);
+    return FALSE;
+  }
+}
+
+/**
+ * Synchronize replaced fields with the regular field values.
+ *
+ * @param $entity_type
+ *   The name of the entity type.
+ * @param $entity
+ *   The entity to work with.
+ * @param $set
+ *   Specifies the direction synchronization must be performed.
+ */
+function title_entity_sync($entity_type, &$entity, $langcode = NULL, $set = FALSE) {
+  $sync = &drupal_static(__FUNCTION__, array());
+  list($id, , $bundle) = entity_extract_ids($entity_type, $entity);
+  $langcode = field_valid_language($langcode, FALSE);
+
+  // We do not need to perform this more than once.
+  if (!empty($sync[$entity_type][$id][$langcode][$set])) {
+    return;
+  }
+
+  $sync[$entity_type][$id][$langcode][$set] = TRUE;
+  $fr_info = title_field_replacement_info($entity_type);
+
+  if ($fr_info) {
+    foreach ($fr_info as $legacy_field => $info) {
+      if (title_field_replacement_enabled($entity_type, $bundle, $legacy_field)) {
+        $function = 'title_field_sync_' . ($set ? 'set' : 'get');
+        $function($entity_type, $entity, $legacy_field, $info, $langcode);
+      }
+    }
+  }
+}
+
+/**
+ * Synchronize a single legacy field with its regular field value.
+ *
+ * @param $entity_type
+ *   The name of the entity type.
+ * @param $entity
+ *   The entity to work with.
+ * @param $legacy_field
+ *   The name of the legacy field to be replaced.
+ * @param $field_name
+ *   The regular field to use as source value.
+ * @param $display
+ *   Specifies if synchronization is being performed on display or on save.
+ * @param $langcode
+ *   The field language to use for the source value.
+ */
+function title_field_sync_get($entity_type, $entity, $legacy_field, $info, $langcode = NULL) {
+  if (isset($entity->{$legacy_field})) {
+    // Find out the actual language to use (field might be untranslatable).
+    $langcode = field_language($entity_type, $entity, $info['field']['field_name'], $langcode);
+    $entity->{$legacy_field} = $info['callbacks']['sync_get']($entity_type, $entity, $legacy_field, $info, $langcode);
+  }
+}
+
+/**
+ * Synchronize a single regular field from its legacy field value.
+ *
+ * @param $entity_type
+ *   The name of the entity type.
+ * @param $entity
+ *   The entity to work with.
+ * @param $legacy_field
+ *   The name of the legacy field to be replaced.
+ * @param $field_name
+ *   The regular field to use as source value.
+ * @param $display
+ *   Specifies if synchronization is being performed on display or on save.
+ * @param $langcode
+ *   The field language to use for the source value.
+ */
+function title_field_sync_set($entity_type, $entity, $legacy_field, $info) {
+  if (isset($entity->{$legacy_field})) {
+    $langcode = title_entity_language($entity_type, $entity);
+    $info['callbacks']['sync_set']($entity_type, $entity, $legacy_field, $info, $langcode);
+  }
+}
+
+/**
+ * Provide the original entity language.
+ *
+ * @param $entity_type
+ * @param $entity
+ *
+ * @return
+ *   A language code
+ */
+function title_entity_language($entity_type, $entity) {
+  // If a language property is defined for the current entity we synchronize
+  // the field value using the entity language, otherwise we fall back to
+  // LANGUAGE_NONE.
+  try {
+    return entity_metadata_wrapper($entity_type, $entity)->language->value();
+  }
+  catch (EntityMetadataWrapperException $e) {
+    return LANGUAGE_NONE;
+  }
+}
+
+/**
+ * Implements hook_field_attach_form().
+ *
+ * Hide legacy field widgets on the assumption that this is always called on
+ * fieldable entity forms.
+ */
+function title_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {
+  list(, , $bundle) = entity_extract_ids($entity_type, $entity);
+  $fr_info = title_field_replacement_info($entity_type);
+
+  if (!empty($fr_info)) {
+    foreach ($fr_info as $legacy_field => $info)  {
+      if (isset($form[$legacy_field]) && title_field_replacement_enabled($entity_type, $bundle, $legacy_field)) {
+        $form[$legacy_field]['#access'] = FALSE;
+        $form[$legacy_field]['#field_replacement'] = TRUE;
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_field_attach_submit().
+ *
+ * Synchronize submitted field values into the corresponding legacy fields.
+ */
+function title_field_attach_submit($entity_type, $entity, $form, &$form_state) {
+  $fr_info = title_field_replacement_info($entity_type);
+
+  if (!empty($fr_info)) {
+    $values = &$form_state['values'];
+    $values = &drupal_array_get_nested_value($values, $form['#parents']);
+    $fr_info = title_field_replacement_info($entity_type);
+
+    foreach ($fr_info as $legacy_field => $info) {
+      if (!empty($form[$legacy_field]['#field_replacement'])) {
+        $field_name = $info['field']['field_name'];
+        $langcode = $form[$field_name]['#language'];
+
+        // Give a chance to operate on submitted values either.
+        if (!empty($info['callbacks']['submit'])) {
+          $info['callbacks']['submit']($values, $legacy_field, $info, $langcode);
+        }
+
+        title_field_sync_get($entity_type, $entity, $legacy_field, $info, $langcode);
+      }
+    }
+  }
+}
+
+/**
+ * Implements of hook_menu().
+ */
+function title_menu() {
+  $items = array();
+
+  foreach (entity_get_info() as $entity_type => $entity_info) {
+    if (!empty($entity_info['field replacement'])) {
+      foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
+        // Blindly taken from field_ui_menu().
+        if (isset($bundle_info['admin'])) {
+          $path = $bundle_info['admin']['path'];
+
+          if (isset($bundle_info['admin']['bundle argument'])) {
+            $bundle_arg = $bundle_info['admin']['bundle argument'];
+          }
+          else {
+            $bundle_arg = $bundle_name;
+          }
+
+          $access = array_intersect_key($bundle_info['admin'], drupal_map_assoc(array('access callback', 'access arguments')));
+          $access += array(
+            'access callback' => 'user_access',
+            'access arguments' => array('administer site configuration'),
+          );
+
+          $items["$path/fields/replace"] = array(
+            'load arguments' => array(),
+            'title' => 'Replace fields',
+            'page callback' => 'drupal_get_form',
+            'page arguments' => array('title_field_replacement_form', $entity_type, $bundle_arg),
+            'file' => 'title.admin.inc',
+          ) + $access;
+        }
+      }
+    }
+  }
+
+  return $items;
+}
+
+/**
+ * Implements hook_field_extra_fields_alter().
+ */
+function title_field_extra_fields_alter(&$info) {
+  $entity_info = entity_get_info();
+  foreach ($info as $entity_type => $bundles) {
+    foreach ($bundles as $bundle_name => $bundle) {
+      if (!empty($entity_info[$entity_type]['field replacement'])) {
+        foreach ($entity_info[$entity_type]['field replacement'] as $field_name => $field_replacement_info) {
+          if (title_field_replacement_enabled($entity_type, $bundle_name, $field_name)) {
+            // Remove the replaced legacy field.
+            unset($info[$entity_type][$bundle_name]['form'][$field_name]);
+          }
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function title_form_field_ui_field_overview_form_alter(&$form, &$form_state) {
+  module_load_include('inc', 'title', 'title.admin');
+  title_form_field_ui_overview($form, $form_state);
+}
