diff --git a/i18n_block/i18n_block.module b/i18n_block/i18n_block.module
index 949f935..b5aa0c1 100644
--- a/i18n_block/i18n_block.module
+++ b/i18n_block/i18n_block.module
@@ -10,6 +10,91 @@
  */
 
 /**
+ * Implements hook_menu().
+ *
+ * Add translate tab to blocks.
+ */
+function i18n_block_menu() {
+  $items['admin/structure/block/manage/%/%/translate'] = array(
+    'title' => 'Translate',
+    'access callback' => 'i18n_block_translate_tab_access',
+    'access arguments' => array(4, 5),
+    'page callback' => 'i18n_block_translate_tab_page',
+    'page arguments' => array(4, 5),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 10,
+  );
+  $items['admin/structure/block/manage/%/%/translate/%language'] = array(
+    'title' => 'Translate',
+    'access callback' => 'i18n_block_translate_tab_access',
+    'access arguments' => array(4, 5),
+    'page callback' => 'i18n_block_translate_tab_page',
+    'page arguments' => array(4, 5, 7),
+    'type' => MENU_CALLBACK,
+    'weight' => 10,
+  );
+  return $items;
+}
+
+/**
+ * Implement hook_menu_alter().
+ *
+ * Reorganize block tabs so that they make sense.
+ */
+function i18n_block_menu_alter(&$items) {
+  // Give the configure tab a short name and make it display.
+  $items['admin/structure/block/manage/%/%/configure']['weight'] = -100;
+  $items['admin/structure/block/manage/%/%/configure']['title'] = 'Configure';
+  unset($items['admin/structure/block/manage/%/%/configure']['context']);
+  // Hide the delete tab. Not sure why this was even set a local task then
+  // set to not show in any context...
+  $items['admin/structure/block/manage/%/%/delete']['type'] = MENU_CALLBACK;
+}
+
+/**
+ * Menu access callback function.
+ *
+ * Only let blocks translated which are configured to be translatable.
+ */
+function i18n_block_translate_tab_access($module, $delta) {
+  $block = block_load($module, $delta);
+  return user_access('translate interface') && isset($block) && ($block->i18n_mode == I18N_MODE_LOCALIZE);
+}
+
+/**
+ * Build a translation page for the given block.
+ */
+function i18n_block_translate_tab_page($module, $delta, $language = NULL) {
+  $block = block_load($module, $delta);
+  $form_meta = array(
+    '#page_title' => t('Translate block'),
+    '#item_title_key' => array('blocks', $block->module, $block->delta, 'title'),
+    '#item_title_default' => $block->title,
+    '#edit' => 'admin/structure/block/manage/' . $block->module . '/' . $block->delta . '/configure',
+    '#translate' => 'admin/structure/block/manage/' . $block->module . '/' . $block->delta . '/translate/',
+    '#items' => array(),
+  );
+  if (!empty($block->title) && $block->title != '<none>') {
+    $form_meta['#items'][] = array(
+      '#title' => t('Block title'),
+      '#string_key' => array('blocks', $block->module, $block->delta, 'title'),
+      '#default_value' => $block->title,
+    );
+  }
+  if ($block->module == 'block') {
+    $custom_block = (object) block_custom_block_get($block->delta);
+    if (!empty($custom_block->body)) {
+      $form_meta['#items'][] = array(
+        '#title' => t('Block body'),
+        '#string_key' => array('blocks', $block->module, $block->delta, 'body'),
+        '#default_value' => $custom_block->body
+      );
+    }
+  }
+  return i18n_string_translate_page($form_meta, $language);
+}
+
+/**
  * Implements hook_block_list_alter().
  *
  * Translate localizable blocks.
diff --git a/i18n_string/i18n_string.inc b/i18n_string/i18n_string.inc
index d2729ce..a09a60a 100644
--- a/i18n_string/i18n_string.inc
+++ b/i18n_string/i18n_string.inc
@@ -581,7 +581,10 @@ class i18n_string_default {
    * Update string translation.
    */
   function update_translation($context, $langcode, $translation) {
-    if ($source = $this->get_source($context, $translation)) {
+    $i18nstring = (object) array(
+      'context' => implode(':', $context),
+    );
+    if ($source = $this->get_source($i18nstring)) {
       $source->language = $langcode;
       $source->translation = $translation;
       $this->save_translation($source);
@@ -770,5 +773,3 @@ function i18n_string_save_translation($translation) {
   }
   return $count;
 }
-
-
diff --git a/i18n_string/i18n_string.module b/i18n_string/i18n_string.module
index 93c8aad..acc2ab5 100644
--- a/i18n_string/i18n_string.module
+++ b/i18n_string/i18n_string.module
@@ -691,3 +691,104 @@ function i18n_string_object_update($type, $object) {
     return i18n_string_textgroup($info['string translation']['textgroup'])->update_object($type, $object);
   }
 }
\ No newline at end of file
+
+/**
+ * Generic translation interface for i18n_strings objects.
+ */
+function i18n_string_translate_page($form_meta, $langcode = NULL) {
+  if (empty($langcode)) {
+    drupal_set_title($form_meta['#page_title']);
+    return i18n_string_translate_page_overview($form_meta);
+  }
+  else {
+    $languages = language_list();
+    drupal_set_title(t('Translate to @language', array('@language' => $languages[$langcode]->name)));
+    return drupal_get_form('i18n_string_translate_page_form', $form_meta, $langcode);
+  }
+}
+
+/**
+ * Provide a core translation module like overview page for this object.
+ */
+function i18n_string_translate_page_overview($form_meta) {
+  include_once DRUPAL_ROOT . '/includes/language.inc';
+
+  $header = array(t('Language'), t('Title'), t('Status'), t('Operations'));
+  $default_language = language_default();
+  $rows = array();
+
+  foreach (language_list() as $langcode => $language) {
+    if ($langcode == $default_language->language) {
+      $rows[] = array(
+        $language->name . ' ' . t('(source)'),
+        $form_meta['#item_title_default'],
+        t('original'),
+        l(t('edit'), $form_meta['#edit']),
+      );
+    }
+    else {
+      // Try to figure out if this item has any of its properties translated.
+      $translated = FALSE;
+      foreach($form_meta['#items'] as $item) {
+        $str = i18n_string($item['#string_key'], $item['#default_value'], array('langcode' => $langcode, 'sanitize' => FALSE));
+        if ($str != $item['#default_value']) {
+          $translated = TRUE;
+          break;
+        }
+      }
+      // Translate the item that was requested to be displayed as title.
+      $item_title = i18n_string($form_meta['#item_title_key'], $form_meta['#item_title_default'], array('langcode' => $langcode));
+      $rows[] = array(
+        $language->name,
+        $item_title,
+        $translated ? t('translated') : t('not translated'),
+        l(t('translate'), $form_meta['#translate'] . $langcode),
+      );
+    }
+  }
+
+  $build['i18n_string_translation_overview'] = array(
+    '#theme' => 'table',
+    '#header' => $header,
+    '#rows' => $rows,
+  );
+
+  return $build;
+}
+
+/**
+ * Form builder callback for in-place string translation.
+ */
+function i18n_string_translate_page_form($form, &$form_state, $form_meta, $langcode) {
+  $form['langcode'] = array(
+    '#type' => 'value',
+    '#value' => $langcode,
+  );
+  $form['strings'] = array(
+    // Use a tree, so we can access the values easily.
+    '#tree' => TRUE,
+  );
+  foreach ($form_meta['#items'] as $item) {
+    $form['strings'][implode(':', $item['#string_key'])] = array(
+      '#title' => $item['#title'],
+      '#type' => 'textarea',
+      '#default_value' => i18n_string($item['#string_key'], $item['#default_value'], array('langcode' => $langcode, 'sanitize' => FALSE)),
+    );
+  }
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save translations'),
+  );
+  return $form;
+}
+
+/**
+ * Form submission callback for in-place string translation.
+ */
+function i18n_string_translate_page_form_submit($form, &$form_state) {
+  foreach($form_state['values']['strings'] as $key => $value) {
+    list($textgroup, $context) = i18n_string_context(explode(':', $key));
+    i18n_string_textgroup($textgroup)->update_translation($context, $form_state['values']['langcode'], $value);
+  }
+  drupal_set_message(t('Translations saved.'));
+}
