<?php
// $Id: potx.module,v 1.1.2.12.2.2.2.10.4.2 2009/08/24 07:33:08 goba Exp $

/**
 * @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) {

  // 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);
  }
}
