You are here

i18n_string.pages.inc in Internationalization 7

Internationalization (i18n) package - translatable strings reusable admin UI.

@author Jose A. Reyero, 2007

File

i18n_string/i18n_string.pages.inc
View source
<?php

/**
 * @file
 * Internationalization (i18n) package - translatable strings reusable admin UI.
 *
 * @author Jose A. Reyero, 2007
 */

// Load locale libraries
include_once DRUPAL_ROOT . '/includes/locale.inc';
include_once drupal_get_path('module', 'locale') . '/locale.admin.inc';

/**
 * Generate translate page from object.
 *
 * @param string $object_type
 *   Obejct type as declared in hook_i18n_object_info().
 * @param object $object_value
 *   Drupal object to translate.
 * @param object $language
 *   Optional language object.
 */
function i18n_string_translate_page_object($object_type, $object_value, $language = NULL) {

  // For backwards compatibility, ensure parameter is a language object
  $language = i18n_language_object($language);
  $langcode = $language ? $language->language : NULL;

  // Get base keys for all these strings. Object key may be multiple like for blocks (module, delta)
  $object = i18n_object($object_type, $object_value);
  $strings = $object
    ->get_strings(array(
    'empty' => TRUE,
  ));

  // If no localizable strings, print message and fail gracefully.
  // Possibly this object comes from some other contrib module.
  // See http://drupal.org/node/1889878
  if (!$strings) {
    return t('This object has no strings available for translation.');
  }
  if (empty($langcode)) {
    drupal_set_title(t('Translate !name', array(
      '!name' => i18n_object_info($object_type, 'title'),
    )));
    return i18n_string_translate_page_overview($object, $strings);
  }
  else {
    drupal_set_title(t('Translate to !language', array(
      '!language' => i18n_language_name($langcode),
    )));
    return drupal_get_form('i18n_string_translate_page_form', $strings, $langcode);
  }
}

/**
 * Provide a core translation module like overview page for this object.
 */
function i18n_string_translate_page_overview($object, $strings) {
  $build['i18n_overview'] = drupal_get_form('i18n_string_translate_page_overview_form', $object, $strings);
  return $build;
}

/**
 * Provide a core translation module like overview page for this object.
 */
function i18n_string_translate_page_overview_form($form, &$form_state, $object, $strings) {

  // Set the default item key, assume it's the first.
  $item_title = reset($strings);
  $header = array(
    'language' => t('Language'),
    'title' => t('Title'),
    'status' => t('Status'),
    'operations' => t('Operations'),
  );
  $source_language = variable_get_value('i18n_string_source_language');
  $rows = array();
  foreach (language_list() as $langcode => $language) {
    if ($langcode == $source_language) {
      $items = array(
        'language' => check_plain($language->name) . ' ' . t('(source)'),
        'title' => check_plain($item_title
          ->get_string()),
        'status' => t('original'),
        'operations' => l(t('edit'), $object
          ->get_edit_path()),
      );
    }
    else {

      // Try to figure out if this item has any of its properties translated.
      $translated = FALSE;
      foreach ($strings as $i18nstring) {
        if ($i18nstring
          ->get_translation($langcode)) {
          $translated = TRUE;
          break;
        }
      }

      // Translate the item that was requested to be displayed as title.
      $items = array(
        'language' => check_plain($language->name),
        'title' => $item_title
          ->format_translation($langcode, array(
          'sanitize default' => TRUE,
        )),
        'status' => $translated ? t('translated') : t('not translated'),
        'operations' => l(t('translate'), $object
          ->get_translate_path($langcode), array(
          'query' => drupal_get_destination(),
        )),
      );
    }
    foreach ($items as $key => $markup) {
      $rows[$langcode][$key] = $markup;

      //$form['#rows'][$langcode][$key]['#markup'] = $markup;
    }
  }

  // Build a form so it can be altered later, with all this information.
  $form['object'] = array(
    '#type' => 'value',
    '#value' => $object,
  );
  $form['source_language'] = array(
    '#type' => 'value',
    '#value' => $source_language,
  );
  $form['languages'] = array(
    '#header' => $header,
    '#rows' => $rows,
    '#theme' => 'table',
  );
  return $form;
}

