diff --git includes/base.inc includes/base.inc
index 98625b6..98798f8 100644
--- includes/base.inc
+++ includes/base.inc
@@ -196,4 +196,76 @@ class views_object {
     }
     return $output;
   }
+
+  /**
+   * Unpacks each handler to store translatable texts.
+   */
+  function unpack_translatables(&$translatable) {
+    dsm(get_class($this));
+    foreach ($this->option_definition() as $option => $definition) {
+      $this->unpack_translatable($translatable, $this->options, $option, $definition, array(), array());
+    }
+  }
+
+  /**
+   * Unpack a single option definition.
+   */
+  function unpack_translatable(&$translatable, $storage, $option, $definition, $parents, $keys = array()) {
+//     dvm($definition);
+
+    // 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' => $translation_keys,
+        'format' => isset($options[$option . '_format']) ? $options[$option . '_format'] : NULL,
+      );
+    }
+  }
 }
diff --git includes/plugins.inc includes/plugins.inc
index b30145b..e8c16ba 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 d6cbee8..4cf0341 100644
--- includes/view.inc
+++ includes/view.inc
@@ -1454,6 +1454,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;
   }
 
@@ -1587,6 +1596,95 @@ 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 TRUE;
+  }
+
+  /**
+   * 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()) {
+      foreach ($this->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'));
+        }
+//         $this->unpack_translatable($translatable, $display_id, $display->display_options);
+        // Unpack handlers
+        $this->display[$display_id]->handler->unpack_translatables($translatable);
+        foreach ($translatable as $data) {
+          $data['keys'] = array_merge(array($this->name, $display_id), $data['keys']);
+          list($string, $keys) = $data;
+          switch ($op) {
+            case 'save':
+              $this->localization_plugin->save($data);
+              break;
+            case 'delete':
+              $this->localization_plugin->delete($data);
+              break;
+            case 'export':
+              $this->localization_plugin->export($data);
+              break;
+          }
+        }
+      }
+    }
+  }
+
 }
 
 /**
diff --git includes/view.inc.orig includes/view.inc.orig
new file mode 100644
index 0000000..d6cbee8
--- /dev/null
+++ includes/view.inc.orig
@@ -0,0 +1,2055 @@
+<?php
+// $Id: view.inc,v 1.151.2.33 2010-04-29 18:39:31 merlinofchaos Exp $
+/**
+ * @file view.inc
+ * Provides the view object type and associated methods.
+ */
+
+/**
+ * @defgroup views_objects Objects that represent a View or part of a view.
+ * @{
+ * These objects are the core of Views do the bulk of the direction and
+ * storing of data. All database activity is in these objects.
+ */
+
+/**
+ * An object to contain all of the data to generate a view, plus the member
+ * functions to build the view query, execute the query and render the output.
+ */
+class view extends views_db_object {
+  var $db_table = 'views_view';
+  var $base_table = 'node';
+
+  // State variables
+  var $built = FALSE;
+  var $executed = FALSE;
+
+  var $args = array();
+  var $build_info = array();
+
+  var $use_ajax = FALSE;
+
+  // Where the results of a query will go.
+  var $result = array();
+
+  // May be used to override the current pager info.
+  var $current_page = NULL;
+  var $items_per_page = NULL;
+  var $offset = NULL;
+
+  // Places to put attached renderings:
+  var $attachment_before = '';
+  var $attachment_after = '';
+
+  // Exposed widget input
+  var $exposed_data = array();
+  var $exposed_input = array();
+
+  // Used to store views that were previously running if we recurse.
+  var $old_view = array();
+
+  // Where the $query object will reside:
+  var $query = NULL;
+  /**
+   * Constructor
+   */
+  function __construct() {
+    parent::init();
+    // Make sure all of our sub objects are arrays.
+    foreach ($this->db_objects() as $object) {
+      $this->$object = array();
+    }
+  }
+
+  /**
+   * Returns a list of the sub-object types used by this view. These types are
+   * stored on the display, and are used in the build process.
+   */
+  function display_objects() {
+    return array('argument', 'field', 'sort', 'filter', 'relationship', 'header', 'footer', 'empty');
+  }
+
+  /**
+   * Returns the complete list of dependent objects in a view, for the purpose
+   * of initialization and loading/saving to/from the database.
+   *
+   * Note: In PHP5 this should be static, but PHP4 doesn't support static
+   * methods.
+   */
+  function db_objects() {
+    return array('display');
+  }
+
+  /**
+   * Set the arguments that come to this view. Usually from the URL
+   * but possibly from elsewhere.
+   */
+  function set_arguments($args) {
+    $this->args = $args;
+  }
+
+  /**
+   * Change/Set the current page for the pager.
+   */
+  function set_current_page($page) {
+    $this->current_page = $page;
+  }
+
+  /**
+   * Get the current page from the pager.
+   */
+  function get_current_page() {
+    if (!empty($this->query->pager)) {
+      return $this->query->pager->get_current_page();
+    }
+  }
+
+  /**
+   * Get the items per page from the pager.
+   */
+  function get_items_per_page() {
+    if (!empty($this->query->pager)) {
+      return $this->query->pager->get_items_per_page();
+    }
+  }
+
+  /**
+   * Set the items per page on the pager.
+   */
+  function set_items_per_page($items_per_page) {
+    $this->items_per_page = $items_per_page;
+
+    // If the pager is already initialized, pass it through to the pager.
+    if (!empty($this->query->pager)) {
+      $this->query->pager->set_items_per_page($items_per_page);
+    }
+  }
+
+  /**
+   * Get the pager offset from the pager.
+   */
+  function get_offset() {
+    if (!empty($this->query->pager)) {
+      return $this->query->pager->get_offset();
+    }
+  }
+
+  /**
+   * Set the offset on the pager.
+   */
+  function set_offset($offset) {
+    $this->offset = $offset;
+
+    // If the pager is already initialized, pass it through to the pager.
+    if (!empty($this->query->pager)) {
+      $this->query->pager->set_offset($offset);
+    }
+  }
+
+  /**
+   * Whether or not AJAX should be used. If AJAX is used, paging,
+   * tablesorting and exposed filters will be fetched via an AJAX call
+   * rather than a page refresh.
+   */
+  function set_use_ajax($use_ajax) {
+    $this->use_ajax = $use_ajax;
+  }
+
+  /**
+   * Set the exposed filters input to an array. If unset they will be taken
+   * from $_GET when the time comes.
+   */
+  function set_exposed_input($filters) {
+    $this->exposed_input = $filters;
+  }
+
+  /**
+   * Figure out what the exposed input for this view is.
+   */
+  function get_exposed_input() {
+    // Fill our input either from $_GET or from something previously set on the
+    // view.
+    if (empty($this->exposed_input)) {
+      $this->exposed_input = $_GET;
+      // unset items that are definitely not our input:
+      foreach (array('page', 'q') as $key) {
+        if (isset($this->exposed_input[$key])) {
+          unset($this->exposed_input[$key]);
+        }
+      }
+
+      // If we have no input at all, check for remembered input via session.
+
+      // If filters are not overridden, store the 'remember' settings on the
+      // default display. If they are, store them on this display. This way,
+      // multiple displays in the same view can share the same filters and
+      // remember settings.
+      $display_id = ($this->display_handler->is_defaulted('filters')) ? 'default' : $this->current_display;
+
+      if (empty($this->exposed_input) && !empty($_SESSION['views'][$this->name][$display_id])) {
+        $this->exposed_input = $_SESSION['views'][$this->name][$display_id];
+      }
+    }
+
+    return $this->exposed_input;
+  }
+
+  /**
+   * Set the display for this view and initialize the display handler.
+   */
+  function init_display($reset = FALSE) {
+    // The default display is always the first one in the list.
+    if (isset($this->current_display)) {
+      return TRUE;
+    }
+
+    // Instantiate all displays
+    foreach (array_keys($this->display) as $id) {
+      // Correct for shallow cloning
+      // Often we'll have a cloned view so we don't mess up each other's
+      // displays, but the clone is pretty shallow and doesn't necessarily
+      // clone the displays. We can tell this by looking to see if a handler
+      // has already been set; if it has, but $this->current_display is not
+      // set, then something is dreadfully wrong.
+      if (!empty($this->display[$id]->handler)) {
+        $this->display[$id] = drupal_clone($this->display[$id]);
+        unset($this->display[$id]->handler);
+      }
+      $this->display[$id]->handler = views_get_plugin('display', $this->display[$id]->display_plugin);
+      if (!empty($this->display[$id]->handler)) {
+        // Initialize the new display handler with data.
+        $this->display[$id]->handler->init($this, $this->display[$id]);
+        // If this is NOT the default display handler, let it know which is
+        // since it may well utilize some data from the default.
+        // This assumes that the 'default' handler is always first. It always
+        // is. Make sure of it.
+        if ($id != 'default') {
+          $this->display[$id]->handler->default_display = &$this->display['default']->handler;
+        }
+      }
+    }
+
+    $this->current_display = 'default';
+    $this->display_handler = &$this->display['default']->handler;
+
+    return TRUE;
+  }
+
+  /**
+   * Get the first display that is accessible to the user.
+   *
+   * @param $displays
+   *   Either a single display id or an array of display ids.
+   */
+  function choose_display($displays) {
+    if (!is_array($displays)) {
+      return $displays;
+    }
+
+    $this->init_display();
+
+    foreach ($displays as $display_id) {
+      if ($this->display[$display_id]->handler->access()) {
+        return $display_id;
+      }
+    }
+
+    return 'default';
+  }
+
+  /**
+   * Set the display as current.
+   *
+   * @param $display_id
+   *   The id of the display to mark as current.
+   */
+  function set_display($display_id = NULL) {
+    // If we have not already initialized the display, do so. But be careful.
+    if (empty($this->current_display)) {
+      $this->init_display();
+
+      // If handlers were not initialized, and no argument was sent, set up
+      // to the default display.
+      if (empty($display_id)) {
+        $display_id = 'default';
+      }
+    }
+
+    $display_id = $this->choose_display($display_id);
+
+    // If no display id sent in and one wasn't chosen above, we're finished.
+    if (empty($display_id)) {
+      return TRUE;
+    }
+
+    // Ensure the requested display exists.
+    if (empty($this->display[$display_id])) {
+      $display_id = 'default';
+      if (empty($this->display[$display_id])) {
+        vpr(t('set_display() called with invalid display id @display.', array('@display' => $display_id)));
+        return FALSE;
+      }
+    }
+
+    // Set the current display.
+    $this->current_display = $display_id;
+
+    // Ensure requested display has a working handler.
+    if (empty($this->display[$display_id]->handler)) {
+      return FALSE;
+    }
+
+    // Set a shortcut
+    $this->display_handler = &$this->display[$display_id]->handler;
+
+    return TRUE;
+  }
+
+  /**
+   * Find and initialize the style plugin.
+   *
+   * Note that arguments may have changed which style plugin we use, so
+   * check the view object first, then ask the display handler.
+   */
+  function init_style() {
+    if (isset($this->style_plugin)) {
+      return is_object($this->style_plugin);
+    }
+
+    if (!isset($this->plugin_name)) {
+      $this->plugin_name = $this->display_handler->get_option('style_plugin');
+      $this->style_options = $this->display_handler->get_option('style_options');
+    }
+
+    $this->style_plugin = views_get_plugin('style', $this->plugin_name);
+
+    if (empty($this->style_plugin)) {
+      return FALSE;
+    }
+
+    // init the new display handler with data.
+    $this->style_plugin->init($this, $this->display[$this->current_display], $this->style_options);
+    return TRUE;
+  }
+
+  /**
+   * Acquire and attach all of the handlers.
+   */
+  function init_handlers() {
+    if (empty($this->inited)) {
+      foreach (views_object_types() as $key => $info) {
+        $this->_init_handler($key, $info);
+      }
+      $this->inited = TRUE;
+    }
+  }
+
+  /**
+   * Initialize the pager
+   *
+   * Like style initialization, pager initialization is held until late
+   * to allow for overrides.
+   */
+  function init_pager() {
+    if (empty($this->query->pager)) {
+      $this->query->pager = $this->display_handler->get_plugin('pager');
+
+      if ($this->query->pager->use_pager()) {
+        $this->query->pager->set_current_page($this->current_page);
+      }
+
+      // These overrides may have been set earlier via $view->set_*
+      // functions.
+      if (isset($this->items_per_page)) {
+        $this->query->pager->set_items_per_page($this->items_per_page);
+      }
+
+      if (isset($this->offset)) {
+        $this->query->pager->set_offset($this->offset);
+      }
+    }
+  }
+
+  /**
+   * Create a list of base tables eligible for this view. Used primarily
+   * for the UI. Display must be already initialized.
+   */
+  function get_base_tables() {
+    $base_tables = array(
+      $this->base_table => TRUE,
+      '#global' => TRUE,
+    );
+
+    foreach ($this->display_handler->get_handlers('relationship') as $handler) {
+      $base_tables[$handler->definition['base']] = TRUE;
+    }
+    return $base_tables;
+  }
+
+  /**
+   * Run the pre_query() on all active handlers.
+   */
+  function _pre_query() {
+    foreach (views_object_types() as $key => $info) {
+      $handlers = &$this->$key;
+      $position = 0;
+      foreach ($handlers as $id => $handler) {
+        $handlers[$id]->position = $position;
+        $handlers[$id]->pre_query();
+        $position++;
+      }
+    }
+  }
+
+  /**
+   * Attach all of the handlers for each type.
+   *
+   * @param $key
+   *   One of 'argument', 'field', 'sort', 'filter', 'relationship'
+   * @param $info
+   *   The $info from views_object_types for this object.
+   */
+  function _init_handler($key, $info) {
+    // Load the requested items from the display onto the object.
+    $this->$key = $this->display_handler->get_handlers($key);
+
+    // This reference deals with difficult PHP indirection.
+    $handlers = &$this->$key;
+
+    // Run through and test for accessibility.
+    foreach ($handlers as $id => $handler) {
+      if (!$handler->access()) {
+        unset($handlers[$id]);
+      }
+    }
+  }
+
+  /**
+   * Build all the arguments.
+   */
+  function _build_arguments() {
+    // Initially, we want to build sorts and fields. This can change, though,
+    // if we get a summary view.
+    if (empty($this->argument)) {
+      return TRUE;
+    }
+ 
+    // Don't render exposed filter form when there's form errors.
+    // Applies when filters are in a block ("exposed_block" option).
+    if (form_get_errors() && empty($form_state['rerender'])) {
+      return NULL;
+    }
+
+    // build arguments.
+    $position = -1;
+
+    // Create a title for use in the breadcrumb trail.
+    $title = $this->display_handler->get_option('title');
+
+    $this->build_info['breadcrumb'] = array();
+    $breadcrumb_args = array();
+    $substitutions = array();
+
+    $status = TRUE;
+
+    // Iterate through each argument and process.
+    foreach ($this->argument as $id => $arg) {
+      $position++;
+      $argument = &$this->argument[$id];
+
+      if ($argument->broken()) {
+        continue;
+      }
+
+      $argument->set_relationship();
+
+      $arg = isset($this->args[$position]) ? $this->args[$position] : NULL;
+      $argument->position = $position;
+
+      if (isset($arg) || $argument->has_default_argument()) {
+        if (!isset($arg)) {
+          $arg = $argument->get_default_argument();
+          // make sure default args get put back.
+          if (isset($arg)) {
+            $this->args[$position] = $arg;
+          }
+        }
+
+        // Set the argument, which will also validate that the argument can be set.
+        if (!$argument->set_argument($arg)) {
+          $status = $argument->validate_fail($arg);
+          break;
+        }
+
+        if ($argument->is_wildcard()) {
+          $arg_title = $argument->wildcard_title();
+        }
+        else {
+          $arg_title = $argument->get_title();
+          $argument->query($this->display_handler->use_group_by());
+        }
+
+        // Add this argument's substitution
+        $substitutions['%' . ($position + 1)] = $arg_title;
+
+        // Since we're really generating the breadcrumb for the item above us,
+        // check the default action of this argument.
+        if ($this->display_handler->uses_breadcrumb() && $argument->uses_breadcrumb()) {
+          $path = $this->get_url($breadcrumb_args);
+          if (strpos($path, '%') === FALSE) {
+            $breadcrumb = !empty($argument->options['breadcrumb'])? $argument->options['breadcrumb'] : $title;
+            $this->build_info['breadcrumb'][$path] = str_replace(array_keys($substitutions), $substitutions, $breadcrumb);
+          }
+        }
+
+        // Allow the argument to muck with this breadcrumb.
+        $argument->set_breadcrumb($this->build_info['breadcrumb']);
+
+        // Test to see if we should use this argument's title
+        if (!empty($argument->options['title'])) {
+          $title = $argument->options['title'];
+        }
+
+        $breadcrumb_args[] = $arg;
+      }
+      else {
+        // determine default condition and handle.
+        $status = $argument->default_action();
+        break;
+      }
+
+      // Be safe with references and loops:
+      unset($argument);
+    }
+
+    // set the title in the build info.
+    if (!empty($title)) {
+      $this->build_info['title'] = str_replace(array_keys($substitutions), $substitutions, $title);
+    }
+
+    // Store the arguments for later use.
+    $this->build_info['substitutions'] = $substitutions;
+
+    return $status;
+  }
+
+  /**
+   * Do some common building initialization.
+   */
+  function init_query() {
+    if (!empty($this->query)) {
+      $class = get_class($this->query);
+      if ($class && $class != 'stdClass') {
+        // return if query is already initialized.
+        return;
+      }
+    }
+
+    // Create and initialize the query object.
+    $views_data = views_fetch_data($this->base_table);
+    $this->base_field = $views_data['table']['base']['field'];
+    if (!empty($views_data['table']['base']['database'])) {
+      $this->base_database = $views_data['table']['base']['database'];
+    }
+
+    // Create and initialize the query object.
+    $plugin = !empty($views_data['table']['base']['query class']) ? $views_data['table']['base']['query class'] : 'views_query';
+    $this->query = views_get_plugin('query', $plugin);
+
+    $this->query->init($this->base_table, $this->base_field);
+  }
+
+  /**
+   * Build the query for the view.
+   */
+  function build($display_id = NULL) {
+    if (!empty($this->built)) {
+      return;
+    }
+
+    if (empty($this->current_display) || $display_id) {
+      if (!$this->set_display($display_id)) {
+        return FALSE;
+      }
+    }
+
+    // Let modules modify the view just prior to executing it.
+    foreach (module_implements('views_pre_build') as $module) {
+      $function = $module . '_views_pre_build';
+      $function($this);
+    }
+
+    // Attempt to load from cache.
+    // @todo Load a build_info from cache.
+
+    $start = views_microtime();
+    // If that fails, let's build!
+    $this->build_info = array(
+      'query' => '',
+      'count_query' => '',
+      'query_args' => array(),
+    );
+
+    $this->init_query();
+
+    // Call a module hook and see if it wants to present us with a
+    // pre-built query or instruct us not to build the query for
+    // some reason.
+    // @todo: Implement this. Use the same mechanism Panels uses.
+
+    // Run through our handlers and ensure they have necessary information.
+    $this->init_handlers();
+
+    // Let the handlers interact with each other if they really want.
+    $this->_pre_query();
+
+    if ($this->display_handler->uses_exposed()) {
+      $exposed_form = $this->display_handler->get_plugin('exposed_form');
+      $this->exposed_widgets = $exposed_form->render_exposed_form();
+      if (form_set_error() || !empty($this->build_info['abort'])) {
+        $this->built = TRUE;
+        return empty($this->build_info['fail']);
+      }
+    }
+
+    // Build all the relationships first thing.
+    $this->_build('relationship');
+
+    // Set the filtering groups.
+    $filter_groups = $this->display_handler->get_option('filter_groups');
+    if ($filter_groups) {
+      $this->query->set_group_operator($filter_groups['operator']);
+      foreach($filter_groups['groups'] as $id => $operator) {
+        $this->query->set_where_group($operator, $id);
+      }
+    }
+
+    // Build all the filters.
+    $this->_build('filter');
+
+    $this->build_sort = TRUE;
+
+    // Arguments can, in fact, cause this whole thing to abort.
+    if (!$this->_build_arguments()) {
+      $this->build_time = views_microtime() - $start;
+      $this->attach_displays();
+      return $this->built;
+    }
+
+    // Initialize the style; arguments may have changed which style we use,
+    // so waiting as long as possible is important. But we need to know
+    // about the style when we go to build fields.
+    if (!$this->init_style()) {
+      $this->build_info['fail'] = TRUE;
+      return FALSE;
+    }
+
+    if ($this->style_plugin->uses_fields()) {
+      $this->_build('field');
+    }
+
+    // Build our sort criteria if we were instructed to do so.
+    if (!empty($this->build_sort)) {
+      // Allow the style handler to deal with sorting.
+      if ($this->style_plugin->build_sort()) {
+        $this->_build('sort');
+      }
+      // allow the plugin to build second sorts as well.
+      $this->style_plugin->build_sort_post();
+    }
+
+    // Allow display handler to affect the query:
+    $this->display_handler->query($this->display_handler->use_group_by());
+
+    // Allow style handler to affect the query:
+    $this->style_plugin->query($this->display_handler->use_group_by());
+
+    // Allow exposed form to affect the query:
+    if (isset($exposed_form)) {
+      $exposed_form->query();
+    }
+
+    if (variable_get('views_sql_signature', FALSE)) {
+      $this->query->add_signature($this);
+    }
+
+    // Let modules modify the query just prior to finalizing it.
+    $this->query->alter($this);
+
+    // Only build the query if we weren't interrupted.
+    if (empty($this->built)) {
+      // Build the necessary info to execute the query.
+      $this->query->build($this);
+    }
+
+    $this->built = TRUE;
+    $this->build_time = views_microtime() - $start;
+
+    // Attach displays
+    $this->attach_displays();
+    return TRUE;
+  }
+
+  /**
+   * Internal method to build an individual set of handlers.
+   */
+  function _build($key) {
+    $handlers = &$this->$key;
+    foreach ($handlers as $id => $data) {
+      if (!empty($handlers[$id]) && is_object($handlers[$id])) {
+        // Give this handler access to the exposed filter input.
+        if (!empty($this->exposed_data)) {
+          $rc = $handlers[$id]->accept_exposed_input($this->exposed_data);
+          $handlers[$id]->store_exposed_input($this->exposed_data, $rc);
+          if (!$rc) {
+            continue;
+          }
+        }
+        $handlers[$id]->set_relationship();
+        $handlers[$id]->query($this->display_handler->use_group_by());
+      }
+    }
+  }
+
+  /**
+   * Execute the view's query.
+   */
+  function execute($display_id = NULL) {
+    if (empty($this->built)) {
+      if (!$this->build($display_id)) {
+        return FALSE;
+      }
+    }
+
+    if (!empty($this->executed)) {
+      return TRUE;
+    }
+
+    // Let modules modify the view just prior to executing it.
+    foreach (module_implements('views_pre_execute') as $module) {
+      $function = $module . '_views_pre_execute';
+      $function($this);
+    }
+
+    $query = db_rewrite_sql($this->build_info['query'], $this->base_table, $this->base_field, array('view' => &$this));
+    $count_query = db_rewrite_sql($this->build_info['count_query'], $this->base_table, $this->base_field, array('view' => &$this));
+
+    $args = $this->build_info['query_args'];
+
+    vpr($query);
+
+    // Check for already-cached results.
+    if (!empty($this->live_preview)) {
+      $cache = FALSE;
+    }
+    else {
+      $cache = $this->display_handler->get_plugin('cache');
+    }
+    if ($cache && $cache->cache_get('results')) {
+      vpr('Used cached results');
+    }
+    else {
+      $this->query->execute($this);
+      if ($cache) {
+        $cache->cache_set('results');
+      }
+    }
+
+    $this->executed = TRUE;
+  }
+
+  /**
+   * Render this view for display.
+   */
+  function render($display_id = NULL) {
+    $this->execute($display_id);
+
+    // Check to see if the build failed.
+    if (!empty($this->build_info['fail'])) {
+      return;
+    }
+
+    init_theme();
+
+    $start = views_microtime();
+    if (!empty($this->live_preview) && variable_get('views_show_additional_queries', FALSE)) {
+      $this->start_query_capture();
+    }
+
+    $exposed_form = $this->display_handler->get_plugin('exposed_form');
+    $exposed_form->pre_render();
+
+    // Check for already-cached output.
+    if (!empty($this->live_preview)) {
+      $cache = FALSE;
+    }
+    else {
+      $cache = $this->display_handler->get_plugin('cache');
+    }
+    if ($cache && $cache->cache_get('output')) {
+      vpr('Used cached output');
+    }
+    else {
+      if ($cache) {
+        $cache->cache_start();
+      }
+
+      // Initialize the style plugin.
+      $this->init_style();
+
+      $this->style_plugin->pre_render($this->result);
+
+      // Let modules modify the view just prior to executing it.
+      foreach (module_implements('views_pre_render') as $module) {
+        $function = $module . '_views_pre_render';
+        $function($this);
+      }
+
+      // Let the theme play too, because pre render is a very themey thing.
+      $function = $GLOBALS['theme'] . '_views_pre_render';
+      if (function_exists($function)) {
+        $function($this, $this->display_handler->output, $cache);
+      }
+
+      // Give field handlers the opportunity to perform additional queries
+      // using the entire resultset prior to rendering.
+      if ($this->style_plugin->uses_fields()) {
+        foreach ($this->field as $id => $handler) {
+          if (!empty($this->field[$id])) {
+            $this->field[$id]->pre_render($this->result);
+          }
+        }
+      }
+      $this->display_handler->output = $this->display_handler->render();
+      if ($cache) {
+        $cache->cache_set('output');
+      }
+    }
+
+    $exposed_form->post_render($this->display_handler->output);
+
+    if ($cache) {
+      $cache->post_render($this->display_handler->output);
+    }
+
+    // Let modules modify the view output after it is rendered.
+    foreach (module_implements('views_post_render') as $module) {
+      $function = $module . '_views_post_render';
+      $function($this, $this->display_handler->output, $cache);
+    }
+
+    // Let the theme play too, because post render is a very themey thing.
+    $function = $GLOBALS['theme'] . '_views_post_render';
+    if (function_exists($function)) {
+      $function($this, $this->display_handler->output, $cache);
+    }
+
+    if (!empty($this->live_preview) && variable_get('views_show_additional_queries', FALSE)) {
+      $this->end_query_capture();
+    }
+    $this->render_time = views_microtime() - $start;
+
+    return $this->display_handler->output;
+  }
+
+  /**
+   * Render a specific field via the field ID and the row #.
+   */
+  function render_field($field, $row) {
+    if (isset($this->field[$field]) && isset($this->result[$row])) {
+      return $this->field[$field]->advanced_render($this->result[$row]);
+    }
+  }
+
+  /**
+   * Execute the given display, with the given arguments.
+   * To be called externally by whatever mechanism invokes the view,
+   * such as a page callback, hook_block, etc.
+   *
+   * This function should NOT be used by anything external as this
+   * returns data in the format specified by the display. It can also
+   * have other side effects that are only intended for the 'proper'
+   * use of the display, such as setting page titles and breadcrumbs.
+   *
+   * If you simply want to view the display, use view::preview() instead.
+   */
+  function execute_display($display_id = NULL, $args = array()) {
+    $timer_id = 'execute_display' . $display_id;
+    timer_start($timer_id);
+    if (empty($this->current_display) || $this->current_display != $this->choose_display($display_id)) {
+      if (!$this->set_display($display_id)) {
+        return FALSE;
+      }
+    }
+
+    $this->pre_execute($args);
+
+    // Execute the view
+    $output = $this->display_handler->execute();
+
+    $this->post_execute();
+
+    vpr("$this->name execute time: " . timer_read($timer_id) . " ms");
+
+    timer_stop($timer_id);
+
+    return $output;
+  }
+
+  /**
+   * Preview the given display, with the given arguments.
+   *
+   * To be called externally, probably by an AJAX handler of some flavor.
+   * Can also be called when views are embedded, as this guarantees
+   * normalized output.
+   */
+  function preview($display_id = NULL, $args = array()) {
+    $timer_id = 'preview' . $display_id;
+    timer_start($timer_id);
+    if (empty($this->current_display) || $this->current_display != $display_id) {
+      if (!$this->set_display($display_id)) {
+        return FALSE;
+      }
+    }
+
+    $this->preview = TRUE;
+    $this->pre_execute($args);
+    // Preview the view.
+    $output = $this->display_handler->preview();
+
+    $this->post_execute();
+
+    vpr("$this->name execute time: " . timer_read($timer_id) . " ms");
+
+    timer_stop($timer_id);
+
+    return $output;
+  }
+
+  /**
+   * Run attachments and let the display do what it needs to do prior
+   * to running.
+   */
+  function pre_execute($args = array()) {
+    $this->old_view[] = views_get_current_view();
+    views_set_current_view($this);
+    $display_id = $this->current_display;
+
+    // Let modules modify the view just prior to executing it.
+    foreach (module_implements('views_pre_view') as $module) {
+      $function = $module . '_views_pre_view';
+      $function($this, $display_id, $args);
+    }
+
+    // Prepare the view with the information we have, but only if we were
+    // passed arguments, as they may have been set previously.
+    if ($args) {
+      $this->set_arguments($args);
+    }
+
+//    $this->attach_displays();
+
+    // Allow the display handler to set up for execution
+    $this->display_handler->pre_execute();
+  }
+
+  /**
+   * Unset the current view, mostly.
+   */
+  function post_execute() {
+    // unset current view so we can be properly destructed later on.
+    // Return the previous value in case we're an attachment.
+
+    if ($this->old_view) {
+      $old_view = array_pop($this->old_view);
+    }
+
+    views_set_current_view(isset($old_view) ? $old_view : FALSE);
+  }
+
+  /**
+   * Run attachment displays for the view.
+   */
+  function attach_displays() {
+    if (!empty($this->is_attachment)) {
+      return;
+    }
+
+    if (!$this->display_handler->accept_attachments()) {
+      return;
+    }
+
+    $this->is_attachment = TRUE;
+    // Give other displays an opportunity to attach to the view.
+    foreach ($this->display as $id => $display) {
+      if (!empty($this->display[$id]->handler)) {
+        $this->display[$id]->handler->attach_to($this->current_display);
+      }
+    }
+    $this->is_attachment = FALSE;
+  }
+
+  /**
+   * Called to get hook_menu() information from the view and the named display handler.
+   *
+   * @param $display_id
+   *   A display id.
+   * @param $callbacks
+   *   A menu callback array passed from views_menu_alter().
+   */
+  function execute_hook_menu($display_id = NULL, $callbacks = array()) {
+    // Prepare the view with the information we have.
+
+    // This was probably already called, but it's good to be safe.
+    if (!$this->set_display($display_id)) {
+      return FALSE;
+    }
+
+    // Execute the view
+    if (isset($this->display_handler)) {
+      return $this->display_handler->execute_hook_menu($callbacks);
+    }
+  }
+
+  /**
+   * Called to get hook_block information from the view and the
+   * named display handler.
+   */
+  function execute_hook_block($display_id = NULL) {
+    // Prepare the view with the information we have.
+
+    // This was probably already called, but it's good to be safe.
+    if (!$this->set_display($display_id)) {
+      return FALSE;
+    }
+
+    // Execute the view
+    if (isset($this->display_handler)) {
+      return $this->display_handler->execute_hook_block();
+    }
+  }
+
+  /**
+   * Determine if the given user has access to the view. Note that
+   * this sets the display handler if it hasn't been.
+   */
+  function access($displays = NULL, $account = NULL) {
+    if (!isset($this->current_display)) {
+      $this->init_display();
+    }
+
+    if (!$account) {
+      $account = $GLOBALS['user'];
+    }
+
+    // We can't use choose_display() here because that function
+    // calls this one.
+    $displays = (array)$displays;
+    foreach ($displays as $display_id) {
+      if (!empty($this->display[$display_id]->handler)) {
+        if ($this->display[$display_id]->handler->access($account)) {
+          return TRUE;
+        }
+      }
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Get the view's current title. This can change depending upon how it
+   * was built.
+   */
+  function get_title() {
+    if (empty($this->display_handler)) {
+      if (!$this->set_display('default')) {
+        return FALSE;
+      }
+    }
+
+    // During building, we might find a title override. If so, use it.
+    if (!empty($this->build_info['title'])) {
+      $title = $this->build_info['title'];
+    }
+    else {
+      $title = $this->display_handler->get_option('title');
+    }
+
+    return $title;
+  }
+
+  /**
+   * Force the view to build a title.
+   */
+  function build_title() {
+    $this->init_display();
+    if (empty($this->built)) {
+      $this->init_query();
+    }
+    $this->init_handlers();
+
+    $this->_build_arguments();
+  }
+
+  /**
+   * Get the URL for the current view.
+   *
+   * This URL will be adjusted for arguments.
+   */
+  function get_url($args = NULL, $path = NULL) {
+    if (!isset($path)) {
+      $path = $this->get_path();
+    }
+    if (!isset($args)) {
+      $args = $this->args;
+    }
+    // Don't bother working if there's nothing to do:
+    if (empty($path) || (empty($args) && strpos($path, '%') === FALSE)) {
+      return $path;
+    }
+
+    $pieces = array();
+    $arguments = isset($arguments) ? $arguments : $this->display_handler->get_option('arguments');
+    $argument_keys = isset($arguments) ? array_keys($arguments) : array();
+    $id = current($argument_keys);
+    foreach (explode('/', $path) as $piece) {
+      if ($piece != '%') {
+        $pieces[] = $piece;
+      }
+      else {
+        if (empty($args)) {
+          // Try to never put % in a url; use the wildcard instead.
+          if ($id && !empty($arguments[$id]['wildcard'])) {
+            $pieces[] = $arguments[$id]['wildcard'];
+          }
+          else {
+            $pieces[] = '*'; // gotta put something if there just isn't one.
+          }
+
+        }
+        else {
+          $pieces[] = array_shift($args);
+        }
+
+        if ($id) {
+          $id = next($argument_keys);
+        }
+      }
+    }
+
+    if (!empty($args)) {
+      $pieces = array_merge($pieces, $args);
+    }
+    return implode('/', $pieces);
+  }
+
+  /**
+   * Get the base path used for this view.
+   */
+  function get_path() {
+    if (!empty($this->override_path)) {
+      return $this->override_path;
+    }
+
+    if (empty($this->display_handler)) {
+      if (!$this->set_display('default')) {
+        return FALSE;
+      }
+    }
+    return $this->display_handler->get_path();
+  }
+
+  /**
+   * Get the breadcrumb used for this view.
+   *
+   * @param $set
+   *   If true, use drupal_set_breadcrumb() to install the breadcrumb.
+   */
+  function get_breadcrumb($set = FALSE) {
+    // Now that we've built the view, extract the breadcrumb.
+    $base = TRUE;
+    $breadcrumb = array();
+
+    if (!empty($this->build_info['breadcrumb'])) {
+      foreach ($this->build_info['breadcrumb'] as $path => $title) {
+        // Check to see if the frontpage is in the breadcrumb trail; if it
+        // is, we'll remove that from the actual breadcrumb later.
+        if ($path == variable_get('site_frontpage', 'node')) {
+          $base = FALSE;
+          $title = t('Home');
+        }
+        if ($title) {
+          $breadcrumb[] = l($title, $path, array('html' => true));
+        }
+      }
+
+      if ($set) {
+        if ($base) {
+          $breadcrumb = array_merge(drupal_get_breadcrumb(), $breadcrumb);
+        }
+        drupal_set_breadcrumb($breadcrumb);
+      }
+    }
+    return $breadcrumb;
+  }
+
+  /**
+   * Is this view cacheable?
+   */
+  function is_cacheable() {
+    return $this->is_cacheable;
+  }
+
+  /**
+   * Set up query capturing.
+   *
+   * db_query() stores the queries that it runs in global $queries,
+   * bit only if dev_query is set to true. In this case, we want
+   * to temporarily override that setting if it's not and we
+   * can do that without forcing a db rewrite by just manipulating
+   * $conf. This is kind of evil but it works.
+   */
+  function start_query_capture() {
+    global $conf, $queries;
+    if (empty($conf['dev_query'])) {
+      $this->fix_dev_query = TRUE;
+      $conf['dev_query'] = TRUE;
+    }
+
+    // Record the last query key used; anything already run isn't
+    // a query that we are interested in.
+    $this->last_query_key = NULL;
+
+    if (!empty($queries)) {
+      $keys = array_keys($queries);
+      $this->last_query_key = array_pop($keys);
+    }
+  }
+
+  /**
+   * Add the list of queries run during render to buildinfo.
+   *
+   * @see view::start_query_capture()
+   */
+  function end_query_capture() {
+    global $conf, $queries;
+    if (!empty($this->fix_dev_query)) {
+      $conf['dev_query'] = FALSE;
+    }
+
+    // make a copy of the array so we can manipulate it with array_splice.
+    $temp = $queries;
+
+    // Scroll through the queries until we get to our last query key.
+    // Unset anything in our temp array.
+    if (isset($this->last_query_key)) {
+      while (list($id, $query) = each($queries)) {
+        if ($id == $this->last_query_key) {
+          break;
+        }
+
+        unset($temp[$id]);
+      }
+    }
+
+    $this->additional_queries = $temp;
+  }
+
+  /**
+   * Load a view from the database based upon either vid or name.
+   *
+   * This is a static factory method that implements internal caching for
+   * view objects.
+   *
+   * @param $arg
+   *  The name of the view or its internal view id (vid)
+   * @param $reset
+   *  If TRUE, reset this entry in the load cache.
+   * @return A view object or NULL if it was not available.
+   */
+  function &load($arg, $reset = FALSE) {
+    static $cache = array();
+
+    // We want a NULL value to return TRUE here, so we can't use isset() or empty().
+    if (!array_key_exists($arg, $cache) || $reset) {
+      $where = (is_numeric($arg) ? "vid =  %d" : "name = '%s'");
+      $data = db_fetch_object(db_query("SELECT * FROM {views_view} WHERE $where", $arg));
+      if (empty($data)) {
+        $cache[$arg] = NULL;
+      }
+      else {
+        $view = new view();
+        $view->load_row($data);
+        $view->type = t('Normal');
+        // Load all of our subtables.
+        foreach ($view->db_objects() as $key) {
+          $object_name = "views_$key";
+          $result = db_query("SELECT * FROM {{$object_name}} WHERE vid = %d ORDER BY position", $view->vid);
+
+          while ($data = db_fetch_object($result)) {
+            $object = new $object_name(FALSE);
+            $object->load_row($data);
+
+            // Because it can get complicated with this much indirection,
+            // make a shortcut reference.
+            $location = &$view->$key;
+
+            // If we have a basic id field, load the item onto the view based on
+            // this ID, otherwise push it on.
+            if (!empty($object->id)) {
+              $location[$object->id] = $object;
+            }
+            else {
+              $location[] = $object;
+            }
+          }
+        }
+
+        $view->loaded = TRUE;
+        $cache[$arg] = $view;
+      }
+    }
+
+    return $cache[$arg];
+  }
+
+  /**
+   * Static factory method to load a list of views based upon a $where clause.
+   *
+   * Although this method could be implemented to simply iterate over views::load(),
+   * that would be very slow.  Buiding the views externally from unified queries is
+   * much faster.
+   */
+  function load_views() {
+    $result = db_query("SELECT DISTINCT v.* FROM {views_view} v");
+    $views = array();
+    $vids = array();
+
+    // Load all the views.
+    while ($data = db_fetch_object($result)) {
+      $view = new view;
+      $view->load_row($data);
+      $view->loaded = TRUE;
+      $view->type = t('Normal');
+      $views[$view->name] = $view;
+      $names[$view->vid] = $view->name;
+    }
+
+    // Stop if we didn't get any views.
+    if (!$views) {
+      return array();
+    }
+
+    $vids = implode(', ', array_keys($names));
+    // Now load all the subtables:
+    foreach (view::db_objects() as $key) {
+      $object_name = "views_$key";
+      $result = db_query("SELECT * FROM {{$object_name}} WHERE vid IN ($vids) ORDER BY vid, position");
+
+      while ($data = db_fetch_object($result)) {
+        $object = new $object_name(FALSE);
+        $object->load_row($data);
+
+        // Because it can get complicated with this much indirection,
+        // make a shortcut reference.
+        $location = &$views[$names[$object->vid]]->$key;
+
+        // If we have a basic id field, load the item onto the view based on
+        // this ID, otherwise push it on.
+        if (!empty($object->id)) {
+          $location[$object->id] = $object;
+        }
+        else {
+          $location[] = $object;
+        }
+      }
+    }
+    return $views;
+  }
+
+  /**
+   * Save the view to the database. If the view does not already exist,
+   * A vid will be assigned to the view and also returned from this function.
+   */
+  function save() {
+    if ($this->vid == 'new') {
+      $this->vid = NULL;
+    }
+
+    // If we have no vid or our vid is a string, this is a new view.
+    if (!empty($this->vid)) {
+      // remove existing table entries
+      foreach ($this->db_objects() as $key) {
+        db_query("DELETE from {views_" . $key . "} WHERE vid = %d", $this->vid);
+      }
+    }
+
+    $this->save_row(!empty($this->vid) ? 'vid' : FALSE);
+
+    // Save all of our subtables.
+    foreach ($this->db_objects() as $key) {
+      $this->_save_rows($key);
+    }
+
+    cache_clear_all('views_urls', 'cache_views');
+    cache_clear_all(); // clear the page cache as well.
+  }
+
+  /**
+   * Save a row to the database for the given key, which is one of the
+   * keys from view::db_objects()
+   */
+  function _save_rows($key) {
+    $count = 0;
+    foreach ($this->$key as $position => $object) {
+      $object->position = ++$count;
+      $object->vid = $this->vid;
+      $object->save_row();
+    }
+  }
+
+  /**
+   * Delete the view from the database.
+   */
+  function delete($clear = TRUE) {
+    if (empty($this->vid)) {
+      return;
+    }
+
+    db_query("DELETE FROM {views_view} WHERE vid = %d", $this->vid);
+    // Delete from all of our subtables as well.
+    foreach ($this->db_objects() as $key) {
+      db_query("DELETE from {views_" . $key . "} WHERE vid = %d", $this->vid);
+    }
+
+    cache_clear_all('views_query:' . $this->name, 'cache_views');
+
+    if ($clear) {
+      cache_clear_all(); // this clears the block and page caches only.
+      menu_rebuild(); // force a menu rebuild when a view is deleted.
+    }
+  }
+
+  /**
+   * Export a view as PHP code.
+   */
+  function export($indent = '') {
+    $this->init_display();
+    $output = '';
+    $output .= $this->export_row('view', $indent);
+    // Set the API version
+    $output .= $indent . '$view->api_version = ' . views_api_version() . ";\n";
+    $output .= $indent . '$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */' . "\n";
+
+    foreach ($this->display as $id => $display) {
+      $output .= $indent . "\n/* Display: $display->display_title */\n";
+      $output .= $indent . '$handler = $view->new_display(' . views_var_export($display->display_plugin, $indent) . ', ' . views_var_export($display->display_title, $indent) . ', \'' . $id . "');\n";
+      if (empty($display->handler)) {
+        // @todo -- probably need a method of exporting broken displays as
+        // they may simply be broken because a module is not installed. That
+        // does not invalidate the display.
+        continue;
+      }
+
+      $output .= $display->handler->export_options($indent, '$handler->options');
+    }
+
+    return $output;
+  }
+
+  /**
+   * Make a copy of this view that has been sanitized of all database IDs
+   * and handlers and other stuff.
+   *
+   * I'd call this clone() but it's reserved.
+   */
+  function copy() {
+    $code = $this->export();
+    eval($code);
+    return $view;
+  }
+
+  /**
+   * Safely clone a view.
+   *
+   * Because views are complicated objects within objects, and PHP loves to
+   * do references to everything, if a View is not properly and safely
+   * cloned it will still have references to the original view, and can
+   * actually cause the original view to point to objects in the cloned
+   * view. This gets ugly fast.
+   *
+   * This will completely wipe a view clean so it can be considered fresh.
+   */
+  function clone_view() {
+    $clone = version_compare(phpversion(), '5.0') < 0 ? $this : clone($this);
+
+    $keys = array('current_display', 'display_handler', 'build_info', 'built', 'executed', 'attachment_before', 'attachment_after', 'field', 'argument', 'filter', 'sort', 'relationship', 'header', 'footer', 'empty', 'query', 'inited', 'style_plugin', 'plugin_name', 'exposed_data', 'exposed_input', 'exposed_widgets', 'many_to_one_tables', 'feed_icon');
+    foreach ($keys as $key) {
+      if (isset($clone->$key)) {
+        unset($clone->$key);
+      }
+    }
+    $clone->built = $clone->executed = FALSE;
+    $clone->build_info = array();
+    $clone->attachment_before = '';
+    $clone->attachment_after = '';
+    $clone->result = array();
+
+    // shallow cloning means that all the display objects
+    // *were not cloned*. We must clone them ourselves.
+    $displays = array();
+    foreach ($clone->display as $id => $display) {
+      $displays[$id] = drupal_clone($display);
+      if (isset($displays[$id]->handler)) {
+        unset($displays[$id]->handler);
+      }
+    }
+    $clone->display = $displays;
+
+    return $clone;
+  }
+
+  /**
+   * Unset references so that a $view object may be properly garbage
+   * collected.
+   */
+  function destroy() {
+    foreach (array_keys($this->display) as $display_id) {
+      if (isset($this->display[$display_id]->handler)) {
+        $this->display[$display_id]->handler->destroy();
+        unset($this->display[$display_id]->handler);
+      }
+
+      foreach (views_object_types() as $type => $info) {
+        if (isset($this->$type)) {
+          $handlers = &$this->$type;
+          foreach ($handlers as $id => $item) {
+            $handlers[$id]->destroy();
+          }
+          unset($handlers);
+        }
+      }
+
+      if (isset($this->style_plugin)) {
+        $this->style_plugin->destroy();
+        unset($this->style_plugin);
+      }
+
+      // Clear these to make sure the view can be processed/used again.
+      if (isset($this->display_handler)) {
+        unset($this->display_handler);
+      }
+
+      if (isset($this->current_display)) {
+        unset($this->current_display);
+      }
+
+      if (isset($this->query)) {
+        unset($this->query);
+      }
+
+      $keys = array('current_display', 'display_handler', 'build_info', 'built', 'executed', 'attachment_before', 'attachment_after', 'field', 'argument', 'filter', 'sort', 'relationship', 'header', 'footer', 'empty', 'query', 'result', 'inited', 'style_plugin', 'plugin_name', 'exposed_data', 'exposed_input', 'many_to_one_tables');
+      foreach ($keys as $key) {
+        if (isset($this->$key)) {
+          unset($this->$key);
+        }
+      }
+      $this->built = $this->executed = FALSE;
+      $this->build_info = array();
+      $this->attachment_before = '';
+      $this->attachment_after = '';
+    }
+  }
+
+  /**
+   * Make sure the view is completely valid.
+   *
+   * @return
+   *   TRUE if the view is valid; an array of error strings if it is not.
+   */
+  function validate() {
+    $this->init_display();
+
+    $errors = array();
+
+    foreach ($this->display as $id => $display) {
+      if ($display->handler) {
+        if (!empty($display->deleted)) {
+          continue;
+        }
+
+        $result = $this->display[$id]->handler->validate();
+        if (!empty($result) && is_array($result)) {
+          $errors = array_merge($errors, $result);
+        }
+      }
+    }
+
+    return $errors ? $errors : TRUE;
+  }
+}
+
+/**
+ * Base class for views' database objects.
+ */
+class views_db_object {
+  /**
+   * Initialize this object, setting values from schema defaults.
+   *
+   * @param $init
+   *   If an array, this is a set of values from db_fetch_object to
+   *   load. Otherwse, if TRUE values will be filled in from schema
+   *   defaults.
+   */
+  function init($init = TRUE) {
+    if (is_array($init)) {
+      return $this->load_row($init);
+    }
+
+    if (!$init) {
+      return;
+    }
+
+    $schema = drupal_get_schema($this->db_table);
+
+    if (!$schema) {
+      return;
+    }
+
+    // Go through our schema and build correlations.
+    foreach ($schema['fields'] as $field => $info) {
+      if ($info['type'] == 'serial') {
+        $this->$field = NULL;
+      }
+      if (!isset($this->$field)) {
+        if (!empty($info['serialize']) && isset($info['serialized default'])) {
+          $this->$field = unserialize($info['serialized default']);
+        }
+        else if (isset($info['default'])) {
+          $this->$field = $info['default'];
+        }
+        else {
+          $this->$field = '';
+        }
+      }
+    }
+  }
+
+  /**
+   * Write the row to the database.
+   *
+   * @param $update
+   *   If true this will be an UPDATE query. Otherwise it will be an INSERT.
+   */
+  function save_row($update = NULL) {
+    $schema = drupal_get_schema($this->db_table);
+    $fields = $defs = $values = $serials = array();
+
+    // Go through our schema and build correlations.
+    foreach ($schema['fields'] as $field => $info) {
+      // special case -- skip serial types if we are updating.
+      if ($info['type'] == 'serial') {
+        $serials[] = $field;
+        continue;
+      }
+      $fields[] = $field;
+      switch ($info['type']) {
+        case 'blob':
+          $defs[] = '%b';
+          break;
+        case 'int':
+          $defs[] = '%d';
+          break;
+        case 'float':
+        case 'numeric':
+          $defs[] = '%f';
+          break;
+        default:
+          $defs[] = "'%s'";
+      }
+      if (empty($info['serialize'])) {
+        $values[] = $this->$field;
+      }
+      else {
+        $values[] = serialize($this->$field);
+      }
+    }
+    $query = '';
+    if (!$update) {
+      $query = "INSERT INTO {" . $this->db_table . "} (" . implode(', ', $fields) . ') VALUES (' . implode(', ', $defs) . ')';
+    }
+    else {
+      $query = '';
+      foreach ($fields as $id => $field) {
+        if ($query) {
+          $query .= ', ';
+        }
+        $query .= $field . ' = ' . $defs[$id];
+      }
+      $query = "UPDATE {" . $this->db_table . "} SET " . $query . " WHERE $update = " . $this->$update;
+    }
+    db_query($query, $values);
+
+    if ($serials && !$update) {
+      // get last insert ids and fill them in.
+      foreach ($serials as $field) {
+        $this->$field = db_last_insert_id($this->db_table, $field);
+      }
+    }
+  }
+
+  /**
+   * Load the object with a row from the database.
+   *
+   * This method is separate from the constructor in order to give us
+   * more flexibility in terms of how the view object is built in different
+   * contexts.
+   *
+   * @param $data
+   *   An object from db_fetch_object. It should contain all of the fields
+   *   that are in the schema.
+   */
+  function load_row($data) {
+    $schema = drupal_get_schema($this->db_table);
+
+    // Go through our schema and build correlations.
+    foreach ($schema['fields'] as $field => $info) {
+      $this->$field = empty($info['serialize']) ? $data->$field : unserialize(db_decode_blob($data->$field));
+    }
+  }
+
+  /**
+   * Export a loaded row, such as an argument, field or the view itself to PHP code.
+   *
+   * @param $identifier
+   *   The variable to assign the PHP code for this object to.
+   * @param $indent
+   *   An optional indentation for prettifying nested code.
+   */
+  function export_row($identifier = NULL, $indent = '') {
+    if (!$identifier) {
+      $identifier = $this->db_table;
+    }
+    $schema = drupal_get_schema($this->db_table);
+
+    $output = $indent . '$' . $identifier . ' = new ' . get_class($this) . ";\n";
+    // Go through our schema and build correlations.
+    foreach ($schema['fields'] as $field => $info) {
+      if (!empty($info['no export'])) {
+        continue;
+      }
+      if (!isset($this->$field)) {
+        if (isset($info['default'])) {
+          $this->$field = $info['default'];
+        }
+        else {
+          $this->$field = '';
+        }
+
+        // serialized defaults must be set as serialized.
+        if (isset($info['serialize'])) {
+          $this->$field = unserialize(db_decode_blob($this->$field));
+        }
+      }
+      $value = $this->$field;
+      if ($info['type'] == 'int') {
+        $value = ($info['size'] == 'tiny') ? (bool) $value : (int) $value;
+      }
+
+      $output .= $indent . '$' . $identifier . '->' . $field . ' = ' . views_var_export($value, $indent) . ";\n";
+    }
+    return $output;
+  }
+
+  /**
+   * Add a new display handler to the view, automatically creating an id.
+   *
+   * @param $type
+   *   The plugin type from the views plugin data. Defaults to 'page'.
+   * @param $title
+   *   The title of the display; optional, may be filled in from default.
+   * @param $id
+   *   The id to use.
+   * @return
+   *   The key to the display in $view->display, so that the new display
+   *   can be easily located.
+   */
+  function add_display($type = 'page', $title = NULL, $id = NULL) {
+    if (empty($type)) {
+      return FALSE;
+    }
+
+    $plugin = views_fetch_plugin_data('display', $type);
+    if (empty($plugin)) {
+      $plugin['title'] = t('Broken');
+    }
+
+    // 'default' is singular and is unique, so just go with 'default'
+    // for it. For all others, start counting.
+    if (empty($id)) {
+      $id = ($type == 'default') ? $type : $type . '_1';
+      $title = $plugin['title'];
+
+      $count = 1;
+
+      // Loop through IDs based upon our style plugin name until
+      // we find one that is unused.
+      while (!empty($this->display[$id])) {
+        $id = $type . '_' . ++$count;
+        if (empty($title)) {
+          $title = $plugin['title'] . ' ' . $count;
+        }
+      }
+    }
+
+    // Create the new display object
+    $display = new views_display;
+    $display->options($type, $id, $title);
+
+    // Add the new display object to the view.
+    $this->display[$id] = $display;
+    return $id;
+  }
+
+  /**
+   * Create a new display and a display handler for it.
+   * @param $type
+   *   The plugin type from the views plugin data. Defaults to 'page'.
+   * @param $title
+   *   The title of the display; optional, may be filled in from default.
+   * @param $id
+   *   The id to use.
+   * @return
+   *   A reference to the new handler object.
+   */
+  function &new_display($type = 'page', $title = NULL, $id = NULL) {
+    $id = $this->add_display($type, $title, $id);
+
+    // Create a handler
+    $this->display[$id]->handler = views_get_plugin('display', $this->display[$id]->display_plugin);
+    if (empty($this->display[$id]->handler)) {
+      // provide a 'default' handler as an emergency. This won't work well but
+      // it will keep things from crashing.
+      $this->display[$id]->handler = views_get_plugin('display', 'default');
+    }
+
+    if (!empty($this->display[$id]->handler)) {
+      // Initialize the new display handler with data.
+      $this->display[$id]->handler->init($this, $this->display[$id]);
+      // If this is NOT the default display handler, let it know which is
+      if ($id != 'default') {
+        $this->display[$id]->handler->default_display = &$this->display['default']->handler;
+      }
+    }
+
+    return $this->display[$id]->handler;
+  }
+
+  /**
+   * Add an item with a handler to the view.
+   *
+   * These items may be fields, filters, sort criteria, or arguments.
+   */
+  function add_item($display_id, $type, $table, $field, $options = array(), $id = NULL) {
+    $types = views_object_types();
+    $this->set_display($display_id);
+
+    $fields = $this->display[$display_id]->handler->get_option($types[$type]['plural']);
+
+    if (empty($id)) {
+      $count = 0;
+      $id = $field;
+      while (!empty($fields[$id])) {
+        $id = $field . '_' . ++$count;
+      }
+    }
+
+    $new_item = array(
+      'id' => $id,
+      'table' => $table,
+      'field' => $field,
+    ) + $options;
+
+    if (!empty($types[$type]['type'])) {
+      $handler_type = $types[$type]['type'];
+    }
+    else {
+      $handler_type = $type;
+    }
+
+    $handler = views_get_handler($table, $field, $handler_type);
+
+    $fields[$id] = $new_item;
+    $this->display[$display_id]->handler->set_option($types[$type]['plural'], $fields);
+
+    return $id;
+  }
+
+  /**
+   * Get an array of items for the current display.
+   */
+  function get_items($type, $display_id = NULL) {
+    $this->set_display($display_id);
+
+    if (!isset($display_id)) {
+      $display_id = $this->current_display;
+    }
+
+    // Get info about the types so we can get the right data.
+    $types = views_object_types();
+    return $this->display[$display_id]->handler->get_option($types[$type]['plural']);
+  }
+
+  /**
+   * Get the configuration of an item (field/sort/filter/etc) on a given
+   * display.
+   */
+  function get_item($display_id, $type, $id) {
+    // Get info about the types so we can get the right data.
+    $types = views_object_types();
+    // Initialize the display
+    $this->set_display($display_id);
+
+    // Get the existing configuration
+    $fields = $this->display[$display_id]->handler->get_option($types[$type]['plural']);
+
+    return isset($fields[$id]) ? $fields[$id] : NULL;
+  }
+
+  /**
+   * Get the configuration of an item (field/sort/filter/etc) on a given
+   * display.
+   *
+   * Pass in NULL for the $item to remove an item.
+   */
+  function set_item($display_id, $type, $id, $item) {
+    // Get info about the types so we can get the right data.
+    $types = views_object_types();
+    // Initialize the display
+    $this->set_display($display_id);
+
+    // Get the existing configuration
+    $fields = $this->display[$display_id]->handler->get_option($types[$type]['plural']);
+    if (isset($item)) {
+      $fields[$id] = $item;
+    }
+    else {
+      unset($fields[$id]);
+    }
+
+    // Store.
+    $this->display[$display_id]->handler->set_option($types[$type]['plural'], $fields);
+  }
+
+  /**
+   * Set an option on an item.
+   *
+   * Use this only if you have just 1 or 2 options to set; if you have
+   * many, consider getting the item, adding the options and doing
+   * set_item yourself.
+   */
+  function set_item_option($display_id, $type, $id, $option, $value) {
+    $item = $this->get_item($display_id, $type, $id);
+    $item[$option] = $value;
+    $this->set_item($display_id, $type, $id, $item);
+  }
+}
+
+/**
+ * A display type in a view.
+ *
+ * This is just the database storage mechanism, and isn't terribly important
+ * to the behavior of the display at all.
+ */
+class views_display extends views_db_object {
+  var $db_table = 'views_display';
+  function views_display($init = TRUE) {
+    parent::init($init);
+  }
+
+  function options($type, $id, $title) {
+    $this->display_plugin = $type;
+    $this->id = $id;
+    $this->display_title = $title;
+  }
+}
+
+/**
+ * Provide a list of views object types used in a view, with some information
+ * about them.
+ */
+function views_object_types() {
+  static $retval = NULL;
+
+  // statically cache this so t() doesn't run a bajillion times.
+  if (!isset($retval)) {
+    $retval = array(
+      'field' => array(
+        'title' => t('Fields'), // title
+        'ltitle' => t('fields'), // lowercase title for mid-sentence
+        'stitle' => t('Field'), // singular title
+        'lstitle' => t('field'), // singular lowercase title for mid sentence
+        'plural' => 'fields',
+      ),
+      'argument' => array(
+        'title' => t('Arguments'),
+        'ltitle' => t('arguments'),
+        'stitle' => t('Argument'),
+        'lstitle' => t('Argument'),
+        'plural' => 'arguments',
+      ),
+      'sort' => array(
+        'title' => t('Sort criteria'),
+        'ltitle' => t('sort criteria'),
+        'stitle' => t('Sort criterion'),
+        'lstitle' => t('sort criterion'),
+        'plural' => 'sorts',
+      ),
+      'filter' => array(
+        'title' => t('Filters'),
+        'ltitle' => t('filters'),
+        'stitle' => t('Filter'),
+        'lstitle' => t('filter'),
+        'plural' => 'filters',
+        'options' => 'views_ui_config_filters_form',
+      ),
+      'relationship' => array(
+        'title' => t('Relationships'),
+        'ltitle' => t('relationships'),
+        'stitle' => t('Relationship'),
+        'lstitle' => t('Relationship'),
+        'plural' => 'relationships',
+      ),
+      'header' => array(
+        'title' => t('Header'),
+        'ltitle' => t('header'),
+        'stitle' => t('Header'),
+        'lstitle' => t('Header'),
+        'plural' => 'header',
+        'type' => 'area',
+      ),
+      'footer' => array(
+        'title' => t('Footer'),
+        'ltitle' => t('footer'),
+        'stitle' => t('Footer'),
+        'lstitle' => t('Footer'),
+        'plural' => 'footer',
+        'type' => 'area',
+      ),
+      'empty' => array(
+        'title' => t('Empty text'),
+        'ltitle' => t('empty text'),
+        'stitle' => t('Empty text'),
+        'lstitle' => t('Empty text'),
+        'plural' => 'empty',
+        'type' => 'area',
+      ),
+    );
+  }
+
+  return $retval;
+}
+
+/**
+ * @}
+ */
diff --git includes/view.inc.rej includes/view.inc.rej
new file mode 100644
index 0000000..4256211
--- /dev/null
+++ includes/view.inc.rej
@@ -0,0 +1,11 @@
+--- includes/view.inc
++++ includes/view.inc
+@@ -1505,7 +1505,7 @@
+   function clone_view() {
+     $clone = version_compare(phpversion(), '5.0') < 0 ? $this : clone($this);
+ 
+-    $keys = array('current_display', 'display_handler', 'build_info', 'built', 'executed', 'attachment_before', 'attachment_after', 'field', 'argument', 'filter', 'sort', 'relationship', 'query', 'inited', 'style_plugin', 'plugin_name', 'exposed_data', 'exposed_input', 'many_to_one_tables');
++    $keys = array('current_display', 'display_handler', 'build_info', 'built', 'executed', 'attachment_before', 'attachment_after', 'field', 'argument', 'filter', 'sort', 'relationship', 'query', 'inited', 'style_plugin', 'plugin_name', 'exposed_data', 'exposed_input', 'many_to_one_tables', 'pager');
+     foreach ($keys as $key) {
+       if (isset($clone->$key)) {
+         unset($clone->$key);
diff --git plugins/views_plugin_display.inc plugins/views_plugin_display.inc
index 35a6653..cfdadbd 100644
--- plugins/views_plugin_display.inc
+++ plugins/views_plugin_display.inc
@@ -408,12 +408,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'),
          ),
       ),
       // Note that exposed_form plugin has options in a separate array,
@@ -424,13 +424,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),
          ),
       ),
