diff --git l10n_community/l10n_community.module l10n_community/l10n_community.module
index bbd3de7..3d32f95 100644
--- l10n_community/l10n_community.module
+++ l10n_community/l10n_community.module
@@ -207,33 +207,10 @@ function l10n_community_menu() {
   $items['translate/details'] = array(
     'title' => 'String details',
     'page callback' => 'l10n_community_string_details',
-    'file' => 'ajax.inc',
-    'access arguments' => array('browse translations'),
-    'type' => MENU_CALLBACK,
-  );
-  $items['translate/suggestions'] = array(
-    'title' => 'String suggestions',
-    'page callback' => 'l10n_community_string_suggestions',
-    'file' => 'ajax.inc',
+    'file' => 'translate.inc',
     'access arguments' => array('browse translations'),
     'type' => MENU_CALLBACK,
   );
-  $items['translate/approve'] = array(
-    'title' => 'Approve suggestion',
-    'page callback' => 'l10n_community_string_approve',
-    'file' => 'ajax.inc',
-    // Permission is enforced in l10n_community_string_ajax_suggestion().
-    'access arguments' => array('access localization community'),
-    'type' => MENU_CALLBACK,
-  );
-  $items['translate/decline'] = array(
-    'title' => 'Decline suggestion',
-    'page callback' => 'l10n_community_string_decline',
-    'file' => 'ajax.inc',
-    // Permission is enforced in l10n_community_string_ajax_suggestion().
-    'access arguments' => array('access localization community'),
-    'type' => MENU_CALLBACK,
-  );
 
   // As soon as we have a language code, we can translate.
   $items['translate/languages/%l10n_community_language'] = array(
@@ -251,8 +228,8 @@ function l10n_community_menu() {
     'weight' => -20,
   );
   // Tabs to translate, import and export projects.
-  $items['translate/languages/%l10n_community_language/view'] = array(
-    'title' => 'Browse',
+  $items['translate/languages/%l10n_community_language/translate'] = array(
+    'title' => 'Translate',
     'page callback' => 'l10n_community_translate_page',
     'page arguments' => array(2),
     'file' => 'translate.inc',
@@ -261,16 +238,6 @@ function l10n_community_menu() {
     'type' => MENU_LOCAL_TASK,
     'weight' => -10,
   );
-  $items['translate/languages/%l10n_community_language/edit'] = array(
-    'title' => 'Translate',
-    'page callback' => 'l10n_community_translate_page',
-    'page arguments' => array(2, 'edit'),
-    'file' => 'translate.inc',
-    'access callback' => 'l10n_community_has_permission',
-    'access arguments' => array(2, (string)L10N_PERM_SUGGEST),
-    'type' => MENU_LOCAL_TASK,
-    'weight' => -8,
-  );
   $items['translate/languages/%l10n_community_language/moderate'] = array(
     'title' => 'Moderate',
     'page callback' => 'l10n_community_moderate_page',
@@ -496,6 +463,7 @@ function l10n_community_block_help() {
       // We are dealing with a groups based permission model.
       $permission_help[] = l10n_groups_block_help($perm, isset($args['langcode']) ? $args['langcode'] : NULL);
     }
+
     if ($perm & L10N_PERM_SUGGEST) {
       $permission_notes[] = t('You can suggest translations to be reviewed by moderators of the team.');
     }
@@ -949,6 +917,19 @@ function l10n_community_get_languages($key = NULL) {
 }
 
 /**
+ * Returns a language object for a specific language.
+ *
+ * @param $langcode
+ *   Language code, for example 'hu', 'pt-br', 'de' or 'it'.
+ * @return
+ *   A populated language object.
+ */
+function l10n_community_get_language($langcode) {
+  $languages = l10n_community_get_languages();
+  return $languages[$langcode];
+}
+
+/**
  * Get translation permission level for a specific user.
  *
  * @param $langcode
@@ -958,7 +939,7 @@ function l10n_community_get_languages($key = NULL) {
  * @return
  *   A bitmask of all permission flags the user has.
  */
-function l10n_community_get_permission($langcode, $account = NULL) {
+function l10n_community_get_permission($langcode, $account = NULL, $check = NULL) {
   static $permissions = array();
   global $user;
 
@@ -1000,12 +981,47 @@ function l10n_community_get_permission($langcode, $account = NULL) {
  * @param $langcode
  *   Language code, for example 'hu', 'pt-br', 'de' or 'it'.
  * @param $permission
- *   The permission to check for.
+ *   The permission to check for. Note that if you pass multiple permission
+ *   constants, this function will return TRUE if the user has at least one
+ *   of them.
  * @return
  *   TRUE if the user has at least one of the specified permissions.
  */
 function l10n_community_has_permission($langcode, $permission) {
-  return (bool)(l10n_community_get_permission($langcode, NULL) & $permission);
+  return (bool)(l10n_community_get_permission($langcode) & $permission);
+}
+
+/**
+ * Checks whether the current user may approve a translation.
+ *
+ * @param $translation
+ *   The translation in question.
+ * @return
+ *   TRUE if the user may approve the translation.
+ */
+function l10n_community_may_approve_translation($translation) {
+  global $user;
+  $required = $translation->uid_entered == $user->uid ? L10N_PERM_MODERATE_OWN : L10N_PERM_MODERATE_OTHERS;
+  return l10n_community_has_permission($translation->language, $required);
+}
+
+/**
+ * Checks whether the current user may decline a translation.
+ *
+ * @param $translation
+ *   The translation in question.
+ * @return
+ *   TRUE if the user may decline the translation.
+ */
+function l10n_community_may_decline_translation($translation) {
+  global $user;
+  if ($translation->uid_entered == $user->uid) {
+    // Everyone may decline their own translations.
+    return TRUE;
+  }
+  else {
+    return l10n_community_has_permission($translation->language, L10N_PERM_MODERATE_OTHERS);
+  }
 }
 
 /**
@@ -1310,15 +1359,6 @@ function l10n_community_project_uri_by_title($title) {
   return db_result(db_query("SELECT uri FROM {l10n_community_project} WHERE title = '%s'", $title));
 }
 
-/**
- * Check whether $suggestion is duplicate for $sid in $langcode.
- */
-function l10n_community_is_duplicate($suggestion, $sid, $langcode) {
-  // Use BINARY matching to avoid marking case-corrections as duplicate.
-  // Matches everything active, regardless of being translations or suggestions.
-  return (bool) db_result(db_query("SELECT s.sid FROM {l10n_community_string} s LEFT JOIN {l10n_community_translation} t ON s.sid = t.sid WHERE t.translation = BINARY '%s' AND t.is_active = 1 AND t.language = '%s' AND s.sid = %d", $suggestion, $langcode, $sid));
-}
-
 // = Theme functions ===========================================================
 
 /**
@@ -1326,16 +1366,6 @@ function l10n_community_is_duplicate($suggestion, $sid, $langcode) {
  */
 function l10n_community_theme($existing, $type, $theme, $path) {
   return array(
-    // l10n_community.module
-    'l10n_community_button' => array(
-      'arguments' => array('type' => NULL, 'class' => NULL, 'extras' => ''),
-    ),
-    'l10n_community_strings' => array(
-      'arguments' => array('items' => NULL, 'form' => TRUE),
-    ),
-    'l10n_community_copy_button' => array(
-      'arguments' => array(),
-    ),
     // pages.inc
     'l10n_community_progress_columns' => array(
       'arguments' => array('sum' => NULL, 'translated' => NULL, 'has_suggestion' => NULL),
@@ -1350,12 +1380,27 @@ function l10n_community_theme($existing, $type, $theme, $path) {
     'l10n_community_filter_form' => array(
       'arguments' => array('form' => NULL),
     ),
-    'l10n_community_translate_form' => array(
-      'arguments' => array('form' => NULL),
+    'l10n_community_translate_translation' => array(
+      'arguments' => array('element' => NULL),
+    ),
+    'l10n_community_translate_actions' => array(
+      'arguments' => array('element' => NULL),
     ),
     'l10n_community_in_context' => array(
       'arguments' => array('source' => NULL),
     ),
+    'l10n_community_translate_radio' => array(
+      'arguments' => array('element' => NULL),
+    ),
+    'l10n_community_translate_source' => array(
+      'arguments' => array('element' => NULL),
+    ),
+    'l10n_community_translate_translation_list' => array(
+      'arguments' => array('element' => NULL),
+    ),
+    'l10n_community_translate_table' => array(
+      'arguments' => array('element' => NULL),
+    ),
     // l10n_community.admin.inc
     'l10n_community_admin_projects_form' => array(
       'arguments' => array('form' => NULL),
@@ -1371,77 +1416,6 @@ function l10n_community_theme($existing, $type, $theme, $path) {
 }
 
 /**
- * Theme a textual button.
- *
- * Text values are centralized here so it is easy to change.
- */
-function theme_l10n_community_button($type, $class, $extras = '') {
-  switch ($type) {
-    case 'translate':
-      $text = t('Translate');
-      break;
-    case 'lookup':
-      $text = t('Information');
-      break;
-    case 'edit':
-      // Source string and translation edit field.
-      $text = t('Edit');
-      break;
-    case 'has-suggestion':
-    case 'has-no-suggestion':
-    case 'untranslated':
-      // Star in a filled circle.
-      $text = t('Suggestions');
-      break;
-    case 'approve':
-      // Checkmark.
-      $text = t('Approve');
-      break;
-    case 'decline':
-      // Checkmark.
-      $text = t('Decline');
-      break;
-    case 'save':
-      // Save button.
-      $text = t('Save');
-      break;
-    case 'clear':
-      // Clear form button.
-      $text = t('Clear');
-      break;
-  }
-  return ' <span title="'. $text .'" class="'. $class .' l10n-button"'. (!empty($extras) ? (' '. $extras) : '') .'><b><b>'. $text .'</b></b></span>';
-}
-
-/**
- * Theme a list of translatable strings. Adds a copy button to each string
- * for quickly copying its source text into a translation form.
- */
-function theme_l10n_community_strings($items, $form = TRUE) {
-  $output = "<ul class='l10n-community-strings'>";
-  foreach ($items as $i => $item) {
-    $output .= "<li class='clear-block'>";
-    if ($form && ($i < 1)) {
-      // Only print copy button if we are displaying a form and on the first item.
-      // For plurals, only the first item will have the copy button, but will copy
-      // all the values into the form.
-      $output .= "<div class='buttons'>". theme('l10n_community_copy_button') ."</div>";
-    }
-    $output .= "<div class='string'>". $item ."</div>";
-    $output .= "</li>";
-  }
-  $output .= "</ul>";
-  return $output;
-}
-
-/**
- * Copy button for string values.
- */
-function theme_l10n_community_copy_button() {
-  return theme('l10n_community_button', 'edit', 'l10n-community-copy');
-}
-
-/**
  * Format string for display. Takes plurals into account.
  */
 function l10n_community_format_string($value, $rich_markup = TRUE) {
diff --git l10n_community/translate.inc l10n_community/translate.inc
index 8d07552..b099c2d 100644
--- l10n_community/translate.inc
+++ l10n_community/translate.inc
@@ -6,50 +6,7 @@
  *   Translation view and editing pages for localization community.
  */
 
-// = Translation interface hub =================================================
-
-/**
- * Menu callback for the translation pages.
- *
- * Displays a translation view or translation edit page depending
- * on permissions. If no strings are found, an error is printed.
- *
- * @param $langcode
- *   Language code, for example 'hu', 'pt-br', 'de', 'it'.
- */
-function l10n_community_translate_page($langcode = NULL, $mode = 'view') {
-
-  // Add missing breadcrumb.
-  drupal_set_breadcrumb(
-    array(
-      l(t('Home'), NULL),
-      l(t('Translate'), 'translate')
-    )
-  );
-
-  $languages = l10n_community_get_languages();
-  $perm = l10n_community_get_permission($langcode);
-
-  $filters = l10n_community_build_filter_values($_GET);
-  $output = drupal_get_form('l10n_community_filter_form', $filters);
-
-  $strings = l10n_community_get_strings($languages[$langcode]->language, $filters, $filters['limit']);
-  if (!count($strings)) {
-    drupal_set_message(t('No strings found with this filter. Try adjusting the filter options.'));
-  }
-  elseif (!($perm & L10N_PERM_SUGGEST) || $mode == 'view') {
-    // For users without permission to translate or suggest, display the view.
-    drupal_set_title(t('@language translations', array('@language' => $languages[$langcode]->name)));
-    $output .= l10n_community_translate_view($strings, $languages[$langcode], $filters);
-  }
-  else {
-    // For users with some permission, display the form.
-    drupal_add_js(drupal_get_path('module', 'l10n_community') .'/l10n_community.js');
-    drupal_set_title(t('Translate to @language', array('@language' => $languages[$langcode]->name)));
-    $output .= drupal_get_form('l10n_community_translate_form', $strings, $languages[$langcode], $filters, $perm);
-  }
-  return $output;
-}
+module_load_include('inc', 'l10n_community', 'api');
 
 // = Filter form handling ======================================================
 
@@ -74,7 +31,7 @@ function l10n_community_filter_form(&$form_state, $filters, $limited = FALSE) {
     L10N_STATUS_NO_SUGGESTION  => t('Has no suggestion'),
     L10N_STATUS_HAS_SUGGESTION => t('Has suggestion'),
   );
-  
+
   $form['project'] = array(
     '#title' => t('Project'),
     '#default_value' => isset($filters['project']) ? $filters['project']->title : '',
@@ -190,386 +147,431 @@ function l10n_community_filter_form_submit($form, &$form_state) {
   }
 }
 
-// = Translation viewer ========================================================
-
 /**
- * Form for translations display.
- *
- * @param $strings
- *   Array of string objects to display on the page.
- * @param $language
- *   Language object corresponding to the page displayed.
- * @param $filters
- *   Filters used to present this listing view.
+ * Theme function for l10n_community_filter_form.
  */
-function l10n_community_translate_view($strings = array(), $language = NULL, $filters = array()) {
-  $output = '';
-  $rows = array();
-  foreach ($strings as $string) {
-    $row = array();
-    // Source display
-    $source = l10n_community_format_string($string->value);
-    $source .= theme('l10n_community_in_context', $string);
-    $row[] = array('data' => $source, 'class' => 'source');
-
-    // Translation display.
-    if (!empty($string->translation)) {
-      if (strpos($string->value, chr(0)) !== FALSE) {
-        $translations = explode(chr(0), l10n_community_format_text($string->translation));
-        // Fill in any missing items, so it is shown that not all items are done.
-        if (count($translations) < $language->plurals) {
-          $translations = array_merge($translations, array_fill(0, count($translations) - $language->plurals, ''));
-        }
-        $translation = theme('item_list', $translations);
-      }
-      else {
-        $translation = l10n_community_format_text($string->translation);
-      }
-      $row[] = $translation;
-    }
-    else {
-      $row[] = '';
+function theme_l10n_community_filter_form($form) {
+  $row = array();
+  $labels = array();
+  // Only display these elements in distinct table cells
+  $elements = array('project', 'release', 'context', 'status', 'author', 'search', 'limit');
+  foreach ($form as $id => &$element) {
+    if (in_array($id, $elements)) {
+      $labels[] = $element['#title'];
+      unset($element['#title']);
+      $row[] = drupal_render($element);
     }
-    $rows[] = $row;
   }
-  $output .= ($pager = theme('pager', NULL, $filters['limit'], 0));
-  $output .= theme('table', array(t('Source Text'), t('Translations')), $rows, array('class' => 'l10n-server-translate'));
-  $output .= $pager;
-  $output = "<div id='l10n-community-translate-view'>". $output ."</div>";
-  return $output;
+  // Fill in the rest of the header above the buttons.
+  $labels[] = '';
+  // Display the rest of the form in the last cell
+  $row[] = array('data' => drupal_render($form), 'class' => 'last');
+  return theme('table', $labels, array($row), array('class' => 'l10n-server-filter'));
 }
 
-// = Translation editor ========================================================
+// = Translation view ==========================================================
 
 /**
- * Translation web interface.
- *
- * @param $strings
- *   Array of string objects to display.
- * @param $language
- *   Language object.
- * @param $filters
- *   Filters used to present this editing view.
- * @param $perm
- *   Community permission level of user watching the page.
+ * Menu callback: List translations and suggestions
  */
-function l10n_community_translate_form(&$form_state, $strings = array(), $language = NULL, $filters = array(), $perm = L10N_PERM_SUGGEST) {
-
-  if (isset($_GET['page'])) {
-    // Ensure that we keep all filter values, even the page number, so
-    // after submission, the same page can be shown.
-    $filters['page'] = (int) $_GET['page'];
-  }
+function l10n_community_translate_page($langcode) {
+  drupal_add_css(drupal_get_path('module', 'l10n_community') .'/editor.css');
+  drupal_add_js(drupal_get_path('module', 'l10n_community') .'/jquery.worddiff.js');
+  drupal_add_js(drupal_get_path('module', 'l10n_community') .'/editor.js');
 
-  $form = array(
-    '#tree' => TRUE,
-    '#redirect' => array($_GET['q'], l10n_community_flat_filters($filters))
-  );
-  $form['pager_top'] = array(
-    '#weight' => -10,
-    '#value' => ($pager = theme('pager', NULL, $filters['limit'], 0)),
-  );
-  $form['pager_bottom'] = array(
-    '#weight' => 10,
-    '#value' => $pager,
-  );
-  // Keep language code and URI in form for further reference.
-  $form['langcode'] = array(
-    '#type' => 'value',
-    '#value' => $language->language
-  );
-  $form['project'] = array(
-    '#type' => 'value',
-    '#value' => isset($project) ? $project->uri : NULL
-  );
+  $language = l10n_community_get_language($langcode);
+  $filters = l10n_community_build_filter_values($_GET);
+  $strings = l10n_community_get_strings($language->language, $filters, $filters['limit']);
 
-  foreach ($strings as $string) {
-    $form[$string->sid] = array(
-      '#tree' => TRUE,
-    );
+  // Set the most appropriate title.
+  if ($filters['project']) {
+    drupal_set_title(t('Translate %project to @language', array('%project' => $filters['project']->title, '@language' => $language->name)));
+  }
+  else {
+    drupal_set_title(t('Translate to @language', array('@language' => $language->name)));
+  }
 
-    // A toolbox which displays action icons on each string editor fieldset.
-    $toolbox = theme('l10n_community_button', 'translate', 'l10n-translate active');
-    $toolbox .= theme('l10n_community_button', 'lookup', 'l10n-lookup');
-    $toolbox .= $string->has_suggestion ? theme('l10n_community_button', 'has-suggestion', 'l10n-suggestions') : "";
-    $toolbox = "<div class='toolbox'>$toolbox</div>";
-    $form[$string->sid]['toolbox'] = array(
-      '#type' => 'markup',
-      '#value' => $toolbox,
-    );
-    $form[$string->sid]['messagebox'] = array(
-      '#type' => 'markup',
-      '#value' => "<div class='messagebox'></div>",
-    );
+  // Add the filter form.
+  $output = drupal_get_form('l10n_community_filter_form', $filters);
 
-    $is_plural = strpos($string->value, "\0");
-    // Multiple source strings if we deal with plurals. The form item and
-    // consequently the JavaScript strings identifiers are the sid and then
-    // the index of the plural being displayed.
-    $string_parts = explode(chr(0), $string->value);
-    foreach ($string_parts as $delta => &$part) {
-      $part = l10n_community_format_text($part, $string->sid, (count($string_parts) > 1) ? $delta : NULL);
-    }
-    $source = theme('l10n_community_strings', $string_parts);
-    $source .= theme('l10n_community_in_context', $string);
+  // Output the actual strings.
+  if (!count($strings)) {
+    drupal_set_message(t('No strings found with this filter. Try adjusting the filter options.'));
+  }
+  else {
+    $output .= drupal_get_form('l10n_community_translate_form', $language, $filters, $strings);
+  }
 
-    $form[$string->sid]['source'] = array(
-      '#type' => 'item',
-      '#value' => $source,
-    );
+  return $output;
+}
 
-    $translated = !empty($string->translation);
-    $form[$string->sid]['translation'] = array(
-      '#type' => 'item',
-      // Hide editing controls of translated stuff to save some space and guide user eyes.
-      '#prefix' => '<div id="l10n-community-wrapper-'. $string->sid .'"'. ($translated ? ' class="hidden"' : '') .'>',
-      '#suffix' => '</div>',
-    );
+/**
+ * Form callback: List translations and suggestions.
+ *
+ * @param $form_state
+ *   The form state array.
+ * @param $language
+ *   A language object.
+ * @param $filters
+ *   An array of filters applied to the strings.
+ * @param $strings
+ *   The strings to render.
+ */
+function l10n_community_translate_form(&$form_state, $language, $filters, $strings) {
+  $permission = l10n_community_get_permission($language->language);
+  $pager = theme('pager', NULL, $filters['limit'], 0);
+  $redirect_url = $_GET;
+  unset($redirect_url['q']);
 
-    if ($is_plural) {
+  $form = array(
+    '#submit' => array('l10n_community_translate_submit'),
+    '#redirect' => array($_GET['q'], $redirect_url),
+    'langcode' => array('#type' => 'value', '#value' => $language->language),
+    'pager_top' => array('#weight' => -10, '#value' => $pager),
+    'submit_top' => array('#type' => 'submit', '#value' => t('Save changes'), '#access' => $permission & L10N_PERM_SUGGEST),
+    'strings' => array('#tree' => TRUE, '#theme' => 'l10n_community_translate_table'),
+    'submit' => array('#type' => 'submit', '#value' => t('Save changes'), '#access' => $permission & L10N_PERM_SUGGEST),
+    'pager_bottom' => array('#weight' => 10, '#value' => $pager),
+  );
 
-      // Dealing with a string with plural versions.
-      if ($translated) {
-        // Add translation form element with all plural versions.
-        $translations = explode("\0", $string->translation);
-        $string_parts = array();
-        for ($i = 0; $i < $language->plurals; $i++) {
-          $target = $string->sid .'-'. $i;
-          $string_parts[] = l10n_community_format_text($translations[$i], $string->sid, $i);
-        }
-        $form[$string->sid]['translation_existing'] = array(
-          '#type' => 'item',
-          '#value' => theme('l10n_community_strings', $string_parts),
-        );
-      }
+  foreach ($strings as $string) {
+    $form['strings'][$string->sid] = _l10n_community_translate_string($form_state, $string, $language, $permission);
+  }
 
-      $string_parts = explode(chr(0), $string->value);
+  return $form;
+}
 
-      for ($i = 0; $i < $language->plurals; $i++) {
-        $target = $string->sid .'-'. $i;
-        if ($translated) {
-          // Already translated so we ask for new translation or suggestion.
-          $description = !($perm & L10N_PERM_MODERATE_OWN) ? t('New suggestion for variant #%d', array('%d' => $i)) : t('New translation for variant #%d', array('%d' => $i));
-        }
-        else {
-          // Not translated yet, so we ask for initial translation or suggestion.
-          $description = !($perm & L10N_PERM_MODERATE_OWN) ? t('Suggestion for variant #%d', array('%d' => $i)) : t('Translation for variant #%d', array('%d' => $i));
-        }
+/**
+ * Return a marked-up string.
+ */
+function _l10n_community_translate_render_strings($strings, $empty = '') {
+  if ($empty) {
+    $empty = ' data-empty="'. check_plain($empty) .'"';
+  }
+  return "<span$empty>". implode("</span><br /><span$empty>", array_map('check_plain', $strings)) .'</span>';
+}
 
-        // Include editing area for each plural variant.
-        $source_index = ($i > 0 ? 1 : 0);
-        $form[$string->sid]['translation']['value'][$i] = array(
-          // Use textarea for long and multiline strings.
-          '#type' => ((strlen($string_parts[$source_index]) > 45) || (count(explode("\n", $string_parts[$source_index])) > 1)) ? 'textarea' : 'textfield',
-          '#description' => $description,
-          '#rows' => 1,
-          '#id' => 'l10n-community-translation-'. $target,
-        );
-      }
-    }
+/**
+ * Creates the form fragment for a source string.
+ */
+function _l10n_community_translate_string(&$form_state, $source, $language, $permission) {
+  // Normalize empty default translation.
+  if (!$source->translation) {
+    $source->tid = '0';
+    $source->translation = array(t('(not translated)'));
+    $source->is_active = '1';
+    $source->is_suggestion = '0';
+  }
+  else {
+    $source->translation = l10n_community_unpack_string($source->translation);
+  }
 
-    // Dealing with a simple string (no plurals).
+  $source->value = l10n_community_unpack_string($source->value);
 
-    else {
-      if ($translated) {
-        $form[$string->sid]['translation_existing'] = array(
-          '#type' => 'item',
-          '#value' => theme('l10n_community_strings', array(l10n_community_format_text($string->translation, $string->sid))),
-        );
-      }
-      $form[$string->sid]['translation']['value'] = array(
-        // Use textarea for long and multiline strings.
-        '#type' => ((strlen($string->value) > 45) || (count(explode("\n", $string->value)) > 1)) ? 'textarea' : 'textfield',
-        // Provide accurate title based on previous data and permission.
-        '#description' => $translated ? (!($perm & L10N_PERM_MODERATE_OWN) ? t('Add a new suggestion') : t('Add a new translation')) : (!($perm & L10N_PERM_MODERATE_OWN) ? t('Suggestion') : ''),
-        '#rows' => 4,
-        '#resizable' => FALSE,
-        '#cols' => NULL,
-        '#size' => NULL,
-        '#id' => 'l10n-community-translation-'. $string->sid,
-      );
-      if (strlen($string->value) > 200) {
-        $form[$string->sid]['translation']['value']['#rows'] = floor(strlen($string->value) * .03);
-        $form[$string->sid]['translation']['value']['#resizable'] = TRUE;
-      }
-    }
+  $form = array(
+    '#string' => $source,
+    '#langcode' => $language->language,
+    'source' => array(
+      'string' => array('#value' => _l10n_community_translate_render_strings($source->value)),
+    ),
+  );
 
-    // Add AJAX saving buttons
-    $form[$string->sid]['translation']['save'] = array(
-      '#prefix' => "<span class='l10n-approval-buttons'>",
-      '#value' => theme('l10n_community_button', 'save', 'l10n-save'),
-      '#type' => 'markup',
-    );
-    $form[$string->sid]['translation']['clear'] = array(
-      '#suffix' => "</span>",
-      '#value' => theme('l10n_community_button', 'clear', 'l10n-clear'),
-      '#type' => 'markup',
+  if ($permission & L10N_PERM_SUGGEST) {
+    $form['source']['edit'] = array(
+      '#value' => t('Edit Copy'),
+      '#prefix' => '<label title="'. t('Edit Copy') .'">',
+      '#suffix' => '</label>',
     );
+  }
 
-    if (!($perm & L10N_PERM_MODERATE_OWN)) {
-      // User with suggestion capability only, record this.
-      $form[$string->sid]['translation']['is_suggestion'] = array(
-        '#type' => 'value',
-        '#value' => TRUE
-      );
-    }
-    else {
-      // User with full privileges, offer option to submit suggestion.
-      $form[$string->sid]['translation']['is_suggestion'] = array(
-        '#title' => t('Suggestion for discussion'),
-        '#type' => 'checkbox',
-      );
+  // Add the current string (either a approved translation or a mock object
+  // for the "untranslated" string).
+  $form[$source->tid] = _l10n_community_translate_translation($form_state, $source, $permission, $source);
+
+  // When there are suggestions, load them from the database.
+  if ($source->has_suggestion) {
+    $result = db_query("SELECT t.tid, t.sid, t.translation, t.uid_entered, t.time_entered, t.is_active, t.is_suggestion, u.name as username FROM {l10n_community_translation} t LEFT JOIN {users} u ON u.uid = t.uid_entered WHERE t.language = '%s' AND t.sid = %d AND t.is_active = 1 AND t.is_suggestion = 1 ORDER BY t.time_entered", $language->language, $source->sid);
+    while ($suggestion = db_fetch_object($result)) {
+      $suggestion->translation = l10n_community_unpack_string($suggestion->translation);
+      // Add the suggestion to the list.
+      $form[$suggestion->tid] = _l10n_community_translate_translation($form_state, $suggestion, $permission, $source);
     }
   }
 
-  // Add all strings for copy-pasting and some helpers.
-  drupal_add_js(
-    array(
-      'l10n_lookup_help'      => t('Show detailed information.'),
-      'l10n_approve_error'    => t('There was an error approving this suggestion. You might not have permission or the suggestion id was invalid.'),
-      'l10n_approve_confirm'  => t('!icon Suggestion approved.', array('!icon' => '&#10004;')),
+  // If the user may add new suggestions, display a textarea.
+  if ($permission & L10N_PERM_SUGGEST) {
+    $textarea = _l10n_community_translate_translation_textarea($source, $language);
+    $form[$textarea->tid] = _l10n_community_translate_translation($form_state, $textarea, $permission, $source);
+  }
 
-      'l10n_decline_error'    => t('There was an error declining this suggestion. You might not have permission or the suggestion id was invalid.'),
-      'l10n_decline_confirm'  => t('Suggestion declined.'),
+  return $form;
+}
 
-      'l10n_details_callback' => url('translate/details/'. $language->language .'/'),
-      'l10n_suggestions_callback' => url('translate/suggestions/'. $language->language .'/'),
-      'l10n_approve_callback' => url('translate/approve/'),
-      'l10n_decline_callback' => url('translate/decline/'),
-      'l10n_form_token_path'  => variable_get('clean_url', '0') ? '?form_token=' : '&form_token=',
-      'l10n_num_plurals'      => $language->plurals
-    ),
-    'setting'
-  );
+/**
+ * Build mock object for new textarea.
+ */
+function _l10n_community_translate_translation_textarea($source, $language) {
+  global $user;
 
-  // Let the user submit the form.
-  $form['submit'] = array(
-    '#type' => 'submit',
-    '#value' => !($perm & L10N_PERM_MODERATE_OWN) ? t('Save suggestions') : t('Save translations')
+  return (object)array(
+    'sid' => $source->sid,
+    'tid' => 'new',
+    'translation' => array_fill(0, count($source->value), ''),
+    'is_active' => '1',
+    'is_suggestion' => '1',
+    'uid_entered' => $user->uid,
   );
+}
 
-  $form['#theme'] = 'l10n_community_translate_form';
+/**
+ * Generates the byline containing meta information about a string.
+ */
+function l10n_community_translate_byline($string) {
+  $params = array(
+    '!author' => theme('username', (object)array('name' => $string->username, 'uid' => $string->uid_entered)),
+    '@date' => format_date($string->time_entered),
+    '@ago' => t('@time ago', array('@time' => format_interval(time() - $string->time_entered))),
+  );
 
-  return $form;
+  if (!empty($string->uid_approved) && $string->uid_entered === $string->uid_approved && $string->time_entered === $string->time_approved) {
+    return t('translated and approved by !author <span title="@ago">on @date</span>', $params);
+  }
+  else {
+    $title = t('suggested by !author <span title="@ago">on @date</span>', $params);
+    if (!empty($string->uid_approved)) {
+      $title .= '<br />'. t('approved by !author <span title="@ago">on @date</span>', array(
+        '!author' => theme('username', (object)array('name' => $string->username_approved, 'uid' => $string->uid_approved)),
+        '@date' => format_date($string->time_approved),
+        '@ago' => t('@time ago', array('@time' => format_interval(time() - $string->time_approved))),
+      ));
+    }
+    return $title;
+  }
 }
 
 /**
- * Save translations entered in the web form.
+ * Creates the form fragment for a translated string.
  */
-function l10n_community_translate_form_submit($form, &$form_state) {
+function _l10n_community_translate_translation(&$form_state, $string, $permission, $source) {
   global $user;
+  $is_own = $user->uid == $string->uid_entered;
+  $is_active = $string->is_active && !$string->is_suggestion;
+  $is_new = $string->tid == 'new';
+  $may_moderate = $permission & ($is_own ? L10N_PERM_MODERATE_OWN : L10N_PERM_MODERATE_OTHERS);
+  //$may_stabilize = $permission & ($is_own ? L10N_PERM_STABILIZE_OWN : L10N_PERM_STABILIZE_OTHERS);
 
-  $inserted = $updated = $unchanged = $suggested = $duplicates = $ignored = 0;
+  $form = array(
+    '#theme' => 'l10n_community_translate_translation',
+    'original' => array('#type' => 'value', '#value' => $string),
+  );
 
-  foreach ($form_state['values'] as $sid => $item) {
-    if (!is_array($item) || !isset($item['translation'])) {
-      // Skip, if we don't have translations in this form item,
-      // which means this is some other form value.
-      continue;
-    }
+  $form['active'] = array(
+    '#type' => 'radio',
+    '#theme' => 'l10n_community_translate_radio',
+    '#title' => _l10n_community_translate_render_strings($string->translation, $is_new ? t('(empty)') : FALSE),
+    '#return_value' => $string->tid,
+    '#default_value' => $is_active ? $string->tid : NULL,
+    '#parents' => array('strings', $string->sid, 'active'),
+    '#disabled' => !$may_moderate && !$is_active,
+    '#attributes' => array('class' => 'selector'),
+  );
 
-    $source_string = db_result(db_query('SELECT value FROM {l10n_community_string} WHERE sid = %d', $sid));
-    $text = '';
-    if (is_string($item['translation']['value']) && strlen(trim($item['translation']['value']))) {
-      // Single string representation: simple translation.
-      $text = l10n_community_trim($item['translation']['value'], $source_string);
+  if ($string->tid) {
+    if ($may_moderate && $string->tid != 'new') {
+      $form['declined'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Declined'),
+        '#default_value' => !($string->is_active || $string->is_suggestion),
+      );
     }
-    if (is_array($item['translation']['value'])) {
-      // Array -> plural variants are provided. Join them with a NULL separator.
-      $text = join("\0", $item['translation']['value']);
-      if (trim($text) == '') {
-        // If the whole string only contains NULL bytes, empty the string, so
-        // we don't save an empty translation. Otherwise the NULL bytes need
-        // to be there, so we know plural variant indices.
-        $text = '';
-      }
+    // if ($may_stabilize) {
+    //   $form['stable'] = array(
+    //     '#type' => 'checkbox',
+    //     '#title' => t('Stable'),
+    //     '#default_value' => FALSE, // $string->is_stable,
+    //   );
+    // }
+    if ($string->tid == 'new') {
+      $form['value'] = array_fill(0, count($source->value), array(
+        '#type' => 'textarea',
+        '#cols' => 60,
+        '#rows' => 3,
+        '#default_value' => t('<New translation>'),
+      ));
     }
+    else {
+      if ($permission & L10N_PERM_SUGGEST) {
+        $form['edit'] = array(
+          '#value' => t('Edit Copy'),
+          '#prefix' => '<label title="'. t('Edit Copy') .'">',
+          '#suffix' => '</label>',
+        );
+      }
+      if (isset($string->username)) {
+        $title = l10n_community_translate_byline($string);
 
-    if (!empty($text)) {
-      // Check for duplicate translation or suggestion.
-      if (l10n_community_is_duplicate($text, $sid, $form_state['values']['langcode'])) {
-        $duplicates++;
-        continue;
+        $form['author'] = array(
+          '#value' => $title,
+        );
       }
+    }
+  }
 
-      // We have some string to save.
-      l10n_community_target_save(
-        $sid, $text, $form_state['values']['langcode'], $user->uid,
-        ($item['translation']['is_suggestion'] == TRUE),
-        $inserted, $updated, $unchanged, $suggested
-      );
+  return $form;
+}
+
+function theme_l10n_community_translate_actions($element) {
+  $actions = '';
+  foreach (array('declined', /*'stable', */'edit') as $type) {
+    if (isset($element[$type])) {
+      $actions .= '<li class="'. $type .'">'. drupal_render($element[$type]) .'</li>';
     }
   }
+  if (!empty($actions)) {
+    return '<ul class="actions">'. $actions .'</ul>';
+  }
+  else {
+    return '';
+  }
+}
+
+function theme_l10n_community_translate_translation($element) {
+  if (!isset($element['#attributes']['class'])) {
+    $element['#attributes']['class'] = '';
+  }
+  $element['#attributes']['class'] .= ' clearfix translation';
+
+  switch ($element['active']['#return_value']) {
+    case 'new':
+      $element['#attributes']['class'] .= ' new-translation';
+      break;
+    case '0':
+      $element['#attributes']['class'] .= ' no-translation';
+      // Fallthrough.
+    default:
+      if ($element['active']['#value'] !== '') {
+        $element['#attributes']['class'] .= ' is-active default';
+      }
+  }
+
+  $output = '<li'. drupal_attributes($element['#attributes']) .'>';
+  $output .= theme('l10n_community_translate_actions', $element);
+  $output .= drupal_render($element['active']);
+
+  if (isset($element['author'])) {
+    $output .= '<div class="author">'. drupal_render($element['author']) .'</div>';
+  }
+
+  if (isset($element['value'])) {
+    $output .= drupal_render($element['value']);
+  }
 
-  // Inform user about changes made to the database.
-  l10n_community_update_message($inserted, $updated, $unchanged, $suggested, $duplicates, $ignored);
+  return $output  . '</li>';
 }
 
-// = Theme functions ===========================================================
+function theme_l10n_community_translate_radio($element) {
+  _form_set_class($element, array('form-radio'));
+  $output = '<input type="radio" ';
+  $output .= 'id="'. $element['#id'] .'" ';
+  $output .= 'name="'. $element['#name'] .'" ';
+  $output .= 'value="'. $element['#return_value'] .'" ';
+  $output .= (check_plain($element['#value']) == $element['#return_value']) ? ' checked="checked" ' : ' ';
+  $output .= drupal_attributes($element['#attributes']) .' />';
+
+  if (isset($element['#title'])) {
+    $output .= '<label class="l10n-string" for="'. $element['#id'] .'">'. $element['#title'] .'</label>';
+  }
 
-/**
- * Theme function for l10n_community_filter_form.
- */
-function theme_l10n_community_filter_form($form) {
-  $row = array();
-  $labels = array();
-  // Only display these elements in distinct table cells
-  $elements = array('project', 'release', 'context', 'status', 'author', 'search', 'limit');
-  foreach ($form as $id => &$element) {
-    if (in_array($id, $elements)) {
-      $labels[] = $element['#title'];
-      unset($element['#title']);
-      $row[] = drupal_render($element);
+  return $output;
+}
+
+function theme_l10n_community_translate_translation_list($element) {
+  $output = '<ul>';
+  foreach (element_children($element) as $child) {
+    if (is_numeric($child) || $child == 'new') {
+      $output .= drupal_render($element[$child]);
     }
   }
-  // Fill in the rest of the header above the buttons.
-  $labels[] = '';
-  // Display the rest of the form in the last cell
-  $row[] = array('data' => drupal_render($form), 'class' => 'last');
-  return theme('table', $labels, array($row), array('class' => 'l10n-server-filter'));
+  $output .= '</ul>';
+
+  return $output;
 }
 
-/**
- * Theme function for l10n_community_translate_form.
- */
-function theme_l10n_community_translate_form($form) {
+function theme_l10n_community_translate_source($element) {
+  $output = theme('l10n_community_translate_actions', $element['source']);
+  $output .= '<label class="l10n-string">'. drupal_render($element['source']['string']) .'</label>';
+  $output .= theme('l10n_community_in_context', $element['#string']);
+  $output .= '<div class="l10n-usage"><a href="'. url('translate/details/'. $element['#langcode'] .'/'. $element['#string']->sid) .'" class="l10n-more">'. t('More information') .'</a></div>';
+  return $output;
+}
+
+function theme_l10n_community_translate_table($element) {
+  $header = array(
+    t('Source Text'),
+    t('Translations'),
+  );
+
   $rows = array();
-  $output = '';
+  foreach (element_children($element) as $key) {
+    $rows[] = array(
+      array('class' => 'source', 'data' => theme('l10n_community_translate_source', $element[$key])),
+      array('class' => 'translation', 'data' => theme('l10n_community_translate_translation_list', $element[$key])),
+    );
+  }
 
-  foreach ($form as $id => &$element) {
-    // if the form id is numeric, this form element is for editing a string
-    if (is_numeric($id)) {
-      $source_pane = drupal_render($element['source']);
-      $translation_pane = "<div class='l10n-panes clear-block'>";
-      $translation_pane .= drupal_render($element['toolbox']);
-      $translation_pane .= "<div class='pane translate'>";
-      $translation_pane .= !empty($element['translation_existing']) ? drupal_render($element['translation_existing']) : '';
-      $translation_pane .= drupal_render($element['messagebox']);
-      $translation_pane .= drupal_render($element['translation']) .'</div>';
-      $translation_pane .= "<div class='pane suggestions'></div><div class='pane lookup'></div>";
-      $translation_pane .= "</div>";
-      $row = array(
-        array(
-          'data' => $source_pane,
-          'class' => 'source',
-          'id' => 'spane-'. $id,
-        ),
-        array(
-          'data' => $translation_pane,
-          'class' => 'translation',
-          'id' => 'tpane-'. $id,
-        ),
-      );
-      $rows[] = $row;
-      unset($form[$id]);
+  return theme('table', $header, $rows, array('class' => 'l10n-table'));
+}
+
+function l10n_community_translate_submit($form, &$form_state) {
+  $langcode = $form_state['values']['langcode'];
+
+  foreach ($form_state['values']['strings'] as $sid => $string) {
+    foreach ($string as $tid => $options) {
+      // Store new suggestion.
+      $empty_values = 0;
+      if (isset($options['value']) && is_array($options['value'])) {
+        foreach ($options['value'] as $key => $value) {
+          if ($value === t('<New translation>')) {
+            $options['value'] = '';
+            $empty_values++;
+          }
+        }
+        if ($tid === 'new' && count($options['value']) > $empty_values) {
+          $tid = l10n_community_add_suggestion($langcode, $sid, $options['value']);
+          if ($tid) {
+            l10n_community_counter('added');
+            if ($string['active'] === 'new') {
+              $string['active'] = $tid;
+            }
+          }
+        }
+      }
+
+      if (is_numeric($tid) && $tid > 0) {
+        if ($tid == $string['active']) {
+          if ($options['original']->is_suggestion) {
+            l10n_community_approve_string($langcode, $sid, $tid);
+            l10n_community_counter('approved');
+          }
+          // if (/*!$options['original']->is_stable && */!empty($options['stable'])) {
+          //   dsm('mark as stable');
+          //   l10n_community_counter('stabilized');
+          // }
+        }
+        elseif (!empty($options['declined'])) {
+          // also remove stable flag!
+          l10n_community_counter($options['original']->is_suggestion ? 'suggestion_declined' : 'declined');
+          l10n_community_decline_string($langcode, $sid, $tid);
+        }
+      }
     }
   }
-  $output .= drupal_render($form['pager_top']);
-  $output .= theme('table', array(t('Source Text'), t('Translations')), $rows, array('class' => 'l10n-server-translate'));
-  $output .= drupal_render($form);
-  return $output;
+
+  l10n_community_update_message();
 }
 
+// = Miscellaneous =============================================================
+
 /**
  * Theme context information for source strings.
  *
@@ -615,10 +617,12 @@ function l10n_community_get_strings($langcode, $filters, $pager = NULL) {
   $join = $join_args = $where = $where_args = array();
   $sql = $sql_count = '';
 
-  $select = "SELECT DISTINCT s.sid, s.value, s.context, t.tid, t.language, t.translation, t.uid_entered, t.uid_approved, t.time_entered, t.time_approved, t.has_suggestion, t.is_suggestion, t.is_active FROM {l10n_community_string} s";
+  $select = "SELECT DISTINCT s.sid, s.value, s.context, t.tid, t.language, t.translation, t.uid_entered, t.uid_approved, t.time_entered, t.time_approved, t.has_suggestion, t.is_suggestion, t.is_active, u.name as username, u2.name as username_approved FROM {l10n_community_string} s";
   $select_count = "SELECT COUNT(DISTINCT(s.sid)) FROM {l10n_community_string} s";
   $join[] = "LEFT JOIN {l10n_community_translation} t ON s.sid = t.sid AND t.language = '%s' AND t.is_active = 1 AND t.is_suggestion = 0";
   $join_args[] = $langcode;
+  $join[] = "LEFT JOIN {users} u ON u.uid = t.uid_entered";
+  $join[] = "LEFT JOIN {users} u2 ON u2.uid = t.uid_approved";
 
   // Add submitted by condition
   if (!empty($filters['author'])) {
@@ -636,7 +640,7 @@ function l10n_community_get_strings($langcode, $filters, $pager = NULL) {
       // Release restriction.
       $where_args[] = $release;
       $where[] = 'l.rid = %d';
-    }  
+    }
     elseif ($project) {
       $where[] = "l.pid = %d";
       $where_args[] = $project->pid;
@@ -659,18 +663,18 @@ function l10n_community_get_strings($langcode, $filters, $pager = NULL) {
 
   // Restriction based on string status by translation / suggestions.
   $status_sql = '';
-  if ($filters['status'] & L10N_STATUS_UNTRANSLATED) {
+  if (isset($filters['status']) && $filters['status'] & L10N_STATUS_UNTRANSLATED) {
     // We are doing a LEFT JOIN especially to look into the case, when we have nothing
     // to match in the translation table, but we still have the string. (We get our
     // records in the result set in this case). The translation field is empty or
     // NULL in this case, as we are not allowing NULL there and only saving an empty
     // translation if there are suggestions but no translation yet.
     $where[] = "(t.translation is NULL OR t.translation = '')";
-  } 
+  }
   elseif ($filters['status'] & L10N_STATUS_TRANSLATED) {
     $where[] = "t.translation != ''";
   }
-  if ($filters['status'] & L10N_STATUS_HAS_SUGGESTION) {
+  if (isset($filters['status']) && $filters['status'] & L10N_STATUS_HAS_SUGGESTION) {
     // Note that we are not searching in the suggestions themselfs, only
     // the source and active translation values. The user interface underlines
     // that we are  looking for strings which have suggestions, not the
@@ -732,9 +736,9 @@ function l10n_community_build_filter_values($params, $suggestions = FALSE) {
     'context' => isset($params['context']) ? (string) $params['context'] : 'all',
     'limit' => (isset($params['limit']) && in_array($params['limit'], array(5, 10, 20, 30))) ? (int) $params['limit'] : 10,
   );
-  
-  // The project can be a dropdown or text field depending on number of 
-  // projects. So we need to sanitize its value. 
+
+  // The project can be a dropdown or text field depending on number of
+  // projects. So we need to sanitize its value.
   if (isset($params['project'])) {
     // Try to load project by uri or title, but give URI priority. URI is used
     // to shorten the URL and have simple redirects. Title is used if the
@@ -756,6 +760,152 @@ function l10n_community_build_filter_values($params, $suggestions = FALSE) {
 }
 
 /**
+ * Ensures that there is a mock translation for a given language/string.
+ *
+ * @param $langcode
+ *   The language to be checked for a mock translation.
+ * @param $sid
+ *   The string ID that needs to have a mock translation.
+ */
+function l10n_community_mock_translation($langcode, $sid) {
+  if (!db_result(db_query("SELECT COUNT(*) FROM {l10n_community_translation} WHERE sid = %d AND language = '%s' AND is_suggestion = 0 AND is_active = 1", $sid, $langcode))) {
+    // Insert mock tuple that acts as placeholder.
+    db_query("INSERT INTO {l10n_community_translation} (sid, translation, language, uid_entered, time_entered, is_suggestion, is_active) VALUES (%d, '', '%s', 0, 0, 0, 1)", $sid, $langcode);
+  }
+}
+
+/**
+ * Adds a suggestion to a language/string.
+ *
+ * @param $langcode
+ *   The language of the new translation.
+ * @param $sid
+ *   The string ID for which a new translation should be added.
+ * @param $translation
+ *   An array of strings which constitute the new translation.
+ */
+function l10n_community_add_suggestion($langcode, $sid, $translation) {
+  global $user;
+
+  // Load source string and adjust translation whitespace based on source.
+  $source_string = db_result(db_query('SELECT value FROM {l10n_community_string} WHERE sid = %d', $sid));
+  $translation = l10n_community_pack_string($translation);
+  $translation = l10n_community_trim($translation, $source_string);
+
+  // Don't store empty translations.
+  if ($translation === '') {
+    return NULL;
+  }
+
+  // Look for an existing active translation, if any.
+  $existing = db_fetch_object(db_query("SELECT tid FROM {l10n_community_translation} WHERE sid = %d AND language = '%s' AND translation = '%s'", $sid, $langcode, $translation));
+  if (!empty($existing)) {
+    // The translation is already in the db. Make it an active suggestion again.
+    db_query("UPDATE {l10n_community_translation} SET is_suggestion = 1, is_active = 1 WHERE tid = %d", $existing->tid);
+    $tid = $existing->tid;
+  }
+  else {
+    // This is a new translation.
+    l10n_community_mock_translation($langcode, $sid);
+    // Insert the new suggestion.
+    db_query("INSERT INTO {l10n_community_translation} (sid, translation, language, uid_entered, time_entered, is_suggestion, is_active) VALUES (%d, '%s', '%s', %d, %d, 1, 1)", $sid, $translation, $langcode, $user->uid, time());
+    $tid = db_last_insert_id('l10n_community_translation', 'tid');
+  }
+
+  // Mark the existing or mock translation has having suggestions.
+  l10n_community_update_suggestion_status($langcode, $sid);
+
+  return $tid;
+}
+
+/**
+ * Marks a translation as declined.
+ *
+ * @param $langcode
+ *   The language of the declined translation.
+ * @param $sid
+ *   The string ID the translation belongs to.
+ * @param $tid
+ *   The translation ID of the translation.
+ */
+function l10n_community_decline_string($langcode, $sid, $tid) {
+  // Mark this translation as inactive.
+  db_query("UPDATE {l10n_community_translation} SET is_suggestion = 0, is_active = 0 WHERE tid = %d", $tid);
+
+  // Make sure the mock translation is there in case we declined the active translation.
+  l10n_community_mock_translation($langcode, $sid);
+  l10n_community_update_suggestion_status($langcode, $sid);
+}
+
+/**
+ * Updates the has_suggestion flag for the active translation.
+ *
+ * @param $langcode
+ *   The language of the string.
+ * @param $sid
+ *   The string ID that should be updated.
+ */
+function l10n_community_update_suggestion_status($langcode, $sid) {
+  // Let's see if we have any suggestions remaining in this language.
+  $count = db_result(db_query("SELECT COUNT(*) FROM {l10n_community_translation}
+      WHERE sid = %d AND is_suggestion = 1 AND is_active = 1 AND language = '%s'", $sid, $langcode));
+
+  // Update the status according to the number of suggestions.
+  db_query("UPDATE {l10n_community_translation} SET has_suggestion = %d
+      WHERE sid = %d AND is_suggestion = 0 AND is_active = 1 AND language = '%s'", $count ? 1 : 0, $sid, $langcode);
+}
+
+/**
+ * Marks a translation as approve.
+ *
+ * @param $langcode
+ *   The language of the approved translation.
+ * @param $sid
+ *   The string ID the translation belongs to.
+ * @param $tid
+ *   The translation ID of the translation.
+ */
+function l10n_community_approve_string($langcode, $sid, $tid) {
+  global $user;
+
+  // Remove placeholder translation record (which was there if
+  // first came suggestions, before an actual translation).
+  db_query("DELETE FROM {l10n_community_translation} WHERE sid = %d AND translation = '' AND language = '%s'", $sid, $langcode);
+
+  // Make the existing approved string a suggestion.
+  db_query("UPDATE {l10n_community_translation} SET is_suggestion = 1 WHERE sid = %d AND language = '%s' AND is_suggestion = 0 AND is_active = 1", $sid, $langcode);
+
+  // Mark this exact suggestion as active, and set approval time.
+  db_query("UPDATE {l10n_community_translation} SET time_approved = %d, uid_approved = %d, is_suggestion = 0, is_active = 1 WHERE tid = %d;", time(), $user->uid, $tid);
+  l10n_community_update_suggestion_status($langcode, $sid);
+}
+
+/**
+ * Unpacks a string as retrieved from the database.
+ *
+ * @param $string
+ *   The string with separation markers (NULL byte)
+ * @return
+ *   An array of strings with one element for each plural form in case of
+ *   a plural string, or one element in case of a regular string.
+ */
+function l10n_community_unpack_string($string) {
+  return explode("\0", $string);
+}
+
+/**
+ * Packs a string for storage in the database.
+ *
+ * @param $string
+ *   An array of strings.
+ * @return
+ *   A packed string with NULL bytes separating each string.
+ */
+function l10n_community_pack_string($strings) {
+  return implode("\0", $strings);
+}
+
+/**
  * Replace complex data filters (objects or arrays) with string representations.
  *
  * @param $filters
@@ -771,3 +921,49 @@ function l10n_community_flat_filters($filters) {
   }
   return $filters;
 }
+
+
+// = AJAX callbacks ============================================================
+
+/**
+ * Return a HTML list of projects, releases and counts of where strings
+ * appear in the managed projects.
+ *
+ * We could have been provided much more information, but usability should
+ * also be kept in mind. It is possible to investigate hidden information
+ * sources though, like tooltips on the release titles presented.
+ *
+ * This callback is invoked from JavaScript and is used as an AHAH provider.
+ *
+ * @param $langcode
+ *   Language code.
+ * @param $sid
+ *   Source string id.
+ */
+function l10n_community_string_details($langcode = NULL, $sid = 0) {
+  // Prevent devel module information.
+  $GLOBALS['devel_shutdown'] = FALSE;
+
+  // List of project releases, where this string is used.
+  $result = db_query('SELECT l.pid, p.title project_title, l.rid, r.title release_title, COUNT(l.lineno) as occurance_count FROM {l10n_community_line} l INNER JOIN {l10n_community_project} p ON l.pid = p.pid INNER JOIN {l10n_community_release} r ON l.rid = r.rid WHERE l.sid = %d AND p.status = 1 GROUP BY l.rid ORDER BY l.pid, l.rid', $sid);
+
+  $list = array();
+  $output = array();
+  $previous_project = '';
+  while ($instance = db_fetch_object($result)) {
+    if ($instance->project_title != $previous_project) {
+      if (!empty($list)) {
+        $output[] = join(', ', $list);
+      }
+      $list = array('<em>'. $instance->project_title .':</em> '. $instance->release_title .' ('. $instance->occurance_count .')');
+    }
+    else {
+      $list[] = $instance->release_title .' ('. $instance->occurance_count .')';
+    }
+    $previous_project = $instance->project_title;
+  }
+  $output[] = join(', ', $list);
+  print '<strong>'. t('Used in:') .'</strong>'. theme('item_list', $output);
+
+  exit;
+}