/**
 * Form builder callback for in-place string translation.
 *
 * @param $strings
 *   Array of strings indexed by string name (may be indexed by group key too if $groups is present)
 * @param $langcode
 *   Language code to translate to.
 * @param $groups
 *   Optional groups to provide some string grouping. Array with group key and title pairs.
 */
function i18n_string_translate_page_form($form, &$form_state, $strings, $langcode, $groups = NULL) {
  $form = i18n_string_translate_page_form_base($form, $langcode);
  if ($groups) {

    // I we've got groups, string list is grouped by group key.
    $form['string_groups'] = array(
      '#type' => 'value',
      '#value' => $strings,
    );
    foreach ($groups as $key => $title) {
      $form['display'] = array(
        '#type' => 'vertical_tabs',
      );
      $form['strings'][$key] = array(
        '#group' => 'display',
        '#title' => $title,
        '#type' => 'fieldset',
      ) + i18n_string_translate_page_form_strings($strings[$key], $langcode);
    }
  }
  else {

    // We add a single group with key 'all', but no tabs.
    $form['string_groups'] = array(
      '#type' => 'value',
      '#value' => array(
        'all' => $strings,
      ),
    );
    $form['strings']['all'] = i18n_string_translate_page_form_strings($strings, $langcode);
  }
  return $form;
}

/**
 * Create base form for string translation
 */
function i18n_string_translate_page_form_base($form, $langcode, $redirect = NULL) {
  $form['langcode'] = array(
    '#type' => 'value',
    '#value' => $langcode,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save translation'),
    '#weight' => 10,
  );
  if ($redirect) {
    $form['#redirect'] = array(
      $redirect,
    );
  }

  // Add explicit validate and submit hooks so this can be used from inside any form.
  $form['#submit'] = array(
    'i18n_string_translate_page_form_submit',
  );
  return $form;
}

/**
 * Create field elements for strings
 *
 * @param i18n_string_object[] $strings
 * @param string $langcode
 *
 * @return array
 */
function i18n_string_translate_page_form_strings($strings, $langcode) {
  global $user;
  $form = array();
  foreach ($strings as $item) {

    // Check permissions to translate this string, depends on format, etc..
    if ($message = $item
      ->check_translate_access()) {

      // We'll display a disabled element with the reason it cannot be translated.
      $disabled = TRUE;
      $description = $message;
    }
    else {
      $disabled = FALSE;
      $description = '';

      // If we don't have a source and it can be translated, we create it.
      if (!$item
        ->get_source()) {

        // Enable messages just as a reminder these strings are not being updated properly.
        $status = $item
          ->update(array(
          'messages' => TRUE,
        ));
        if ($status === FALSE || $status === SAVED_DELETED) {

          // We don't have a source string so nothing to translate here
          $disabled = TRUE;
        }
      }
    }
    $default_value = $item
      ->format_translation($langcode, array(
      'langcode' => $langcode,
      'sanitize' => FALSE,
      'debug' => FALSE,
    ));
    $available_formats = array_keys(filter_formats($user));
    if (!in_array($item->format, $available_formats)) {
      $item->format = NULL;
    }
    $form[$item
      ->get_name()] = array(
      '#title' => $item
        ->get_title(),
      '#type' => $item->format ? 'text_format' : 'textarea',
      '#default_value' => $default_value,
      '#format' => $item->format,
      // This will trigger i18n_string_pre_render_text_format() to actually
      // alter the element.
      '#i18n_string_is_translation' => TRUE,
      '#disabled' => $disabled,
      '#description' => $description,
      // If disabled, provide smaller textarea (that can be expanded anyway).
      '#rows' => $disabled ? 1 : min(ceil(str_word_count($default_value) / 12), 10),
      // Change the parent for disabled strings so we don't get empty values later
      '#parents' => array(
        $disabled ? 'disabled_strings' : 'strings',
        $item
          ->get_name(),
      ),
    );
  }
  return $form;
}