@@ -441,6 +441,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(),
@@ -449,6 +450,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(),
@@ -462,14 +464,17 @@ class views_plugin_display extends views_plugin {
       'header' => array(
         'default' => array(),
         'export' => 'export_handler',
+        'unpack_translatable' => 'unpack_plugin',
       ),
       'footer' => array(
         'default' => array(),
         'export' => 'export_handler',
+        'unpack_translatable' => 'unpack_plugin',
       ),
       'empty' => array(
         'default' => array(),
         'export' => 'export_handler',
+        'unpack_translatable' => 'unpack_plugin',
       ),
 
       // We want these to export last.
@@ -499,6 +504,7 @@ class views_plugin_display extends views_plugin {
       'filters' => array(
         'default' => array(),
         'export' => 'export_handler',
+        'unpack_translatable' => 'unpack_plugin',
       ),
 
 
@@ -2345,6 +2351,19 @@ 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);
+    }
+
+  }
 }
 
 
diff --git plugins/views_plugin_localization.inc plugins/views_plugin_localization.inc
new file mode 100644
index 0000000..12e25d6
--- /dev/null
+++ plugins/views_plugin_localization.inc
@@ -0,0 +1,127 @@
+<?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();
+
+  /**
+   * 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;
+  }
+}
diff --git plugins/views_plugin_localization_core.inc plugins/views_plugin_localization_core.inc
new file mode 100644
index 0000000..f7a2983
--- /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) {
+    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..42969bb
--- /dev/null
+++ plugins/views_plugin_localization_none.inc
@@ -0,0 +1,36 @@
+<?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 {
+
+  /**
+   * 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 plugins/views_plugin_localization_test.inc plugins/views_plugin_localization_test.inc
new file mode 100644
index 0000000..00c7896
--- /dev/null
+++ plugins/views_plugin_localization_test.inc
@@ -0,0 +1,17 @@
+<?php
+// $Id$
+
+/**
+ * A stump localisation plugin which has static variables to cache the input.
+ */
+class views_plugin_localization_test extends views_plugin_localization {
+  function export($source) {
+    if (!empty($source['value'])) {
+      $this->export_strings[] = $source['value'];
+    }
+  }
+
+  function get_export_strings() {
+    return $this->export_strings;
+  }
+}
diff --git tests/views_query.test tests/views_query.test
index eaec5d8..3daafab 100644
--- tests/views_query.test
+++ tests/views_query.test
@@ -244,7 +244,15 @@ abstract class ViewsSqlTest extends ViewsTestCase {
    * The views plugin definition. Override it if you test provides a plugin.
    */
   protected function viewsPlugins() {
-    return array();
+    return array(
+      'localization' => array(
+        'test' => array(
+          'title' => t('Test'),
+          'help' => t('This is a test description.'),
+          'handler' => 'views_plugin_localization_test',
+        ),
+      ),
+    );
   }
 
   /**
diff --git tests/views_translatable.test tests/views_translatable.test
new file mode 100644
index 0000000..852dff3
--- /dev/null
+++ tests/views_translatable.test
@@ -0,0 +1,74 @@
+<?php
+// $Id$
+
+class viewsTranslatableTest extends ViewsSqlTest {
+  function getInfo() {
+    return array(
+      'name' => 'Views Translatable Test',
+      'description' => 'Tests the pluggable transltations',
+      'group' => 'Views',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+    variable_set('views_localization_plugin', 'none');
+  }
+
+  function viewsPlugins() {
+    return array(
+    );
+  }
+
+  function testUnpackTranslatable() {
+    $view = $this->view_unpack_translatable();
+    $view->set_display('default');
+    $view->export();
+
+    $expected_strings = array('Defaults', 'Apply1', 'Sort By1', 'Asc1', 'Desc1', 'simple1');
+    $result_strings = $view->localization_plugin->get_export_strings();
+    $this->assertEqual($expected_strings, $result_strings, 'Localisation plugin got every translatable string.');
+  }
+
+  function view_unpack_translatable() {
+    $view = new view;
+    $view->name = 'view_unpack_translatable';
+    $view->description = '';
+    $view->tag = '';
+    $view->view_php = '';
+    $view->base_table = 'node';
+    $view->is_cacheable = FALSE;
+    $view->api_version = 2;
+    $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+    /* Display: Defaults */
+    $handler = $view->new_display('default', 'Defaults', 'default');
+    $handler->display->display_options['access']['type'] = 'none';
+    $handler->display->display_options['cache']['type'] = 'none';
+    $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']['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['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'] = 'simple';
+    $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;
+  }
+}
