potx.module

  1. potx
    1. 5
    2. 6.3
    3. 6.2
    4. 6

Gettext translation template and translation 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.

Functions & methods

NameDescription
potx_coder_reviewCallback implementation for coder review of one file.
potx_helpImplementation of hook_help().
potx_menuImplementation of hook_menu().
potx_reviewsImplementation of hook_reviews(). Coder module integration.
potx_select_formComponent selection interface.
potx_select_form_submitGenerate translation template or translation file for the requested component.
potx_select_form_validateValidation handler for potx component selection form.
_potx_component_listGenerate a hierarchical structured list of components.
_potx_component_selectorBuild a chunk of the component selection form.
_potx_form_idGenerate a sane form element ID for the current radio button.
View source
<?php
// $Id$

/**
 * @file
 *   Gettext translation template and translation 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.
 */

/**
 * Implementation of hook_help().
 */
function potx_help($path, $arg) {
  switch ($path) {
    case 'admin/build/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>';
  }
}

/**
 * Implementation of hook_menu().
 */
function potx_menu() {
  $items['admin/build/translate/extract'] = array(
    'title' => 'Extract',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('potx_select_form'),
    'access arguments' => array('translate interface'),
    'weight' => 200,
    'type' => MENU_LOCAL_TASK,
  );
  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.
 *
 * @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");
  while ($component = db_fetch_object($result)) {
    // 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.
 */
function potx_reviews() {
  $review = array(
    '#title' => t('Interface text translatability'),
    '#link' => 'http://drupal.org/translators',
    '#rules' => array(
      array(
        '#type' => 'callback',
        '#value' => 'potx_coder_review',
      ),
    )
  );
  return array('potx' => $review);
}

/**
 * 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).
  $filename = realpath($coder_args['#filename']);
  _potx_process_file($filename);
  // Grab the errors and empty the error list.
  $errors = potx_status('get', TRUE);

  $severity_name = _coder_severity_name($coder_args, $review, $rule);
  foreach ($errors as $error) {
    // Errors contain the message, file name (which we did not use here), in
    // 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;
    if (empty($lineno)) {
      // $lineno might be NULL so set to 0.
      $lineno = 0;
      $line = '';
    }
    else {
      // potx numbers lines from 1 (as in text editors),
      // coder from 0 (as in the array index I guess).
      $lineno--;
      $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>.') : '');
    _coder_error($results, $rule, $severity_name, $lineno, $line);
  }
}