/**
 * Form submission callback for in-place string translation.
 */
function i18n_string_translate_page_form_submit($form, &$form_state) {
  $count = $success = 0;
  foreach ($form_state['values']['strings'] as $name => $value) {
    $count++;
    list($textgroup, $context) = i18n_string_context(explode(':', $name));
    if (is_array($value)) {
      if (isset($value['value'])) {
        $value = $value['value'];
        $form_state['values']['strings'][$name] = $value;
      }
      else {
        form_set_error("strings][{$name}", t('Unable to get the translated string value.'));
        watchdog('locale', 'Unable to get the translated string value, string array is: %string', array(
          '%string' => var_dump($value),
        ), WATCHDOG_WARNING);
      }
    }
    $result = i18n_string_textgroup($textgroup)
      ->update_translation($context, $form_state['values']['langcode'], $value);
    $success += $result ? 1 : 0;
  }
  if ($success) {
    drupal_set_message(format_plural($success, 'A translation was saved successfully.', '@count translations were saved successfully.'));
  }
  if ($error = $count - $success) {
    drupal_set_message(format_plural($error, 'A translation could not be saved.', '@count translations could not be saved.'), 'warning');
  }
  if (isset($form['#redirect'])) {
    $form_state['redirect'] = $form['#redirect'];
  }
}

/**
 * Menu callback. Saves a string translation coming as POST data.
 */
function i18n_string_l10n_client_save_string() {
  global $user, $language;
  if (user_access('use on-page translation')) {
    $textgroup = !empty($_POST['textgroup']) ? $_POST['textgroup'] : 'default';

    // Other textgroups will be handled by l10n_client module
    if (!i18n_string_group_info($textgroup)) {
      return l10n_client_save_string();
    }
    elseif (isset($_POST['source']) && isset($_POST['target']) && !empty($_POST['context']) && !empty($_POST['form_token']) && drupal_valid_token($_POST['form_token'], 'l10n_client_form')) {
      $name = $textgroup . ':' . $_POST['context'];
      if ($i18nstring = i18n_string_get_source($name)) {

        // Since this is not a real form, we double check access permissions here too.
        if ($error = $i18nstring
          ->check_translate_access()) {
          $message = theme('l10n_client_message', array(
            'message' => t('Not saved due to: !reason', array(
              '!reason' => $error,
            )),
            'level' => WATCHDOG_WARNING,
          ));
        }
        else {
          $result = i18n_string_translation_update($name, $_POST['target'], $language->language, $_POST['source']);
          if ($result) {
            $message = theme('l10n_client_message', array(
              'message' => t('Translation saved locally for user defined string.'),
              'level' => WATCHDOG_INFO,
            ));
          }
          elseif ($result === FALSE) {
            $message = theme('l10n_client_message', array(
              'message' => t('Not saved due to insufficient permissions.'),
            ));
          }
          else {
            $message = theme('l10n_client_message', array(
              'message' => t('Not saved due to unknown reason.'),
            ));
          }
        }
      }
      else {
        $message = theme('l10n_client_message', array(
          'message' => t('Not saved due to source string missing.'),
        ));
      }
    }
    else {
      $message = theme('l10n_client_message', array(
        'message' => t('Not saved due to missing form values.'),
      ));
    }
    drupal_json_output($message);
    exit;
  }
}

/**
 * User interface for string editing.
 */
