diff --git a/potx.admin.inc b/potx.admin.inc
deleted file mode 100644
index e5fc13d..0000000
--- a/potx.admin.inc
+++ /dev/null
@@ -1,274 +0,0 @@
-<?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);
-  module_load_include('inc', 'potx');
-  module_load_include('inc', 'potx', 'potx.local');
-
-  // 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'], '.'));
-  }
-
-  if (isset($pathinfo['extension'])) {
-    // A specific module or theme file was requested (otherwise there should be no extension).
-    potx_local_init($pathinfo['dirname']);
-    $files = _potx_explore_dir($pathinfo['dirname'] .'/', $pathinfo['filename']);
-    $strip_prefix = 1 + strlen($pathinfo['dirname']);
-    $outputname = $pathinfo['filename'];
-  }
-  // A directory name was requested.
-  else {
-    potx_local_init($component);
-    $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);
-  }
-  potx_finish_processing('_potx_save_string');
-
-  // 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.
- *
- * @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 string
- *   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
- *  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.inc b/potx.inc
index 64baccc..8cd2401 100644
--- a/potx.inc
+++ b/potx.inc
@@ -33,7 +33,7 @@ use Symfony\Component\Yaml\Exception\ParseException;
  *
  * This should be the only difference between different branches of potx.inc
  */
