You are here

l10n_client_contributor.module in Localization client 8

Submits translations to a remote localization server.

File

l10n_client_contributor/l10n_client_contributor.module
View source
<?php

/**
 * @file
 * Submits translations to a remote localization server.
 */
use Drupal\Component\Gettext\PoItem;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\user\UserInterface;
use Drupal\user\Entity\User;

/**
 * Implements hook_help().
 */
function l10n_client_contributor_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.l10n_client_contributor':
      $output = '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('Localization Client Contributor allows users to contribute translations to the community from the convenience of their site. The community server is configured globally while each user needs access on the server and their own API key configured locally to identify with the server for contribution to happen.') . '</p>';
      $output .= '<p>' . t('The default community server is <a href=":server">:server</a> and may be changed on <a href=":configure">the configuration page</a>.', array(
        ':server' => 'https://localize.drupal.org/',
        ':configure' => Url::fromRoute('locale.settings'),
      )) . '</p>';
      $output .= '<p>' . t('Users need the <em>Contribute translations to localization server</em> permission to contribute to the community server as well as their personal API key configured on their user profile.') . '</p>';
      $output .= '<p>' . t('To contribute to a community server, permissions are required on the server side as well. <a href=":server">To contribute to :server, see the <em>How to contribute</em> section there.</a>', array(
        ':server' => 'https://localize.drupal.org/',
      )) . '</p>';
      return $output;
    case 'locale.translate_page':

      /** @var \Drupal\user\UserInterface $account */
      $config = \Drupal::config('l10n_client_contributor.settings');
      if (!$config
        ->get('use_server')) {
        return;
      }
      $account = User::load(\Drupal::currentUser()
        ->id());
      if ($account
        ->hasPermission('contribute translations to localization server')) {
        if ($account
          ->hasField('l10n_client_contributor_key')) {
          if ($account
            ->get('l10n_client_contributor_key')
            ->isEmpty()) {
            \Drupal::messenger()
              ->addWarning(t('<a href=":edit">Set your personal API key</a> to contribute translations automatically to :server. Thanks for contributing!', array(
              ':edit' => Url::fromRoute('entity.user.edit_form', array(
                'user' => $account
                  ->id(),
              ))
                ->toString(),
              ':server' => $config
                ->get('server'),
            )));
          }
          else {
            return '<p><strong>' . t('All changes made will be automatically submitted to :server too. Thanks for contributing!', array(
              ':server' => $config
                ->get('server'),
            )) . '</strong></p>';
          }
        }
        else {
          \Drupal::messenger()
            ->addWarning(t('The Localization Client Contributor module was not properly installed. The contributor API key field is missing from user accounts. Translation submissions will not be saved remotely.'));
        }
      }
      break;
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function l10n_client_contributor_form_locale_translate_settings_alter(&$form, FormStateInterface $form_state, $form_id) {
  $config = \Drupal::configFactory()
    ->getEditable('l10n_client_contributor.settings');
  $form['l10n_client_contributor_use_server'] = array(
    '#title' => t('Enable sharing translation modifications with server'),
    '#type' => 'checkbox',
    '#default_value' => $config
      ->get('use_server'),
  );
  $form['l10n_client_contributor_server'] = array(
    '#title' => t('Address of localization server to use'),
    '#type' => 'textfield',
    '#description' => t('Each local translation submission or change will also be submitted to this server. We suggest you enter <a href="@localize">https://localize.drupal.org/</a> to share with the greater Drupal community. Make sure you set up an API-key in the user account settings for each user that will participate in the translations.', array(
      '@localize' => 'https://localize.drupal.org/',
    )),
    '#default_value' => $config
      ->get('server'),
    '#states' => array(
      'visible' => array(
        ':input[name=l10n_client_contributor_use_server]' => array(
          'checked' => TRUE,
        ),
      ),
    ),
  );
  $form['#validate'][] = 'l10n_client_contributor_form_locale_translate_settings_validate';
  $form['#submit'][] = 'l10n_client_contributor_form_locale_translate_settings_submit';
}

/**
 * Validation for added settings on localization client UI settings form.
 */