function i18n_string_locale_translate_edit_form($form, &$form_state, $lid) {

  // Fetch source string, if possible.
  $source = db_query('SELECT source, context, textgroup, location FROM {locales_source} WHERE lid = :lid', array(
    ':lid' => $lid,
  ))
    ->fetchObject();
  if (!$source) {
    drupal_set_message(t('String not found.'), 'error');
    drupal_goto('admin/config/regional/translate/translate');
  }

  // Add original text to the top and some values for form altering.
  $form['original'] = array(
    '#type' => 'item',
    '#title' => t('Original text'),
    '#markup' => check_plain(wordwrap($source->source, 0)),
  );
  if (!empty($source->context)) {
    $form['context'] = array(
      '#type' => 'item',
      '#title' => t('Context'),
      '#markup' => check_plain($source->context),
    );
  }
  $form['lid'] = array(
    '#type' => 'value',
    '#value' => $lid,
  );
  $form['textgroup'] = array(
    '#type' => 'value',
    '#value' => $source->textgroup,
  );
  $form['location'] = array(
    '#type' => 'value',
    '#value' => $source->location,
  );

  // Include default form controls with empty values for all languages.
  // This ensures that the languages are always in the same order in forms.
  $languages = language_list();

  // We don't need the default language value, that value is in $source.
  $omit = $source->textgroup == 'default' ? 'en' : i18n_string_source_language();
  unset($languages[$omit]);
  $form['translations'] = array(
    '#tree' => TRUE,
  );

  // Approximate the number of rows to use in the default textarea.
  $rows = min(ceil(str_word_count($source->source) / 12), 10);
  foreach ($languages as $langcode => $language) {
    $form['translations'][$langcode] = array(
      '#type' => 'textarea',
      '#title' => t($language->name),
      '#rows' => $rows,
      '#default_value' => '',
    );
  }

  // Fetch translations and fill in default values in the form.
  $result = db_query("SELECT DISTINCT translation, language FROM {locales_target} WHERE lid = :lid AND language <> :omit", array(
    ':lid' => $lid,
    ':omit' => $omit,
  ));
  foreach ($result as $translation) {
    $form['translations'][$translation->language]['#default_value'] = $translation->translation;
  }
  $form['actions'] = array(
    '#type' => 'actions',
  );
  $form['actions']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save translations'),
  );

  // Restrict filter permissions and handle validation and submission for i18n strings.
  if (i18n_string_group_info($source->textgroup)) {
    if ($i18nstring = i18n_string_get_by_lid($form['lid']['#value'])) {
      $form['i18n_string'] = array(
        '#type' => 'value',
        '#value' => $i18nstring,
      );
      if ($message = $i18nstring
        ->check_translate_access()) {
        drupal_set_message($message);
        $disabled = TRUE;
      }

      // Add format help anyway, though the form may be disabled.
      $form['translations']['format_help']['#markup'] = _i18n_string_translate_format_help($i18nstring->format);
    }
    else {
      drupal_set_message(t('Source string not found.'), 'warning');
      $disabled = TRUE;
    }
    if (!empty($disabled)) {

      // Disable all form elements
      $form['submit']['#disabled'] = TRUE;
      foreach (element_children($form['translations']) as $langcode) {
        $form['translations'][$langcode]['#disabled'] = TRUE;
      }
    }
  }
  return $form;
}

/**
 * Process string editing form validations.
 *
 * If it is an allowed format, skip default validation, the text will be filtered later
 */
function i18n_string_locale_translate_edit_form_validate($form, &$form_state) {
  if (empty($form_state['values']['i18n_string'])) {

    // If not i18n string use regular locale validation.
    $copy_state = $form_state;
    locale_translate_edit_form_validate($form, $copy_state);
  }
}

/**
 * Process string editing form submissions.
 *
 * Mark translations as current.
 */
function i18n_string_locale_translate_edit_form_submit($form, &$form_state) {

  // Invoke locale submission.
  locale_translate_edit_form_submit($form, $form_state);
  $lid = $form_state['values']['lid'];
  if ($i18n_string_object = i18n_string_get_by_lid($lid)) {
    $i18n_string_object
      ->cache_reset();
  }
  foreach ($form_state['values']['translations'] as $key => $value) {
    if (!empty($value)) {

      // An update has been made, so we assume the translation is now current.
      db_update('locales_target')
        ->fields(array(
        'i18n_status' => I18N_STRING_STATUS_CURRENT,
      ))
        ->condition('lid', $lid)
        ->condition('language', $key)
        ->execute();
    }
  }
}