-define('POTX_API_CURRENT', 7);
+define('POTX_API_CURRENT', 8);
 
 /**
  * Silence status reports.
@@ -182,7 +182,6 @@ define('POTX_JS_OBJECT_CONTEXT', '
  *   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) {
-  global $_potx_tokens, $_potx_lookup;
 
   // Figure out the basename and extension to select extraction method.
   $basename = basename($file_path);
@@ -209,6 +208,15 @@ function _potx_process_file($file_path, $strip_prefix = 0, $save_callback = '_po
     _potx_parse_twig_file($code, $file_name, $save_callback);
   }
 
+  _potx_parse_php_file($code, $file_name, $save_callback, $name_parts, $basename, $api_version);
+}
+
+/**
+ * Parse a PHP file for translatables.
+ */
+function _potx_parse_php_file($code, $file_name, $save_callback, $name_parts, $basename, $api_version) {
+  global $_potx_tokens, $_potx_lookup;
+
   $constraint_extract = FALSE;
   if (substr($name_parts['filename'], -10) == 'Constraint' && $api_version > POTX_API_7) {
     $constraint_extract = TRUE;
diff --git a/potx.info b/potx.info
deleted file mode 100644
index 3b29dfd..0000000
--- a/potx.info
+++ /dev/null
@@ -1,9 +0,0 @@
-name = Translation template extractor
-description = Provides a web interface and an API to extract translatable text from the sources of installed components.
-dependencies[] = locale
-files[] = potx.inc
-files[] = potx.local.inc
-files[] = tests/potx.test
-core = 7.x
-package = Multilingual
-php = 5.1.2
diff --git a/potx.info.yml b/potx.info.yml
new file mode 100644
index 0000000..4670bd9
--- /dev/null
+++ b/potx.info.yml
@@ -0,0 +1,8 @@
+name: Translation template extractor
+description: Provides a web interface and an API to extract translatable text from the sources of installed components.
+core: 8.x
+package: Multilingual
+version: 8.x-1.x-dev
+dependencies:
+  - locale
+type: module
diff --git a/potx.install b/potx.install
index 0cbf947..16184c1 100644
--- a/potx.install
+++ b/potx.install
@@ -16,13 +16,12 @@
  */
 function potx_requirements($phase) {
   $requirements = array();
-  $t = get_t();
 
   if (!function_exists('token_get_all')) {
     $requirements['potx_tokenizer'] = array(
-      'title' => $t('PHP tokenizer for Translation template extractor'),
-      'value' => $t('Not available'),
-      'description' => $t('The <a href="@tokenizer">PHP tokenizer functions</a> are required.', array('@tokenizer' => 'http://php.net/tokenizer')),
+      'title' => t('PHP tokenizer for Translation template extractor'),
+      'value' => t('Not available'),
+      'description' => t('The <a href="@tokenizer">PHP tokenizer functions</a> are required.', array('@tokenizer' => 'http://php.net/tokenizer')),
       'severity' => REQUIREMENT_ERROR,
     );
   }
diff --git a/potx.links.task.yml b/potx.links.task.yml
new file mode 100644
index 0000000..d71f1ef
--- /dev/null
+++ b/potx.links.task.yml
@@ -0,0 +1,5 @@
+potx.extract_translation:
+  route_name: potx.extract_translation
+  base_route: locale.translate_page
+  title: Extract
+  weight: 40
diff --git a/potx.module b/potx.module
index 945a2bc..524f96b 100644
--- a/potx.module
+++ b/potx.module
@@ -13,30 +13,14 @@
 /**
  * Implementation of hook_help().
  */
-function potx_help($path, $arg) {
-  switch ($path) {
-    case 'admin/config/regional/translate/extract':
+function potx_help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'potx.extract_translation':
       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>';
   }
 }
 
 /**
- * Implementation of hook_menu().
- */
-function potx_menu() {
-  $items['admin/config/regional/translate/extract'] = array(
-    'title' => 'Extract',
-    'page callback' => 'drupal_get_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;
-}
-
-/**
  * Implementation of hook_reviews(). Coder module integration.
  *
  * Provides the list of reviews provided by this module.
diff --git a/potx.routing.yml b/potx.routing.yml
new file mode 100644
index 0000000..9a74e20
--- /dev/null
+++ b/potx.routing.yml
@@ -0,0 +1,7 @@
+potx.extract_translation:
+  path: '/admin/config/regional/translate/extract'
+  defaults:
+    _form: '\Drupal\potx\Form\PotxExtractTranslationForm'
+    _title: 'Extract'
+  requirements:
+    _permission: 'translate interface'
diff --git a/src/Form/PotxExtractTranslationForm.php b/src/Form/PotxExtractTranslationForm.php
new file mode 100644
index 0000000..36a2047
--- /dev/null
+++ b/src/Form/PotxExtractTranslationForm.php
@@ -0,0 +1,334 @@
+<?php
+
+namespace Drupal\potx\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Extension\ThemeHandlerInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+
+class PotxExtractTranslationForm extends FormBase {
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\language\ConfigurableLanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The theme handler.
+   *
+   * @var \Drupal\Core\Extension\ThemeHandlerInterface
+   */
+  protected $themeHandler;
+
+  /**
+   * Constructs a new PotxExtractTranslationForm object.
+   *
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager service.
+   */
+  public function __construct(LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
+    $this->languageManager = $language_manager;
+    $this->moduleHandler = $module_handler;
+    $this->themeHandler = $theme_handler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('language_manager'),
+      $container->get('module_handler'),
+      $container->get('theme_handler')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'potx_extract_transation';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $components = $this->generateComponentList();
+    $this->buildComponentSelector($form, $components);
+
+    // Generate translation file for a specific language if possible.
+    $languages = $this->languageManager->getLanguages();
+    if (count($languages) > 1 || !isset($languages['en'])) {
+      // We have more languages, or the single language we have is not English.
+      $options = array('n/a' => $this->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] = $this->t('Template file for @langname translations', array('@langname' => $this->t($language->getName())));
+      }
+      $form['langcode'] = array(
+        '#type' => 'radios',
+        '#title' => $this->t('Template language'),
+        '#default_value' => 'n/a',
+        '#options' => $options,
+        '#description' => $this->t('Export a language independent or language dependent (plural forms, language team name, etc.) template.'),
+      );
+      $form['translations'] = array(
+        '#type' => 'checkbox',
+        '#title' => $this->t('Include translations'),
+        '#description' => $this->t('Include translations of strings in the file generated. Not applicable for language independent templates.')
+      );
+    }
+
+    $form['submit'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Extract'),
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    if (empty($form_state->getValue('component'))) {
+      $form_state->setErrorByName('component', $this->t('You should select a component to export.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $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);
+    $this->moduleHandler->loadInclude('potx', 'inc');
+    $this->moduleHandler->loadInclude('potx', 'inc', 'potx.local');
+
+    // Silence status messages.
+    potx_status('set', POTX_STATUS_MESSAGE);
+
+    // $form_state->getValue('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->getValue('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'], '.'));
+    }
+
+    if (isset($pathinfo['extension'])) {
+      // A specific module or theme file was requested (otherwise there should be no extension).
+      potx_local_init($pathinfo['dirname']);
+      $files = _potx_explore_dir($pathinfo['dirname'] .'/', $pathinfo['filename']);
+      $strip_prefix = 1 + strlen($pathinfo['dirname']);
+      $outputname = $pathinfo['filename'];
+    }
+    // A directory name was requested.
+    else {
+      potx_local_init($component);
+      $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 ($form_state->hasValue('langcode') && ($form_state->getValue('langcode') != 'n/a')) {
+      $template_langcode = $form_state->getValue('langcode');
+      $outputname .= '.'. $template_langcode;
+      if (!empty($form_state->getValue('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);
+    }
+    potx_finish_processing('_potx_save_string');
+
+    // 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;
+  }
+
+  /**
+   * Generate a hierarchical structured list of components.
+   *
+   * @return array
+   *  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
+   */
+  private function generateComponentList() {
+
+    $components = array();
+
+    // Get a list of all enabled modules and themes.
+    $modules = $this->moduleHandler->getModuleList();
+    $themes = $this->themeHandler->listInfo();
+    $result = array_merge($modules, $themes);
+    foreach ($result as $component) {
+      // Build directory tree structure.
+      $path_parts = explode('/', $component->getPath());
+      $dir =& $components;
+      foreach ($path_parts as $dirname) {
+        if (!isset($dir[$dirname])) {
+          $dir[$dirname] = array();
+        }
+        $dir =& $dir[$dirname];
+      }
+
+      // Information about components in this directory.
+      $dir['#' . $component->getExtensionFilename()] = $component;
+      $dir['#-count'] = isset($dir['#-count']) ? $dir['#-count'] + 1 : 1;
+    }
+
+    return $components;
+  }
+
+  /**
+   * Build a chunk of the component selection form.
+   *
+   * @param $form
+   *   Form to populate with fields.
+   * @param $components
+   *   Structured array with components as returned by $this->generateComponentList().
+   * @param $dirname
+   *   Name of directory handled.
+   */
+  function buildComponentSelector(&$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 = $component->getPath();
+      $form[$this->getFormElementID('dir', $dirname)] = array(
+        '#type' => 'radio',
+        '#title' => t('Extract from %name in the %directory directory', array('%directory' => $dirname, '%name' => $component->getName())),
+        '#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[$this->getFormElementID('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[$this->getFormElementID('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' => $components[$entry]->getPath(),
+          '%name'      => $components[$entry]->getName(),
+          '%pattern'   => $components[$entry]->getName() .'.*',
+        );
+        $element[$this->getFormElementID('com', $components[$entry]->getExtensionFilename())] = array(
+          '#type' => 'radio',
+          '#title' => $this->t('Extract from %name', $t_args),
+          '#description' => $this->t('Extract from files named %pattern in the %directory directory.', $t_args),
+          '#default_value' => 0,
+          '#return_value' => $components[$entry]->getExtensionPathname(),
+          // Get all radio buttons into the same group.
+          '#parents' => array('component'),
+        );
+      }
+      // A subdirectory we need to look into.
+      else {
+        $this->buildComponentSelector($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 string
+   *   The generated ID.
+   */
+  function getFormElementID($type, $path) {
+    return 'potx-'. $type .'-'. preg_replace('/[^a-zA-Z0-9]+/', '-', $path);
+  }
+}
diff --git a/tests/potx.test b/src/Tests/PotxTestCaseTest.php
similarity index 81%
rename from tests/potx.test
rename to src/Tests/PotxTestCaseTest.php
index 5294f6c..70b4f04 100644
--- a/tests/potx.test
+++ b/src/Tests/PotxTestCaseTest.php
@@ -1,30 +1,28 @@
 <?php
 
+namespace Drupal\potx\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
 /**
- * @file
- *   Tests to ensure that the template extractor works as intended.
+ * Ensure that the translation template extractor functions properly.
+ *
+ * @group potx
  */
 
-class PotxTestCase extends DrupalWebTestCase {
+class PotxTestCaseTest extends WebTestBase {
 
-  public static function getInfo() {
-    return array(
-      'name' => t('Translation template extractor'),
-      'description' => t('Ensure that the translation template extractor functions properly.'),
-      'group' => t('Translation template extractor'),
-    );
-  }
+  public static $modules = array('locale', 'potx');
 
   public function setUp() {
-    // Set up required modules for potx.
-    parent::setUp('locale', 'potx');
 
     // Add potx.inc which we test for its functionality.
-    include_once(__DIR__ . '/../potx.inc');
-    include_once(__DIR__ . '/../potx.local.inc');
+    include_once(__DIR__ . '/../../potx.inc');
+    include_once(__DIR__ . '/../../potx.local.inc');
     potx_local_init();
     // 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.');
+    $this->tests_root = __DIR__ . '/../../tests';
   }
 
   /**
@@ -32,7 +30,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   public function testDrupal5() {
     // Parse and build the Drupal 5 module file.
-    $filename = __DIR__ . '/potx_test_5.module';
+    $filename = $this->tests_root . '/modules/potx_test_5.module';
     $this->parseFile($filename, POTX_API_5);
 
     // Assert strings found in module source code.
@@ -72,11 +70,13 @@ 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($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(count($this->potx_status) == 4, '4 error messages found');
+    $this->assert($this->potx_status[0][0] == $this->empty_error, 'First empty error found.');
+    $this->assert($this->potx_status[1][0] == $this->empty_error, 'Second empty error found.');
+    $this->assert($this->potx_status[2][0] == t('In @function(), the singular and plural strings should be literal strings. There should be no variables, concatenation, constants or even a t() call there.', array(
+      '@function' => 'format_plural'
+    )), 'Fourth error found.');
+    $this->assert($this->potx_status[3][0] == $this->empty_error, 'Third empty error found.');
   }
 
   /**
@@ -84,7 +84,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   public function testDrupal6() {
     // Parse and build the Drupal 6 module file.
-    $filename = __DIR__ . '/potx_test_6.module';
+    $filename = $this->tests_root . '/modules/potx_test_6.module';
     $this->parseFile($filename, POTX_API_6);
 
     // Assert strings found in module source code.
@@ -125,10 +125,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) == 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] == $this->empty_error, t('Third empty error found.'));
+    $this->assert(count($this->potx_status) == 3, '3 error messages found');
+    $this->assert($this->potx_status[0][0] == $this->empty_error, 'First empty error found.');
+    $this->assert($this->potx_status[1][0] == $this->empty_error, 'Second empty error found.');
+    $this->assert($this->potx_status[2][0] == $this->empty_error, 'Third empty error found.');
   }
 
   /**
@@ -136,7 +136,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   public function testDrupal7() {
     // Parse and build the Drupal 7 module file.
-    $filename = __DIR__ . '/potx_test_7.module';
+    $filename = $this->tests_root . '/modules/potx_test_7.module';
     $this->parseFile($filename, POTX_API_7);
 
     // Assert strings found in module source code.
@@ -182,15 +182,35 @@ class PotxTestCase extends DrupalWebTestCase {
     $this->assertMsgIDContext('Installer string in context', 'Installer context');
     $this->assertMsgIDContext('Dynamic string in context', 'Dynamic context');
 
-    $this->assert(count($this->potx_status) == 2, t('2 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(count($this->potx_status) == 2, '2 error messages found');
+    $this->assert($this->potx_status[0][0] == $this->empty_error, 'First empty error found.');
+    $this->assert($this->potx_status[1][0] == $this->empty_error, 'Second empty error found.');
+  }
+
+  /**
+   * Test parsing of Drupal 7 module with a syntax error.
+   */
+  public function testDrupal7WithSyntaxError() {
+    // Parse and build the Drupal 7 module file.
+    $filename = $this->tests_root . '/modules/potx_test_7_with_error.txt';
+    $file_content = "
+<?php
+
+function potx_test_7_with_syntax_error() {
+  t('Oh god why would @you omit a parenthesis?', array('@you' => 'fool');
+  t('PHP Syntax error gracefully handled');
+}
+    ";
+
+    $this->parsePHPContent($file_content, $filename, POTX_API_7);
 
+    $this->assert(count($this->potx_status) == 1, '1 error messages found');
+    $this->assert($this->potx_status[0][0] == t('Unexpected ;'), 'Unexpected semicolon found.');
     $this->assertMsgID('PHP Syntax error gracefully handled');
   }
 
   public function testDrupal8LanguageManager() {
-    $filename = __DIR__ . '/LanguageManager.php';
+    $filename = $this->tests_root . '/files/LanguageManager.php';
     $this->parseFile($filename, POTX_API_8);
 
     $this->assertMsgID('Test English language');
@@ -200,7 +220,7 @@ class PotxTestCase extends DrupalWebTestCase {
    * Test parsing of Drupal 8 Twig templates.
    */
   public function testDrupal8Twig() {
-    $filename = __DIR__ . '/potx_test_8.html.twig';
+    $filename = $this->tests_root . '/modules/potx_test_8.html.twig';
     $this->parseFile($filename, POTX_API_8);
 
     $this->assertMsgID('This is a translated string.');
@@ -213,8 +233,8 @@ class PotxTestCase extends DrupalWebTestCase {
     $this->assertNoMsgID('that should not be picked up.');
     $this->assertNoMsgID('This is an untranslated string.');
 
-    $this->assert(count($this->potx_status) == 1, t('1 error message found'));
-    $this->assert($this->potx_status[0][0] == t('Uses of the t filter in Twig templates should start with a single literal string, and should not be chained.'), t('Concatenation error found.'));
+    $this->assert(count($this->potx_status) == 1, '1 error message found');
+    $this->assert($this->potx_status[0][0] == t('Uses of the t filter in Twig templates should start with a single literal string, and should not be chained.'), 'Concatenation error found.');
 
     $this->assertMsgID('Hello sun.');
     $this->assertMsgIDContext('Hello sun, with context.', 'Lolspeak');
@@ -242,7 +262,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   public function testDrupal8() {
     // Parse and build the Drupal 8 module file.
-    $filename = __DIR__ . '/potx_test_8.module.txt';
+    $filename = $this->tests_root . '/modules/potx_test_8.module';
     $this->parseFile($filename, POTX_API_8);
 
     // Test parsing $this->t calls in D8 code
@@ -258,9 +278,9 @@ class PotxTestCase extends DrupalWebTestCase {
     $this->assertMsgID('Translation in good context');
     $this->assertMsgIDContext('Translation in good context', 'Translation test');
 
-    $this->assert(count($this->potx_status) == 2, t('2 error messages found'));
-    $this->assert($this->potx_status[0][0] == 'In @Translation, only one, non-empty static string is allowed in double quotes.', t('Incorrect @Translation found.'));
-    $this->assert($this->potx_status[1][0] == $this->empty_error, t('Second empty error found.'));
+    $this->assert(count($this->potx_status) == 2, '2 error messages found');
+    $this->assert($this->potx_status[0][0] == t('In @Translation, only one, non-empty static string is allowed in double quotes.'), 'Incorrect @Translation found.');
+    $this->assert($this->potx_status[1][0] == $this->empty_error, 'Second empty error found.');
 
     $this->assertPluralID('1 formatPlural test string', '@count formatPlural test strings');
     $this->assertPluralIDContext('1 formatPlural test string in context', '@count formatPlural test strings in context', 'Test context');
@@ -303,7 +323,7 @@ class PotxTestCase extends DrupalWebTestCase {
    * Test parsing of Drupal 8 .info.yml files.
    */
   public function testDrupal8InfoYml() {
-    $filename = __DIR__ . '/potx_test_8.info.yml';
+    $filename = $this->tests_root . '/modules/potx_test_8.info.yml';
     $this->parseFile($filename, POTX_API_8);
 
     // Look for name, description and package name extracted.
@@ -316,7 +336,7 @@ class PotxTestCase extends DrupalWebTestCase {
    * Test parsing of Drupal 8 .routing.yml files.
    */
   public function testDrupal8RoutingYml() {
-    $filename = __DIR__ . '/potx_test_8.routing.yml';
+    $filename = $this->tests_root . '/modules/potx_test_8.routing.yml';
     $this->parseFile($filename, POTX_API_8);
 
     // Look for all title can be extracted
@@ -330,9 +350,9 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   public function testDrupal8LocalContextualYml() {
     $filenames = array(
-      __DIR__ . '/potx_test_8.links.task.yml',
-      __DIR__ . '/potx_test_8.links.action.yml',
-      __DIR__ . '/potx_test_8.links.contextual.yml'
+      $this->tests_root . '/modules/potx_test_8.links.task.yml',
+      $this->tests_root . '/modules/potx_test_8.links.action.yml',
+      $this->tests_root . '/modules/potx_test_8.links.contextual.yml'
     );
 
     $this->parseFile($filenames[0], POTX_API_8);
@@ -355,7 +375,7 @@ class PotxTestCase extends DrupalWebTestCase {
    * Test parsing of Drupal 8 menu link files.
    */
   public function testDrupal8MenuLinksYml() {
-    $this->parseFile(__DIR__ . '/potx_test_8.links.menu.yml', POTX_API_8);
+    $this->parseFile($this->tests_root . '/modules/potx_test_8.links.menu.yml', POTX_API_8);
     $this->assertMsgID('Test menu link title');
     $this->assertMsgID('Test menu link description.');
     $this->assertMsgIDContext('Test menu link title with context', 'Menu item context');
@@ -365,7 +385,7 @@ class PotxTestCase extends DrupalWebTestCase {
    * Test parsing of custom yaml files.
    */
   public function testDrupal8CustomYml() {
-    $files = _potx_explore_dir(__DIR__ . '/potx_test_8/', '*', POTX_API_8);
+    $files = _potx_explore_dir($this->tests_root . '/files/potx_test_8/', '*', POTX_API_8);
     _potx_init_yaml_translation_patterns();
     $this->parseFile($files[0], POTX_API_8);
     $this->assertMsgID('Test custom yaml translatable');
@@ -374,13 +394,13 @@ class PotxTestCase extends DrupalWebTestCase {
     // Test that translation patterns for a module won't be used for extracting
     // translatable strings for another module.
     potx_finish_processing('_potx_save_string', POTX_API_8);
-    $files = _potx_explore_dir(__DIR__ . '/potx_test_yml/', '*', POTX_API_8);
-    $this->parseFile(__DIR__ . '/potx_test_yml/potx_test_8.test2.yml', POTX_API_8);
+    $files = _potx_explore_dir($this->tests_root . '/files/potx_test_yml/', '*', POTX_API_8);
+    $this->parseFile($this->tests_root . '/files/potx_test_yml/potx_test_8.test2.yml', POTX_API_8);
     $this->assertNoMsgID('Not translatable string');
     $this->assertMsgID('Translatable string');
     $this->assertMsgIDContext('Test custom yaml translatable field with context', 'Yaml translatable context');
     // Test that custom translation patterns are extracted from subfolders.
-    $this->parseFile(__DIR__ . '/potx_test_yml/test_folder/potx_test_8.test3.yml', POTX_API_8);
+    $this->parseFile($this->tests_root . '/files/potx_test_yml/test_folder/potx_test_8.test3.yml', POTX_API_8);
     $this->assertMsgID('Translatable string inside directory');
   }
 
@@ -388,7 +408,7 @@ class PotxTestCase extends DrupalWebTestCase {
    * Test parsing of Drupal 8 .breakpoints.yml files.
    */
   public function testDrupal8BreakpointsYml() {
-    $filename = __DIR__ . '/potx_test_8.breakpoints.yml';
+    $filename = $this->tests_root . '/modules/potx_test_8.breakpoints.yml';
     $this->parseFile($filename, POTX_API_8);
     $this->assertMsgID('Mobile');
     $this->assertMsgID('Standard');
@@ -399,7 +419,7 @@ class PotxTestCase extends DrupalWebTestCase {
    * Test parsing of Drupal 8 permissions files.
    */
   public function testDrupal8PermissionsYml() {
-    $this->parseFile(__DIR__ . '/potx_test_8.permissions.yml', POTX_API_8);
+    $this->parseFile($this->tests_root . '/modules/potx_test_8.permissions.yml', POTX_API_8);
     $this->assertMsgID('Title potx_test_8_a');
     $this->assertMsgID('Description: potx_test_8_a');
     $this->assertMsgID('Title potx_test_8_b');
@@ -413,7 +433,7 @@ class PotxTestCase extends DrupalWebTestCase {
 
     global $_potx_store, $_potx_strings, $_potx_install;
     $_potx_store = $_potx_strings = $_potx_install = array();
-    $test_d8_path = __DIR__ . '/drupal8';
+    $test_d8_path = $this->tests_root . '/files/drupal8';
 
     $files = _potx_explore_dir($test_d8_path, '*', POTX_API_8, TRUE);
 
@@ -489,7 +509,7 @@ class PotxTestCase extends DrupalWebTestCase {
    * Test parsing Drupal 8 validation constraint messages.
    */
   public function testDrupal8ConstraintMessages() {
-    $filename = __DIR__ . '/TestConstraint.php';
+    $filename = $this->tests_root . '/files/TestConstraint.php';
     $this->parseFile($filename, POTX_API_8);
 
     $this->assertMsgID('Test message');
@@ -503,7 +523,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   public function testDrupalInfo() {
     // Parse and build the Drupal 6 module file.
-    $filename = __DIR__ . '/potx_test_6.info';
+    $filename = $this->tests_root . '/modules/potx_test_6.info';
     $this->parseFile($filename, POTX_API_6);
 
     // Look for name, description and package name extracted.
@@ -517,7 +537,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   public function testDrupalJS() {
     // Parse and build the Drupal JS file (from above Drupal 5).
-    $filename = __DIR__ . '/potx_test.js';
+    $filename = $this->tests_root . '/files/potx_test.js';
     $this->parseFile($filename, POTX_API_6);
 
     // Assert strings found in JS source code.
@@ -531,8 +551,8 @@ class PotxTestCase extends DrupalWebTestCase {
     $this->assertPluralIDContext('1 test string in JS in test context', '@count test strings in JS in test context', 'Test context');
     $this->assertPluralIDContext('1 test string in JS with context and @placeholder', '@count test strings in JS with context and @placeholder', 'Test context');
 
-    $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.'));
+    $this->assert(count($this->potx_status) == 1, '1 error message found');
+    $this->assert($this->potx_status[0][0] == $this->empty_error, 'Empty error found.');
   }
 
   /**
@@ -556,6 +576,29 @@ class PotxTestCase extends DrupalWebTestCase {
   }
 
   /**
+   * Parse the given file with the given API version.
+   */
+  private function parsePHPContent($code, $filename, $api_version, $string_mode = POTX_STRING_RUNTIME) {
+    global $_potx_store, $_potx_strings, $_potx_install;
+    $_potx_store = $_potx_strings = $_potx_install = array();
+
+    $basename = basename($filename);
+    $name_parts = pathinfo($basename);
+
+    potx_status('set', POTX_STATUS_STRUCTURED);
+    _potx_parse_php_file($code, $filename, '_potx_save_string', $name_parts, $basename, $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));
+  }
+
+  /**
    * Build the output from parsed files.
    */
   private function buildOutput($api_version, $string_mode = POTX_STRING_RUNTIME)
@@ -575,7 +618,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   private function assertMsgID($string, $message = '', $group = 'Other') {
     if (!$message) {
-      $message = t('MsgID "@raw" found', array('@raw' => check_plain($string)));
+      $message = format_string('MsgID "@raw" found', array('@raw' => $string));
     }
     $this->assert(strpos($this->potx_output, 'msgid "'. _potx_format_quoted_string('"'. $string . '"') .'"') !== FALSE, $message, $group);
   }
@@ -585,7 +628,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   private function assertNoMsgID($string, $message = '', $group = 'Other') {
     if (!$message) {
-      $message = t('MsgID "@raw" not found', array('@raw' => check_plain($string)));
+      $message = format_string('MsgID "@raw" not found', array('@raw' => $string));
     }
     $this->assert(strpos($this->potx_output, 'msgid "'. _potx_format_quoted_string('"'. $string . '"') .'"') === FALSE, $message, $group);
   }
@@ -595,7 +638,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   private function assertMsgIDContext($string, $context, $message = '', $group = 'Other') {
     if (!$message) {
-      $message = t('MsgID "@raw" in context "@context" found', array('@raw' => check_plain($string), '@context' => check_plain($context)));
+      $message = format_string('MsgID "@raw" in context "@context" found', array('@raw' => $string, '@context' => $context));
     }
     $this->assert(strpos($this->potx_output, 'msgctxt "'. _potx_format_quoted_string('"'. $context . '"') . "\"\nmsgid \"". _potx_format_quoted_string('"'. $string . '"') .'"') !== FALSE, $message, $group);
   }
@@ -605,7 +648,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   private function assertNoMsgIDContext($string, $context, $message = '', $group = 'Other') {
     if (!$message) {
-      $message = t('No MsgID "@raw" in context "@context" found', array('@raw' => check_plain($string), '@context' => check_plain($context)));
+      $message = format_string('No MsgID "@raw" in context "@context" found', array('@raw' => $string, '@context' => $context));
     }
     $this->assert(strpos($this->potx_output, 'msgid "'. _potx_format_quoted_string('"'. $string . '"') .'"'. "\nmsgctxt \"". _potx_format_quoted_string('"'. $context . '"') . '"') === FALSE, $message, $group);
   }
@@ -615,7 +658,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   private function assertPluralID($string, $plural, $message = '', $group = 'Other') {
     if (!$message) {
-      $message = t('Plural ID "@raw" found', array('@raw' => check_plain($string)));
+      $message = format_string('Plural ID "@raw" found', array('@raw' => $string));
     }
     $this->assert(strpos($this->potx_output, 'msgid "'. _potx_format_quoted_string('"'. $string . '"') ."\"\nmsgid_plural \"". _potx_format_quoted_string('"'. $plural . '"') .'"') !== FALSE, $message, $group);
   }
@@ -625,7 +668,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   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));
+      $message = format_string('Plural ID "@raw" found with context "@context"', array('@raw' => $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);
   }
@@ -635,7 +678,7 @@ class PotxTestCase extends DrupalWebTestCase {
    */
   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));
+      $message = format_string('No plural ID "@raw" found with context "@context"', array('@raw' => $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);
   }
diff --git a/tests/LanguageManager.php b/tests/files/LanguageManager.php
similarity index 100%
rename from tests/LanguageManager.php
rename to tests/files/LanguageManager.php
diff --git a/tests/TestConstraint.php b/tests/files/TestConstraint.php
similarity index 100%
rename from tests/TestConstraint.php
rename to tests/files/TestConstraint.php
diff --git a/tests/drupal8/broken_twig.html.twig b/tests/files/drupal8/broken_twig.html.twig
similarity index 100%
rename from tests/drupal8/broken_twig.html.twig
rename to tests/files/drupal8/broken_twig.html.twig
diff --git a/tests/drupal8/config/install/broken_yaml.yml b/tests/files/drupal8/config/install/broken_yaml.yml
similarity index 100%
rename from tests/drupal8/config/install/broken_yaml.yml
rename to tests/files/drupal8/config/install/broken_yaml.yml
diff --git a/tests/drupal8/config/install/test.settings.yml b/tests/files/drupal8/config/install/test.settings.yml
similarity index 100%
rename from tests/drupal8/config/install/test.settings.yml
rename to tests/files/drupal8/config/install/test.settings.yml
diff --git a/tests/drupal8/config/optional/test.settings.yml b/tests/files/drupal8/config/optional/test.settings.yml
similarity index 100%
rename from tests/drupal8/config/optional/test.settings.yml
rename to tests/files/drupal8/config/optional/test.settings.yml
diff --git a/tests/drupal8/config/schema/test.data_types.schema.yml b/tests/files/drupal8/config/schema/test.data_types.schema.yml
similarity index 100%
rename from tests/drupal8/config/schema/test.data_types.schema.yml
rename to tests/files/drupal8/config/schema/test.data_types.schema.yml
diff --git a/tests/drupal8/config/schema/test.schema.yml b/tests/files/drupal8/config/schema/test.schema.yml
similarity index 100%
rename from tests/drupal8/config/schema/test.schema.yml
rename to tests/files/drupal8/config/schema/test.schema.yml
diff --git a/tests/drupal8/config/schema/test.sequences.schema.yml b/tests/files/drupal8/config/schema/test.sequences.schema.yml
similarity index 100%
rename from tests/drupal8/config/schema/test.sequences.schema.yml
rename to tests/files/drupal8/config/schema/test.sequences.schema.yml
diff --git a/tests/drupal8/config/schema/test.variables.schema.yml b/tests/files/drupal8/config/schema/test.variables.schema.yml
similarity index 100%
rename from tests/drupal8/config/schema/test.variables.schema.yml
rename to tests/files/drupal8/config/schema/test.variables.schema.yml
diff --git a/tests/drupal8/core/config/schema/core.data_types.schema.yml b/tests/files/drupal8/core/config/schema/core.data_types.schema.yml
similarity index 100%
rename from tests/drupal8/core/config/schema/core.data_types.schema.yml
rename to tests/files/drupal8/core/config/schema/core.data_types.schema.yml
diff --git a/tests/drupal8/core/config/schema/core.entity.schema.yml b/tests/files/drupal8/core/config/schema/core.entity.schema.yml
similarity index 100%
rename from tests/drupal8/core/config/schema/core.entity.schema.yml
rename to tests/files/drupal8/core/config/schema/core.entity.schema.yml
diff --git a/tests/drupal8/core/config/schema/core.extension.schema.yml b/tests/files/drupal8/core/config/schema/core.extension.schema.yml
similarity index 100%
rename from tests/drupal8/core/config/schema/core.extension.schema.yml
rename to tests/files/drupal8/core/config/schema/core.extension.schema.yml
diff --git a/tests/drupal8/core/config/schema/core.menu.schema.yml b/tests/files/drupal8/core/config/schema/core.menu.schema.yml
similarity index 100%
rename from tests/drupal8/core/config/schema/core.menu.schema.yml
rename to tests/files/drupal8/core/config/schema/core.menu.schema.yml
diff --git a/tests/potx_test.js b/tests/files/potx_test.js
similarity index 100%
rename from tests/potx_test.js
rename to tests/files/potx_test.js
diff --git a/tests/potx_test_8/potx_test_8.test.yml b/tests/files/potx_test_8/potx_test_8.test.yml
similarity index 100%
rename from tests/potx_test_8/potx_test_8.test.yml
rename to tests/files/potx_test_8/potx_test_8.test.yml
diff --git a/tests/potx_test_8/yaml_translation_patterns.yml b/tests/files/potx_test_8/yaml_translation_patterns.yml
similarity index 100%
rename from tests/potx_test_8/yaml_translation_patterns.yml
rename to tests/files/potx_test_8/yaml_translation_patterns.yml
diff --git a/tests/potx_test_yml/broken_yaml.yml b/tests/files/potx_test_yml/broken_yaml.yml
similarity index 100%
rename from tests/potx_test_yml/broken_yaml.yml
rename to tests/files/potx_test_yml/broken_yaml.yml
diff --git a/tests/potx_test_yml/potx_test_8.test2.yml b/tests/files/potx_test_yml/potx_test_8.test2.yml
similarity index 100%
rename from tests/potx_test_yml/potx_test_8.test2.yml
rename to tests/files/potx_test_yml/potx_test_8.test2.yml
diff --git a/tests/potx_test_yml/test_folder/potx_test_8.test3.yml b/tests/files/potx_test_yml/test_folder/potx_test_8.test3.yml
similarity index 100%
rename from tests/potx_test_yml/test_folder/potx_test_8.test3.yml
rename to tests/files/potx_test_yml/test_folder/potx_test_8.test3.yml
diff --git a/tests/potx_test_yml/yaml_translation_patterns.yml b/tests/files/potx_test_yml/yaml_translation_patterns.yml
similarity index 100%
rename from tests/potx_test_yml/yaml_translation_patterns.yml
rename to tests/files/potx_test_yml/yaml_translation_patterns.yml
diff --git a/tests/potx_test_5.module b/tests/modules/potx_test_5.module
similarity index 100%
rename from tests/potx_test_5.module
rename to tests/modules/potx_test_5.module
diff --git a/tests/potx_test_6.info b/tests/modules/potx_test_6.info
similarity index 100%
rename from tests/potx_test_6.info
rename to tests/modules/potx_test_6.info
diff --git a/tests/potx_test_6.module b/tests/modules/potx_test_6.module
similarity index 100%
rename from tests/potx_test_6.module
rename to tests/modules/potx_test_6.module
diff --git a/tests/potx_test_7.module b/tests/modules/potx_test_7.module
similarity index 94%
rename from tests/potx_test_7.module
rename to tests/modules/potx_test_7.module
index fded28c..7dd96a8 100644
--- a/tests/potx_test_7.module
+++ b/tests/modules/potx_test_7.module
@@ -52,9 +52,6 @@ function potx_test_7_page() {
   $t('Dynamic string in context', array(), array('context' => 'Dynamic context'));
 
   dt('This could have been in a drush file');
-
-  t('Oh god why would @you omit a parenthesis?', array('@you' => 'fool');
-  t('PHP Syntax error gracefully handled');
 }
 
 function potx_test_7_perm() {
diff --git a/tests/potx_test_8.breakpoints.yml b/tests/modules/potx_test_8.breakpoints.yml
similarity index 100%
rename from tests/potx_test_8.breakpoints.yml
rename to tests/modules/potx_test_8.breakpoints.yml
diff --git a/tests/potx_test_8.html.twig b/tests/modules/potx_test_8.html.twig
similarity index 100%
rename from tests/potx_test_8.html.twig
rename to tests/modules/potx_test_8.html.twig
diff --git a/tests/potx_test_8.info.yml b/tests/modules/potx_test_8.info.yml
similarity index 100%
rename from tests/potx_test_8.info.yml
rename to tests/modules/potx_test_8.info.yml
diff --git a/tests/potx_test_8.links.action.yml b/tests/modules/potx_test_8.links.action.yml
similarity index 100%
rename from tests/potx_test_8.links.action.yml
rename to tests/modules/potx_test_8.links.action.yml
diff --git a/tests/potx_test_8.links.contextual.yml b/tests/modules/potx_test_8.links.contextual.yml
similarity index 100%
rename from tests/potx_test_8.links.contextual.yml
rename to tests/modules/potx_test_8.links.contextual.yml
diff --git a/tests/potx_test_8.links.menu.yml b/tests/modules/potx_test_8.links.menu.yml
similarity index 100%
rename from tests/potx_test_8.links.menu.yml
rename to tests/modules/potx_test_8.links.menu.yml
diff --git a/tests/potx_test_8.links.task.yml b/tests/modules/potx_test_8.links.task.yml
similarity index 100%
rename from tests/potx_test_8.links.task.yml
rename to tests/modules/potx_test_8.links.task.yml
diff --git a/tests/potx_test_8.module.txt b/tests/modules/potx_test_8.module
similarity index 94%
rename from tests/potx_test_8.module.txt
rename to tests/modules/potx_test_8.module
index f97e69d..d0ef0c3 100644
--- a/tests/potx_test_8.module.txt
+++ b/tests/modules/potx_test_8.module
@@ -1,13 +1,6 @@
 <?php
 
 /**
- * @file
- *   File used purely to test the parser in potx. This is file has a .txt
- *   extension so that it does not fail linting tests run by continuous
- *   integration.
- */
-
-/**
  * @TestAnnotation(
  *   id = "test_annotation",
  *   test_label = @Translation("Good translation annotation"),
@@ -22,7 +15,7 @@ function potx_test_8_example() {
   // Test with short format array styles mixed with other tokens.
   $a = new TranslatableMarkup('TranslatableMarkup string');
   $c = new TranslatableMarkup('TranslatableMarkup string without context', []);
-  $d = new TranslatableMarkup('TranslatableMarkup string with long array context', [], array('context' => 'With context']));
+  $d = new TranslatableMarkup('TranslatableMarkup string with long array context', [], array('context' => 'With context'));
   $d = new TranslatableMarkup('TranslatableMarkup string with short array context', [], ['context' => 'With context']);
   $e = new TranslatableMarkup('TranslatableMarkup string with long array followed by short array context', array(), ['context' => 'With context']);
   $f = new TranslatableMarkup('TranslatableMarkup string with complicated tokens', array([], 2, potx_test_8_sample(), array(), [[]]), ['context' => 'With context']);
diff --git a/tests/potx_test_8.permissions.yml b/tests/modules/potx_test_8.permissions.yml
similarity index 100%
rename from tests/potx_test_8.permissions.yml
rename to tests/modules/potx_test_8.permissions.yml
diff --git a/tests/potx_test_8.routing.yml b/tests/modules/potx_test_8.routing.yml
similarity index 100%
rename from tests/potx_test_8.routing.yml
rename to tests/modules/potx_test_8.routing.yml
