diff --git help/localization.html help/localization.html
new file mode 100644
index 0000000..2594c08
--- /dev/null
+++ help/localization.html
@@ -0,0 +1,30 @@
+<!-- $Id: $ -->
+<p>On multilingual sites, custom and overridden views may contain text that could be translated into one or more languages. Views makes this data available for translation.</p>
+
+<p>You can select which localization plugin to use at <strong>Administer &gt;&gt; Site building &gt;&gt; Views &gt;&gt; Tools</strong>. By default, Views supports "None" (don't localize these strings) and "Core" (use Drupal core's t() function).</p>
+
+<p>While it "works", the Core plugin is not recommended, as it doesn't support updates to existing strings. If you need to translate Views labels into other languages, consider installing the <a href="http://drupal.org/project/i18n">Internationalization</a> package's Views translation module.</p>
+
+<p>To prevent security issues, you may wish to install the PHP translation module, also part of the <a href="http://drupal.org/project/i18n">Internationalization</a> package.
+
+When this module is installed, PHP code is replaced with placeholders before being passed for translation. For example, a header with the following text
+
+<code>Welcome, you are visitor number &lt;?php echo visitor_count(); ?&gt;.</code>
+
+would be passed as
+
+<code>Welcome, you are visitor number !php0.</code>
+
+As well as addressing potential security holes, using placeholders in translations avoids presenting confusing code to translators.</p>
+
+<p>To prevent the possible insertion of additional PHP in translations, translated text is passed through strip_tags(), a function used to strip out PHP and HTML tags from text.</p>
+
+<p>If you have enabled PHP translation and wish to retain some HTML in e.g. a header or footer that accepts PHP:
+
+<ul>
+<li>Create a filter format that has both the "HTML filter" and "PHP evaluator" filters.</li>
+<li>Rearrange the order of the filters if necessary to ensure that the PHP evaluator runs before the HTML filter (because otherwise the HTML filter will remove PHP before it can be run).</li>
+<li>Select this filter format for your Views property (e.g., header).</li>
+</ul>
+
+Following this approach will ensure that you can retain a subset of HTML tags while safely using PHP in translatable Views text.</p>
diff --git includes/admin.inc includes/admin.inc
index 53d59a7..811a5c4 100644
--- includes/admin.inc
+++ includes/admin.inc
@@ -3219,6 +3219,7 @@ function views_ui_config_item_form_submit($form, &$form_state) {
   // Create a new handler and unpack the options from the form onto it. We
   // can use that for storage.
   $handler = views_get_handler($item['table'], $item['field'], $type);
+  $handler->init($form_state['view'], $item);
 
   // Add the incoming options to existing options because items using
   // the extra form may not have everything in the form here.
@@ -3684,6 +3685,14 @@ function views_ui_admin_tools() {
     '#default_value' => variable_get('views_no_javascript', FALSE),
   );
 
+  $form['views_localization_plugin'] =  array(
+    '#type' => 'radios',
+    '#title' => t('Translation method'),
+    '#options' => views_fetch_plugin_names('localization', NULL, array(), TRUE),
+    '#default_value' => variable_get('views_localization_plugin', 'core'),
+    '#description' => t('Select a translation method to use for Views data like header, footer, and empty text.'),
+  );
+
   $regions = system_region_list(variable_get('theme_default', 'garland'));
   $regions['watchdog'] = t('Watchdog');
 
diff --git includes/base.inc includes/base.inc
index 70321aa..2000243 100644
--- includes/base.inc
+++ includes/base.inc
@@ -76,7 +76,7 @@ class views_object {
    * Unpack options over our existing defaults, drilling down into arrays
    * so that defaults don't get totally blown away.
    */
-  function unpack_options(&$storage, $options, $definition = NULL, $all = TRUE, $check = TRUE) {
+  function unpack_options(&$storage, $options, $definition = NULL, $all = TRUE, $check = TRUE, $localization_keys = array()) {
     if ($check && !is_array($options)) {
       return;
     }
@@ -84,6 +84,9 @@ class views_object {
     if (!isset($definition)) {
       $definition = $this->option_definition();
     }
+    // Ensure we have a localization plugin.
+    $this->view->init_localization();
+
     foreach ($options as $key => $value) {
       if (is_array($value)) {
         // Ignore arrays with no definition.
@@ -103,11 +106,26 @@ class views_object {
           continue;
         }
 
-        $this->unpack_options($storage[$key], $value, isset($definition[$key]['contains']) ? $definition[$key]['contains'] : array(), $all, FALSE);
-      }
-      else if (!empty($definition[$key]['translatable']) && !empty($value)) {
-        $storage[$key] = t($value);
+        $this->unpack_options($storage[$key], $value, isset($definition[$key]['contains']) ? $definition[$key]['contains'] : array(), $all, FALSE, array_merge($localization_keys, array($key)));
       }
+     else if (!empty($definition[$key]['translatable']) && !empty($value) || !empty($definition['contains'][$key]['translatable']) && !empty($value)) {
+        if ($this->view->is_translatable()) {
+          // Allow other modules to make changes to the string before it's
+          // sent for translation.
+          // The $keys array is built from the view name, any localization keys
+          // sent in, and the name of the property being processed.        
+          $translation_data = array(
+            'value' => $value,
+            'format' => isset($options[$key . '_format']) ? $options[$key . '_format'] : NULL,
+            'keys' => array_merge(array($this->view->name), $localization_keys, array($key)),
+          );
+          $storage[$key] = $this->view->localization_plugin->translate($translation_data);
+        }
+        // Otherwise, this is a code-based string, so we can use t().
+        else {
+          $storage[$key] = t($value);
+        }
+     }
       else if ($all || !empty($definition[$key])) {
         $storage[$key] = $value;
       }
@@ -196,4 +214,83 @@ class views_object {
     }
     return $output;
   }
+
+  /**
+   * Unpacks each handler to store translatable texts.
+   */
+  function unpack_translatables(&$translatable) {
+    foreach ($this->option_definition() as $option => $definition) {
+      $this->unpack_translatable($translatable, $this->options, $option, $definition, array(), array());
+    }
+  }
+
+  /**
+   * Unpack a single option definition.
+   *
+   * This function run's through all suboptions recursive.
+   *
+   * @param $translatable
+   *   Stores all availible translatable items.
+   * @param $storage
+   * @param $option
+   * @param $definition
+   * @param $parents
+   * @param $keys
+   */
+  function unpack_translatable(&$translatable, $storage, $option, $definition, $parents, $keys = array()) {
+    // Do not export options for which we have no settings.
+    if (!isset($storage[$option])) {
+      return;
+    }
+
+    // Special handling for some items
+    if (isset($definition['unpack_translatable']) && method_exists($this, $definition['unpack_translatable'])) {
+      return $this->{$definition['unpack_translatable']}($translatable, $storage, $option, $definition, $parents, $keys);
+    }
+
+    if (isset($definition['translatable'])) {
+      if ($definition['translatable'] === FALSE) {
+        return;
+      }
+    }
+
+    // Add the current option to the parents tree.
+    $parents[] = $option;
+
+    // If it has child items, unpack those separately.
+    if (isset($definition['contains'])) {
+      foreach ($definition['contains'] as $sub_option => $sub_definition) {
+        $translation_keys = array_merge($keys, array($sub_option));
+        $this->unpack_translatable($translatable, $storage[$option], $sub_option, $sub_definition, $parents, $translation_keys);
+      }
+    }
+
+    // @todo Figure out this double definition stuff.
+    $options = $storage[$option];
+    if (is_array($options)) {
+      foreach ($options as $key => $value) {
+        $translation_keys = array_merge($keys, array($key));
+        if (is_array($value)) {
+          $this->unpack_translatable($translatable, $storage, $key, $definition, $parents, $translation_keys);
+        }
+        else if (!empty($definition[$key]['translatable']) && !empty($value)) {
+          // Build source data and add to the array
+          $translatable[] = array(
+            'value' => $value,
+            'keys' => $translation_keys,
+            'format' => isset($options[$key . '_format']) ? $options[$key . '_format'] : NULL,
+          );
+        }
+      }
+    }
+    else if (!empty($definition['translatable']) && !empty($options)) {
+      $value = $options;
+      // Build source data and add to the array
+      $translatable[] = array(
+        'value' => $value,
+        'keys' => isset($translation_keys) ? $translation_keys : array(),
+        'format' => isset($options[$option . '_format']) ? $options[$option . '_format'] : NULL,
+      );
+    }
+  }
 }
diff --git includes/plugins.inc includes/plugins.inc
index ef9557c..0d26814 100644
--- includes/plugins.inc
+++ includes/plugins.inc
@@ -325,7 +325,30 @@ function views_views_plugins() {
         'parent' => 'full',
       ),
     ),
+    'localization' => array(
+      'parent' => array(
+        'no ui' => TRUE,
+        'handler' => 'views_plugin_localization',
+        'parent' => '',
+      ),
+     'none' => array(
+        'title' => t('None'),
+        'help' => t('Do not pass admin strings for translation.'),
+        'handler' => 'views_plugin_localization_none',
+        'help topic' => 'localization-none',
+      ),
+      'core' => array(
+        'title' => t('Core'),
+        'help' => t("Use Drupal core t() function. Not recommended, as it doesn't support updates to existing strings."),
+        'handler' => 'views_plugin_localization_core',
+        'help topic' => 'localization-core',
+      ),
+    ),
   );
+  // Add a help message pointing to the i18views module if it is not present.
+  if (!module_exists('i18nviews')) {
+    $plugins['localization']['core']['help'] .= ' ' . t('If you need to translate Views labels into other languages, consider installing the <a href="!path">Internationalization</a> package\'s Views translation module.', array('!path' => url('http://drupal.org/project/i18n', array('absolute' => TRUE))));
+  }
 
   if (module_invoke('ctools', 'api_version', '1.3')) {
     $plugins['style']['jump_menu_summary'] = array(
diff --git includes/view.inc includes/view.inc
index 5884476..5acdca4 100644
--- includes/view.inc
+++ includes/view.inc
@@ -1470,6 +1470,15 @@ class view extends views_db_object {
       $output .= $display->handler->export_options($indent, '$handler->options');
     }
 
+    // Give the localization system a chance to export translatables to code.
+    if ($this->init_localization()) {
+      $this->export_locale_strings('export');
+      $translatables = $this->localization_plugin->export_render($indent);
+      if (!empty($translatables)) {
+        $output .= $translatables;
+      }
+    }
+
     return $output;
   }
 
@@ -1603,6 +1612,71 @@ class view extends views_db_object {
 
     return $errors ? $errors : TRUE;
   }
+
+  /**
+   * Find and initialize the localizer plugin.
+   */
+  function init_localization() {
+    if (isset($this->localization_plugin) && is_object($this->localization_plugin)) {
+      return TRUE;
+    }
+
+    $this->localization_plugin = views_get_plugin('localization', variable_get('views_localization_plugin', 'core'));
+
+    if (empty($this->localization_plugin)) {
+      return FALSE;
+    }
+
+    /**
+    * Figure out whether there should be options.
+    */
+    $this->localization_plugin->init($this);
+
+    return $this->localization_plugin->translate;
+  }
+
+  /**
+   * Determine whether a view supports admin string translation.
+   */
+  function is_translatable() {
+    // If the view is normal or overridden, use admin string translation.
+    // A newly created view won't have a type. Accept this.
+    return (!isset($this->type) || in_array($this->type, array(t('Normal'), t('Overridden')))) ? TRUE : FALSE;
+  }
+
+  /**
+   * Send strings for localization.
+   */
+  function save_locale_strings() {
+    $this->process_locale_strings('save');
+  }
+
+  /**
+   * Delete localized strings.
+   */
+  function delete_locale_strings() {
+    $this->process_locale_strings('delete');
+  }
+
+ /**
+  * Export localized strings.
+  */
+  function export_locale_strings() {
+    $this->process_locale_strings('export');
+  }
+
+  /**
+   * Process strings for localization, deletion or export to code.
+   */
+  function process_locale_strings($op) {
+    // Ensure this view supports translation, we have a display, and we
+    // have a localization plugin.
+    // @fixme Export does not init every handler.
+    if (($this->is_translatable() || $op == 'export') && $this->init_display() && $this->init_localization()) {
+      $this->localization_plugin->process_locale_strings($op);
+    }
+  }
+
 }
 
 /**
diff --git plugins/views_plugin_display.inc plugins/views_plugin_display.inc
index db8ccb9..a8982db 100644
--- plugins/views_plugin_display.inc
+++ plugins/views_plugin_display.inc
@@ -390,12 +390,12 @@ class views_plugin_display extends views_plugin {
       // and therefore need special handling.
       'access' => array(
         'contains' => array(
-          'type' => array('default' => 'none', 'export' => 'export_plugin'),
+          'type' => array('default' => 'none', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'),
          ),
       ),
       'cache' => array(
         'contains' => array(
-          'type' => array('default' => 'none', 'export' => 'export_plugin'),
+          'type' => array('default' => 'none', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'),
          ),
       ),
       'query' => array(
@@ -412,13 +412,13 @@ class views_plugin_display extends views_plugin {
       // should be copied.
       'exposed_form' => array(
         'contains' => array(
-          'type' => array('default' => 'basic', 'export' => 'export_plugin'),
+          'type' => array('default' => 'basic', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'),
           'options' => array('default' => array(), 'export' => FALSE),
          ),
       ),
       'pager' => array(
         'contains' => array(
-          'type' => array('default' => 'full', 'export' => 'export_plugin'),
+          'type' => array('default' => 'full', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'),
           'options' => array('default' => array(), 'export' => FALSE),
          ),
       ),
@@ -429,6 +429,7 @@ class views_plugin_display extends views_plugin {
       'style_plugin' => array(
         'default' => 'default',
         'export' => 'export_style',
+        'unpack_translatable' => 'unpack_plugin',
       ),
       'style_options' => array(
         'default' => array(),
@@ -437,6 +438,7 @@ class views_plugin_display extends views_plugin {
       'row_plugin' => array(
         'default' => 'fields',
         'export' => 'export_style',
+        'unpack_translatable' => 'unpack_plugin',
       ),
       'row_options' => array(
         'default' => array(),
@@ -450,14 +452,17 @@ class views_plugin_display extends views_plugin {
       'header' => array(
         'default' => array(),
         'export' => 'export_handler',
+        'unpack_translatable' => 'unpack_handler',
       ),
       'footer' => array(
         'default' => array(),
         'export' => 'export_handler',
+        'unpack_translatable' => 'unpack_handler',
       ),
       'empty' => array(
         'default' => array(),
         'export' => 'export_handler',
+        'unpack_translatable' => 'unpack_handler',
       ),
 
       // We want these to export last.
@@ -465,18 +470,23 @@ class views_plugin_display extends views_plugin {
       'relationships' => array(
         'default' => array(),
         'export' => 'export_handler',
+        'unpack_translatable' => 'unpack_handler',
+
       ),
       'fields' => array(
         'default' => array(),
         'export' => 'export_handler',
+        'unpack_translatable' => 'unpack_handler',
       ),
       'sorts' => array(
         'default' => array(),
         'export' => 'export_handler',
+        'unpack_translatable' => 'unpack_handler',
       ),
       'arguments' => array(
         'default' => array(),
         'export' => 'export_handler',
+        'unpack_translatable' => 'unpack_handler',
       ),
       'filter_groups' => array(
         'contains' => array(
@@ -487,6 +497,7 @@ class views_plugin_display extends views_plugin {
       'filters' => array(
         'default' => array(),
         'export' => 'export_handler',
+        'unpack_translatable' => 'unpack_handler',
       ),
 
 
@@ -2369,6 +2380,58 @@ class views_plugin_display extends views_plugin {
 
     return $output;
   }
+  
+  /**
+   * Special handling for plugin unpacking.
+   */
+  function unpack_plugin(&$translatable, $storage, $option, $definition, $parents) {
+    $plugin_type = end($parents);
+    $plugin = $this->get_plugin($plugin_type);
+    if ($plugin) {
+      // Write which plugin to use.
+      return $plugin->unpack_translatables($translatable);
+    }
+  }
+  
+    /**
+   * Special method to unpack items that have handlers.
+   *
+   * This method was specified in the option_definition() as the method to utilize to
+   * export fields, filters, sort criteria, relationships and arguments. This passes
+   * the export off to the individual handlers so that they can export themselves
+   * properly.
+   */
+  function unpack_handler(&$translatable, $storage, $option, $definition, $parents) {
+    $output = '';
+
+    // cut the 's' off because the data is stored as the plural form but we need
+    // the singular form. Who designed that anyway? Oh yeah, I did. :(
+    if ($option != 'header' && $option != 'footer' && $option != 'empty') {
+      $type = substr($option, 0, -1);
+    }
+    else {
+      $type = $option;
+    }
+    $types = views_object_types();
+    foreach ($storage[$option] as $id => $info) {
+      if (!empty($types[$type]['type'])) {
+        $handler_type = $types[$type]['type'];
+      }
+      else {
+        $handler_type = $type;
+      }
+      $handler = views_get_handler($info['table'], $info['field'], $handler_type);
+      if ($handler) {
+        $handler->init($this->view, $info);
+        $handler->unpack_translatables($translatable);
+      }
+
+      // Prevent reference problems.
+      unset($handler);
+    }
+
+    return $output;
+  }
 }
 
 
diff --git plugins/views_plugin_exposed_form.inc plugins/views_plugin_exposed_form.inc
index 8fdf199..e01f6dd 100644
--- plugins/views_plugin_exposed_form.inc
+++ plugins/views_plugin_exposed_form.inc
@@ -165,12 +165,12 @@ class views_plugin_exposed_form extends views_plugin {
   function exposed_form_alter(&$form, &$form_state) {
     if (!empty($this->options['reset_button'])) {
       $form['reset'] = array(
-        '#value' => t($this->options['reset_button_label']),
+        '#value' => $this->options['reset_button_label'],
         '#type' => 'submit',
       );
     }
 
-    $form['submit']['#value'] = t($this->options['submit_button']);
+    $form['submit']['#value'] = $this->options['submit_button'];
     // Check if there is exposed sorts for this view
     $exposed_sorts = array();
     foreach ($this->view->sort as $id => $handler) {
@@ -186,8 +186,8 @@ class views_plugin_exposed_form extends views_plugin {
         '#title' => t($this->options['exposed_sorts_label']),
       );
       $sort_order = array(
-        'ASC' => t($this->options['sort_asc_label']),
-        'DESC' => t($this->options['sort_desc_label']),
+        'ASC' => $this->options['sort_asc_label'],
+        'DESC' => $this->options['sort_desc_label'],
       );
       $first_sort = reset($this->view->sort);
       $form['sort_order'] = array(
@@ -227,7 +227,7 @@ class views_plugin_exposed_form extends views_plugin {
    *   $view->exposed_raw_input
    */
   function exposed_form_submit(&$form, &$form_state, &$exclude) {
-    if (!empty($form_state['values']['op']) && $form_state['values']['op'] == t($this->options['reset_button_label'])) {
+    if (!empty($form_state['values']['op']) && $form_state['values']['op'] == $this->options['reset_button_label']) {
       $this->reset_form($form, $form_state);
     }
     if (isset($form_state['pager_plugin'])) {
diff --git plugins/views_plugin_localization.inc plugins/views_plugin_localization.inc
new file mode 100644
index 0000000..155b4d9
--- /dev/null
+++ plugins/views_plugin_localization.inc
@@ -0,0 +1,157 @@
+<?php
+// $Id: $
+
+/**
+ * @file
+ * Contains the base class for views localization plugins.
+ */
+
+/**
+ * The base plugin to handle localization of Views strings.
+ *
+ * @ingroup views_localization_plugins
+ */
+class views_plugin_localization extends views_plugin {
+  // Store for exported strings
+  var $export_strings = array();
+  var $translate = TRUE;
+
+  /**
+   * Initialize the plugin.
+   *
+   * @param $view
+   *   The view object.
+   */
+  function init(&$view) {
+    $this->view = &$view;
+  }
+
+  /**
+   * Translate a string / text with format
+   * 
+   * The $source parameter is an array with the following elements:
+   * - value, source string
+   * - format, input format in case the text has some format to be applied
+   * - keys. An array of keys to identify the string. Generally constructed from
+   *   view name, display_id, and a property, e.g., 'header'.
+   *
+   * @param $source
+   *   Full data for the string to be translated.
+   *
+   * @return string
+   *   Translated string / text
+   */
+  function translate($source) {
+    // Allow other modules to make changes to the string before and after translation
+    $source['pre_process'] = $this->invoke_translation_process($source, 'pre');
+    $source['translation'] = $this->translate_string($source['value'], $source['keys']);
+    $source['post_process'] = $this->invoke_translation_process($source, 'post');
+    return $source['translation'];
+  }
+
+  /**
+   * Translate a string.
+   *
+   * @param $string
+   *   The string to be translated.
+   * @param $keys
+   *   An array of keys to identify the string. Generally constructed from
+   *   view name, display_id, and a property, e.g., 'header'.
+   */
+  function translate_string($string, $keys = array()) {}
+  
+  /**
+   * Save string source for translation.
+   *
+   * @param $source
+   *   Full data for the string to be translated.
+   */
+  function save($source) {
+    // Allow other modules to make changes to the string before saving
+    $source['pre_process'] = $this->invoke_translation_process($source, 'pre');
+    $this->save_string($source['value'], $source['keys']);
+  }
+
+  /**
+   * Save a string for translation
+   *
+   * @param $string
+   *   The string to be translated.
+   * @param $keys
+   *   An array of keys to identify the string. Generally constructed from
+   *   view name, display_id, and a property, e.g., 'header'.
+   */
+  function save_string($string, $keys = array()) {}
+
+  /**
+   * Delete a string.
+   *
+   * @param $source
+   *   Full data for the string to be translated.
+   */
+  function delete($source) { }
+
+  /**
+   * Collect strings to be exported to code.
+   *
+   * @param $source
+   *   Full data for the string to be translated.
+   */
+  function export($source) { }
+
+  /**
+   * Render any collected exported strings to code.
+   *
+   * @param $indent
+   *   An optional indentation for prettifying nested code.
+   */
+  function export_render($indent = '  ') { }
+
+  /**
+   * Invoke hook_translation_pre_process() or hook_translation_post_process().
+   *
+   * Like node_invoke_nodeapi(), this function is needed to enable both passing
+   * by reference and fetching return values.
+   */
+  function invoke_translation_process(&$value, $op) {
+    $return = array();
+    $hook = 'translation_' . $op . '_process';
+    foreach (module_implements($hook) as $module) {
+      $function = $module . '_' . $hook;
+      $result = $function($value);
+      if (isset($result)) {
+        $return[$module] = $result;
+      }
+    }
+    return $return;
+  }
+
+  function process_locale_strings($op) {
+    $this->view->init_display();
+
+    foreach ($this->view->display as $display_id => $display) {
+      $translatable = array();
+      // Special handling for display title.
+      if (isset($display->display_title)) {
+        $translatable[] = array('value' => $display->display_title, 'keys' => array('display_title'));
+      }
+      // Unpack handlers.
+      $this->view->display[$display_id]->handler->unpack_translatables($translatable);
+      foreach ($translatable as $data) {
+        $data['keys'] = array_merge(array($this->view->name, $display_id), $data['keys']);
+        list($string, $keys) = $data;
+        switch ($op) {
+          case 'save':
+            $this->save($data);
+            break;
+          case 'delete':
+            $this->delete($data);
+            break;
+          case 'export':
+            $this->export($data);
+            break;
+        }
+      }
+    }
+  }
+}
diff --git plugins/views_plugin_localization_core.inc plugins/views_plugin_localization_core.inc
new file mode 100644
index 0000000..f6ae85d
--- /dev/null
+++ plugins/views_plugin_localization_core.inc
@@ -0,0 +1,104 @@
+<?php
+// $Id: $
+
+/**
+ * @file
+ * Contains the Drupal core localization plugin.
+ */
+
+/**
+ * Localization plugin to pass translatable strings through t().
+ *
+ * @ingroup views_localization_plugins
+ */
+class views_plugin_localization_core extends views_plugin_localization {
+
+  /**
+   * Translate a string.
+   *
+   * @param $string
+   *   The string to be translated.
+   * @param $keys
+   *   An array of keys to identify the string. Generally constructed from
+   *   view name, display_id, and a property, e.g., 'header'.
+   */
+  function translate_string($string, $keys = array()) {
+    return t($string); 
+  }
+
+  /**
+   * Save a string for translation.
+   *
+   * @param $string
+   *   The string to be translated.
+   * @param $keys
+   *   An array of keys to identify the string. Generally constructed from
+   *   view name, display_id, and a property, e.g., 'header'.
+   */
+  function save_string($string, $keys = array()) {
+    global $language;
+
+    // If the current language is 'en', we need to reset the language
+    // in order to trigger an update.
+    // TODO: add test for number of languages.
+    if ($language->language == 'en') {
+      $changed = TRUE;
+      $languages = language_list();
+      $cached_language = $language;
+      unset($languages['en']);
+      $language = current($languages);
+    }
+
+    t($string);
+
+    if (isset($cached_language)) {
+      $language = $cached_language;
+    }
+    return TRUE;
+  }
+
+  /**
+   * Delete a string.
+   *
+   * Deletion is not supported.
+   *
+   * @param $source
+   *   Full data for the string to be translated.
+   */
+  function delete($source) {
+    return FALSE;
+  }
+
+  /**
+   * Collect strings to be exported to code.
+   *
+   * String identifiers are not supported so strings are anonymously in an array.
+   *
+   * @param $source
+   *   Full data for the string to be translated.
+   */
+  function export($source) {
+    if (!empty($source['value'])) {
+      $this->export_strings[] = $source['value'];
+    }
+  }
+
+  /**
+   * Render any collected exported strings to code.
+   *
+   * @param $indent
+   *   An optional indentation for prettifying nested code.
+   */
+  function export_render($indent = '  ') {
+    $output = '';
+    if (!empty($this->export_strings)) {
+      $this->export_strings = array_unique($this->export_strings);
+      $output = $indent . '$translatables[\'' . $this->view->name . '\'] = array(' . "\n";
+      foreach ($this->export_strings as $string) {
+        $output .= $indent . "  t('" . str_replace("'", "\'", $string) . "'),\n";
+      }
+      $output .= $indent . ");\n";
+    }
+    return $output;
+  }
+}
diff --git plugins/views_plugin_localization_none.inc plugins/views_plugin_localization_none.inc
new file mode 100644
index 0000000..330f304
--- /dev/null
+++ plugins/views_plugin_localization_none.inc
@@ -0,0 +1,37 @@
+<?php
+// $Id: $
+
+/**
+ * @file
+ * Contains the 'none' localization plugin.
+ */
+
+/**
+ * Localization plugin for no localization.
+ *
+ * @ingroup views_localization_plugins
+ */
+class views_plugin_localization_none extends views_plugin_localization {
+  var $translate = FALSE;
+
+  /**
+   * Translate a string; simply return the string.
+   */
+  function translate($source) {
+    return $source['value'];
+  }
+
+  /**
+   * Save a string for translation; not supported.
+   */
+  function save($source) {
+    return FALSE;
+  }
+
+  /**
+   * Delete a string; not supported.
+   */
+  function delete($source) {
+    return FALSE;
+  }
+}
diff --git tests/views_plugin_localization_test.inc tests/views_plugin_localization_test.inc
new file mode 100644
index 0000000..3c6facb
--- /dev/null
+++ tests/views_plugin_localization_test.inc
@@ -0,0 +1,30 @@
+<?php
+// $Id$
+
+/**
+ * A stump localisation plugin which has static variables to cache the input.
+ */
+class views_plugin_localization_test extends views_plugin_localization {
+  /**
+   * Just return the object and do nothing.
+   */
+  function translate_string($string, $keys = array()) {
+    return $string;
+  }
+
+  /**
+   * Store the export strings.
+   */
+  function export($source) {
+    if (!empty($source['value'])) {
+      $this->export_strings[] = $source['value'];
+    }
+  }
+
+  /**
+   * Return the stored strings for the simpletest.
+   */
+  function get_export_strings() {
+    return $this->export_strings;
+  }
+}
diff --git tests/views_query.test tests/views_query.test
index e59c6f9..f77e23a 100644
--- tests/views_query.test
+++ tests/views_query.test
@@ -240,13 +240,6 @@ abstract class ViewsSqlTest extends ViewsTestCase {
     return $data;
   }
 
-  /*
-   * The views plugin definition. Override it if you test provides a plugin.
-   */
-  protected function viewsPlugins() {
-    return array();
-  }
-
   /**
    * A very simple test dataset.
    */
diff --git tests/views_translatable.test tests/views_translatable.test
new file mode 100644
index 0000000..35e287e
--- /dev/null
+++ tests/views_translatable.test
@@ -0,0 +1,105 @@
+<?php
+// $Id$
+
+module_load_include('test', 'views', 'tests/views_query');
+
+class viewsTranslatableTest extends ViewsSqlTest {
+  public static function getInfo() {
+    return array(
+      'name' => 'Views Translatable Test',
+      'description' => 'Tests the pluggable translations',
+      'group' => 'Views',
+    );
+  }
+
+  /*
+   * The views plugin definition. Override it if you test provides a plugin.
+   */
+  public function viewsPlugins() {
+    return array(
+      'localization' => array(
+        'test' => array(
+          'no ui' => TRUE,
+          'title' => t('Test'),
+          'help' => t('This is a test description.'),
+          'handler' => 'views_plugin_localization_test',
+          'parent' => 'parent',
+          'path' => drupal_get_path('module', 'views') .'/tests',
+        ),
+      ),
+    );
+  }
+
+  public function setUp() {
+    parent::setUp();
+
+    variable_set('views_localization_plugin', 'test');
+    // Reset the plugin data.
+    views_fetch_plugin_data(NULL, NULL, TRUE);
+  }
+
+  public function testUnpackTranslatable() {
+    $view = $this->view_unpack_translatable();
+    $view->init_localization();
+
+    $this->assertEqual('views_plugin_localization_test', get_class($view->localization_plugin), 'Take sure that init_localization initiales the right translation plugin');
+
+    $view->export_locale_strings();
+
+    $expected_strings = array('Defaults1', 'Apply1', 'Sort By1', 'Asc1', 'Desc1', 'more1', 'Reset1', 'Offset1', 'Defaults1', 'title1', 'Items per page1', 'simple1');
+    $result_strings = $view->localization_plugin->get_export_strings();
+    $this->assertEqual(sort($expected_strings), sort($result_strings), 'Localisation plugin got every translatable string.');
+  }
+
+  public function view_unpack_translatable() {
+    $view = new view;
+    $view->name = 'view_unpack_translatable';
+    $view->description = '';
+    $view->tag = '';
+    $view->base_table = 'node';
+    $view->api_version = '3.0-alpha1';
+    $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+    /* Display: Defaults */
+    $handler = $view->new_display('default', 'Defaults1', 'default');
+    $handler->display->display_options['title'] = 'title1';
+    $handler->display->display_options['use_more_text'] = 'more1';
+    $handler->display->display_options['access']['type'] = 'none';
+    $handler->display->display_options['cache']['type'] = 'none';
+    $handler->display->display_options['query']['type'] = 'views_query';
+    $handler->display->display_options['exposed_form']['type'] = 'basic';
+    $handler->display->display_options['exposed_form']['options']['submit_button'] = 'Apply1';
+    $handler->display->display_options['exposed_form']['options']['reset_button'] = TRUE;
+    $handler->display->display_options['exposed_form']['options']['reset_button_label'] = 'Reset1';
+    $handler->display->display_options['exposed_form']['options']['exposed_sorts_label'] = 'Sort By1';
+    $handler->display->display_options['exposed_form']['options']['sort_asc_label'] = 'Asc1';
+    $handler->display->display_options['exposed_form']['options']['sort_desc_label'] = 'Desc1';
+    $handler->display->display_options['pager']['type'] = 'full';
+    $handler->display->display_options['pager']['options']['items_per_page'] = '10';
+    $handler->display->display_options['pager']['options']['offset'] = '0';
+    $handler->display->display_options['pager']['options']['id'] = '0';
+    $handler->display->display_options['pager']['options']['expose']['items_per_page'] = TRUE;
+    $handler->display->display_options['pager']['options']['expose']['items_per_page_label'] = 'Items per page1';
+    $handler->display->display_options['pager']['options']['expose']['offset'] = TRUE;
+    $handler->display->display_options['pager']['options']['expose']['offset_label'] = 'Offset1';
+    $handler->display->display_options['style_plugin'] = 'default';
+    $handler->display->display_options['row_plugin'] = 'fields';
+    /* Field: Node: Nid */
+    $handler->display->display_options['fields']['nid']['id'] = 'nid';
+    $handler->display->display_options['fields']['nid']['table'] = 'node';
+    $handler->display->display_options['fields']['nid']['field'] = 'nid';
+    $handler->display->display_options['fields']['nid']['label'] = 'simple1';
+    $handler->display->display_options['fields']['nid']['alter']['alter_text'] = 0;
+    $handler->display->display_options['fields']['nid']['alter']['make_link'] = 0;
+    $handler->display->display_options['fields']['nid']['alter']['trim'] = 0;
+    $handler->display->display_options['fields']['nid']['alter']['word_boundary'] = 1;
+    $handler->display->display_options['fields']['nid']['alter']['ellipsis'] = 1;
+    $handler->display->display_options['fields']['nid']['alter']['strip_tags'] = 0;
+    $handler->display->display_options['fields']['nid']['alter']['html'] = 0;
+    $handler->display->display_options['fields']['nid']['hide_empty'] = 0;
+    $handler->display->display_options['fields']['nid']['empty_zero'] = 0;
+    $handler->display->display_options['fields']['nid']['link_to_node'] = 0;
+
+    return $view;
+  }
+}