/**
 * Help for text format.
 */
function _i18n_string_translate_format_help($format_id) {
  $output = '';
  if ($format = filter_format_load($format_id)) {
    $title = t('Text format: @name', array(
      '@name' => $format->name,
    ));
    $tips = theme('filter_tips', array(
      'tips' => _filter_tips($format_id, FALSE),
    ));
  }
  elseif ($format_id == I18N_STRING_FILTER_XSS) {
    $title = t('Standard XSS filter.');
    $allowed_html = '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>';
    $tips[] = t('Allowed HTML tags: @tags', array(
      '@tags' => $allowed_html,
    ));
  }
  elseif ($format_id == I18N_STRING_FILTER_XSS_ADMIN) {
    $title = t('Administration XSS filter.');
    $tips[] = t('It will allow most HTML tags but not scripts nor styles.');
  }
  elseif ($format_id) {
    $title = t('Unknown filter: @name', array(
      '@name' => $format_id,
    ));
  }
  if (!empty($title)) {
    $output .= '<h5>' . $title . '</h5>';
  }
  if (!empty($tips)) {
    $output .= is_array($tips) ? theme('item_list', array(
      'items' => $tips,
    )) : $tips;
  }
  return $output;
}

/**
 * String search & translate screen.
 *
 * Almost exactly the same as the core locale module's implementation, but
 * taking i18n_string_source_language into account for the languages column.
 */
function i18n_string_locale_translate_seek_screen() {

  // Add CSS.
  drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css');
  $elements = drupal_get_form('locale_translation_filter_form');
  $output = drupal_render($elements);
  $output .= _i18n_string_locale_translate_seek();
  return $output;
}

/**
 * Perform a string search and display results in a table
 */
