diff --git a/potx-cli.php b/potx-cli.php
deleted file mode 100755
index 0239e48..0000000
--- a/potx-cli.php
+++ /dev/null
@@ -1,137 +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' => ':)'));
-
-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.drush.inc b/potx.drush.inc
new file mode 100644
index 0000000..3da9193
--- /dev/null
+++ b/potx.drush.inc
@@ -0,0 +1,101 @@
+<?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 multiple core',
+    ),
+    'options' => array(
+      'modules' => 'Comma delimited list of modules to extract translatable strings from.',
+      'files' => 'Comma delimited list of files to extract translatable strings from.',
+    ),
+    '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.'
+    ),
+  );
+  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". dt(' core      Drupal core output mode, .info files folded into general.pot.');
+    $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 = NULL) {
+  // Include library.
+  include_once drupal_get_path('module', 'potx') . '/potx.inc';
+
+  $files = array();
+  $build_mode = POTX_BUILD_SINGLE;
+
+  if (!is_null($mode) && in_array($mode, array('core', 'multiple', 'single'))) {
+    // First argument could be any of the mode names.
+    $build_mode = constant('POTX_BUILD_'. strtoupper($args[0]));
+  }
+
+  $modules_option = drush_get_option('modules');
+  $files_option = drush_get_option('files');
+
+  if (!empty($modules_option)) {
+    $modules = explode(',', $modules_option);
+    foreach ($modules as $module) {
+      $files = array_merge($files, _potx_explore_dir(drupal_get_path('module', $module), '*', POTX_API_CURRENT, TRUE));
+    }
+  }
+  elseif (!empty($files_option)) {
+    $files = explode(',', $files_option);
+  }
+  else {
+    // No file list provided so autodiscover files in current directory.
+    $files = _potx_explore_dir(drush_cwd(), '*', POTX_API_CURRENT, TRUE);
+  }
+
+  foreach ($files as $file) {
+    drush_print("Processing $file...");
+    _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();
+  drush_print("Done.");
+}
diff --git a/potx.inc b/potx.inc
index 38a9523..1598f12 100644
--- a/potx.inc
+++ b/potx.inc
@@ -107,14 +107,6 @@ define('POTX_API_6', 6);
 define('POTX_API_7', 7);
 
 /**
- * Parse source files in Drupal 8.x format.
- *
- * Changes since 7.x documented at
- * http://drupal.org/list-changes/drupal?to_branch=8.x
- */
-define('POTX_API_8', 8);
-
-/**
  * When no context is used. Makes it easy to look these up.
  */
 define('POTX_CONTEXT_NONE', NULL);
@@ -151,7 +143,7 @@ function _potx_process_file($file_path, $strip_prefix = 0, $save_callback = '_po
   _potx_find_version_number($code, $file_name, $version_callback);
 
   // The .info files are not PHP code, no need to tokenize.
-  if ($name_parts['extension'] == 'info' && $api_version < POTX_API_8) {
+  if ($name_parts['extension'] == 'info') {
     _potx_find_info_file_strings($file_path, $file_name, $save_callback, $api_version);
     return;
   }
@@ -197,24 +189,18 @@ function _potx_process_file($file_path, $strip_prefix = 0, $save_callback = '_po
 
   // Regular t() calls with different usages.
   if ($api_version > POTX_API_6) {
-    // Drupal 7 onwards supports context on t().
+    // Drupal 7 onwards supports context on t() and st().
     _potx_find_t_calls_with_context($file_name, $save_callback);
-    if ($api_version < POTX_API_8) {
-      // st() and $t() are supported up to Drupal 7.
-      _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);
-    }
+    _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 {
-    // Context-less API up to Drupal 6.
     _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);
   }
-  if ($api_version < POTX_API_8) {
-    // This does not support context even in Drupal 7.
-    _potx_find_t_calls($file_name, $save_callback, '_locale_import_message', POTX_STRING_BOTH);
-  }
+  // This does not support context even in Drupal 7.
+  _potx_find_t_calls($file_name, $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.
@@ -234,29 +220,25 @@ function _potx_process_file($file_path, $strip_prefix = 0, $save_callback = '_po
       _potx_find_perm_hook($file_name, $name_parts['filename'], $save_callback);
     }
     if ($api_version > POTX_API_5) {
-      // @todo: if tabs are not defined on the menu hook anymore, exclude this for 8.
       _potx_find_menu_hooks($file_name, $name_parts['filename'], $save_callback);
     }
   }
 
   // Special handling of some Drupal core files.
-  if ($api_version < POTX_API_8 && (($basename == 'locale.inc' && $api_version < POTX_API_7) || $basename == 'iso.inc')) {
+  if (($basename == 'locale.inc' && $api_version < POTX_API_7) || $basename == 'iso.inc') {
     _potx_find_language_names($file_name, $save_callback, $api_version);
   }
   elseif ($basename == 'locale.module') {
-    // Applies to all Drupal versions, yay!
     _potx_add_date_strings($file_name, $save_callback, $api_version);
   }
   elseif ($basename == 'common.inc') {
-    // Applies to all Drupal versions, yay!
     _potx_add_format_interval_strings($file_name, $save_callback, $api_version);
   }
   elseif ($basename == 'system.module') {
-    // Applies to all Drupal versions, yay!
     _potx_add_default_region_names($file_name, $save_callback, $api_version);
   }
-  elseif ($basename == 'user.module' && $api_version < POTX_API_8) {
-    // Save default user role names (up to Drupal 7).
+  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);
     if ($api_version > POTX_API_6) {
@@ -1310,10 +1292,10 @@ function _potx_add_default_region_names($file, $save_callback, $api_version = PO
     'footer' => 'Footer',
   );
   if ($api_version > POTX_API_6) {
-    $regions['highlight'] = 'Highlighted';
+    // @todo: Update with final region list when D7 stabilizes.
+    $regions['highlight'] = 'Highlighted content';
     $regions['help'] = 'Help';
     $regions['page_top'] = 'Page top';
-    $regions['page_top'] = 'Page bottom';
   }
   foreach ($regions as $region) {
     // Regions come with the default context.
diff --git a/tests/potx.test b/tests/potx.test
index 1fb1e70..799677b 100644
--- a/tests/potx.test
+++ b/tests/potx.test
@@ -42,7 +42,8 @@ class PotxTestCase extends DrupalWebTestCase {
     $this->assertNoMsgID('My watchdog message');
     $this->assertMsgID('test potx permission');
     $this->assertMsgID('one more test potx permission');
-    $this->assertPluralID('1 test string', '@count test strings');
+    $this->assertMsgID('1 test string');
+    $this->assertPluralID('@count test strings');
 
     // Installer string should not appear in runtime output.
     $this->assertNoMsgID('Installer only test string');
@@ -54,10 +55,6 @@ class PotxTestCase extends DrupalWebTestCase {
     $this->assertMsgID('Test string in context');
     $this->assertNoMsgIDContext('Dynamic string in context', 'Dynamic context');
     $this->assertMsgID('Dynamic string in context');
-    // The singular/plural will not even be found without context, because
-    // Drupal 5 does not have support for args on format_plural.
-    $this->assertNoMsgID('1 test string in context');
-    $this->assertNoPluralIDContext('1 test string in context', '@count test strings in context', 'Test context');
 
     // Look at installer strings.
     $this->parseFile($filename, POTX_API_5, POTX_STRING_INSTALLER);
@@ -70,11 +67,10 @@ class PotxTestCase extends DrupalWebTestCase {
     $this->assertNoMsgIDContext('Dynamic string in context', 'Dynamic context');
     $this->assertMsgID('Dynamic string in context');
 
-    $this->assert(count($this->potx_status) == 4, t('4 error messages found'));
+    $this->assert(count($this->potx_status) == 3, t('3 error messages found'));
     $this->assert($this->potx_status[0][0] == $this->empty_error, t('First empty error found.'));
     $this->assert($this->potx_status[1][0] == $this->empty_error, t('Second empty error found.'));
-    $this->assert($this->potx_status[2][0] == '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.', t('Fourth error found.'));
-    $this->assert($this->potx_status[3][0] == $this->empty_error, t('Third empty error found.'));
+    $this->assert($this->potx_status[2][0] == $this->empty_error, t('Third empty error found.'));
   }
 
   /**
@@ -93,7 +89,8 @@ class PotxTestCase extends DrupalWebTestCase {
     $this->assertMsgID('My watchdog message');
     $this->assertMsgID('test potx permission');
     $this->assertMsgID('one more test potx permission');
-    $this->assertPluralID('1 test string', '@count test strings');
+    $this->assertMsgID('1 test string');
+    $this->assertPluralID('@count test strings');
     $this->assertMsgID('Test menu item description');
     $this->assertMsgID('Test menu item description altered (1)');
     $this->assertMsgID('Test menu item description altered (2)');
@@ -109,8 +106,6 @@ class PotxTestCase extends DrupalWebTestCase {
     $this->assertMsgID('Test string in context');
     $this->assertNoMsgIDContext('Dynamic string in context', 'Dynamic context');
     $this->assertMsgID('Dynamic string in context');
-    $this->assertPluralID('1 test string in context', '@count test strings in context');
-    $this->assertNoPluralIDContext('1 test string in context', '@count test strings in context', 'Test context');
 
     // Look at installer strings.
     $this->parseFile($filename, POTX_API_6, POTX_STRING_INSTALLER);
@@ -152,8 +147,8 @@ class PotxTestCase extends DrupalWebTestCase {
     $this->assertMsgID('One more test potx permission');
     $this->assertMsgID('One more test potx permission description');
 
-    $this->assertPluralID('1 test string', '@count test strings');
-    $this->assertPluralIDContext('1 test string in context', '@count test strings in context', 'Test context');
+    $this->assertMsgID('1 test string');
+    $this->assertPluralID('@count test strings');
 
     $this->assertMsgID('Test menu item description');
     $this->assertMsgID('Test menu item description altered (1)');
@@ -206,10 +201,10 @@ class PotxTestCase extends DrupalWebTestCase {
 
     // Assert strings found in JS source code.
     $this->assertMsgID('Test string in JS');
-    $this->assertPluralID('1 test string in JS', '@count test strings in JS');
+    $this->assertMsgID('1 test string in JS');
+    $this->assertPluralID('@count test strings in JS');
     $this->assertMsgID('Another test string in JS');
     $this->assertMsgID('Embedded test string in JS');
-    $this->assertMsgID('String with @placeholder value');
 
     $this->assert(count($this->potx_status) == 1, t('1 error message found'));
     $this->assert($this->potx_status[0][0] == $this->empty_error, t('Empty error found.'));
@@ -278,31 +273,11 @@ class PotxTestCase extends DrupalWebTestCase {
   /**
    * Helper function to assert an msgid_plural construct in the .po file.
    */
-  private function assertPluralID($string, $plural, $message = '', $group = 'Other') {
+  private function assertPluralID($string, $message = '', $group = 'Other') {
     if (!$message) {
       $message = t('Plural ID "@raw" found', array('@raw' => check_plain($string)));
     }
-    $this->assert(strpos($this->potx_output, 'msgid "'. _potx_format_quoted_string('"'. $string . '"') ."\"\nmsgid_plural \"". _potx_format_quoted_string('"'. $plural . '"') .'"') !== FALSE, $message, $group);
-  }
-
-  /**
-   * Helper function to assert an msgid_plural with context in the .po file.
-   */
-  private function assertPluralIDContext($string, $plural, $context, $message = '', $group = 'Other') {
-    if (!$message) {
-      $message = t('Plural ID "@raw" found with context "@context"', array('@raw' => check_plain($string), '@context' => $context));
-    }
-    $this->assert(strpos($this->potx_output, 'msgctxt "'. _potx_format_quoted_string('"'. $context . '"') . "\"\nmsgid \"". _potx_format_quoted_string('"'. $string . '"') ."\"\nmsgid_plural \"". _potx_format_quoted_string('"'. $plural . '"') .'"') !== FALSE, $message, $group);
-  }
-
-  /**
-   * Helper function to assert lack of msgid_plural with context in the .po file.
-   */
-  private function assertNoPluralIDContext($string, $plural, $context, $message = '', $group = 'Other') {
-    if (!$message) {
-      $message = t('No plural ID "@raw" found with context "@context"', array('@raw' => check_plain($string), '@context' => $context));
-    }
-    $this->assert(strpos($this->potx_output, 'msgctxt "'. _potx_format_quoted_string('"'. $context . '"') . "\"\nmsgid \"". _potx_format_quoted_string('"'. $string . '"') ."\"\nmsgid_plural \"". _potx_format_quoted_string('"'. $plural . '"') .'"') === FALSE, $message, $group);
+    $this->assert(strpos($this->potx_output, 'msgid_plural "'. _potx_format_quoted_string('"'. $string . '"') .'"') !== FALSE, $message, $group);
   }
 
   /**
diff --git a/tests/potx_test.js b/tests/potx_test.js
index c0cf8ee..bc0773f 100644
--- a/tests/potx_test.js
+++ b/tests/potx_test.js
@@ -2,12 +2,8 @@
 /**
  * Test Drupal.t and Drupal.formatPlural() invocation.
  */
-
+ 
 Drupal.t('Test string in JS');
 Drupal.formatPlural(count, '1 test string in JS', '@count test strings in JS');
-
 Drupal.t('Another test string in JS', {'test': Drupal.t('Embedded test string in JS')});
-
 Drupal.t('');
-
-Drupal.t('String with @placeholder value', {'@placeholder': 'placeholder'});
diff --git a/tests/potx_test_5.module b/tests/potx_test_5.module
index 2675e46..8663be8 100644
--- a/tests/potx_test_5.module
+++ b/tests/potx_test_5.module
@@ -35,7 +35,6 @@ function potx_test_5_page() {
   $t('Dynamic callback test string');
 
   t('Test string in context', array(), array('context' => 'Test context'));
-  format_plural($count, '1 test string in context', '@count test strings in context', array(), array('context' => 'Test context'));
   t('');
 
   st('Installer string in context', array(), array('context' => 'Installer context'));
diff --git a/tests/potx_test_6.info b/tests/potx_test_6.info
index 450c065..a836531 100644
--- a/tests/potx_test_6.info
+++ b/tests/potx_test_6.info
@@ -3,4 +3,3 @@ description = Test description
 package = Test package
 version = 6.x
 core = 6.x
-hidden = TRUE
diff --git a/tests/potx_test_6.module b/tests/potx_test_6.module
index ca7178b..64f5985 100644
--- a/tests/potx_test_6.module
+++ b/tests/potx_test_6.module
@@ -45,7 +45,6 @@ function potx_test_6_page() {
   $t('Dynamic callback test string');
 
   t('Test string in context', array(), array('context' => 'Test context'));
-  format_plural($count, '1 test string in context', '@count test strings in context', array(), array('context' => 'Test context'));
   t('');
 
   st('Installer string in context', array(), array('context' => 'Installer context'));
diff --git a/tests/potx_test_7.module b/tests/potx_test_7.module
index 377b668..3706aa3 100644
--- a/tests/potx_test_7.module
+++ b/tests/potx_test_7.module
@@ -45,7 +45,6 @@ function potx_test_7_page() {
   $t('Dynamic callback test string');
 
   t('Test string in context', array(), array('context' => 'Test context'));
-  format_plural($count, '1 test string in context', '@count test strings in context', array(), array('context' => 'Test context'));
   t('');
 
   st('Installer string in context', array(), array('context' => 'Installer context'));