function l10n_client_contributor_form_locale_translate_settings_validate($form, FormStateInterface $form_state) {

  // Verify connection with the server if enabled.
  if ($form_state
    ->getValue('l10n_client_contributor_use_server')) {
    if (!$form_state
      ->isValueEmpty('l10n_client_contributor_server')) {

      // Try to invoke the remote string submission with a test request.
      $response = xmlrpc($form_state
        ->getValue('l10n_client_contributor_server') . '/xmlrpc.php', array(
        'l10n.server.test' => array(
          '2.0',
        ),
      ));
      if ($response && !empty($response['name']) && !empty($response['version'])) {
        if (empty($response['supported']) || !$response['supported']) {
          $form_state
            ->setErrorByName('l10n_client_contributor_server', t('The given server could not handle the v2.0 remote submission API.'));
        }
        else {
          \Drupal::messenger()
            ->addMessage(t('Verified that the specified server can handle remote string submissions. Supported languages: %languages.', array(
            '%languages' => $response['languages'],
          )));
        }
      }
      else {
        $form_state
          ->setErrorByName('l10n_client_contributor_server', t('Invalid localization server address specified. Make sure you specified the right server address.'));
      }
    }
    else {
      $form_state
        ->setErrorByName('l10n_client_contributor_server', t('You should provide a server address, such as https://localize.drupal.org/'));
    }
  }
}

/**
 * Submission function for additional settings on localization client settings.
 */
function l10n_client_contributor_form_locale_translate_settings_submit($form, FormStateInterface $form_state) {
  \Drupal::configFactory()
    ->getEditable('l10n_client_contributor.settings')
    ->set('use_server', $form_state
    ->getValue('l10n_client_contributor_use_server'))
    ->set('server', $form_state
    ->getValue('l10n_client_contributor_server'))
    ->save();
}

/**
 * Implements hook_entity_field_access().
 */
function l10n_client_contributor_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {

  // Never allow viewing the API key on an entity.
  if ($field_definition
    ->getFieldStorageDefinition()
    ->getType() == 'l10n_client_contributor_key' && $operation == 'view') {
    return AccessResult::forbidden();
  }
  return AccessResult::neutral();
}

/**
 * Get user based semi unique token. Ensure keys are unique for each client.
 */
function l10n_client_contributor_user_token(UserInterface $account) {
  $key = \Drupal::service('private_key')
    ->get();
  return md5('l10n_client_contributor' . $account
    ->id() . $key);
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function l10n_client_contributor_form_locale_translate_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $config = \Drupal::config('l10n_client_contributor.settings');
  if ($config
    ->get('use_server')) {

    /** @var \Drupal\user\UserInterface $account */
    $account = User::load(\Drupal::currentUser()
      ->id());
    if ($account
      ->hasPermission('contribute translations to localization server') && $account
      ->hasField('l10n_client_contributor_key') && !$account
      ->get('l10n_client_contributor_key')
      ->isEmpty() && isset($form['actions']['submit'])) {

      // Make the button more expressive of what is happening.
      $form['actions']['submit']['#value'] = t('Save and contribute');
      $form['actions']['submit']['#button_type'] = 'primary';

      // Wire in remote submission to the form submission.
      array_unshift($form['#submit'], 'l10n_client_contributor_form_locale_translate_edit_form_submit');
    }
  }
}

/**
 * Additional submission functionality for translation editing.
 */
function l10n_client_contributor_form_locale_translate_edit_form_submit($form, FormStateInterface $form_state) {
  $lids = array_keys($form_state
    ->getValue('strings'));
  $langcode = $form_state
    ->getValue('langcode');
  $strings = $form_state
    ->getValue('strings');
  l10n_client_contributor_save_translation($langcode, $lids, $strings);
}

/**
 * Save the suggested translations and it sends them to the localization server.
 *
 * @param string $langcode
 *   The language code of a translated string.
 * @param array $lids
 *   Local ideas of the strings.
 * @param array $strings
 *   List of strings and its translations.
 *
 * @return \Drupal\Core\StringTranslation\TranslatableMarkup|string
 *   The reponse message.
 */