function _i18n_string_locale_translate_seek() {
  $output = '';

  // We have at least one criterion to match
  if (!($query = _locale_translate_seek_query())) {
    $query = array(
      'translation' => 'all',
      'group' => 'all',
      'language' => 'all',
      'string' => '',
    );
  }
  $sql_query = db_select('locales_source', 's');
  $limit_language = NULL;
  if ($query['language'] != 'en' && $query['language'] != 'all') {
    $sql_query
      ->leftJoin('locales_target', 't', "t.lid = s.lid AND t.language = :langcode", array(
      ':langcode' => $query['language'],
    ));
    $limit_language = $query['language'];
  }
  else {
    $sql_query
      ->leftJoin('locales_target', 't', 't.lid = s.lid');
  }
  $sql_query
    ->fields('s', array(
    'source',
    'location',
    'context',
    'lid',
    'textgroup',
  ));
  $sql_query
    ->fields('t', array(
    'translation',
    'language',
  ));

  // Compute LIKE section.
  switch ($query['translation']) {
    case 'translated':
      $sql_query
        ->condition('t.translation', '%' . db_like($query['string']) . '%', 'LIKE');
      $sql_query
        ->orderBy('t.translation', 'DESC');
      break;
    case 'untranslated':
      $sql_query
        ->condition(db_and()
        ->condition('s.source', '%' . db_like($query['string']) . '%', 'LIKE')
        ->isNull('t.translation'));
      $sql_query
        ->orderBy('s.source');
      break;
    case 'all':
    default:
      $condition = db_or()
        ->condition('s.source', '%' . db_like($query['string']) . '%', 'LIKE');
      if ($query['language'] != 'en') {

        // Only search in translations if the language is not forced to English.
        $condition
          ->condition('t.translation', '%' . db_like($query['string']) . '%', 'LIKE');
      }
      $sql_query
        ->condition($condition);
      break;
  }

  // Add a condition on the text group.
  if (!empty($query['group']) && $query['group'] != 'all') {
    $sql_query
      ->condition('s.textgroup', $query['group']);
  }
  $sql_query = $sql_query
    ->extend('PagerDefault')
    ->limit(50);
  $locales = $sql_query
    ->execute();
  $groups = module_invoke_all('locale', 'groups');
  $header = array(
    t('Text group'),
    t('String'),
    t('Context'),
    $limit_language ? t('Language') : t('Languages'),
    array(
      'data' => t('Operations'),
      'colspan' => '2',
    ),
  );
  $strings = array();
  foreach ($locales as $locale) {
    if (!isset($strings[$locale->lid])) {
      $strings[$locale->lid] = array(
        'group' => $locale->textgroup,
        'languages' => array(),
        'location' => $locale->location,
        'source' => $locale->source,
        'context' => $locale->context,
      );
    }
    if (isset($locale->language)) {
      $strings[$locale->lid]['languages'][$locale->language] = $locale->translation;
    }
  }
  $rows = array();
  foreach ($strings as $lid => $string) {
    $rows[] = array(
      $groups[$string['group']],
      array(
        'data' => check_plain(truncate_utf8($string['source'], 150, FALSE, TRUE)) . '<br /><small>' . $string['location'] . '</small>',
      ),
      $string['context'],
      array(
        'data' => _i18n_string_locale_translate_language_list($string, $limit_language),
        'align' => 'center',
      ),
      array(
        'data' => l(t('edit'), "admin/config/regional/translate/edit/{$lid}", array(
          'query' => drupal_get_destination(),
        )),
        'class' => array(
          'nowrap',
        ),
      ),
      array(
        'data' => l(t('delete'), "admin/config/regional/translate/delete/{$lid}", array(
          'query' => drupal_get_destination(),
        )),
        'class' => array(
          'nowrap',
        ),
      ),
    );
  }
  $output .= theme('table', array(
    'header' => $header,
    'rows' => $rows,
    'empty' => t('No strings available.'),
  ));
  $output .= theme('pager');
  return $output;
}

/**
 * List languages in search result table.
 */
function _i18n_string_locale_translate_language_list($string, $limit_language) {

  // Add CSS.
  drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css');

  // Include both translated and not yet translated target languages in the
  // list. The source language is English for built-in strings and the default
  // language for other strings.
  $languages = language_list();
  $default = language_default();
  $omit = $string['group'] == 'default' ? 'en' : variable_get('i18n_string_source_language', $default->language);
  unset($languages[$omit]);
  $output = '';
  foreach ($languages as $langcode => $language) {
    if (!$limit_language || $limit_language == $langcode) {
      $output .= !empty($string['languages'][$langcode]) ? $langcode . ' ' : "<em class=\"locale-untranslated\">{$langcode}</em> ";
    }
  }
  return $output;
}

Functions

Namesort descending Description
i18n_string_l10n_client_save_string Menu callback. Saves a string translation coming as POST data.
i18n_string_locale_translate_edit_form User interface for string editing.
i18n_string_locale_translate_edit_form_submit Process string editing form submissions.
i18n_string_locale_translate_edit_form_validate Process string editing form validations.
i18n_string_locale_translate_seek_screen String search & translate screen.
i18n_string_translate_page_form Form builder callback for in-place string translation.
i18n_string_translate_page_form_base Create base form for string translation
i18n_string_translate_page_form_strings Create field elements for strings
i18n_string_translate_page_form_submit Form submission callback for in-place string translation.
i18n_string_translate_page_object Generate translate page from object.
i18n_string_translate_page_overview Provide a core translation module like overview page for this object.
i18n_string_translate_page_overview_form Provide a core translation module like overview page for this object.
_i18n_string_locale_translate_language_list List languages in search result table.
_i18n_string_locale_translate_seek Perform a string search and display results in a table
_i18n_string_translate_format_help Help for text format.