diff --git a/potx-cli.php b/potx-cli.php
deleted file mode 100755
index bd84f60..0000000
--- a/potx-cli.php
+++ /dev/null
@@ -1,144 +0,0 @@
-#!/usr/bin/php
-<?php
-
-/**
- * @file
- *   Translation template generator for Drupal (command line version).
- *
- *   Extracts translatable strings from t(), t(,array()), format_plural()
- *   and other function calls, plus adds some file specific strings. Only
- *   literal strings with no embedded variables can be extracted. Generates
- *   POT files, errors are printed on STDERR.
- */
-
-if (isset($_SERVER['REQUEST_METHOD'])) {
-  // Try to prevent running this script from the web. It is not designed so.
-  print 'The potx-cli.php script is designed to be used from the command line. Please use the Drupal module web interface to extract strings through the web, instead of this script, if you prefer a web interface.';
-}
-
-// Functions shared with web based interface
-include dirname(__FILE__) .'/potx.inc';
-
-// We need a lot of resources probably, so try to set memory
-// limit higher and set unlimited time for our work.
-$memory_limit = @ini_get('memory_limit');
-if ($memory_limit != '' && $memory_limit != -1 && (int)$memory_limit < 16) {
-  // ini_get returns the original set value, such as "32M",
-  // so we check for the int version. Before PHP 5.2, this
-  // limit was less then 16M.
-  @ini_set('memory_limit', 16777216);
-}
-@set_time_limit(0);
-
-if (!defined("STDERR")) {
-  define('STDERR', fopen('php://stderr', 'w'));
-}
-
-$files = array();
-$build_mode = POTX_BUILD_SINGLE;
-$argv = $GLOBALS['argv'];
-array_shift ($argv);
-if (count($argv)) {
-  switch ($argv[0]) {
-    case '--help' :
-      print <<<END
-Drupal command line translation template generator
-Usage: potx-cli.php [OPTION]
-
-Possible options:
- --auto
-     Autodiscovers files in current folder (default).
- --files
-     Specify a list of files to generate templates for.
- --mode=core
-     Core extraction mode, .info files folded into general.pot.
- --mode=multiple
-     Multiple file output mode, .info files folded into module pot files.
- --mode=single
-     Single file output mode, every file folded into the single outpout file (default).
- --debug
-     Only perform a 'self test'.
- --help
-     Display this message.
-
-END;
-      return 1;
-      break;
-    case '--files' :
-      array_shift($argv);
-      $files = $argv;
-      break;
-    case '--mode=core' :
-      $build_mode = POTX_BUILD_CORE;
-      break;
-    case '--mode=multiple' :
-      $build_mode = POTX_BUILD_MULTIPLE;
-      break;
-    case '--mode=single' :
-      $build_mode = POTX_BUILD_SINGLE;
-      break;
-    case '--debug' :
-      $files = array(__FILE__);
-      break;
-    case '--auto' :
-      $files = _potx_explore_dir('', '*', POTX_API_CURRENT, TRUE);
-      break;
-  }
-}
-
-// Fall back to --auto, if --files are not specified
-if (empty($files)) {
-  $files = _potx_explore_dir('', '*', POTX_API_CURRENT, TRUE);
-}
-
-foreach ($files as $file) {
-  potx_status('status', "Processing $file...\n");
-  _potx_process_file($file);
-}
-
-_potx_build_files(POTX_STRING_RUNTIME, $build_mode);
-_potx_build_files(POTX_STRING_INSTALLER, POTX_BUILD_SINGLE, 'installer');
-_potx_write_files();
-potx_status('status', "\nDone.\n");
-
-return;
-
-// These are never executed, you can run potx-cli.php on itself to test it
-// -----------------------------------------------------------------------------
-
-$a = t("Double quoted test string" );
-$b = t("Test string with %variable", array('%variable' => t('variable replacement')));
-$c = t('Single qouted test string');
-$d = t("Special\ncharacters");
-$e = t('Special\ncharacters');
-$f = t("Embedded $variable");
-$g = t('Embedded $variable');
-$h = t("more \$special characters");
-$i = t('even more \$special characters');
-$j = t("Mixed 'quote' \"marks\"");
-$k = t('Mixed "quote" \'marks\'');
-$l = t('Some repeating text');
-$m = t("Some repeating text");
-$n = t(embedded_function_call(3));
-$o = format_plural($days, 'one day', '@count days');
-$p = format_plural(embedded_function_call($count), 'one day', '@count days');
-$q = t('Concatenated' . 'string.' . 'You should never do this.');
-$r = t("Test string with @complex %variables !smile", array('@complex' => time(), '%variable' => t('variables'), '!smile' => ':)'));
-$s = t('Test context', array(), array('context' => 'Context \'support'));
-$t = t('Test context', $array_var, array('context' => 'Context support'));
-$u = t('Test context', $array_var, array('not-context' => 'Some other string'));
-$v = t("Test string with @complex %variables !smile", array('@complex' => time(), '%variable' => t('variables'), '!smile' => ':)'), array('context' => 'Test strings'));
-$w = t("Test string with @complex %variables !smile", array('@complex' => time(), '%variable' => t('variables'), '!smile' => ':)'), array('context' => t('Test strings')));
-$x = format_plural($days, 'one day', '@count days', array(), array('context' => 'Dates'));
-$y = format_plural($days, 'one day', '@count days', array(), array('context' => t('Dates')));
-
-function embedded_function_call($dummy) { return 12; }
-
-function potxcli_perm() {
-  return array("access potx data", 'administer potx data');
-}
-
-function potxcli_help($section = 'default') {
-  watchdog('help', t('Help called'));
-  return t('This is some help');
-}
diff --git a/potx.admin.inc b/potx.admin.inc
new file mode 100644
index 0000000..1a6878f
--- /dev/null
+++ b/potx.admin.inc
@@ -0,0 +1,271 @@
+<?php
+
+/**
+ * @file
+ *   Administrative interface for the module.
+ */
+
+/**
+ * Component selection interface.
+ */
+function potx_select_component_form() {
+
+  $form = array();
+  $components = _potx_component_list();
+  _potx_component_selector($form, $components);
+
+  // Generate translation file for a specific language if possible.
+  $languages = language_list();
+  if (count($languages) > 1 || !isset($languages['en'])) {
+    // We have more languages, or the single language we have is not English.
+    $options = array('n/a' => t('Language independent template'));
+    foreach ($languages as $langcode => $language) {
+      // Skip English, as we should not have translations for this language.
+      if ($langcode == 'en') {
+        continue;
+      }
+      $options[$langcode] = t('Template file for !langname translations', array('!langname' => t($language->name)));
+    }
+    $form['langcode'] = array(
+      '#type' => 'radios',
+      '#title' => t('Template language'),
+      '#default_value' => 'n/a',
+      '#options' => $options,
+      '#description' => t('Export a language independent or language dependent (plural forms, language team name, etc.) template.'),
+    );
+    $form['translations'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Include translations'),
+      '#description' => t('Include translations of strings in the file generated. Not applicable for language independent templates.')
+    );
+  }
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Extract'),
+  );
+
+  return $form;
+}
+
+/**
+ * Validation handler for potx component selection form.
+ */
+function potx_select_component_form_validate($form, &$form_state) {
+  if (empty($form_state['values']['component'])) {
+    form_set_error('', t('You should select a component to export.'));
+  }
+}
+
+/**
+ * Generate translation template or translation file for the requested component.
+ */
+function potx_select_component_form_submit($form, &$form_state) {
+  global $devel_shutdown;
+
+  // Avoid devel.module putting extra output to the end of files exported.
+  $devel_shutdown = FALSE;
+
+  // This could take some time.
+  @set_time_limit(0);
+  include_once drupal_get_path('module', 'potx') . '/potx.inc';
+
+  // $form_state['values']['component'] either contains a specific file name
+  // with path, or a directory name for a module/theme or a module suite.
+  // Examples:
+  //   modules/watchdog
+  //   sites/all/modules/coder
+  //   sites/all/modules/i18n/i18n.module
+  //   themes/garland
+
+  $component = $form_state['values']['component'];
+  $pathinfo = pathinfo($component);
+  if (!isset($pathinfo['filename'])) {
+    // The filename key is only available in PHP 5.2.0+
+    $pathinfo['filename'] = substr($pathinfo['basename'], 0, strrpos($pathinfo['basename'], '.'));
+  }
+  $strip_prefix = 0;
+
+  if (isset($pathinfo['extension'])) {
+    // A specific module or theme file was requested (otherwise there should be no extension).
+    $files = potx_parser_source_files($pathinfo['dirname'] . '/', $pathinfo['filename']);
+    $strip_prefix = 1 + strlen($pathinfo['dirname']);
+    $outputname = $pathinfo['filename'];
+  }
+  // A directory name was requested.
+  else {
+    $files = potx_parser_source_files($component . '/');
+    $strip_prefix = 1 + strlen($component);
+    $outputname = $pathinfo['basename'];
+  }
+
+  // Decide on template or translation file generation.
+  $template_langcode = $translation_langcode = NULL;
+  if (isset($form_state['values']['langcode']) && ($form_state['values']['langcode'] != 'n/a')) {
+    $template_langcode = $form_state['values']['langcode'];
+    $outputname .= '.' . $template_langcode;
+    if (!empty($form_state['values']['translations'])) {
+      $translation_langcode = $template_langcode;
+      $outputname .= '.po';
+    }
+    else {
+      $outputname .= '.pot';
+    }
+  }
+  else {
+    $outputname .= '.pot';
+  }
+
+  // Collect every string in affected files. Installer related strings are discared.
+  foreach ($files as $file) {
+    potx_parser_parse_file($file, ($strip_prefix > 0 ? substr($file, $strip_prefix) : $file));
+  }
+
+  // HTTP output.
+  header('Content-Type: text/plain; charset=utf-8');
+  header('Content-Transfer-Encoding: 8bit');
+  header("Content-Disposition: 'attachment'; filename=$outputname");
+
+  // Need to include full parameter list to get to passing the language codes.
+  print potx_parser_build_output(POTX_STRING_RUNTIME, 'potx_parser_callback_save_string', 'potx_parser_callback_version', 'potx_parser_callback_header', $template_langcode, $translation_langcode);
+
+  exit;
+}
+
+/**
+ * Build a chunk of the component selection form.
+ *
+ * @param $form
+ *   Form to populate with fields.
+ * @param $components
+ *   Structured array with components as returned by _potx_component_list().
+ * @param $dirname
+ *   Name of directory handled.
+ */
+function _potx_component_selector(&$form, &$components, $dirname = '') {
+
+  // Pop off count of components in this directory.
+  if (isset($components['#-count'])) {
+    $component_count = $components['#-count'];
+    unset($components['#-count']);
+  }
+
+  //ksort($components);
+  $dirkeys = array_keys($components);
+
+  // A directory with one component.
+  if (isset($component_count) && (count($components) == 1)) {
+    $component = array_shift($components);
+    $dirname = dirname($component->filename);
+    $form[_potx_form_id('dir', $dirname)] = array(
+      '#type' => 'radio',
+      '#title' => t('Extract from %name in the %directory directory', array('%directory' => $dirname, '%name' => $component->name)),
+      '#description' => t('Generates output from all files found in this directory.'),
+      '#default_value' => 0,
+      '#return_value' => $dirname,
+      // Get all radio buttons into the same group.
+      '#parents' => array('component'),
+    );
+    return;
+  }
+
+  // A directory with multiple components in it.
+  if (preg_match('!/(modules|themes)\\b(/.+)?!', $dirname, $pathmatch)) {
+    $t_args = array('@directory' => substr($dirname, 1));
+    if (isset($pathmatch[2])) {
+      $form[_potx_form_id('dir', $dirname)] = array(
+        '#type' => 'radio',
+        '#title' => t('Extract from all in directory "@directory"', $t_args),
+        '#description' => t('To extract from a single component in this directory, choose the desired entry in the fieldset below.'),
+        '#default_value' => 0,
+        '#return_value' => substr($dirname, 1),
+        // Get all radio buttons into the same group.
+        '#parents' => array('component'),
+      );
+    }
+    $element = array(
+      '#type' => 'fieldset',
+      '#title' => t('Directory "@directory"', $t_args),
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+    );
+    $form[_potx_form_id('fs', $dirname)] =& $element;
+  }
+  else {
+    $element =& $form;
+  }
+
+  foreach ($dirkeys as $entry) {
+    // A component in this directory with multiple components.
+    if ($entry[0] == '#') {
+      // Component entry.
+      $t_args = array(
+        '%directory' => dirname($components[$entry]->filename),
+        '%name'      => $components[$entry]->name,
+        '%pattern'   => $components[$entry]->name . '.*',
+      );
+      $element[_potx_form_id('com', $components[$entry]->basename)] = array(
+        '#type' => 'radio',
+        '#title' => t('Extract from %name', $t_args),
+        '#description' => t('Extract from files named %pattern in the %directory directory.', $t_args),
+        '#default_value' => 0,
+        '#return_value' => $components[$entry]->filename,
+        // Get all radio buttons into the same group.
+        '#parents' => array('component'),
+      );
+    }
+    // A subdirectory we need to look into.
+    else {
+      _potx_component_selector($element, $components[$entry], "$dirname/$entry");
+    }
+  }
+
+  return count($components);
+}
+
+/**
+ * Generate a sane form element ID for the current radio button.
+ *
+ * @param $type
+ *   Type of ID generated: 'fs' for fieldset, 'dir' for directory, 'com' for component.
+ * @param $path
+ *   Path of file we generate an ID for.
+ * @return
+ *   The generated ID.
+ */
+function _potx_form_id($type, $path) {
+  return 'potx-' . $type . '-' . preg_replace('/[^a-zA-Z0-9]+/', '-', $path);
+}
+
+/**
+ * Generate a hierarchical structured list of components.
+ *
+ * @return
+ *  Array in the directory structure identified.
+ *    - 'normal'  keyed elements being subfolders
+ *    - '#name'   elements being component objects for the 'name' component
+ *    - '#-count' being the file count of all components in the directory
+ */
+function _potx_component_list() {
+  $components = array();
+  // Get a list of all enabled modules and themes.
+  $result = db_query("SELECT name, filename, type, status FROM {system} WHERE type IN ('module', 'theme') ORDER BY filename ASC");
+  foreach ($result as $component) {
+    // Build directory tree structure.
+    $path_parts = explode('/', dirname($component->filename));
+    $dir =& $components;
+    foreach ($path_parts as $dirname) {
+      if (!isset($dir[$dirname])) {
+        $dir[$dirname] = array();
+      }
+      $dir =& $dir[$dirname];
+    }
+
+    // Information about components in this directory.
+    $component->basename = basename($component->filename);
+    $dir['#' . $component->basename] = $component;
+    $dir['#-count'] = isset($dir['#-count']) ? $dir['#-count'] + 1 : 1;
+  }
+
+  return $components;
+}
diff --git a/potx.drush.inc b/potx.drush.inc
new file mode 100644
index 0000000..94e858a
--- /dev/null
+++ b/potx.drush.inc
@@ -0,0 +1,147 @@
+<?php
+
+/**
+ * @file
+ *   Translation template extractor module drush integration.
+ */
+
+/**
+ * Implements hook_drush_command().
+ *
+ * @see drush_parse_command() for a list of recognized keys.
+ *
+ * @return
+ *   An associative array describing our command.
+ */
+function potx_drush_command() {
+  $items['potx'] = array(
+    'callback' => 'potx_drush_extract',
+    'description' => 'Extract translatable strings from Drupal source code.',
+    'arguments' => array(
+      'mode' => 'potx output mode e.g. single or multiple',
+    ),
+    'options' => array(
+      'modules' => 'Comma delimited list of modules to extract translatable strings from.',
+      'files' => 'Comma delimited list of files to extract translatable strings from.',
+      'folder' => 'Folder where the translation extraction is done. If no other option is set this defaults to the current directory.',
+      'api' => 'Drupal core version to use for extraction settings.',
+    ),
+    'examples' => array(
+      'potx single' => 'Extract translatable strings from applicable files in current directory and write to single output file',
+      'potx multiple --modules=example' => 'Extract translatable strings from applicable files of example module and write to module-specific output file.',
+      'potx --files=sites/all/modules/example/example.module' => 'Extract translatable strings from example.module and write to single output file.',
+      'potx single --api=8 --folder=projects/drupal/8' => 'Extract strings from folder projects/drupal/8 using API version 8.',
+    ),
+  );
+  return $items;
+}
+
+/**
+ * Implementation of hook_drush_help().
+ *
+ * This function is called whenever a drush user calls
+ * 'drush help potx'.
+ *
+ * @param
+ *   A string with the help section (prepended with 'drush:').
+ *
+ * @return
+ *   A string with the help text for our command.
+ */
+function potx_drush_help($section) {
+  if ($section == 'drush:potx') {
+    $help = dt('Generates translation templates from the given Drupal source code in the current working directory.');
+    $help .= "\n\n". dt('Possible potx modes are:');
+    $help .= "\n". dt(' single    Single file output mode, every file folded into the single output file (default).');
+    $help .= "\n". dt(' multiple  Multiple file output mode, .info files folded into module .pot files.');
+    $help .= "\n\n". dt('If no files are specified, potx will autodiscover files from the current working directory. You can specify concrete files to look at to limit the scope of the operation.');
+    return $help;
+  }
+}
+
+/**
+ * Drush command callback.
+ */
+function potx_drush_extract($mode = 'single') {
+  // Include library.
+  include_once(dirname(__FILE__) . '/potx.inc');
+  global $_potx_strings;
+
+  // Get Drush options.
+  $modules_option = drush_get_option('modules');
+  $files_option = drush_get_option('files');
+  $folder_option = drush_get_option('folder');
+  $api_option = drush_get_option('api');
+  if (empty($api_option) || !in_array($api_option, array(5, 6, 7, 8))) {
+    $api_option = POTX_API_CURRENT;
+  }
+
+  if (!empty($modules_option)) {
+    $modules = explode(',', $modules_option);
+    foreach ($modules as $module) {
+      $output = '';
+      $files = potx_parser_source_files(drupal_get_path('module', $module), '*', $api_option, TRUE);
+
+      // Parse strings, collect output and write file for current module.
+      foreach ($files as $file) {
+        drush_print("Processing $file...");
+        $split = explode(DIRECTORY_SEPARATOR, $file);
+        $file_name = array_pop($split);
+        potx_parser_parse_file($file, $file_name, 'potx_parser_callback_save_string', 'potx_parser_callback_version', $api_option);
+        $output .= potx_parser_build_output(POTX_STRING_RUNTIME, 'potx_parser_callback_save_string', 'potx_parser_callback_version', NULL);
+      }
+      potx_drush_write_file($module . '.pot', $output);
+      $_potx_strings = array();
+    }
+    drush_print("Done.");
+    return;
+  }
+  elseif (!empty($files_option)) {
+    $files = explode(',', $files_option);
+  }
+  elseif (!empty($folder_option)) {
+    $files = potx_parser_source_files($folder_option, '*', $api_option, TRUE);
+  }
+  else {
+    // No file list provided so autodiscover files in current directory.
+    $files = potx_parser_source_files(drush_cwd(), '*', $api_option, TRUE);
+  }
+
+  $output = '';
+  foreach ($files as $file) {
+    drush_print("Processing $file...");
+    $split = explode(DIRECTORY_SEPARATOR, $file);
+    $file_name = array_pop($split);
+    potx_parser_parse_file($file, $file_name, 'potx_parser_callback_save_string', 'potx_parser_callback_version', $api_option);
+    if (count($_potx_strings) > 0) {
+      if ($mode == 'single') {
+        $output = potx_parser_build_output(POTX_STRING_RUNTIME, 'potx_parser_callback_save_string', 'potx_parser_callback_version', 'potx_parser_callback_header');
+        potx_drush_write_file(str_replace('.', '-', preg_replace('![/]?([a-zA-Z_0-9]*/)*!', '', $file_name)) .'.pot', $output);
+      }
+      elseif ($mode == 'multiple') {
+        $output .= potx_parser_build_output(POTX_STRING_RUNTIME, 'potx_parser_callback_save_string', 'potx_parser_callback_version');
+      }
+      $_potx_strings = array();
+    }
+  }
+
+  if ($mode == 'multiple') {
+    potx_drush_write_file('extract_' . date('mdY') . '.pot', $output);
+  }
+
+  drush_print("Done.");
+}
+
+/**
+ * Writes a file with given strings.
+ *
+ * @param string $file_name
+ *   Target file name.
+ * @param string $output
+ *   The string that will be written in the file.
+ */
+function potx_drush_write_file($file_name, $output) {
+  $fp = fopen($file_name, 'w');
+  fwrite($fp, $output);
+  fclose($fp);
+}
diff --git a/potx.inc b/potx.inc
index ce68201..16a973b 100644
--- a/potx.inc
+++ b/potx.inc
@@ -2,7 +2,10 @@
 
 /**
  * @file
- *   Extraction API used by the web and command line interface.
+ *   Localization string extraction API.
+ *
+ *   The main goal of the API is to let callers parse Drupal source code files
+ *   and get strings found using the localization API.
  *
  *   This include file implements the default string and file version
  *   storage as well as formatting of POT files for web download or
@@ -12,10 +15,6 @@
  *   functions can be implemented to use the functionality provided here as an
  *   API for Drupal code to translatable string conversion.
  *
- *   The potx-cli.php script can be used with this include file as
- *   a command line interface to string extraction. The potx.module
- *   can be used as a web interface for manual extraction.
- *
  *   For a module using potx as an extraction API, but providing more
  *   sophisticated functionality on top of it, look into the
  *   'Localization server' module: http://drupal.org/project/l10n_server
@@ -29,50 +28,6 @@
 define('POTX_API_CURRENT', 7);
 
 /**
- * Silence status reports.
- */
-define('POTX_STATUS_SILENT', 0);
-
-/**
- * Drupal message based status reports.
- */
-define('POTX_STATUS_MESSAGE', 1);
-
-/**
- * Command line status reporting.
- *
- * Status goes to standard output, errors to standard error.
- */
-define('POTX_STATUS_CLI', 2);
-
-/**
- * Structured array status logging.
- *
- * Useful for coder review status reporting.
- */
-define('POTX_STATUS_STRUCTURED', 3);
-
-/**
- * Core parsing mode:
- *  - .info files folded into general.pot
- *  - separate files generated for modules
- */
-define('POTX_BUILD_CORE', 0);
-
-/**
- * Multiple files mode:
- *  - .info files folded into their module pot files
- *  - separate files generated for modules
- */
-define('POTX_BUILD_MULTIPLE', 1);
-
-/**
- * Single file mode:
- *  - all files folded into one pot file
- */
-define('POTX_BUILD_SINGLE', 2);
-
-/**
  * Save string to both installer and runtime collection.
  */
 define('POTX_STRING_BOTH', 0);