function l10n_client_contributor_save_translation($langcode, array $lids, array $strings) {

  /** @var \Drupal\user\UserInterface $account */
  $account = User::load(\Drupal::currentUser()
    ->id());
  $translation_objects = $source_objects = [];
  $locale_storage = \Drupal::service('locale.storage');
  foreach ($locale_storage
    ->getTranslations(array(
    'lid' => $lids,
    'language' => $langcode,
    'translated' => TRUE,
  )) as $existing_translation_object) {
    $translation_objects[$existing_translation_object->lid] = $existing_translation_object;
  }
  foreach ($locale_storage
    ->getStrings(array(
    'lid' => $lids,
  )) as $source_object) {
    $source_objects[$source_object->lid] = $source_object;
  }
  $contributed = 0;
  $refused = [];
  $code = 200;
  $message = t('The translation saving was successfully, but nothing changed.');
  foreach ($strings as $lid => $new_translation) {
    $existing_translation = isset($translation_objects[$lid]);

    // Plural translations are saved in a delimited string. To be able to
    // compare the new strings with the existing strings a string in the same
    // format is created.
    $new_translation_string_delimited = implode(PoItem::DELIMITER, $new_translation['translations']);

    // Generate an imploded string without delimiter, to be able to run
    // empty() on it.
    $new_translation_string = implode('', $new_translation['translations']);
    $is_changed = FALSE;
    if ($existing_translation && $translation_objects[$lid]->translation != $new_translation_string_delimited) {

      // If there is an existing translation in the DB and the new translation
      // is not the same as the existing one.
      $is_changed = TRUE;
    }
    elseif (!$existing_translation && !empty($new_translation_string)) {

      // Newly entered translation.
      $is_changed = TRUE;
    }

    // Changed or new translation and its not empty.
    if ($is_changed && !empty($new_translation_string)) {
      list($code, $message) = l10n_client_contributor_send_translation($langcode, $source_objects[$lid]
        ->getString(), $new_translation_string_delimited, $account->l10n_client_contributor_key->value, l10n_client_contributor_user_token($account));
      if ($code !== FALSE) {
        $contributed++;
      }
      else {
        $refused[] = $message;
      }
    }
  }

  // END mostly copy of TranslateEditForm::submitForm().
  if ($contributed) {
    \Drupal::messenger()
      ->addMessage(\Drupal::translation()
      ->formatPlural($contributed, 'Just contributed a translation to the community. Great job!', 'Contributed @count translations to the community, rock!'));
  }
  if ($refused) {
    \Drupal::messenger()
      ->addError(t('Errors while contributing translations:') . '<ul><li>' . implode('</li><li>', $refused) . '</li></ul>');
  }
  return $message;
}

/**
 * Send translation to the server.
 *
 * @param string $langcode
 *   The language code of a translated string.
 * @param string $source
 *   The translatable string.
 * @param string $translation
 *   The translated string.
 * @param string $user_key
 *   User API key to localization server.
 * @param string $user_token
 *   User based semi unique token.
 *
 * @return array
 *    The result of the send request.
 *    [response code/FALSE, message]
 */
function l10n_client_contributor_send_translation($langcode, $source, $translation, $user_key, $user_token) {
  $server_uid = current(explode(':', $user_key));
  $signature = md5($user_key . $langcode . $source . $translation . $user_token);
  $config = \Drupal::config('l10n_client_contributor.settings');
  $server_url = $config
    ->get('server');
  $response = xmlrpc($server_url . '/xmlrpc.php', array(
    'l10n.submit.translation' => array(
      $langcode,
      $source,
      $translation,
      (int) $server_uid,
      $user_token,
      $signature,
    ),
  ));
  if (!empty($response) && isset($response['status'])) {
    if ($response['status']) {
      $message = t('Translation sent and accepted by @server.', array(
        '@server' => $server_url,
      ));
      \Drupal::logger('l10n_client_contributor')
        ->notice('Translation sent and accepted by @server.', array(
        '@server' => $server_url,
      ));
    }
    else {
      $message = t('Translation rejected by @server. Reason: %reason', array(
        '%reason' => $response['reason'],
        '@server' => $server_url,
      ));
      \Drupal::logger('l10n_client_contributor')
        ->error('Translation rejected by @server. Reason: %reason', array(
        '%reason' => $response['reason'],
        '@server' => $server_url,
      ));
    }
    return array(
      $response['status'],
      $message,
    );
  }
  else {
    module_load_include('inc', 'xmlrpc', 'xmlrpc');
    $message = t('The connection with @server failed with the following error: %error_code: %error_message.', array(
      '%error_code' => xmlrpc_errno(),
      '%error_message' => xmlrpc_error_msg(),
      '@server' => $server_url,
    ));
    \Drupal::logger('l10n_client_contributor')
      ->error('The connection with @server failed with the following error: %error_code: %error_message.', array(
      '%error_code' => xmlrpc_errno(),
      '%error_message' => xmlrpc_error_msg(),
      '@server' => $server_url,
    ));
    return array(
      FALSE,
      $message,
    );
  }
}

Functions

Namesort descending Description
l10n_client_contributor_entity_field_access Implements hook_entity_field_access().
l10n_client_contributor_form_locale_translate_edit_form_alter Implements hook_form_FORM_ID_alter().
l10n_client_contributor_form_locale_translate_edit_form_submit Additional submission functionality for translation editing.
l10n_client_contributor_form_locale_translate_settings_alter Implements hook_form_FORM_ID_alter().
l10n_client_contributor_form_locale_translate_settings_submit Submission function for additional settings on localization client settings.
l10n_client_contributor_form_locale_translate_settings_validate Validation for added settings on localization client UI settings form.
l10n_client_contributor_help Implements hook_help().
l10n_client_contributor_save_translation Save the suggested translations and it sends them to the localization server.
l10n_client_contributor_send_translation Send translation to the server.
l10n_client_contributor_user_token Get user based semi unique token. Ensure keys are unique for each client.