@@ -116,46 +71,123 @@ define('POTX_CONTEXT_NONE', NULL);
  */
 define('POTX_CONTEXT_ERROR', FALSE);
 
+// === File lookup, parsing and output building ================================
+
+/**
+ * Collect a list of source file names relevant for extraction.
+ *
+ * @param $path
+ *   Where to start searching for files recursively.
+ *   Provide non-empty path values with a trailing slash.
+ * @param $basename
+ *   Allows the restriction of search to a specific basename
+ *   (ie. to collect files for a specific module).
+ * @param $api_version
+ *   Drupal API version to work with.
+ */
+function potx_parser_source_files($path = '', $basename = '*', $api_version = POTX_API_CURRENT) {
+  // It would be so nice to just use GLOB_BRACE, but it is not available on all
+  // operarting systems, so we are working around the missing functionality by
+  // going thrugh per extension.
+  $extensions = array('php', 'inc', 'module', 'engine', 'theme', 'install', 'info', 'profile');
+  if ($api_version > POTX_API_5) {
+    $extensions[] = 'js';
+  }
+  $files = array();
+  foreach ($extensions as $extension) {
+    $files_here = glob($path . $basename . '.' . $extension);
+    if (is_array($files_here)) {
+      $files = array_merge($files, $files_here);
+    }
+    if ($basename != '*') {
+      // Basename was specific, so look for things like basename.admin.inc as well.
+      // If the basnename was *, the above glob() already covered this case.
+      $files_here = glob($path . $basename . '.*.' . $extension);
+      if (is_array($files_here)) {
+        $files = array_merge($files, $files_here);
+      }
+    }
+  }
+
+  // Grab subdirectories.
+  $dirs = glob($path . '*', GLOB_ONLYDIR);
+  if (is_array($dirs)) {
+    foreach ($dirs as $dir) {
+      // Skip CVS, svn and git data as well as tests.
+      if (!preg_match("!(^|.+/)(CVS|\.svn|\.git|tests)$!", $dir)) {
+        $files = array_merge($files, potx_parser_source_files("$dir/", $basename));
+      }
+    }
+  }
+
+  foreach ($files as $id => $file_name) {
+    // Skip API and test files.
+    if (preg_match('!(\.api\.php|\.test)$!', $file_name)) {
+      unset($files[$id]);
+    }
+  }
+  return $files;
+}
+
 /**
- * Process a file and put extracted information to the given parameters.
+ * Process a file and save extracted information with the callbacks given.
  *
  * @param $file_path
  *   Comlete path to file to process.
- * @param $strip_prefix
- *   An integer denoting the number of chars to strip from filepath for output.
- * @param $save_callback
+ * @param $display_name
+ *   Display oriented file name for saved string data and error messages.
+ * @param $string_save_callback
  *   Callback function to use to save the collected strings.
- * @param $version_callback
+ * @param $file_version_callback
  *   Callback function to use to save collected version numbers.
  * @param $api_version
  *   Drupal API version to work with.
  */
-function _potx_process_file($file_path, $strip_prefix = 0, $save_callback = '_potx_save_string', $version_callback = '_potx_save_version', $api_version = POTX_API_CURRENT) {
+function potx_parser_parse_file($file_path, $display_name = '', $string_save_callback = 'potx_parser_callback_save_string', $file_version_callback = 'potx_parser_callback_version', $api_version = POTX_API_CURRENT) {
   global $_potx_tokens, $_potx_lookup;
 
-  // Figure out the basename and extension to select extraction method.
   $basename = basename($file_path);
   $name_parts = pathinfo($basename);
+  $display_name = empty($display_name) ? $file_path : $display_name;
 
-  // Always grab the CVS version number from the code
+  // Grab the CVS version number from the code if available.
+  // @todo: Drupal is moving to git, so this will not have much meaning then.
   $code = file_get_contents($file_path);
-  $file_name = $strip_prefix > 0 ? substr($file_path, $strip_prefix) : $file_path;
-  _potx_find_version_number($code, $file_name, $version_callback);
+  potx_parser_get_version_number($code, $display_name, $file_version_callback);
 
-  // The .info files are not PHP code, no need to tokenize.
   if ($name_parts['extension'] == 'info') {
-    _potx_find_info_file_strings($file_path, $file_name, $save_callback, $api_version);
+    // .info files are not PHP code, no need to tokenize.
+    potx_parser_get_info_strings($file_path, $display_name, $string_save_callback, $api_version);
     return;
   }
-  elseif ($name_parts['extension'] == 'js' && $api_version > POTX_API_5) {
-    // @todo: D7 context support.
-    _potx_parse_js_file($code, $file_name, $save_callback);
+  elseif ($name_parts['extension'] == 'js') {
+    // JS files are not PHP code, no need to tokenize.
+    if ($api_version > POTX_API_5) {
+      potx_parser_get_js_strings($code, $display_name, $string_save_callback);
+    }
+    return;
   }
 
   // Extract raw PHP language tokens.
   $raw_tokens = token_get_all($code);
   unset($code);
 
+  // List of tokens to get an index of, so we can do quick lookups of use of
+  // these functions and don't need to look through the whole token array.
+  $tokens_to_index = array(
+    't',
+    'st',
+    '_locale_import_message',
+    'watchdog',
+    'format_plural',
+    $name_parts['filename'] . '_perm',
+    'node_perm',
+    $name_parts['filename'] . '_menu',
+    $name_parts['filename'] . '_menu_alter',
+    '_locale_get_predefined_list',
+    '_locale_get_iso639_list',
+  );
+
   // Remove whitespace and possible HTML (the later in templates for example),
   // count line numbers so we can include them in the output.
   $_potx_tokens = array();
@@ -163,11 +195,13 @@ function _potx_process_file($file_path, $strip_prefix = 0, $save_callback = '_po
   $token_number = 0;
   $line_number = 1;
   foreach ($raw_tokens as $token) {
-    if ((!is_array($token)) || (($token[0] != T_WHITESPACE) && ($token[0] != T_INLINE_HTML))) {
+    if (!is_array($token) || (($token[0] != T_WHITESPACE) && ($token[0] != T_INLINE_HTML))) {
       if (is_array($token)) {
         $token[] = $line_number;
-         // Fill array for finding token offsets quickly.
-         if ($token[0] == T_STRING || ($token[0] == T_VARIABLE && $token[1] == '$t')) {
+         // Fill an array for finding token offsets quickly. We need the $t
+         // entries, as well as other functions invoked in the code that we
+         // look for.
+         if (($token[0] == T_STRING && in_array($token[1], $tokens_to_index)) || ($token[0] == T_VARIABLE && $token[1] == '$t')) {
            if (!isset($_potx_lookup[$token[1]])) {
              $_potx_lookup[$token[1]] = array();
            }
@@ -177,7 +211,9 @@ function _potx_process_file($file_path, $strip_prefix = 0, $save_callback = '_po
       $_potx_tokens[] = $token;
       $token_number++;
     }
-    // Collect line numbers.
+
+    // Keep track of line numbers based on newlines in the token we just
+    // processed.
     if (is_array($token)) {
       $line_number += count(explode("\n", $token[1])) - 1;
     }
@@ -187,79 +223,72 @@ function _potx_process_file($file_path, $strip_prefix = 0, $save_callback = '_po
   }
   unset($raw_tokens);
 
-  // Regular t() calls with different usages.
-  if ($api_version > POTX_API_6) {
-    // Drupal 7 onwards supports context on t() and st().
-    _potx_find_t_calls_with_context($file_name, $save_callback);
-    _potx_find_t_calls_with_context($file_name, $save_callback, '$t', POTX_STRING_BOTH);
-    _potx_find_t_calls_with_context($file_name, $save_callback, 'st', POTX_STRING_INSTALLER);
-  }
-  else {
-    _potx_find_t_calls($file_name, $save_callback);
-    _potx_find_t_calls($file_name, $save_callback, '$t', POTX_STRING_BOTH);
-    _potx_find_t_calls($file_name, $save_callback, 'st', POTX_STRING_INSTALLER);
-  }
+  // Drupal 7 onwards supports context on t(), st() and $t().
+  $use_context = ($api_version > POTX_API_6);
+  potx_parser_find_t_strings($display_name, $string_save_callback, 't',  POTX_STRING_RUNTIME, $use_context);
+  potx_parser_find_t_strings($display_name, $string_save_callback, '$t', POTX_STRING_BOTH, $use_context);
+  potx_parser_find_t_strings($display_name, $string_save_callback, 'st', POTX_STRING_INSTALLER, $use_context);
+
   // This does not support context even in Drupal 7.
-  _potx_find_t_calls($file_name, $save_callback, '_locale_import_message', POTX_STRING_BOTH);
+  potx_parser_find_t_strings($display_name, $string_save_callback, '_locale_import_message', POTX_STRING_BOTH);
 
   if ($api_version > POTX_API_5) {
     // Watchdog calls have both of their arguments translated from Drupal 6.x.
-    _potx_find_watchdog_calls($file_name, $save_callback);
+    potx_parser_find_watchdog_strings($display_name, $string_save_callback);
   }
   else {
     // Watchdog calls only have their first argument translated in Drupal 5.x
     // and before.
-    _potx_find_t_calls($file_name, $save_callback, 'watchdog');
+    potx_parser_find_t_strings($display_name, $string_save_callback, 'watchdog');
   }
 
   // Plurals need unique parsing.
-  _potx_find_format_plural_calls($file_name, $save_callback, $api_version);
+  potx_parser_find_format_plural_strings($display_name, $string_save_callback, $api_version);
 
   if ($name_parts['extension'] == 'module') {
     if ($api_version < POTX_API_7) {
-      _potx_find_perm_hook($file_name, $name_parts['filename'], $save_callback);
+      potx_parser_find_perm_strings($display_name, $name_parts['filename'], $string_save_callback);
     }
     if ($api_version > POTX_API_5) {
-      _potx_find_menu_hooks($file_name, $name_parts['filename'], $save_callback);
+      potx_parser_find_menu_strings($display_name, $name_parts['filename'], $string_save_callback);
     }
   }
 
   // Special handling of some Drupal core files.
   if (($basename == 'locale.inc' && $api_version < POTX_API_7) || $basename == 'iso.inc') {
-    _potx_find_language_names($file_name, $save_callback, $api_version);
+    potx_parser_find_language_strings($display_name, $string_save_callback, $api_version);
   }
   elseif ($basename == 'locale.module') {
-    _potx_add_date_strings($file_name, $save_callback, $api_version);
+    potx_parser_add_date_strings($display_name, $string_save_callback, $api_version);
   }
   elseif ($basename == 'common.inc') {
-    _potx_add_format_interval_strings($file_name, $save_callback, $api_version);
+    potx_parser_add_format_interval_strings($display_name, $string_save_callback, $api_version);
   }
   elseif ($basename == 'system.module') {
-    _potx_add_default_region_names($file_name, $save_callback, $api_version);
+    potx_parser_add_default_region_strings($display_name, $string_save_callback, $api_version);
   }
   elseif ($basename == 'user.module') {
     // Save default user role names.
-    $save_callback('anonymous user', POTX_CONTEXT_NONE, $file_name);
-    $save_callback('authenticated user', POTX_CONTEXT_NONE, $file_name);
+    $string_save_callback('anonymous user', POTX_CONTEXT_NONE, $display_name);
+    $string_save_callback('authenticated user', POTX_CONTEXT_NONE, $display_name);
     if ($api_version > POTX_API_6) {
       // Administator role is included by default from Drupal 7.
-      $save_callback('administrator', POTX_CONTEXT_NONE, $file_name);
+      $string_save_callback('administrator', POTX_CONTEXT_NONE, $display_name);
     }
   }
+
+  // Drop this data set that we don't need anymore.
+  $_potx_tokens = $_potx_lookup = array();
 }
 
 /**
- * Creates complete file strings with _potx_store()
+ * Build a complete .po file based on string data collected earlier.
  *
  * @param $string_mode
  *   Strings to generate files for: POTX_STRING_RUNTIME or POTX_STRING_INSTALLER.
- * @param $build_mode
- *   Storage mode used: single, multiple or core
- * @param $force_name
- *   Forces a given file name to get used, if single mode is on, without extension
- * @param $save_callback
+ * @param $string_save_callback
  *   Callback used to save strings previously.
- * @param $version_callback
+ * @param $file_version_callback
  *   Callback used to save versions previously.
  * @param $header_callback
  *   Callback to invoke to get the POT header.
@@ -271,12 +300,19 @@ function _potx_process_file($file_path, $strip_prefix = 0, $save_callback = '_po
  * @param $api_version
  *   Drupal API version to work with.
  */
-function _potx_build_files($string_mode = POTX_STRING_RUNTIME, $build_mode = POTX_BUILD_SINGLE, $force_name = 'general',  $save_callback = '_potx_save_string', $version_callback = '_potx_save_version', $header_callback = '_potx_get_header', $template_export_langcode = NULL, $translation_export_langcode = NULL, $api_version = POTX_API_CURRENT) {
-  global $_potx_store;
+function potx_parser_build_output($string_mode = POTX_STRING_RUNTIME,  $string_save_callback = 'potx_parser_callback_save_string', $file_version_callback = 'potx_parser_callback_version', $header_callback = NULL, $template_export_langcode = NULL, $translation_export_langcode = NULL, $api_version = POTX_API_CURRENT) {
+
+  // Initialize storage for .po file output.
+  $data = array(
+    'sources' => array(),
+    'strings' => '',
+  );
+
+  $data['header'] = (!empty($header_callback) && function_exists($header_callback)) ? $header_callback($template_export_langcode, $api_version) : '';
 
   // Get strings and versions by reference.
-  $strings  = $save_callback(NULL, NULL, NULL, 0, $string_mode);
-  $versions = $version_callback();
+  $strings  = $string_save_callback(NULL, NULL, NULL, 0, $string_mode);
+  $versions = $file_version_callback();
 
   // We might not have any string recorded in this string mode.
   if (!is_array($strings)) {
@@ -287,53 +323,20 @@ function _potx_build_files($string_mode = POTX_STRING_RUNTIME, $build_mode = POT
     foreach ($string_info as $context => $file_info) {
       // Build a compact list of files this string occured in.
       $occured = $file_list = array();
-      // Look for strings appearing in multiple directories (ie.
-      // different subprojects). So we can include them in general.pot.
-      $names = array_keys($file_info);
-      $last_location = dirname(array_shift($names));
-      $multiple_locations = FALSE;
       foreach ($file_info as $file => $lines) {
-        $occured[] = "$file:". join(';', $lines);
+        $occured[] = $file . ':' . join(';', $lines);
         if (isset($versions[$file])) {
           $file_list[] = $versions[$file];
         }
-        if (dirname($file) != $last_location) {
-          $multiple_locations = TRUE;
-        }
-        $last_location = dirname($file);
       }
 
       // Mark duplicate strings (both translated in the app and in the installer).
-      $comment = join(" ", $occured);
+      $comment = join(' ', $occured);
       if (strpos($comment, '(dup)') !== FALSE) {
-        $comment = '(duplicate) '. str_replace('(dup)', '', $comment);
+        $comment = '(duplicate) ' . str_replace('(dup)', '', $comment);
       }
       $output = "#: $comment\n";
 
-      if ($build_mode == POTX_BUILD_SINGLE) {
-        // File name forcing in single mode.
-        $file_name = $force_name;
-      }
-      elseif (strpos($comment, '.info')) {
-        // Store .info file strings either in general.pot or the module pot file,
-        // depending on the mode used.
-        $file_name = ($build_mode == POTX_BUILD_CORE ? 'general' : str_replace('.info', '.module', $file_name));
-      }
-      elseif ($multiple_locations) {
-        // Else if occured more than once, store in general.pot.
-        $file_name = 'general';
-      }
-      else {
-        // Fold multiple files in the same folder into one.
-        if (empty($last_location) || $last_location == '.') {
-          $file_name = 'root';
-        }
-        else {
-          $file_name = str_replace('/', '-', $last_location);
-        }
-      }
-
-
       if (strpos($string, "\0") !== FALSE) {
         // Plural strings have a null byte delimited format.
         list($singular, $plural) = explode("\0", $string);
@@ -365,23 +368,23 @@ function _potx_build_files($string_mode = POTX_STRING_RUNTIME, $build_mode = POT
       }
       $output .= "\n";
 
-      // Store the generated output in the given file storage.
-      if (!isset($_potx_store[$file_name])) {
-        $_potx_store[$file_name] = array(
-          'header'  => $header_callback($file_name, $template_export_langcode, $api_version),
-          'sources' => $file_list,
-          'strings' => $output,
-          'count'   => 1,
-        );
-      }
-      else {
-        // Maintain a list of unique file names.
-        $_potx_store[$file_name]['sources']  = array_unique(array_merge($_potx_store[$file_name]['sources'], $file_list));
-        $_potx_store[$file_name]['strings'] .= $output;
-        $_potx_store[$file_name]['count']   += 1;
-      }
+      // Maintain a list of unique file names.
+      $data['sources']  = array_unique(array_merge($data['sources'], $file_list));
+      $data['strings'] .= $output;
     }
   }
+
+  // Build replacement for file listing.
+  if (count($data['sources']) > 1) {
+    $filelist = "Generated from files:\n#  " . join("\n#  ", $data['sources']);
+  }
+  elseif (count($data['sources']) == 1) {
+    $filelist = "Generated from file: " . join('', $data['sources']);
+  }
+  else {
+    $filelist = 'No version information was available in the source files.';
+  }
+  return str_replace('--VERSIONS--', $filelist, $data['header']) . $data['strings'];
 }
 
 /**
@@ -409,7 +412,7 @@ function _potx_translation_export($translation_export_langcode, $string, $plural
   if (!isset($plural)) {
     // Single string to look translation up for.
     if ($translation = db_query("SELECT t.translation FROM {locales_source} s LEFT JOIN {locales_target} t ON t.lid = s.lid WHERE s.source = :source AND t.{$language_column} = :langcode", array(':source' => $string, ':langcode' => $translation_export_langcode))->fetchField()) {
-      return 'msgstr '. _locale_export_string($translation);
+      return 'msgstr ' . _locale_export_string($translation);
     }
     return "msgstr \"\"\n";
   }
@@ -418,7 +421,7 @@ function _potx_translation_export($translation_export_langcode, $string, $plural
     // String with plural variants. Fill up source string array first.
     $plural = stripcslashes($plural);
     $strings = array();
-    $number_of_plurals = db_query('SELECT plurals FROM {'. $language_table ."} WHERE {$language_column} = :langcode", array(':langcode' => $translation_export_langcode))->fetchField();
+    $number_of_plurals = db_query('SELECT plurals FROM {' . $language_table . "} WHERE {$language_column} = :langcode", array(':langcode' => $translation_export_langcode))->fetchField();
     $plural_index = 0;
     while ($plural_index < $number_of_plurals) {
       if ($plural_index == 0) {
@@ -432,7 +435,7 @@ function _potx_translation_export($translation_export_langcode, $string, $plural
       else {
         // More plural versions only if required, with the lookup source
         // string modified as imported into the database.
-        $strings[] = str_replace('@count', '@count['. $plural_index .']', $plural);
+        $strings[] = str_replace('@count', '@count[' . $plural_index . ']', $plural);
       }
       $plural_index++;
     }
@@ -442,10 +445,10 @@ function _potx_translation_export($translation_export_langcode, $string, $plural
       // Source string array was done, so export translations.
       foreach ($strings as $index => $string) {
         if ($translation = db_query("SELECT t.translation FROM {locales_source} s LEFT JOIN {locales_target} t ON t.lid = s.lid WHERE s.source = :source AND t.{$language_column} = :langcode", array(':source' => $string, ':langcode' => $translation_export_langcode))->fetchField()) {
-          $output .= 'msgstr['. $index .'] '. _locale_export_string(_locale_export_remove_plural($translation));
+          $output .= 'msgstr[' . $index . '] ' . _locale_export_string(_locale_export_remove_plural($translation));
         }
         else {
-          $output .= "msgstr[". $index ."] \"\"\n";
+          $output .= 'msgstr[' . $index . "] \"\"\n";
         }
       }
     }
@@ -458,253 +461,7 @@ function _potx_translation_export($translation_export_langcode, $string, $plural
   }
 }
 
-/**
- * Returns a header generated for a given file
- *
- * @param $file
- *   Name of POT file to generate header for
- * @param $template_export_langcode
- *   Language code if the template should have language dependent content
- *   (like plural formulas and language name) included.
- * @param $api_version
- *   Drupal API version to work with.
- */
-function _potx_get_header($file, $template_export_langcode = NULL, $api_version = POTX_API_CURRENT) {
-  // We only have language to use if we should export with that langcode.
-  $language = NULL;
-  if (isset($template_export_langcode)) {
-    $language = db_query($api_version > POTX_API_5 ? "SELECT language, name, plurals, formula FROM {languages} WHERE language = :langcode" : "SELECT locale, name, plurals, formula FROM {locales_meta} WHERE locale = :langcode", array(':langcode' => $template_export_langcode))->fetchObject();
-  }
-
-  $output  = '# $'.'Id'.'$'."\n";
-  $output .= "#\n";
-  $output .= '# '. (isset($language) ? $language->name : 'LANGUAGE') .' translation of Drupal ('. $file .")\n";
-  $output .= "# Copyright YEAR NAME <EMAIL@ADDRESS>\n";
-  $output .= "# --VERSIONS--\n";
-  $output .= "#\n";
-  $output .= "#, fuzzy\n";
-  $output .= "msgid \"\"\n";
-  $output .= "msgstr \"\"\n";
-  $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
-  $output .= '"POT-Creation-Date: '. date("Y-m-d H:iO") ."\\n\"\n";
-  $output .= '"PO-Revision-Date: '. (isset($language) ? date("Y-m-d H:iO") : 'YYYY-mm-DD HH:MM+ZZZZ') ."\\n\"\n";
-  $output .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
-  $output .= "\"Language-Team: ". (isset($language) ? $language->name : 'LANGUAGE') ." <EMAIL@ADDRESS>\\n\"\n";
-  $output .= "\"MIME-Version: 1.0\\n\"\n";
-  $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
-  $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
-  if (isset($language->formula) && isset($language->plurals)) {
-    $output .= "\"Plural-Forms: nplurals=". $language->plurals ."; plural=". strtr($language->formula, array('$' => '')) .";\\n\"\n\n";
-  }
-  else {
-    $output .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n\n";
-  }
-  return $output;
-}
-
-/**
- * Write out generated files to the current folder.
- *
- * @param $http_filename
- *   File name for content-disposition header in case of usage
- *   over HTTP. If not given, files are written to the local filesystem.
- * @param $content_disposition
- *   See RFC2183. 'inline' or 'attachment', with a default of
- *   'inline'. Only used if $http_filename is set.
- * @todo
- *   Look into whether multiple files can be output via HTTP.
- */
-function _potx_write_files($http_filename = NULL, $content_disposition = 'inline') {
-  global $_potx_store;
-
-  // Generate file lists and output files.
-  if (is_array($_potx_store)) {
-    foreach ($_potx_store as $file => $contents) {
-      // Build replacement for file listing.
-      if (count($contents['sources']) > 1) {
-        $filelist = "Generated from files:\n#  " . join("\n#  ", $contents['sources']);
-      }
-      elseif (count($contents['sources']) == 1) {
-        $filelist = "Generated from file: " . join('', $contents['sources']);
-      }
-      else {
-        $filelist = 'No version information was available in the source files.';
-      }
-      $output = str_replace('--VERSIONS--', $filelist, $contents['header'] . $contents['strings']);
-
-      if ($http_filename) {
-        // HTTP output.
-        header('Content-Type: text/plain; charset=utf-8');
-        header('Content-Transfer-Encoding: 8bit');
-        header("Content-Disposition: $content_disposition; filename=$http_filename");
-        print $output;
-        return;
-      }
-      else {
-        // Local file output, flatten directory structure.
-        $file = str_replace('.', '-', preg_replace('![/]?([a-zA-Z_0-9]*/)*!', '', $file)) .'.pot';
-        $fp = fopen($file, 'w');
-        fwrite($fp, $output);
-        fclose($fp);
-      }
-    }
-  }
-}
-
-/**
- * Escape quotes in a strings depending on the surrounding
- * quote type used.
- *
- * @param $str
- *   The strings to escape
- */
-function _potx_format_quoted_string($str) {
-  $quo = substr($str, 0, 1);
-  $str = substr($str, 1, -1);
-  if ($quo == '"') {
-    $str = stripcslashes($str);
-  }
-  else {
-    $str = strtr($str, array("\\'" => "'", "\\\\" => "\\"));
-  }
-  return addcslashes($str, "\0..\37\\\"");
-}
-
-/**
- * Output a marker error with an extract of where the error was found.
- *
- * @param $file
- *   Name of file
- * @param $line
- *   Line number of error
- * @param $marker
- *   Function name with which the error was identified
- * @param $ti
- *   Index on the token array
- * @param $error
- *   Helpful error message for users.
- * @param $docs_url
- *   Documentation reference.
- */
-function _potx_marker_error($file, $line, $marker, $ti, $error, $docs_url = NULL) {
-  global $_potx_tokens;
-
-  $tokens = '';
-  $ti += 2;
-  $tc = count($_potx_tokens);
-  $par = 1;
-  while ((($tc - $ti) > 0) && $par) {
-    if (is_array($_potx_tokens[$ti])) {
-      $tokens .= $_potx_tokens[$ti][1];
-    }
-    else {
-      $tokens .= $_potx_tokens[$ti];
-      if ($_potx_tokens[$ti] == "(") {
-        $par++;
-      }
-      else if ($_potx_tokens[$ti] == ")") {
-        $par--;
-      }
-    }
-    $ti++;
-  }
-  potx_status('error', $error, $file, $line, $marker .'('. $tokens, $docs_url);
-}
-
-/**
- * Status notification function.
- *
- * @param $op
- *   Operation to perform or type of message text.
- *     - set:    sets the reporting mode to $value
- *               use one of the POTX_STATUS_* constants as $value
- *     - get:    returns the list of error messages recorded
- *               if $value is true, it also clears the internal message cache
- *     - error:  sends an error message in $value with optional $file and $line
- *     - status: sends a status message in $value
- * @param $value
- *   Value depending on $op.
- * @param $file
- *   Name of file the error message is related to.
- * @param $line
- *   Number of line the error message is related to.
- * @param $excerpt
- *   Excerpt of the code in question, if available.
- * @param $docs_url
- *   URL to the guidelines to follow to fix the problem.
- */
-function potx_status($op, $value = NULL, $file = NULL, $line = NULL, $excerpt = NULL, $docs_url = NULL) {
-  static $mode = POTX_STATUS_CLI;
-  static $messages = array();
-
-  switch ($op) {
-    case 'set':
-      // Setting the reporting mode.
-      $mode = $value;
-      return;
-
-    case 'get':
-      // Getting the errors. Optionally deleting the messages.
-      $errors = $messages;
-      if (!empty($value)) {
-        $messages = array();
-      }
-      return $errors;
-
-    case 'error':
-    case 'status':
-
-      // Location information is required in 3 of the four possible reporting
-      // modes as part of the error message. The structured mode needs the
-      // file, line and excerpt info separately, not in the text.
-      $location_info = '';
-      if (($mode != POTX_STATUS_STRUCTURED) && isset($file)) {
-        if (isset($line)) {
-          if (isset($excerpt)) {
-            $location_info = t('At %excerpt in %file on line %line.', array('%excerpt' => $excerpt, '%file' => $file, '%line' => $line));
-          }
-          else {
-            $location_info = t('In %file on line %line.', array('%file' => $file, '%line' => $line));
-          }
-        }
-        else {
-          if (isset($excerpt)) {
-            $location_info = t('At %excerpt in %file.', array('%excerpt' => $excerpt, '%file' => $file));
-          }
-          else {
-            $location_info = t('In %file.', array('%file' => $file));
-          }
-        }
-      }
-
-      // Documentation helpers are provided as readable text in most modes.
-      $read_more = '';
-      if (($mode != POTX_STATUS_STRUCTURED) && isset($docs_url)) {
-        $read_more = ($mode == POTX_STATUS_CLI) ? t('Read more at @url', array('@url' => $docs_url)) : t('Read more at <a href="@url">@url</a>', array('@url' => $docs_url));
-      }
-
-      // Error message or progress text to display.
-      switch ($mode) {
-        case POTX_STATUS_MESSAGE:
-          drupal_set_message(join(' ', array($value, $location_info, $read_more)), $op);
-          break;
-        case POTX_STATUS_CLI:
-          fwrite($op == 'error' ? STDERR : STDOUT, join("\n", array($value, $location_info, $read_more)) ."\n\n");
-          break;
-        case POTX_STATUS_SILENT:
-          if ($op == 'error') {
-            $messages[] = join(' ', array($value, $location_info, $read_more));
-          }
-          break;
-        case POTX_STATUS_STRUCTURED:
-          if ($op == 'error') {
-            $messages[] = array($value, $file, $line, $excerpt, $docs_url);
-          }
-          break;
-      }
-      return;
-  }
-}
+// === Token based code tree parsers ===========================================
 
 /**
  * Detect all occurances of t()-like calls.
@@ -712,19 +469,24 @@ function potx_status($op, $value = NULL, $file = NULL, $line = NULL, $excerpt =
  * These sequences are searched for:
  *   T_STRING("$function_name") + "(" + T_CONSTANT_ENCAPSED_STRING + ")"
  *   T_STRING("$function_name") + "(" + T_CONSTANT_ENCAPSED_STRING + ","
+ *   and then an optional value for the replacements and an optional array
+ *   for the options with an optional context key (for Drupal 7+).
  *
- * @param $file
+ * @param $display_name
  *   Name of file parsed.
- * @param $save_callback
+ * @param $string_save_callback
  *   Callback function used to save strings.
  * @param function_name
  *   The name of the function to look for (could be 't', '$t', 'st'
- *   or any other t-like function).
+ *   or any other t-like function). Drupal 7 only supports context on t(), st()
+ *   and $t().
  * @param $string_mode
  *   String mode to use: POTX_STRING_INSTALLER, POTX_STRING_RUNTIME or
  *   POTX_STRING_BOTH.
+ * @param $context_support
+ *   Whether we should look for string context provided.
  */
-function _potx_find_t_calls($file, $save_callback, $function_name = 't', $string_mode = POTX_STRING_RUNTIME) {
+function potx_parser_find_t_strings($display_name, $string_save_callback, $function_name = 't', $string_mode = POTX_STRING_RUNTIME, $context_support = FALSE) {
   global $_potx_tokens, $_potx_lookup;
 
   // Lookup tokens by function name.
@@ -735,12 +497,25 @@ function _potx_find_t_calls($file, $save_callback, $function_name = 't', $string
       if ($par == "(") {
         if (in_array($rig, array(")", ","))
           && (is_array($mid) && ($mid[0] == T_CONSTANT_ENCAPSED_STRING))) {
-            // This function is only used for context-less call types.
-            $save_callback(_potx_format_quoted_string($mid[1]), POTX_CONTEXT_NONE, $file, $line, $string_mode);
+          // By default, there is no context.
+          $context = POTX_CONTEXT_NONE;
+          if ($context_support && ($rig == ',')) {
+            // If there was a comma after the string, we need to look forward
+            // to try and find the context.
+            $context = potx_parser_util_context($ti, $ti + 4, $display_name, $function_name);
+          }
+          if ($context !== POTX_CONTEXT_ERROR) {
+            // Only save if there was no error in context parsing.
+            $string_save_callback(potx_parser_util_format_string($mid[1]), $context, $display_name, $line, $string_mode);
+          }
+          else {
+            // $function_name() found, but the context is not good.
+            potx_parser_util_token_error($display_name, $line, $function_name, $ti, t('The context specified for @function() is not valid.', array('@function' => $function_name)));
+          }
         }
         else {
           // $function_name() found, but inside is something which is not a string literal.
-          _potx_marker_error($file, $line, $function_name, $ti, t('The first parameter to @function() should be a literal string. There should be no variables, concatenation, constants or other non-literal strings there.', array('@function' => $function_name)), 'http://drupal.org/node/322732');
+          potx_parser_util_token_error($display_name, $line, $function_name, $ti, t('The first parameter to @function() should be a literal string. There should be no variables, concatenation, constants or other non-literal strings there.', array('@function' => $function_name)), 'http://drupal.org/node/322732');
         }
       }
     }
@@ -748,71 +523,18 @@ function _potx_find_t_calls($file, $save_callback, $function_name = 't', $string
 }
 
 /**
- * Detect all occurances of t()-like calls from Drupal 7 (with context).
- *
- * These sequences are searched for:
- *   T_STRING("$function_name") + "(" + T_CONSTANT_ENCAPSED_STRING + ")"
- *   T_STRING("$function_name") + "(" + T_CONSTANT_ENCAPSED_STRING + ","
- *   and then an optional value for the replacements and an optional array
- *   for the options with an optional context key.
- *
- * @param $file
- *   Name of file parsed.
- * @param $save_callback
- *   Callback function used to save strings.
- * @param function_name
- *   The name of the function to look for (could be 't', '$t', 'st'
- *   or any other t-like function). Drupal 7 only supports context on t(), st()
- *   and $t().
- * @param $string_mode
- *   String mode to use: POTX_STRING_INSTALLER, POTX_STRING_RUNTIME or
- *   POTX_STRING_BOTH.
- */
-function _potx_find_t_calls_with_context($file, $save_callback, $function_name = 't', $string_mode = POTX_STRING_RUNTIME) {
-  global $_potx_tokens, $_potx_lookup;
-
-  // Lookup tokens by function name.
-  if (isset($_potx_lookup[$function_name])) {
-    foreach ($_potx_lookup[$function_name] as $ti) {
-      list($ctok, $par, $mid, $rig) = array($_potx_tokens[$ti], $_potx_tokens[$ti+1], $_potx_tokens[$ti+2], $_potx_tokens[$ti+3]);
-      list($type, $string, $line) = $ctok;
-      if ($par == "(") {
-        if (in_array($rig, array(")", ","))
-          && (is_array($mid) && ($mid[0] == T_CONSTANT_ENCAPSED_STRING))) {
-          // By default, there is no context.
-          $context = POTX_CONTEXT_NONE;
-          if ($rig == ',') {
-            // If there was a comma after the string, we need to look forward
-            // to try and find the context.
-            $context = _potx_find_context($ti, $ti + 4, $file, $function_name);
-          }
-          if ($context !== POTX_CONTEXT_ERROR) {
-            // Only save if there was no error in context parsing.
-            $save_callback(_potx_format_quoted_string($mid[1]), $context, $file, $line, $string_mode);
-          }
-        }
-        else {
-          // $function_name() found, but inside is something which is not a string literal.
-          _potx_marker_error($file, $line, $function_name, $ti, t('The first parameter to @function() should be a literal string. There should be no variables, concatenation, constants or other non-literal strings there.', array('@function' => $function_name)), 'http://drupal.org/node/322732');
-        }
-      }
-    }
-  }
-}
-
-/**
- * Detect all occurances of watchdog() calls. Only from Drupal 6.
+ * Detect all occurances of watchdog() calls. Only from Drupal 6.
  *
  * These sequences are searched for:
  *   watchdog + "(" + T_CONSTANT_ENCAPSED_STRING + "," +
  *   T_CONSTANT_ENCAPSED_STRING + something
  *
- * @param $file
+ * @param $display_name
  *   Name of file parsed.
- * @param $save_callback
+ * @param $string_save_callback
  *   Callback function used to save strings.
  */
-function _potx_find_watchdog_calls($file, $save_callback) {
+function potx_parser_find_watchdog_strings($display_name, $string_save_callback) {
   global $_potx_tokens, $_potx_lookup;
 
   // Lookup tokens by function name.
@@ -826,12 +548,12 @@ function _potx_find_watchdog_calls($file, $save_callback) {
           && (is_array($mtype) && ($mtype[0] == T_CONSTANT_ENCAPSED_STRING))
           && (is_array($message) && ($message[0] == T_CONSTANT_ENCAPSED_STRING))) {
             // Context is not supported on watchdog().
-            $save_callback(_potx_format_quoted_string($mtype[1]), POTX_CONTEXT_NONE, $file, $line);
-            $save_callback(_potx_format_quoted_string($message[1]), POTX_CONTEXT_NONE, $file, $line);
+            $string_save_callback(potx_parser_util_format_string($mtype[1]), POTX_CONTEXT_NONE, $display_name, $line);
+            $string_save_callback(potx_parser_util_format_string($message[1]), POTX_CONTEXT_NONE, $display_name, $line);
         }
         else {
           // watchdog() found, but inside is something which is not a string literal.
-          _potx_marker_error($file, $line, 'watchdog', $ti, t('The first two watchdog() parameters should be literal strings. There should be no variables, concatenation, constants or even a t() call there.'), 'http://drupal.org/node/323101');
+          potx_parser_util_token_error($display_name, $line, 'watchdog', $ti, t('The first two watchdog() parameters should be literal strings. There should be no variables, concatenation, constants or even a t() call there.'), 'http://drupal.org/node/323101');
         }
       }
     }
@@ -847,14 +569,14 @@ function _potx_find_watchdog_calls($file, $save_callback) {
  *   "," + T_CONSTANT_ENCAPSED_STRING + parenthesis (or comma allowed from
  *   Drupal 6)
  *
- * @param $file
+ * @param $display_name
  *   Name of file parsed.
- * @param $save_callback
+ * @param $string_save_callback
  *   Callback function used to save strings.
  * @param $api_version
  *   Drupal API version to work with.
  */
-function _potx_find_format_plural_calls($file, $save_callback, $api_version = POTX_API_CURRENT) {
+function potx_parser_find_format_plural_strings($display_name, $string_save_callback, $api_version = POTX_API_CURRENT) {
   global $_potx_tokens, $_potx_lookup;
 
   if (isset($_potx_lookup['format_plural'])) {
@@ -881,24 +603,28 @@ function _potx_find_format_plural_calls($file, $save_callback, $api_version = PO
           (is_array($plural) && ($plural[0] == T_CONSTANT_ENCAPSED_STRING))) {
           // By default, there is no context.
           $context = POTX_CONTEXT_NONE;
-          if ($par2 == ',' && $api_version > POTX_API_6) {
+          if ($par2 == ',' && ($api_version > POTX_API_6)) {
             // If there was a comma after the plural, we need to look forward
             // to try and find the context.
-            $context = _potx_find_context($ti, $tn + 5, $file, 'format_plural');
+            $context = potx_parser_util_context($ti, $tn + 5, $display_name, 'format_plural');
           }
           if ($context !== POTX_CONTEXT_ERROR) {
             // Only save if there was no error in context parsing.
-            $save_callback(
-              _potx_format_quoted_string($singular[1]) ."\0". _potx_format_quoted_string($plural[1]),
+            $string_save_callback(
+              potx_parser_util_format_string($singular[1]) . "\0" . potx_parser_util_format_string($plural[1]),
               $context,
-              $file,
+              $display_name,
               $line
             );
           }
+          else {
+            // format_plural() found, but the context is not good.
+            potx_parser_util_token_error($display_name, $line, 'format_plural', $ti, t('The context specified for format_plural() is not valid.'));
+          }
         }
         else {
           // format_plural() found, but the parameters are not correct.
-          _potx_marker_error($file, $line, "format_plural", $ti, t('In format_plural(), the singular and plural strings should be literal strings. There should be no variables, concatenation, constants or even a t() call there.'), 'http://drupal.org/node/323072');
+          potx_parser_util_token_error($display_name, $line, 'format_plural', $ti, t('In format_plural(), the singular and plural strings should be literal strings. There should be no variables, concatenation, constants or even a t() call there.'), 'http://drupal.org/node/323072');
         }
       }
     }
@@ -907,20 +633,21 @@ function _potx_find_format_plural_calls($file, $save_callback, $api_version = PO
 
 /**
  * Detect permission names from the hook_perm() implementations.
+ *
  * Note that this will get confused with a similar pattern in a comment,
  * and with dynamic permissions, which need to be accounted for.
  *
- * @param $file
+ * @param $display_name
  *   Full path name of file parsed.
  * @param $filebase
  *   Filenaname of file parsed.
- * @param $save_callback
+ * @param $string_save_callback
  *   Callback function used to save strings.
  */
-function _potx_find_perm_hook($file, $filebase, $save_callback) {
+function potx_parser_find_perm_strings($display_name, $filebase, $string_save_callback) {
   global $_potx_tokens, $_potx_lookup;
 
-  if (isset($_potx_lookup[$filebase .'_perm'])) {
+  if (isset($_potx_lookup[$filebase . '_perm'])) {
     // Special case for node module, because it uses dynamic permissions.
     // Include the static permissions by hand. That's about all we can do here.
     if ($filebase == 'node') {
@@ -930,163 +657,41 @@ function _potx_find_perm_hook($file, $filebase, $save_callback) {
       foreach ($nodeperms as $item) {
         // hook_perm() is only ever found on a Drupal system which does not
         // support context.
-        $save_callback($item, POTX_CONTEXT_NONE, $file, $line);
+        $string_save_callback($item, POTX_CONTEXT_NONE, $display_name, $line);
       }
     }
     else {
       $count = 0;
-      foreach ($_potx_lookup[$filebase .'_perm'] as $ti) {
+      foreach ($_potx_lookup[$filebase . '_perm'] as $ti) {
         $tn = $ti;
         while (is_array($_potx_tokens[$tn]) || $_potx_tokens[$tn] != '}') {
           if (is_array($_potx_tokens[$tn]) && $_potx_tokens[$tn][0] == T_CONSTANT_ENCAPSED_STRING) {
             // hook_perm() is only ever found on a Drupal system which does not
             // support context.
-            $save_callback(_potx_format_quoted_string($_potx_tokens[$tn][1]), POTX_CONTEXT_NONE, $file, $_potx_tokens[$tn][2]);
+            $string_save_callback(potx_parser_util_format_string($_potx_tokens[$tn][1]), POTX_CONTEXT_NONE, $display_name, $_potx_tokens[$tn][2]);
             $count++;
           }
           $tn++;
         }
       }
       if (!$count) {
-        potx_status('error', t('%hook should have an array of literal string permission names.', array('%hook' => $filebase .'_perm()')), $file, NULL, NULL, 'http://drupal.org/node/323101');
-      }
-    }
-  }
-}
-
-/**
- * Helper function to look up the token closing the current function.
- *
- * @param $here
- *   The token at the function name
- */
-function _potx_find_end_of_function($here) {
-  global $_potx_tokens;
-
-  // Seek to open brace.
-  while (is_array($_potx_tokens[$here]) || $_potx_tokens[$here] != '{') {
-    $here++;
-  }
-  $nesting = 1;
-  while ($nesting > 0) {
-    $here++;
-    if (!is_array($_potx_tokens[$here])) {
-      if ($_potx_tokens[$here] == '}') {
-        $nesting--;
-      }
-      if ($_potx_tokens[$here] == '{') {
-        $nesting++;
-      }
-    }
-  }
-  return $here;
-}
-
-/**
- * Helper to move past t() and format_plural() arguments in search of context.
- *
- * @param $here
- *   The token before the start of the arguments
- */
-function _potx_skip_args($here) {
-  global $_potx_tokens;
-
-  $nesting = 0;
-  // Go through to either the end of the function call or to a comma
-  // after the current position on the same nesting level.
-  while (!(($_potx_tokens[$here] == ',' && $nesting == 0) ||
-           ($_potx_tokens[$here] == ')' && $nesting == -1))) {
-    $here++;
-    if (!is_array($_potx_tokens[$here])) {
-      if ($_potx_tokens[$here] == ')') {
-        $nesting--;
-      }
-      if ($_potx_tokens[$here] == '(') {
-        $nesting++;
+        potx_parser_util_set_message(t('%hook should have an array of literal string permission names.', array('%hook' => $filebase . '_perm()')), $display_name, NULL, NULL, 'http://drupal.org/node/323101');
       }
     }
   }
-  // If we run out of nesting, it means we reached the end of the function call,
-  // so we skipped the arguments but did not find meat for looking at the
-  // specified context.
-  return ($nesting == 0 ? $here : FALSE);
-}
-
-/**
- * Helper to find the value for 'context' on t() and format_plural().
- *
- * @param $tf
- *   Start position of the original function.
- * @param $ti
- *   Start position where we should search from.
- * @param $file
- *   Full path name of file parsed.
- * @param function_name
- *   The name of the function to look for. Either 'format_plural' or 't'
- *   given that Drupal 7 only supports context on these.
- */
-function _potx_find_context($tf, $ti, $file, $function_name) {
-  global $_potx_tokens;
-
-  // Start from after the comma and skip the possible arguments for the function
-  // so we can look for the context.
-  if (($ti = _potx_skip_args($ti)) && ($_potx_tokens[$ti] == ',')) {
-    // Now we actually might have some definition for a context. The $options
-    // argument is coming up, which might have a key for context.
-    list($com, $arr, $par) = array($_potx_tokens[$ti], $_potx_tokens[$ti+1], $_potx_tokens[$ti+2]);
-    if ($com == ',' && $arr[1] == 'array' && $par == '(') {
-      $nesting = 0;
-      $ti += 3;
-      // Go through to either the end of the array or to the key definition of
-      // context on the same nesting level.
-      while (!((is_array($_potx_tokens[$ti]) && (in_array($_potx_tokens[$ti][1], array('"context"', "'context'"))) && ($_potx_tokens[$ti][0] == T_CONSTANT_ENCAPSED_STRING) && ($nesting == 0)) ||
-               ($_potx_tokens[$ti] == ')' && $nesting == -1))) {
-        $ti++;
-        if (!is_array($_potx_tokens[$ti])) {
-          if ($_potx_tokens[$ti] == ')') {
-            $nesting--;
-          }
-          if ($_potx_tokens[$ti] == '(') {
-            $nesting++;
-          }
-        }
-      }
-      if ($nesting == 0) {
-        // Found the 'context' key on the top level of the $options array.
-        list($arw, $str) = array($_potx_tokens[$ti+1], $_potx_tokens[$ti+2]);
-        if (is_array($arw) && $arw[1] == '=>' && is_array($str) && $str[0] == T_CONSTANT_ENCAPSED_STRING) {
-          return _potx_format_quoted_string($str[1]);
-        }
-        else {
-          list($type, $string, $line) = $_potx_tokens[$ti];
-          // @todo: fix error reference.
-          _potx_marker_error($file, $line, $function_name, $tf, t('The context element in the options array argument to @function() should be a literal string. There should be no variables, concatenation, constants or other non-literal strings there.', array('@function' => $function_name)), 'http://drupal.org/node/322732');
-          // Return with error.
-          return POTX_CONTEXT_ERROR;
-        }
-      }
-      else {
-        // Did not found 'context' key in $options array.
-        return POTX_CONTEXT_NONE;
-      }
-    }
-  }
-
-  // After skipping args, we did not find a comma to look for $options.
-  return POTX_CONTEXT_NONE;
 }
 
 /**
  * List of menu item titles. Only from Drupal 6.
  *
- * @param $file
+ * @param $display_name
  *   Full path name of file parsed.
  * @param $filebase
  *   Filenaname of file parsed.
- * @param $save_callback
+ * @param $string_save_callback
  *   Callback function used to save strings.
  */
-function _potx_find_menu_hooks($file, $filebase, $save_callback) {
+function potx_parser_find_menu_strings($display_name, $filebase, $string_save_callback) {
   global $_potx_tokens, $_potx_lookup;
 
   $hooks = array('_menu', '_menu_alter');
@@ -1096,7 +701,7 @@ function _potx_find_menu_hooks($file, $filebase, $save_callback) {
     if (isset($_potx_lookup[$filebase . $hook]) && is_array($_potx_lookup[$filebase . $hook])) {
       // We have this menu hook in this file.
       foreach ($_potx_lookup[$filebase . $hook] as $ti) {
-        $end = _potx_find_end_of_function($ti);
+        $end = potx_parser_util_end_of_function($ti);
         $tn = $ti;
         while ($tn < $end) {
 
@@ -1105,16 +710,16 @@ function _potx_find_menu_hooks($file, $filebase, $save_callback) {
           if ($_potx_tokens[$tn][0] == T_CONSTANT_ENCAPSED_STRING && in_array($_potx_tokens[$tn][1], $keys) && $_potx_tokens[$tn+1][0] == T_DOUBLE_ARROW) {
             if ($_potx_tokens[$tn+2][0] == T_CONSTANT_ENCAPSED_STRING) {
               // We cannot export menu item context.
-              $save_callback(
-                _potx_format_quoted_string($_potx_tokens[$tn+2][1]),
+              $string_save_callback(
+                potx_parser_util_format_string($_potx_tokens[$tn+2][1]),
                 POTX_CONTEXT_NONE,
-                $file,
+                $display_name,
                 $_potx_tokens[$tn+2][2]
               );
               $tn+=2; // Jump forward by 2.
             }
             else {
-              potx_status('error', t('Invalid menu %element definition found in %hook. Title and description keys of the menu array should be literal strings.', array('%element' => $_potx_tokens[$tn][1], '%hook' => $filebase . $hook .'()')), $file, $_potx_tokens[$tn][2], NULL, 'http://drupal.org/node/323101');
+              potx_parser_util_set_message(t('Invalid menu %element definition found in %hook. Title and description keys of the menu array should be literal strings.', array('%element' => $_potx_tokens[$tn][1], '%hook' => $filebase . $hook . '()')), $display_name, $_potx_tokens[$tn][2], NULL, 'http://drupal.org/node/323101');
             }
           }
 
@@ -1123,16 +728,16 @@ function _potx_find_menu_hooks($file, $filebase, $save_callback) {
           if (is_string($_potx_tokens[$tn]) && $_potx_tokens[$tn] == '[' && $_potx_tokens[$tn+1][0] == T_CONSTANT_ENCAPSED_STRING && in_array($_potx_tokens[$tn+1][1], $keys) && is_string($_potx_tokens[$tn+2]) && $_potx_tokens[$tn+2] == ']') {
             if (is_string($_potx_tokens[$tn+3]) && $_potx_tokens[$tn+3] == '=' && $_potx_tokens[$tn+4][0] == T_CONSTANT_ENCAPSED_STRING) {
               // We cannot export menu item context.
-              $save_callback(
-                _potx_format_quoted_string($_potx_tokens[$tn+4][1]),
+              $string_save_callback(
+                potx_parser_util_format_string($_potx_tokens[$tn+4][1]),
                 POTX_CONTEXT_NONE,
-                $file,
+                $display_name,
                 $_potx_tokens[$tn+4][2]
               );
               $tn+=4; // Jump forward by 4.
             }
             else {
-              potx_status('error', t('Invalid menu %element definition found in %hook. Title and description keys of the menu array should be literal strings.', array('%element' => $_potx_tokens[$tn+1][1], '%hook' => $filebase . $hook .'()')), $file, $_potx_tokens[$tn+1][2], NULL, 'http://drupal.org/node/323101');
+              potx_parser_util_set_message(t('Invalid menu %element definition found in %hook. Title and description keys of the menu array should be literal strings.', array('%element' => $_potx_tokens[$tn+1][1], '%hook' => $filebase . $hook . '()')), $display_name, $_potx_tokens[$tn+1][2], NULL, 'http://drupal.org/node/323101');
             }
           }
           $tn++;
@@ -1145,14 +750,14 @@ function _potx_find_menu_hooks($file, $filebase, $save_callback) {
 /**
  * Get languages names from Drupal's locale.inc.
  *
- * @param $file
+ * @param $display_name
  *   Full path name of file parsed
- * @param $save_callback
+ * @param $string_save_callback
  *   Callback function used to save strings.
  * @param $api_version
  *   Drupal API version to work with.
  */
-function _potx_find_language_names($file, $save_callback, $api_version = POTX_API_CURRENT) {
+function potx_parser_find_language_strings($display_name, $string_save_callback, $api_version = POTX_API_CURRENT) {
   global $_potx_tokens, $_potx_lookup;
 
   foreach ($_potx_lookup[$api_version > POTX_API_5 ? '_locale_get_predefined_list' : '_locale_get_iso639_list'] as $ti) {
@@ -1162,7 +767,7 @@ function _potx_find_language_names($file, $save_callback, $api_version = POTX_AP
     }
   }
 
-  $end = _potx_find_end_of_function($ti);
+  $end = potx_parser_util_end_of_function($ti);
   $ti += 7; // function name, (, ), {, return, array, (
   while ($ti < $end) {
     while ($_potx_tokens[$ti][0] != T_ARRAY) {
@@ -1175,84 +780,66 @@ function _potx_find_language_names($file, $save_callback, $api_version = POTX_AP
     }
     $ti += 2; // array, (
     // Language names are context-less.
-    $save_callback(_potx_format_quoted_string($_potx_tokens[$ti][1]), POTX_CONTEXT_NONE, $file, $_potx_tokens[$ti][2]);
+    $string_save_callback(potx_parser_util_format_string($_potx_tokens[$ti][1]), POTX_CONTEXT_NONE, $display_name, $_potx_tokens[$ti][2]);
   }
 }
 
-/**
- * Get the exact CVS version number from the file, so we can
- * push that into the generated output.
- *
- * @param $code
- *   Complete source code of the file parsed.
- * @param $file
- *   Name of the file parsed.
- * @param $version_callback
- *   Callback used to save the version information.
- */
-function _potx_find_version_number($code, $file, $version_callback) {
-  // Prevent CVS from replacing this pattern with actual info.
-  if (preg_match('!\\$I'.'d: ([^\\$]+) Exp \\$!', $code, $version_info)) {
-    $version_callback($version_info[1], $file);
-  }
-  else {
-    // Unknown version information.
-    $version_callback($file .': n/a', $file);
-  }
-}
+// === Standard set of string additions ========================================
 
 /**
  * Add date strings, which cannot be extracted otherwise.
+ *
  * This is called for locale.module.
  *
- * @param $file
+ * @param $display_name
  *   Name of the file parsed.
- * @param $save_callback
+ * @param $string_save_callback
  *   Callback function used to save strings.
  * @param $api_version
  *   Drupal API version to work with.
  */
-function _potx_add_date_strings($file, $save_callback, $api_version = POTX_API_CURRENT) {
+function potx_parser_add_date_strings($display_name, $string_save_callback, $api_version = POTX_API_CURRENT) {
   for ($i = 1; $i <= 12; $i++) {
     $stamp = mktime(0, 0, 0, $i, 1, 1971);
     if ($api_version > POTX_API_6) {
       // From Drupal 7, long month names are saved with this context.
-      $save_callback(date("F", $stamp), 'Long month name', $file);
+      $string_save_callback(date("F", $stamp), 'Long month name', $display_name);
     }
     elseif ($api_version > POTX_API_5) {
       // Drupal 6 uses a little hack. No context.
-      $save_callback('!long-month-name '. date("F", $stamp), POTX_CONTEXT_NONE, $file);
+      $string_save_callback('!long-month-name ' . date("F", $stamp), POTX_CONTEXT_NONE, $display_name);
     }
     else {
       // Older versions just accept the confusion, no context.
-      $save_callback(date("F", $stamp), POTX_CONTEXT_NONE, $file);
+      $string_save_callback(date("F", $stamp), POTX_CONTEXT_NONE, $display_name);
     }
     // Short month names lack a context anyway.
-    $save_callback(date("M", $stamp), POTX_CONTEXT_NONE, $file);
+    $string_save_callback(date("M", $stamp), POTX_CONTEXT_NONE, $display_name);
   }
   for ($i = 0; $i <= 7; $i++) {
     $stamp = $i * 86400;
-    $save_callback(date("D", $stamp), POTX_CONTEXT_NONE, $file);
-    $save_callback(date("l", $stamp), POTX_CONTEXT_NONE, $file);
+    $string_save_callback(date("D", $stamp), POTX_CONTEXT_NONE, $display_name);
+    $string_save_callback(date("l", $stamp), POTX_CONTEXT_NONE, $display_name);
   }
-  $save_callback('am', POTX_CONTEXT_NONE, $file);
-  $save_callback('pm', POTX_CONTEXT_NONE, $file);
-  $save_callback('AM', POTX_CONTEXT_NONE, $file);
-  $save_callback('PM', POTX_CONTEXT_NONE, $file);
+  $string_save_callback('am', POTX_CONTEXT_NONE, $display_name);
+  $string_save_callback('pm', POTX_CONTEXT_NONE, $display_name);
+  $string_save_callback('AM', POTX_CONTEXT_NONE, $display_name);
+  $string_save_callback('PM', POTX_CONTEXT_NONE, $display_name);
 }
 
 /**
- * Add format_interval special strings, which cannot be
- * extracted otherwise. This is called for common.inc
+ * Add format_interval special strings.
  *
- * @param $file
+ * These cannot be extracted otherwise. This is called for common.inc
+ *
+ * @param $display_name
  *   Name of the file parsed.
- * @param $save_callback
+ * @param $string_save_callback
  *   Callback function used to save strings.
  * @param $api_version
  *   Drupal API version to work with.
  */
-function _potx_add_format_interval_strings($file, $save_callback, $api_version = POTX_API_CURRENT) {
+function potx_parser_add_format_interval_strings($display_name, $string_save_callback, $api_version = POTX_API_CURRENT) {
   $components = array(
     '1 year' => '@count years',
     '1 week' => '@count weeks',
@@ -1268,57 +855,61 @@ function _potx_add_format_interval_strings($file, $save_callback, $api_version =
 
   foreach ($components as $singular => $plural) {
     // Intervals support no context.
-    $save_callback($singular ."\0". $plural, POTX_CONTEXT_NONE, $file);
+    $string_save_callback($singular . "\0" . $plural, POTX_CONTEXT_NONE, $display_name);
   }
 }
 
 /**
  * Add default theme region names, which cannot be extracted otherwise.
+ *
  * These default names are defined in system.module
  *
- * @param $file
+ * @param $display_name
  *   Name of the file parsed.
- * @param $save_callback
+ * @param $string_save_callback
  *   Callback function used to save strings.
  * @param $api_version
  *   Drupal API version to work with.
  */
-function _potx_add_default_region_names($file, $save_callback, $api_version = POTX_API_CURRENT) {
+function potx_parser_add_default_region_strings($display_name, $string_save_callback, $api_version = POTX_API_CURRENT) {
   $regions = array(
-    'left' => 'Left sidebar',
-    'right' => 'Right sidebar',
-    'content' => 'Content',
-    'header' => 'Header',
-    'footer' => 'Footer',
+    'Left sidebar',
+    'Right sidebar',
+    'Content',
+    'Header',
+    'Footer',
   );
   if ($api_version > POTX_API_6) {
-    // @todo: Update with final region list when D7 stabilizes.
-    $regions['highlight'] = 'Highlighted content';
-    $regions['help'] = 'Help';
-    $regions['page_top'] = 'Page top';
+    $regions[] = 'Highlighted';
+    $regions[] = 'Help';
+    $regions[] = 'Page top';
+    $regions[] = 'Page bottom';
   }
   foreach ($regions as $region) {
     // Regions come with the default context.
-    $save_callback($region, POTX_CONTEXT_NONE, $file);
+    $string_save_callback($region, POTX_CONTEXT_NONE, $display_name);
   }
 }
 
+// === Non-token based parsing functions =======================================
+
 /**
  * Parse an .info file and add relevant strings to the list.
  *
  * @param $file_path
  *   Complete file path to load contents with.
- * @param $file_name
- *   Stripped file name to use in outpout.
- * @param $strings
- *   Current strings array
+ * @param $display_name
+ *   Display oriented file name for saved string data and error messages.
+ * @param $string_save_callback
+ *   Callback function to use to save the collected strings.
  * @param $api_version
  *   Drupal API version to work with.
  */
-function _potx_find_info_file_strings($file_path, $file_name, $save_callback, $api_version = POTX_API_CURRENT) {
+function potx_parser_get_info_strings($file_path, $display_name, $string_save_callback, $api_version = POTX_API_CURRENT) {
   $info = array();
 
   if (file_exists($file_path)) {
+    // Drupal 6+ has drupal_parse_info_file() and a special .info format.
     $info = $api_version > POTX_API_5 ? drupal_parse_info_file($file_path) : parse_ini_file($file_path);
   }
 
@@ -1328,7 +919,7 @@ function _potx_find_info_file_strings($file_path, $file_name, $save_callback, $a
   foreach (array('name', 'description', 'package') as $key) {
     if (isset($info[$key])) {
       // No context support for .info file strings.
-      $save_callback(addcslashes($info[$key], "\0..\37\\\""), POTX_CONTEXT_NONE, $file_name);
+      $string_save_callback(addcslashes($info[$key], "\0..\37\\\""), POTX_CONTEXT_NONE, $display_name);
     }
   }
 
@@ -1336,7 +927,7 @@ function _potx_find_info_file_strings($file_path, $file_name, $save_callback, $a
   if (isset($info['regions']) && is_array($info['regions'])) {
     foreach ($info['regions'] as $region => $region_name) {
       // No context support for .info file strings.
-      $save_callback(addcslashes($region_name, "\0..\37\\\""), POTX_CONTEXT_NONE, $file_name);
+      $string_save_callback(addcslashes($region_name, "\0..\37\\\""), POTX_CONTEXT_NONE, $display_name);
     }
   }
 }
@@ -1349,33 +940,33 @@ function _potx_find_info_file_strings($file_path, $file_name, $save_callback, $a
  *
  * Regex code lifted from _locale_parse_js_file().
  */
-function _potx_parse_js_file($code, $file, $save_callback) {
+function potx_parser_get_js_strings($code, $display_name, $string_save_callback) {
   $js_string_regex = '(?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+';
 
   // Match all calls to Drupal.t() in an array.
   // Note: \s also matches newlines with the 's' modifier.
-  preg_match_all('~[^\w]Drupal\s*\.\s*t\s*\(\s*('. $js_string_regex .')\s*[,\)]~s', $code, $t_matches, PREG_SET_ORDER);
+  preg_match_all('~[^\w]Drupal\s*\.\s*t\s*\(\s*(' . $js_string_regex . ')\s*[,\)]~s', $code, $t_matches, PREG_SET_ORDER);
   if (isset($t_matches) && count($t_matches)) {
     foreach ($t_matches as $match) {
       // Remove match from code to help us identify faulty Drupal.t() calls.
       $code = str_replace($match[0], '', $code);
       // @todo: figure out how to parse out context, once Drupal supports it.
-      $save_callback(_potx_parse_js_string($match[1]), POTX_CONTEXT_NONE, $file, 0);
+      $string_save_callback(potx_parser_get_js_string($match[1]), POTX_CONTEXT_NONE, $display_name, 0);
     }
   }
 
   // Match all Drupal.formatPlural() calls in another array.
-  preg_match_all('~[^\w]Drupal\s*\.\s*formatPlural\s*\(\s*.+?\s*,\s*('. $js_string_regex .')\s*,\s*((?:(?:\'(?:\\\\\'|[^\'])*@count(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*@count(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+)\s*[,\)]~s', $code, $plural_matches, PREG_SET_ORDER);
+  preg_match_all('~[^\w]Drupal\s*\.\s*formatPlural\s*\(\s*.+?\s*,\s*(' . $js_string_regex . ')\s*,\s*((?:(?:\'(?:\\\\\'|[^\'])*@count(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*@count(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+)\s*[,\)]~s', $code, $plural_matches, PREG_SET_ORDER);
   if (isset($plural_matches) && count($plural_matches)) {
     foreach ($plural_matches as $index => $match) {
       // Remove match from code to help us identify faulty
       // Drupal.formatPlural() calls later.
       $code = str_replace($match[0], '', $code);
       // @todo: figure out how to parse out context, once Drupal supports it.
-      $save_callback(
-        _potx_parse_js_string($match[1]) ."\0". _potx_parse_js_string($match[2]),
+      $string_save_callback(
+        potx_parser_get_js_string($match[1]) . "\0" . potx_parser_get_js_string($match[2]),
         POTX_CONTEXT_NONE,
-        $file,
+        $display_name,
         0
       );
     }
@@ -1388,7 +979,7 @@ function _potx_parse_js_file($code, $file, $save_callback) {
   if (isset($faulty_matches) && count($faulty_matches)) {
     foreach ($faulty_matches as $index => $match) {
       $message = ($match[1] == 't') ? t('Drupal.t() calls should have a single literal string as their first parameter.') : t('The singular and plural string parameters on Drupal.formatPlural() calls should be literal strings, plural containing a @count placeholder.');
-      potx_status('error', $message, $file, NULL, $match[0], 'http://drupal.org/node/323109');
+      potx_parser_util_set_message($message, $display_name, NULL, $match[0], 'http://drupal.org/node/323109');
     }
   }
 }
@@ -1396,95 +987,62 @@ function _potx_parse_js_file($code, $file, $save_callback) {
 /**
  * Clean up string found in JavaScript source code. Only from Drupal 6.
  */
-function _potx_parse_js_string($string) {
-  return _potx_format_quoted_string(implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', $string)));
+function potx_parser_get_js_string($string) {
+  return potx_parser_util_format_string(implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', $string)));
 }
 
 /**
- * Collect a list of file names relevant for extraction,
- * starting from the given path.
+ * Get the exact CVS version number from a file.
  *
- * @param $path
- *   Where to start searching for files recursively.
- *   Provide non-empty path values with a trailing slash.
- * @param $basename
- *   Allows the restriction of search to a specific basename
- *   (ie. to collect files for a specific module).
- * @param $api_version
- *   Drupal API version to work with.
- * @param $skip_self
- *   Skip potx related files. To be used when command line type of extraction
- *   is used and the potx files at in the webroot, and their strings should not
- *   end up in the generated template. Set to TRUE if skiping is needed.
+ * @todo This will be obsoleted by the drupal.org GIT migration.
+ *
+ * @param $code
+ *   Complete source code of the file parsed.
+ * @param $display_name
+ *   Name of the file parsed (to be passed to $file_version_callback).
+ * @param $file_version_callback
+ *   Callback used to save the version information.
  */
-function _potx_explore_dir($path = '', $basename = '*', $api_version = POTX_API_CURRENT, $skip_self = FALSE) {
-  // It would be so nice to just use GLOB_BRACE, but it is not available on all
-  // operarting systems, so we are working around the missing functionality.
-  $extensions = array('php', 'inc', 'module', 'engine', 'theme', 'install', 'info', 'profile');
-  if ($api_version > POTX_API_5) {
-    $extensions[] = 'js';
-  }
-  $files = array();
-  foreach ($extensions as $extension) {
-    $files_here = glob($path . $basename .'.'. $extension);
-    if (is_array($files_here)) {
-      $files = array_merge($files, $files_here);
-    }
-    if ($basename != '*') {
-      // Basename was specific, so look for things like basename.admin.inc as well.
-      // If the basnename was *, the above glob() already covered this case.
-      $files_here = glob($path . $basename .'.*.'. $extension);
-      if (is_array($files_here)) {
-        $files = array_merge($files, $files_here);
-      }
-    }
-  }
-
-  // Grab subdirectories.
-  $dirs = glob($path .'*', GLOB_ONLYDIR);
-  if (is_array($dirs)) {
-    foreach ($dirs as $dir) {
-      if (!preg_match("!(^|.+/)(CVS|\.svn|\.git|tests)$!", $dir)) {
-        $files = array_merge($files, _potx_explore_dir("$dir/", $basename));
-      }
-    }
+function potx_parser_get_version_number($code, $display_name, $file_version_callback) {
+  // Prevent CVS from replacing this pattern with actual info.
+  if (preg_match('!\\$I' . 'd: ([^\\$]+) Exp \\$!', $code, $version_info)) {
+    $file_version_callback($version_info[1], $display_name);
   }
-
-  // Skip API and test files. Also skip our own files, if that was requested.
-  $skip_pattern = ($skip_self ? '!(potx-cli\.php|potx\.inc|\.api\.php|\.test)$!' : '!(\.api\.php|\.test)$!');
-  foreach ($files as $id => $file_name) {
-    if (preg_match($skip_pattern, $file_name)) {
-      unset($files[$id]);
-    }
+  else {
+    // Unknown version information.
+    $file_version_callback($display_name . ': n/a', $display_name);
   }
-  return $files;
 }
 
 /**
- * Default $version_callback used by the potx system. Saves values
- * to a global array to reduce memory consumption problems when
+ * Default $file_version_callback used by the potx system.
+ *
+ * Saves values to a global array to reduce memory consumption problems when
  * passing around big chunks of values.
  *
  * @param $value
- *   The ersion number value of $file. If NULL, the collected
+ *   The version number value of $file. If NULL, the collected
  *   values are returned.
- * @param $file
+ * @param $display_name
  *   Name of file where the version information was found.
  */
-function _potx_save_version($value = NULL, $file = NULL) {
+function potx_parser_callback_version($value = NULL, $display_name = NULL) {
   global $_potx_versions;
 
   if (isset($value)) {
-    $_potx_versions[$file] = $value;
+    $_potx_versions[$display_name] = $value;
   }
   else {
     return $_potx_versions;
   }
 }
 
+// === Default file parse callbacks ============================================
+
 /**
- * Default $save_callback used by the potx system. Saves values
- * to global arrays to reduce memory consumption problems when
+ * Default $string_save_callback used by the potx system.
+ *
+ * Saves values to global arrays to reduce memory consumption problems when
  * passing around big chunks of values.
  *
  * @param $value
@@ -1493,7 +1051,7 @@ function _potx_save_version($value = NULL, $file = NULL) {
  * @param $context
  *   From Drupal 7, separate contexts are supported. POTX_CONTEXT_NONE is
  *   the default, if the code does not specify a context otherwise.
- * @param $file
+ * @param $display_name
  *   Name of file where the string was found.
  * @param $line
  *   Line number where the string was found.
@@ -1501,7 +1059,7 @@ function _potx_save_version($value = NULL, $file = NULL) {
  *   String mode: POTX_STRING_INSTALLER, POTX_STRING_RUNTIME
  *   or POTX_STRING_BOTH.
  */
-function _potx_save_string($value = NULL, $context = NULL, $file = NULL, $line = 0, $string_mode = POTX_STRING_RUNTIME) {
+function potx_parser_callback_save_string($value = NULL, $context = NULL, $display_name = NULL, $line = 0, $string_mode = POTX_STRING_RUNTIME) {
   global $_potx_strings, $_potx_install;
 
   if (isset($value)) {
@@ -1511,7 +1069,7 @@ function _potx_save_string($value = NULL, $context = NULL, $file = NULL, $line =
     // whitespace as it appears in the string otherwise.
     $check_empty = trim($value);
     if (empty($check_empty)) {
-      potx_status('error', t('Empty string attempted to be localized. Please do not leave test code for localization in your source.'), $file, $line);
+      potx_parser_util_set_message(t('Empty string attempted to be localized. Please do not leave test code for localization in your source.'), $display_name, $line);
       return;
     }
 
@@ -1519,15 +1077,15 @@ function _potx_save_string($value = NULL, $context = NULL, $file = NULL, $line =
       case POTX_STRING_BOTH:
         // Mark installer strings as duplicates of runtime strings if
         // the string was both recorded in the runtime and in the installer.
-        $_potx_install[$value][$context][$file][] = $line .' (dup)';
+        $_potx_install[$value][$context][$display_name][] = $line . ' (dup)';
         // Break intentionally missing.
       case POTX_STRING_RUNTIME:
         // Mark runtime strings as duplicates of installer strings if
         // the string was both recorded in the runtime and in the installer.
-        $_potx_strings[$value][$context][$file][] = $line . ($string_mode == POTX_STRING_BOTH ? ' (dup)' : '');
+        $_potx_strings[$value][$context][$display_name][] = $line . ($string_mode == POTX_STRING_BOTH ? ' (dup)' : '');
         break;
       case POTX_STRING_INSTALLER:
-        $_potx_install[$value][$context][$file][] = $line;
+        $_potx_install[$value][$context][$display_name][] = $line;
         break;
     }
   }
@@ -1536,77 +1094,297 @@ function _potx_save_string($value = NULL, $context = NULL, $file = NULL, $line =
   }
 }
 
-if (!function_exists('t')) {
-  // If invoked outside of Drupal, t() will not exist, but
-  // used to format the error message, so we provide a replacement.
-  function t($string, $args = array()) {
-    return strtr($string, $args);
+/**
+ * Returns a header generated for a given file.
+ *
+ * @param $template_export_langcode
+ *   Language code if the template should have language dependent content
+ *   (like plural formulas and language name) included.
+ * @param $api_version
+ *   Drupal API version to work with.
+ */
+function potx_parser_callback_header($template_export_langcode = NULL, $api_version = POTX_API_CURRENT) {
+  // We only have language to use if we should export with that langcode.
+  $language = NULL;
+  if (isset($template_export_langcode)) {
+    $language = db_query($api_version > POTX_API_5 ? "SELECT language, name, plurals, formula FROM {languages} WHERE language = :langcode" : "SELECT locale, name, plurals, formula FROM {locales_meta} WHERE locale = :langcode", array(':langcode' => $template_export_langcode))->fetchObject();
+  }
+
+  $output  = '# $' . 'Id' . '$' . "\n";
+  $output .= "#\n";
+  $output .= '# ' . (isset($language) ? $language->name : 'LANGUAGE') . " translation\n";
+  $output .= "# Copyright YEAR NAME <EMAIL@ADDRESS>\n";
+  $output .= "# --VERSIONS--\n";
+  $output .= "#\n";
+  $output .= "#, fuzzy\n";
+  $output .= "msgid \"\"\n";
+  $output .= "msgstr \"\"\n";
+  $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
+  $output .= '"POT-Creation-Date: ' . date("Y-m-d H:iO") . "\\n\"\n";
+  $output .= '"PO-Revision-Date: ' . (isset($language) ? date("Y-m-d H:iO") : 'YYYY-mm-DD HH:MM+ZZZZ') . "\\n\"\n";
+  $output .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
+  $output .= "\"Language-Team: " . (isset($language) ? $language->name : 'LANGUAGE') . " <EMAIL@ADDRESS>\\n\"\n";
+  $output .= "\"MIME-Version: 1.0\\n\"\n";
+  $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
+  $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
+  if (isset($language->formula) && isset($language->plurals)) {
+    $output .= "\"Plural-Forms: nplurals=" . $language->plurals . "; plural=" . strtr($language->formula, array('$' => '')) . ";\\n\"\n\n";
   }
+  else {
+    $output .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n\n";
+  }
+  return $output;
 }
 
-if (!function_exists('drupal_parse_info_file')) {
-  // If invoked outside of Drupal, drupal_parse_info_file() will not be available,
-  // but we need this function to properly parse Drupal 6 .info files.
-  // Directly copied from common.inc,v 1.756.2.76 2010/02/01 16:01:41 goba Exp.
-  function drupal_parse_info_file($filename) {
-    $info = array();
-    $constants = get_defined_constants();
+// === Utility functions =======================================================
 
-    if (!file_exists($filename)) {
-      return $info;
+/**
+ * Helper function to look up the token closing the current function.
+ *
+ * @param $here
+ *   The token at the function name
+ */
+function potx_parser_util_end_of_function($here) {
+  global $_potx_tokens;
+
+  // Seek to open brace.
+  while (is_array($_potx_tokens[$here]) || $_potx_tokens[$here] != '{') {
+    $here++;
+  }
+  $nesting = 1;
+  while ($nesting > 0) {
+    $here++;
+    if (!is_array($_potx_tokens[$here])) {
+      if ($_potx_tokens[$here] == '}') {
+        $nesting--;
+      }
+      if ($_potx_tokens[$here] == '{') {
+        $nesting++;
+      }
     }
+  }
+  return $here;
+}
 
-    $data = file_get_contents($filename);
-    if (preg_match_all('
-      @^\s*                           # Start at the beginning of a line, ignoring leading whitespace
-      ((?:
-        [^=;\[\]]|                    # Key names cannot contain equal signs, semi-colons or square brackets,
-        \[[^\[\]]*\]                  # unless they are balanced and not nested
-      )+?)
-      \s*=\s*                         # Key/value pairs are separated by equal signs (ignoring white-space)
-      (?:
-        ("(?:[^"]|(?<=\\\\)")*")|     # Double-quoted string, which may contain slash-escaped quotes/slashes
-        (\'(?:[^\']|(?<=\\\\)\')*\')| # Single-quoted string, which may contain slash-escaped quotes/slashes
-        ([^\r\n]*?)                   # Non-quoted string
-      )\s*$                           # Stop at the next end of a line, ignoring trailing whitespace
-      @msx', $data, $matches, PREG_SET_ORDER)) {
-      foreach ($matches as $match) {
-        // Fetch the key and value string
-        $i = 0;
-        foreach (array('key', 'value1', 'value2', 'value3') as $var) {
-          $$var = isset($match[++$i]) ? $match[$i] : '';
-        }
-        $value = stripslashes(substr($value1, 1, -1)) . stripslashes(substr($value2, 1, -1)) . $value3;
+/**
+ * Helper to move past t() and format_plural() arguments in search of context.
+ *
+ * @param $here
+ *   The token before the start of the arguments
+ */
+function potx_parser_util_skip_args($here) {
+  global $_potx_tokens;
 
-        // Parse array syntax
-        $keys = preg_split('/\]?\[/', rtrim($key, ']'));
-        $last = array_pop($keys);
-        $parent = &$info;
+  $nesting = 0;
+  // Go through to either the end of the function call or to a comma
+  // after the current position on the same nesting level.
+  while (!(($_potx_tokens[$here] == ',' && $nesting == 0) ||
+           ($_potx_tokens[$here] == ')' && $nesting == -1))) {
+    $here++;
+    if (!is_array($_potx_tokens[$here])) {
+      if ($_potx_tokens[$here] == ')') {
+        $nesting--;
+      }
+      if ($_potx_tokens[$here] == '(') {
+        $nesting++;
+      }
+    }
+  }
+  // If we run out of nesting, it means we reached the end of the function call,
+  // so we skipped the arguments but did not find meat for looking at the
+  // specified context.
+  return ($nesting == 0 ? $here : FALSE);
+}
+
+/**
+ * Helper to find the value for 'context' on t() and format_plural().
+ *
+ * @param $tf
+ *   Start position of the original function.
+ * @param $ti
+ *   Start position where we should search from.
+ * @param $file
+ *   Full path name of file parsed.
+ * @param function_name
+ *   The name of the function to look for. Either 'format_plural' or 't'
+ *   given that Drupal 7 only supports context on these.
+ */
+function potx_parser_util_context($tf, $ti, $file, $function_name) {
+  global $_potx_tokens;
 
-        // Create nested arrays
-        foreach ($keys as $key) {
-          if ($key == '') {
-            $key = count($parent);
+  // Start from after the comma and skip the possible arguments for the function
+  // so we can look for the context.
+  if (($ti = potx_parser_util_skip_args($ti)) && ($_potx_tokens[$ti] == ',')) {
+    // Now we actually might have some definition for a context. The $options
+    // argument is coming up, which might have a key for context.
+    list($com, $arr, $par) = array($_potx_tokens[$ti], $_potx_tokens[$ti+1], $_potx_tokens[$ti+2]);
+    if ($com == ',' && $arr[1] == 'array' && $par == '(') {
+      $nesting = 0;
+      $ti += 3;
+      // Go through to either the end of the array or to the key definition of
+      // context on the same nesting level.
+      while (!((is_array($_potx_tokens[$ti]) && (in_array($_potx_tokens[$ti][1], array('"context"', "'context'"))) && ($_potx_tokens[$ti][0] == T_CONSTANT_ENCAPSED_STRING) && ($nesting == 0)) ||
+               ($_potx_tokens[$ti] == ')' && $nesting == -1))) {
+        $ti++;
+        if (!is_array($_potx_tokens[$ti])) {
+          if ($_potx_tokens[$ti] == ')') {
+            $nesting--;
           }
-          if (!isset($parent[$key]) || !is_array($parent[$key])) {
-            $parent[$key] = array();
+          if ($_potx_tokens[$ti] == '(') {
+            $nesting++;
           }
-          $parent = &$parent[$key];
         }
-
-        // Handle PHP constants.
-        if (isset($constants[$value])) {
-          $value = $constants[$value];
+      }
+      if ($nesting == 0) {
+        // Found the 'context' key on the top level of the $options array.
+        list($arw, $str) = array($_potx_tokens[$ti+1], $_potx_tokens[$ti+2]);
+        if (is_array($arw) && $arw[1] == '=>' && is_array($str) && $str[0] == T_CONSTANT_ENCAPSED_STRING) {
+          return potx_parser_util_format_string($str[1]);
         }
-
-        // Insert actual value
-        if ($last == '') {
-          $last = count($parent);
+        else {
+          list($type, $string, $line) = $_potx_tokens[$ti];
+          // @todo: fix error reference.
+          potx_parser_util_token_error($file, $line, $function_name, $tf, t('The context element in the options array argument to @function() should be a literal string. There should be no variables, concatenation, constants or other non-literal strings there.', array('@function' => $function_name)), 'http://drupal.org/node/322732');
+          // Return with error.
+          return POTX_CONTEXT_ERROR;
         }
-        $parent[$last] = $value;
+      }
+      else {
+        // Did not found 'context' key in $options array.
+        return POTX_CONTEXT_NONE;
       }
     }
+  }
+
+  // After skipping args, we did not find a comma to look for $options.
+  return POTX_CONTEXT_NONE;
+}
 
-    return $info;
+/**
+ * Escape quotes in a strings depending on the surrounding quote type used.
+ *
+ * @param $str
+ *   The strings to escape
+ */
+function potx_parser_util_format_string($str) {
+  $quo = substr($str, 0, 1);
+  $str = substr($str, 1, -1);
+  if ($quo == '"') {
+    $str = stripcslashes($str);
   }
+  else {
+    $str = strtr($str, array("\\'" => "'", "\\\\" => "\\"));
+  }
+  return addcslashes($str, "\0..\37\\\"");
+}
+
+/**
+ * Save an error with an extract of where the error was found.
+ *
+ * @param $display_name
+ *   Display name of file.
+ * @param $line
+ *   Line number of error.
+ * @param $marker
+ *   Function name with which the error was identified.
+ * @param $ti
+ *   Index on the token array.
+ * @param $error
+ *   Helpful error message for users.
+ * @param $docs_url
+ *   Optional documentation reference.
+ */
+function potx_parser_util_token_error($file, $line, $marker, $ti, $error, $docs_url = NULL) {
+  global $_potx_tokens;
+
+  $tokens = '';
+  $ti += 2;
+  $tc = count($_potx_tokens);
+  $par = 1;
+  while ((($tc - $ti) > 0) && $par) {
+    if (is_array($_potx_tokens[$ti])) {
+      $tokens .= $_potx_tokens[$ti][1];
+    }
+    else {
+      $tokens .= $_potx_tokens[$ti];
+      if ($_potx_tokens[$ti] == "(") {
+        $par++;
+      }
+      elseif ($_potx_tokens[$ti] == ")") {
+        $par--;
+      }
+    }
+    $ti++;
+  }
+  potx_parser_util_set_message($error, $file, $line, $marker . '(' . $tokens, $docs_url);
+}
+
+/**
+ * Parse error message collection function.
+ *
+ * @param $value
+ *   Error message or NULL to get the errors collected (and reset the list).
+ * @param $file
+ *   Name of file the error message is related to.
+ * @param $line
+ *   Number of line the error message is related to.
+ * @param $excerpt
+ *   Excerpt of the code in question, if available.
+ * @param $docs_url
+ *   URL to the guidelines to follow to fix the problem.
+ */
+function potx_parser_util_set_message($value = NULL, $file = NULL, $line = NULL, $excerpt = NULL, $docs_url = NULL) {
+  static $messages = array();
+
+  if (empty($value)) {
+    $errors = $messages;
+    $messages = array();
+    return $errors;
+  }
+
+  $location_info = '';
+  if (isset($file)) {
+    if (isset($line)) {
+      if (isset($excerpt)) {
+        $location_info = t('At %excerpt in %file on line %line.', array('%excerpt' => $excerpt, '%file' => $file, '%line' => $line));
+      }
+      else {
+        $location_info = t('In %file on line %line.', array('%file' => $file, '%line' => $line));
+      }
+    }
+    else {
+      if (isset($excerpt)) {
+        $location_info = t('At %excerpt in %file.', array('%excerpt' => $excerpt, '%file' => $file));
+      }
+      else {
+        $location_info = t('In %file.', array('%file' => $file));
+      }
+    }
+  }
+
+  // Documentation helpers are provided as readable text in most modes.
+  $read_more = '';
+  if (isset($docs_url)) {
+    $read_more = t('Read more at <a href="@url">@url</a>', array('@url' => $docs_url));
+  }
+
+  $readable_error_message = join(' ', array($value, $location_info, $read_more));
+  $messages[] = array($value, $file, $line, $excerpt, $docs_url, $readable_error_message);
+}
+
+/**
+ * Get the list of messages saved when parsing one or more files.
+ *
+ * Also cleans off the list of messages.
+ */
+function potx_parser_util_get_messages() {
+  return potx_parser_util_set_message();
+}
+
+/**
+ * Reset global variables used for storing cross-function data.
+ */
+function potx_parser_util_reset() {
+  global $_potx_strings, $_potx_install, $_potx_versions;
+  $_potx_strings = $_potx_install = $_potx_versions = array();
 }
diff --git a/potx.module b/potx.module
index e26f58c..0a7fca6 100644
--- a/potx.module
+++ b/potx.module
@@ -2,12 +2,12 @@
 
 /**
  * @file
- *   Gettext translation template and translation extractor.
+ *   Drupal translation template extractor.
  *
  *   This module helps people extract translatable strings from Drupal source
  *   code. The user interface allows translators to choose which part of the
  *   current Drupal instance to translate. The module also provides an API for
- *   other modules (such as l10n_server) to use.
+ *   other modules (such as coder and l10n_server) to use.
  */
 
 /**
@@ -16,7 +16,7 @@
 function potx_help($path, $arg) {
   switch ($path) {
     case 'admin/config/regional/translate/extract':
-      return '<p>'. t('This page allows you to generate translation templates for module and theme files. Select the module or theme you wish to generate a template file for. A single Gettext Portable Object (Template) file is generated, so you can easily save it and start translation.') .'</p>';
+      return '<p>' . t('This page allows you to generate translation templates for module and theme files. Select the module or theme you wish to generate a template file for. A single Gettext Portable Object (Template) file is generated, so you can easily save it and start translation.') . '</p>';
   }
 }
 
@@ -27,282 +27,19 @@ function potx_menu() {
   $items['admin/config/regional/translate/extract'] = array(
     'title' => 'Extract',
     'page callback' => 'drupal_get_form',
-    'page arguments' => array('potx_select_form'),
+    'page arguments' => array('potx_select_component_form'),
     'access arguments' => array('translate interface'),
     'weight' => 200,
     'type' => MENU_LOCAL_TASK,
+    'file' => 'potx.admin.inc',
   );
   return $items;
 }
 
 /**
- * Component selection interface.
- */
-function potx_select_form() {
-
-  $form = array();
-  $components = _potx_component_list();
-  _potx_component_selector($form, $components);
-
-  // Generate translation file for a specific language if possible.
-  $languages = language_list();
-  if (count($languages) > 1 || !isset($languages['en'])) {
-    // We have more languages, or the single language we have is not English.
-    $options = array('n/a' => t('Language independent template'));
-    foreach ($languages as $langcode => $language) {
-      // Skip English, as we should not have translations for this language.
-      if ($langcode == 'en') {
-        continue;
-      }
-      $options[$langcode] = t('Template file for !langname translations', array('!langname' => t($language->name)));
-    }
-    $form['langcode'] = array(
-      '#type' => 'radios',
-      '#title' => t('Template language'),
-      '#default_value' => 'n/a',
-      '#options' => $options,
-      '#description' => t('Export a language independent or language dependent (plural forms, language team name, etc.) template.'),
-    );
-    $form['translations'] = array(
-      '#type' => 'checkbox',
-      '#title' => t('Include translations'),
-      '#description' => t('Include translations of strings in the file generated. Not applicable for language independent templates.')
-    );
-  }
-
-  $form['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Extract'),
-  );
-
-  return $form;
-}
-
-/**
- * Validation handler for potx component selection form.
- */
-function potx_select_form_validate($form, &$form_state) {
-  if (empty($form_state['values']['component'])) {
-    form_set_error('', t('You should select a component to export.'));
-  }
-}
-
-/**
- * Generate translation template or translation file for the requested component.
- */
-function potx_select_form_submit($form, &$form_state) {
-  global $devel_shutdown;
-
-  // Avoid devel.module putting extra output to the end of files exported.
-  $devel_shutdown = FALSE;
-
-  // This could take some time.
-  @set_time_limit(0);
-  include_once drupal_get_path('module', 'potx') .'/potx.inc';
-
-  // Silence status messages.
-  potx_status('set', POTX_STATUS_MESSAGE);
-
-  // $form_state['values']['component'] either contains a specific file name
-  // with path, or a directory name for a module/theme or a module suite.
-  // Examples:
-  //   modules/watchdog
-  //   sites/all/modules/coder
-  //   sites/all/modules/i18n/i18n.module
-  //   themes/garland
-
-  $component = $form_state['values']['component'];
-  $pathinfo = pathinfo($component);
-  if (!isset($pathinfo['filename'])) {
-    // The filename key is only available in PHP 5.2.0+
-    $pathinfo['filename'] = substr($pathinfo['basename'], 0, strrpos($pathinfo['basename'], '.'));
-  }
-  $strip_prefix = 0;
-
-  if (isset($pathinfo['extension'])) {
-    // A specific module or theme file was requested (otherwise there should be no extension).
-    $files = _potx_explore_dir($pathinfo['dirname'] .'/', $pathinfo['filename']);
-    $strip_prefix = 1 + strlen($pathinfo['dirname']);
-    $outputname = $pathinfo['filename'];
-  }
-  // A directory name was requested.
-  else {
-    $files = _potx_explore_dir($component .'/');
-    $strip_prefix = 1 + strlen($component);
-    $outputname = $pathinfo['basename'];
-  }
-
-  // Decide on template or translation file generation.
-  $template_langcode = $translation_langcode = NULL;
-  if (isset($form_state['values']['langcode']) && ($form_state['values']['langcode'] != 'n/a')) {
-    $template_langcode = $form_state['values']['langcode'];
-    $outputname .= '.'. $template_langcode;
-    if (!empty($form_state['values']['translations'])) {
-      $translation_langcode = $template_langcode;
-      $outputname .= '.po';
-    }
-    else {
-      $outputname .= '.pot';
-    }
-  }
-  else {
-    $outputname .= '.pot';
-  }
-
-  // Collect every string in affected files. Installer related strings are discared.
-  foreach ($files as $file) {
-    _potx_process_file($file, $strip_prefix);
-  }
-  // Need to include full parameter list to get to passing the language codes.
-  _potx_build_files(POTX_STRING_RUNTIME, POTX_BUILD_SINGLE, 'general', '_potx_save_string', '_potx_save_version', '_potx_get_header', $template_langcode, $translation_langcode);
-
-  _potx_write_files($outputname, 'attachment');
-
-  exit;
-}
-
-/**
- * Build a chunk of the component selection form.
+ * Implements hook_reviews().
  *
- * @param $form
- *   Form to populate with fields.
- * @param $components
- *   Structured array with components as returned by _potx_component_list().
- * @param $dirname
- *   Name of directory handled.
- */
-function _potx_component_selector(&$form, &$components, $dirname = '') {
-
-  // Pop off count of components in this directory.
-  if (isset($components['#-count'])) {
-    $component_count = $components['#-count'];
-    unset($components['#-count']);
-  }
-
-  //ksort($components);
-  $dirkeys = array_keys($components);
-
-  // A directory with one component.
-  if (isset($component_count) && (count($components) == 1)) {
-    $component = array_shift($components);
-    $dirname = dirname($component->filename);
-    $form[_potx_form_id('dir', $dirname)] = array(
-      '#type' => 'radio',
-      '#title' => t('Extract from %name in the %directory directory', array('%directory' => $dirname, '%name' => $component->name)),
-      '#description' => t('Generates output from all files found in this directory.'),
-      '#default_value' => 0,
-      '#return_value' => $dirname,
-      // Get all radio buttons into the same group.
-      '#parents' => array('component'),
-    );
-    return;
-  }
-
-  // A directory with multiple components in it.
-  if (preg_match('!/(modules|themes)\\b(/.+)?!', $dirname, $pathmatch)) {
-    $t_args = array('@directory' => substr($dirname, 1));
-    if (isset($pathmatch[2])) {
-      $form[_potx_form_id('dir', $dirname)] = array(
-        '#type' => 'radio',
-        '#title' => t('Extract from all in directory "@directory"', $t_args),
-        '#description' => t('To extract from a single component in this directory, choose the desired entry in the fieldset below.'),
-        '#default_value' => 0,
-        '#return_value' => substr($dirname, 1),
-        // Get all radio buttons into the same group.
-        '#parents' => array('component'),
-      );
-    }
-    $element = array(
-      '#type' => 'fieldset',
-      '#title' => t('Directory "@directory"', $t_args),
-      '#collapsible' => TRUE,
-      '#collapsed' => TRUE,
-    );
-    $form[_potx_form_id('fs', $dirname)] =& $element;
-  }
-  else {
-    $element =& $form;
-  }
-
-  foreach ($dirkeys as $entry) {
-    // A component in this directory with multiple components.
-    if ($entry[0] == '#') {
-      // Component entry.
-      $t_args = array(
-        '%directory' => dirname($components[$entry]->filename),
-        '%name'      => $components[$entry]->name,
-        '%pattern'   => $components[$entry]->name .'.*',
-      );
-      $element[_potx_form_id('com', $components[$entry]->basename)] = array(
-        '#type' => 'radio',
-        '#title' => t('Extract from %name', $t_args),
-        '#description' => t('Extract from files named %pattern in the %directory directory.', $t_args),
-        '#default_value' => 0,
-        '#return_value' => $components[$entry]->filename,
-        // Get all radio buttons into the same group.
-        '#parents' => array('component'),
-      );
-    }
-    // A subdirectory we need to look into.
-    else {
-      _potx_component_selector($element, $components[$entry], "$dirname/$entry");
-    }
-  }
-
-  return count($components);
-}
-
-/**
- * Generate a sane form element ID for the current radio button.
- *
- * @param $type
- *   Type of ID generated: 'fs' for fieldset, 'dir' for directory, 'com' for component.
- * @param $path
- *   Path of file we generate an ID for.
- * @return
- *   The generated ID.
- */
-function _potx_form_id($type, $path) {
-  return 'potx-'. $type .'-'. preg_replace('/[^a-zA-Z0-9]+/', '-', $path);
-}
-
-/**
- * Generate a hierarchical structured list of components.
- *
- * @return
- *  Array in the directory structure identified.
- *    - 'normal'  keyed elements being subfolders
- *    - '#name'   elements being component objects for the 'name' component
- *    - '#-count' being the file count of all components in the directory
- */
-function _potx_component_list() {
-  $components = array();
-  // Get a list of all enabled modules and themes.
-  $result = db_query("SELECT name, filename, type, status FROM {system} WHERE type IN ('module', 'theme') ORDER BY filename ASC");
-  foreach ($result as $component) {
-    // Build directory tree structure.
-    $path_parts = explode('/', dirname($component->filename));
-    $dir =& $components;
-    foreach ($path_parts as $dirname) {
-      if (!isset($dir[$dirname])) {
-        $dir[$dirname] = array();
-      }
-      $dir =& $dir[$dirname];
-    }
-
-    // Information about components in this directory.
-    $component->basename = basename($component->filename);
-    $dir['#'. $component->basename] = $component;
-    $dir['#-count'] = isset($dir['#-count']) ? $dir['#-count'] + 1 : 1;
-  }
-
-  return $components;
-}
-
-/**
- * Implementation of hook_reviews(). Coder module integration.
- *
- * Provides the list of reviews provided by this module.
+ * Coder module integration. Returns the review provided by this module.
  */
 function potx_reviews() {
   $review = array(
@@ -322,15 +59,17 @@ function potx_reviews() {
  * Callback implementation for coder review of one file.
  */
 function potx_coder_review(&$coder_args, $review, $rule, $lines, &$results) {
-  include_once drupal_get_path('module', 'potx') .'/potx.inc';
 
-  // Request collection of error messages internally in a structured format.
-  potx_status('set', POTX_STATUS_STRUCTURED);
-  // Process the file (but throw away the result).
+  // Include potx API.
+  include_once drupal_get_path('module', 'potx') . '/potx.inc';
+
+  // Process the file (but throw away the result). We are only interested in
+  // the errors found, no the strings identified.
   $filename = realpath($coder_args['#filename']);
-  _potx_process_file($filename);
+  potx_parser_parse_file($filename);
+
   // Grab the errors and empty the error list.
-  $errors = potx_status('get', TRUE);
+  $errors = potx_parser_util_get_messages();
 
   $severity_name = _coder_review_severity_name($coder_args, $review, $rule);
   foreach ($errors as $error) {
@@ -338,7 +77,7 @@ function potx_coder_review(&$coder_args, $review, $rule, $lines, &$results) {
     // most cases the line number and in some cases a code excerpt for the
     // error. Not all errors know about the exact line number, so it might
     // not be there, in which case we provide some sensible defaults.
-    list($message, $file, $lineno, $excerpt, $docs_url) = $error;
+    list($message, $file, $lineno, $excerpt, $docs_url, $human_text) = $error;
     if (empty($lineno)) {
       // $lineno might be NULL so set to 0.
       $lineno = 0;
@@ -347,7 +86,7 @@ function potx_coder_review(&$coder_args, $review, $rule, $lines, &$results) {
     else {
       $line = $coder_args['#all_lines'][$lineno];
     }
-    $rule['#warning'] = htmlspecialchars_decode($message, ENT_QUOTES) . (!empty($docs_url) ? (' <a href="'. $docs_url .'">'. t('Read the documentation') .'</a>.') : '');
+    $rule['#warning'] = htmlspecialchars_decode($message, ENT_QUOTES) . (!empty($docs_url) ? (' <a href="' . $docs_url . '">' . t('Read the documentation') . '</a>.') : '');
     _coder_review_error($results, $rule, $severity_name, $lineno, $line);
   }
 }
diff --git a/tests/potx.test b/tests/potx.test
index 6c3bf8c..002acc3 100644
--- a/tests/potx.test
+++ b/tests/potx.test
@@ -20,7 +20,7 @@ class PotxTestCase extends DrupalWebTestCase {
     parent::setUp(array('locale', 'potx'));
 
     // Add potx.inc which we test for its functionality.
-    include_once(drupal_get_path('module', 'potx') .'/potx.inc');
+    include_once(drupal_get_path('module', 'potx') . '/potx.inc');
     // Store empty error message for reuse in multiple cases.
     $this->empty_error = t('Empty string attempted to be localized. Please do not leave test code for localization in your source.');
   }
@@ -30,7 +30,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   public function testDrupal5() {
     // Parse and build the Drupal 5 module file.
-    $filename = drupal_get_path('module', 'potx') .'/tests/potx_test_5.module';
+    $filename = drupal_get_path('module', 'potx') . '/tests/potx_test_5.module';
     $this->parseFile($filename, POTX_API_5);
 
     // Assert strings found in module source code.
@@ -78,7 +78,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   public function testDrupal6() {
     // Parse and build the Drupal 6 module file.
-    $filename = drupal_get_path('module', 'potx') .'/tests/potx_test_6.module';
+    $filename = drupal_get_path('module', 'potx') . '/tests/potx_test_6.module';
     $this->parseFile($filename, POTX_API_6);
 
     // Assert strings found in module source code.
@@ -129,7 +129,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   public function testDrupal7() {
     // Parse and build the Drupal 7 module file.
-    $filename = drupal_get_path('module', 'potx') .'/tests/potx_test_7.module';
+    $filename = drupal_get_path('module', 'potx') . '/tests/potx_test_7.module';
     $this->parseFile($filename, POTX_API_7);
 
     // Assert strings found in module source code.
@@ -182,7 +182,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   public function testDrupalInfo() {
     // Parse and build the Drupal 6 module file.
-    $filename = drupal_get_path('module', 'potx') .'/tests/potx_test_6.info';
+    $filename = drupal_get_path('module', 'potx') . '/tests/potx_test_6.info';
     $this->parseFile($filename, POTX_API_6);
 
     // Look for name, description and package name extracted.
@@ -196,7 +196,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   public function testDrupalJS() {
     // Parse and build the Drupal JS file (from above Drupal 5).
-    $filename = drupal_get_path('module', 'potx') .'/tests/potx_test.js';
+    $filename = drupal_get_path('module', 'potx') . '/tests/potx_test.js';
     $this->parseFile($filename, POTX_API_6);
 
     // Assert strings found in JS source code.
@@ -214,20 +214,10 @@ class PotxTestCase extends DrupalWebTestCase {
    * Parse the given file with the given API version.
    */
   private function parseFile($filename, $api_version, $string_mode = POTX_STRING_RUNTIME) {
-    global $_potx_store, $_potx_strings, $_potx_install;
-    $_potx_store = $_potx_strings = $_potx_install = array();
-
-    potx_status('set', POTX_STATUS_STRUCTURED);
-    _potx_process_file($filename, 0, '_potx_save_string', '_potx_save_version', $api_version);
-    _potx_build_files($string_mode, POTX_BUILD_SINGLE, 'general', '_potx_save_string', '_potx_save_version', '_potx_get_header', NULL, NULL, $api_version);
-
-    // Grab .po representation of parsed content.
-    ob_start();
-    _potx_write_files('potx-test.po');
-    $this->potx_output = ob_get_clean();
-    //debug(var_export($this->potx_output, TRUE));
-    $this->potx_status = potx_status('get', TRUE);
-    //debug(var_export($this->potx_status, TRUE));
+    potx_parser_util_reset();
+    potx_parser_parse_file($filename, 0, 'potx_parser_callback_save_string', 'potx_parser_callback_version', $api_version);
+    $this->potx_output = potx_parser_build_output($string_mode, 'potx_parser_callback_save_string', 'potx_parser_callback_version', 'potx_parser_callback_header', NULL, NULL, $api_version);
+    $this->potx_status = potx_parser_util_get_messages();
 }
 
   /**
@@ -237,7 +227,7 @@ class PotxTestCase extends DrupalWebTestCase {
     if (!$message) {
       $message = t('MsgID "@raw" found', array('@raw' => check_plain($string)));
     }
-    $this->assert(strpos($this->potx_output, 'msgid "'. _potx_format_quoted_string('"'. $string . '"') .'"') !== FALSE, $message, $group);
+    $this->assert(strpos($this->potx_output, 'msgid "' . potx_parser_util_format_string('"' . $string . '"') . '"') !== FALSE, $message, $group);
   }
 
   /**
@@ -247,7 +237,7 @@ class PotxTestCase extends DrupalWebTestCase {
     if (!$message) {
       $message = t('MsgID "@raw" not found', array('@raw' => check_plain($string)));
     }
-    $this->assert(strpos($this->potx_output, 'msgid "'. _potx_format_quoted_string('"'. $string . '"') .'"') === FALSE, $message, $group);
+    $this->assert(strpos($this->potx_output, 'msgid "' . potx_parser_util_format_string('"' . $string . '"') . '"') === FALSE, $message, $group);
   }
 
   /**
@@ -257,7 +247,7 @@ class PotxTestCase extends DrupalWebTestCase {
     if (!$message) {
       $message = t('MsgID "@raw" in context "@context" found', array('@raw' => check_plain($string), '@context' => check_plain($context)));
     }
-    $this->assert(strpos($this->potx_output, 'msgctxt "'. _potx_format_quoted_string('"'. $context . '"') . "\"\nmsgid \"". _potx_format_quoted_string('"'. $string . '"') .'"') !== FALSE, $message, $group);
+    $this->assert(strpos($this->potx_output, 'msgctxt "' . potx_parser_util_format_string('"' . $context . '"') . "\"\nmsgid \"" . potx_parser_util_format_string('"' . $string . '"') . '"') !== FALSE, $message, $group);
   }
 
   /**
@@ -267,7 +257,7 @@ class PotxTestCase extends DrupalWebTestCase {
     if (!$message) {
       $message = t('No MsgID "@raw" in context "@context" found', array('@raw' => check_plain($string), '@context' => check_plain($context)));
     }
-    $this->assert(strpos($this->potx_output, 'msgid "'. _potx_format_quoted_string('"'. $string . '"') .'"'. "\nmsgctxt \"". _potx_format_quoted_string('"'. $context . '"') . '"') === FALSE, $message, $group);
+    $this->assert(strpos($this->potx_output, 'msgid "' . potx_parser_util_format_string('"' . $string . '"') . '"' . "\nmsgctxt \"" . potx_parser_util_format_string('"' . $context . '"') . '"') === FALSE, $message, $group);
   }
 
   /**
@@ -277,21 +267,7 @@ class PotxTestCase extends DrupalWebTestCase {
     if (!$message) {
       $message = t('Plural ID "@raw" found', array('@raw' => check_plain($string)));
     }
-    $this->assert(strpos($this->potx_output, 'msgid_plural "'. _potx_format_quoted_string('"'. $string . '"') .'"') !== FALSE, $message, $group);
-  }
-
-  /**
-   * Debug functionality until simpletest built-in debugging is backported.
-   */
-  private function outputScreenContents($description = 'output', $basename = 'output') {
-    // This is a hack to get a directory that won't be cleaned up by simpletest
-    $file_dir = file_directory_path() .'/../simpletest_output_pages';
-    if (!is_dir($file_dir)) {
-      mkdir($file_dir, 0777, TRUE);
-    }
-    $output_path = "$file_dir/$basename.". $this->randomName(10) .'.html';
-    $rv = file_put_contents($output_path, $this->drupalGetContent());
-    $this->pass("$description: ". l('Contents of result page', $output_path));
+    $this->assert(strpos($this->potx_output, 'msgid_plural "' . potx_parser_util_format_string('"' . $string . '"') . '"') !== FALSE, $message, $group);
   }
 
 }
