Commit a93acb12 authored by Felip Manyer i Ballester's avatar Felip Manyer i Ballester
Browse files

Issue #3282696: Port of l10n_community: translation form (submit)

parent 5e9e1c9f
Loading
Loading
Loading
Loading
+2 −44
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\l10n_community\Form;
@@ -6,7 +7,6 @@ namespace Drupal\l10n_community\Form;
use Drupal\Component\Utility\Environment;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\l10n_server\L10nPo;

/**
 * Translation export form.
@@ -147,7 +147,7 @@ class ImportForm extends FormBase {
        $form_state->getValue('import_uid'),
      ]);
      if ($parsed) {
         $this->updateMessage();
        $po->updateMessage();
        // @todo Clear cache where stats are shown.
        // phpcs:ignore
        // cache_clear_all('l10n:stats:' . $form_state['values']['langcode'], 'cache');
@@ -161,46 +161,4 @@ class ImportForm extends FormBase {

  }

  /**
   * Set a message based on the number of translations changed.
   *
   * Used by both the save and import process.
   */
  public function updateMessage() {
    $counters = L10nPo::counter();
    $messages = [];

    if (!empty($counters[L10N_COUNT_DECLINED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_DECLINED], '1 translation declined', '@count translations declined');
    }
    if (!empty($counters[L10N_COUNT_SUGGESTION_DECLINED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_SUGGESTION_DECLINED], '1 suggestion declined', '@count suggestions declined');
    }
    if (!empty($counters[L10N_COUNT_APPROVED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_APPROVED], '1 translation approved', '@count translations approved');
    }
    if (!empty($counters[L10N_COUNT_ADDED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_ADDED], '1 translation added', '@count translations added');
    }
    if (!empty($counters[L10N_COUNT_SUGGESTED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_SUGGESTED], '1 suggestion added', '@count suggestions added');
    }
    if (!empty($counters[L10N_COUNT_UPDATED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_UPDATED], '1 translation updated', '@count translations updated');
    }
    if (!empty($counters[L10N_COUNT_DUPLICATE])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_DUPLICATE], '1 duplicate translation not saved', '@count duplicate translations not saved');
    }
    if (!empty($counters[L10N_COUNT_IGNORED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_IGNORED], '1 source string not found; its translation was ignored', '@count source strings not found; their translations were ignored');
    }
    if (!empty($counters[L10N_COUNT_UNCHANGED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_UNCHANGED], '1 translation unchanged', '@count translations unchanged');
    }

    if ($messages) {
      \Drupal::messenger()->addStatus(implode(', ', $messages));
    }
  }

}
+89 −8
Original line number Diff line number Diff line
@@ -7,17 +7,20 @@ use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Http\RequestStack;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Link;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Url;
use Drupal\l10n_community\L10nAccess;
use Drupal\l10n_community\L10nTranslator;
use Drupal\l10n_server\L10nPo;
use Drupal\locale\PluralFormulaInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

@@ -96,10 +99,24 @@ class TranslateForm extends FormBase implements TrustedCallbackInterface {
   */
  protected $renderer;

  /**
   * L10n helper.
   *
   * @var \Drupal\l10n_server\L10nPo
   */
  protected $l10nPo;

  /**
   * Current request object.
   *
   * @var \Symfony\Component\HttpFoundation\Request
   */
  protected $currentRequest;

  /**
   * Constructor.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, L10nTranslator $translator, L10nAccess $access_manager, MessengerInterface $messenger, AccountProxyInterface $current_user, DateFormatterInterface $date_formatter, TimeInterface $time, PluralFormulaInterface $plural_formula, RouteMatchInterface $current_route_match, RendererInterface $renderer) {
  public function __construct(EntityTypeManagerInterface $entity_type_manager, L10nTranslator $translator, L10nAccess $access_manager, MessengerInterface $messenger, AccountProxyInterface $current_user, DateFormatterInterface $date_formatter, TimeInterface $time, PluralFormulaInterface $plural_formula, RouteMatchInterface $current_route_match, RendererInterface $renderer, L10nPo $l10n_po, RequestStack $request_stack) {
    $this->entityTypeManager = $entity_type_manager;
    $this->translator = $translator;
    $this->accessManager = $access_manager;
@@ -110,6 +127,8 @@ class TranslateForm extends FormBase implements TrustedCallbackInterface {
    $this->pluralFormula = $plural_formula;
    $this->currentRouteMatch = $current_route_match;
    $this->renderer = $renderer;
    $this->l10nPo = $l10n_po;
    $this->currentRequest = $request_stack->getCurrentRequest();
  }

  /**
@@ -126,7 +145,9 @@ class TranslateForm extends FormBase implements TrustedCallbackInterface {
      $container->get('datetime.time'),
      $container->get('locale.plural.formula'),
      $container->get('current_route_match'),
      $container->get('renderer')
      $container->get('renderer'),
      $container->get('l10n_server.po'),
      $container->get('request_stack')
    );
  }

@@ -169,11 +190,6 @@ class TranslateForm extends FormBase implements TrustedCallbackInterface {
    // has permissions to submit suggestions. This allows the use of this form
    // to review strings in the database.
    $form = [
      'target' => [
        '#type' => 'value',
        // @todo injection.
        '#value' => $_GET,
      ],
      'langcode' => [
        '#type' => 'value',
        '#value' => $langcode,
@@ -438,7 +454,7 @@ class TranslateForm extends FormBase implements TrustedCallbackInterface {

    foreach (Element::children($form_item) as $child) {
      if (is_numeric($child) || $child == 'new') {
        $items[] = $this->translationListItem($form_item[$child]);
        $items[$child] = $this->translationListItem($form_item[$child]);
      }
    }

@@ -853,6 +869,71 @@ class TranslateForm extends FormBase implements TrustedCallbackInterface {
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $langcode = $form_state->getValue('langcode');

    // Iterate outer structure built in self::setRows().
    foreach ($form_state->getValue('strings') as $sid => $string) {

      // Iterate inner structure built in self::formItem().
      // Form items have numeric $tid values and other keys here.
      foreach ($string['translation'] as $tid => $options) {

        // Store new suggestion.
        $all_values = 0;
        $empty_values = 0;
        // $options['value'] is the result of (a series of) textareas.
        if (isset($options['value']) && is_array($options['value'])) {
          foreach ($options['value'] as $key => $value) {
            $all_values++;
            if ($value === t('<New translation>')) {
              $options['value'] = '';
              $empty_values++;
            }
          }
          // If we had value in any of the textareas, add new suggestion.
          if ($tid === 'new' && $all_values > $empty_values) {
            $tid = $this->l10nPo->addSuggestion($sid, $this->packString($options['value']), $langcode, $this->currentUser->id(), $this->currentUser->id(), L10N_SERVER_MEDIUM_WEB);
            if ($tid) {
              if ($string['active'] === 'new') {
                // This new string was selected to be approved, so remember $tid
                // for later, so we can save this as an approved translation.
                $string['active'] = $tid;
                $this->l10nPo->counter(L10N_COUNT_ADDED);
              }
              else {
                $this->l10nPo->counter(L10N_COUNT_SUGGESTED);
              }
            }
            elseif ($tid === FALSE) {
              // We found this as an active string already in the DB.
              $this->l10nPo->counter(L10N_COUNT_DUPLICATE);
            }
          }
        }

        if (is_numeric($tid) && $tid > 0) {
          if ($tid == $string['active']) {
            if ($options['original']->suggestion) {
              // $tid is a suggestion that was made active.
              $this->l10nPo->approveString($langcode, $sid, $tid);
              $this->l10nPo->counter(L10N_COUNT_APPROVED);
            }
          }
          elseif (!empty($options['declined'])) {
            // The decline checkbox for this suggestion was checked.
            $this->l10nPo->counter($options['original']->suggestion ? L10N_COUNT_SUGGESTION_DECLINED : L10N_COUNT_DECLINED);
            $this->l10nPo->declineString($langcode, $sid, $tid, $user->uid);
          }
        }
      }
    }

    // Tell the user what happened.
    $this->l10nPo->updateMessage();

    // Keep existing filters and other query arguments on form submission.
    $route_match = RouteMatch::createFromRequest($this->currentRequest);
    $form_state->setRedirect('l10n_community.language.translate.translate', ['group' => $route_match->getRawParameter('group')], ['query' => $this->currentRequest->query->all()]);
  }

}
+128 −32
Original line number Diff line number Diff line
@@ -40,7 +40,7 @@ class L10nPo {
   *   - does not support in-memory import ($op parameter)
   *   - calls $string_callback() to save string
   *   - passes on $callback_arguments additionaly to the found string
   *   - algorithm untouched except using Drupal 7 code to support msgctxt
   *   - algorithm untouched except using Drupal 7 code to support msgctxt.
   *
   * @param \Drupal\file\FileInterface $file
   *   Drupal file object corresponding to the PO file to import.
@@ -50,7 +50,8 @@ class L10nPo {
   *   Array of arguments to pass on to the callback after the string found.
   */
  public function parse(FileInterface $file, string $string_callback, array $callback_arguments) {
    $fd = fopen($file->getFileUri(), "rb"); // File will get closed by PHP on return
    // File will get closed by PHP on return.
    $fd = fopen($file->getFileUri(), "rb");
    if (!$fd) {
      \Drupal::messenger()->addError($this->t('The Gettext file import failed, because the file %filename could not be read.', [
        '%filename' => $file->getFileUri(),
@@ -58,13 +59,18 @@ class L10nPo {
      return FALSE;
    }

    $context = "COMMENT"; // Parser context: COMMENT, MSGID, MSGID_PLURAL, MSGSTR and MSGSTR_ARR
    $current = []; // Current entry being read
    $plural  = 0; // Current plural form
    $lineno  = 0; // Current line
    // Parser context: COMMENT, MSGID, MSGID_PLURAL, MSGSTR and MSGSTR_ARR.
    $context = "COMMENT";
    // Current entry being read.
    $current = [];
    // Current plural form.
    $plural = 0;
    // Current line.
    $lineno = 0;

    while (!feof($fd)) {
      $line = fgets($fd, 10 * 1024); // A line should not be this long
      // A line should not be this long.
      $line = fgets($fd, 10 * 1024);
      if ($lineno == 0) {
        // The first line might come with a UTF-8 BOM, which should be removed.
        $line = str_replace("\xEF\xBB\xBF", '', $line);
@@ -72,17 +78,21 @@ class L10nPo {
      $lineno++;
      $line = trim(strtr($line, ["\\\n" => ""]));

      if (!strncmp("#", $line, 1)) { // A comment
        if ($context == "COMMENT") { // Already in comment context: add
      // A comment.
      if (!strncmp("#", $line, 1)) {
        // Already in comment context: add.
        if ($context == "COMMENT") {
          $current["#"][] = substr($line, 1);
        }
        elseif (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one
        elseif (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) {
          // End current entry, start a new one.
          call_user_func_array($string_callback, array_merge([$current], $callback_arguments));
          $current = [];
          $current["#"][] = substr($line, 1);
          $context = "COMMENT";
        }
        else { // Parse error
        else {
          // Parse error.
          \Drupal::messenger()->addError($this->t('%filename contains an error: "msgstr" was expected but not found on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
@@ -91,7 +101,8 @@ class L10nPo {
        }
      }
      elseif (!strncmp("msgid_plural", $line, 12)) {
        if ($context != "MSGID") { // Must be plural form for current entry
        if ($context != "MSGID") {
          // Must be plural form for current entry.
          \Drupal::messenger()->addError($this->t('%filename contains an error: "msgid_plural" was expected but not found on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
@@ -111,11 +122,13 @@ class L10nPo {
        $context = "MSGID_PLURAL";
      }
      elseif (!strncmp("msgid", $line, 5)) {
        if ($context == "MSGSTR") { // End current entry, start a new one
          call_user_func_array($string_callback, array_merge(array($current), $callback_arguments));
          $current = array();
        if ($context == "MSGSTR") {
          // End current entry, start a new one.
          call_user_func_array($string_callback, array_merge([$current], $callback_arguments));
          $current = [];
        }
        elseif ($context == "MSGID") { // Already in this context? Parse error
        elseif ($context == "MSGID") {
          // Already in this context? Parse error.
          \Drupal::messenger()->addError($this->t('%filename contains an error: "msgid" is unexpected on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
@@ -135,11 +148,13 @@ class L10nPo {
        $context = "MSGID";
      }
      elseif (!strncmp("msgctxt", $line, 7)) {
        if ($context == "MSGSTR") { // End current entry, start a new one
          call_user_func_array($string_callback, array_merge(array($current), $callback_arguments));
          $current = array();
        if ($context == "MSGSTR") {
          // End current entry, start a new one.
          call_user_func_array($string_callback, array_merge([$current], $callback_arguments));
          $current = [];
        }
        elseif (!empty($current["msgctxt"])) { // Already in this context? Parse error
        elseif (!empty($current["msgctxt"])) {
          // Already in this context? Parse error.
          \Drupal::messenger()->addError($this->t('%filename contains an error: "msgctxt" is unexpected on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
@@ -159,7 +174,8 @@ class L10nPo {
        $context = "MSGCTXT";
      }
      elseif (!strncmp("msgstr[", $line, 7)) {
        if (($context != "MSGID") && ($context != "MSGCTXT") && ($context != "MSGID_PLURAL") && ($context != "MSGSTR_ARR")) { // Must come after msgid, msgxtxt, msgid_plural, or msgstr[]
        if (($context != "MSGID") && ($context != "MSGCTXT") && ($context != "MSGID_PLURAL") && ($context != "MSGSTR_ARR")) {
          // Must come after msgid, msgxtxt, msgid_plural, or msgstr[].
          \Drupal::messenger()->addError($this->t('%filename contains an error: "msgstr[]" is unexpected on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
@@ -188,7 +204,8 @@ class L10nPo {
        $context = "MSGSTR_ARR";
      }
      elseif (!strncmp("msgstr", $line, 6)) {
        if (($context != "MSGID") && ($context != "MSGCTXT")) { // Should come just after a msgid or msgctxt block
        if (($context != "MSGID") && ($context != "MSGCTXT")) {
          // Should come just after a msgid or msgctxt block.
          \Drupal::messenger()->addError($this->t('%filename contains an error: "msgstr" is unexpected on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
@@ -238,7 +255,7 @@ class L10nPo {
      }
    }

    // End of PO file, flush last entry
    // End of PO file, flush last entry.
    if (!empty($current)) {
      call_user_func_array($string_callback, array_merge([$current], $callback_arguments));
    }
@@ -256,7 +273,7 @@ class L10nPo {
  /**
   * Parses a string in quotes.
   *
   * @param $string
   * @param string $string
   *   A string specified with enclosing quotes.
   *
   * @return string|false
@@ -266,24 +283,24 @@ class L10nPo {
    if (substr($string, 0, 1) != substr($string, -1, 1)) {
      return FALSE;

      // Start and end quotes must be the same
      // Start and end quotes must be the same.
    }
    $quote = substr($string, 0, 1);
    $string = substr($string, 1, -1);
    if ($quote == '"') {

      // Double quotes: strip slashes
      // Double quotes: strip slashes.
      return stripcslashes($string);
    }
    elseif ($quote == "'") {

      // Simple quote: return as-is
      // Simple quote: return as-is.
      return $string;
    }
    else {
      return FALSE;

      // Unrecognized quote
      // Unrecognized quote.
    }
  }

@@ -311,7 +328,6 @@ class L10nPo {

    if (!empty($value['msgid']) && !empty($value['msgstr'])) {
      // We only save non-empty translations/suggestions.

      if (empty($uid)) {
        $uid = $user->id();
      }
@@ -349,7 +365,6 @@ class L10nPo {
          static::counter(L10nServerTranslationHistory::COUNT_DUPLICATE);
        }


      }
      else {
        // Source string not found, string ignored.
@@ -372,7 +387,7 @@ class L10nPo {
   * @param int $uid_user
   *   User ID to use to keep history of.
   * @param string $medium
   *   Medium type constant L10N_SERVER_MEDIUM_*
   *   Medium type constant L10N_SERVER_MEDIUM_*.
   * @param bool $force
   *   Force replacing a suggestion if it already exists.
   *
@@ -490,7 +505,10 @@ class L10nPo {

    // Make the existing approved string a suggestion (if applicable).
    // There should only ever be one string like this.
    $translations = $translation_storage->loadByProperties(['sid' => $sid, 'language' => $langcode]);
    $translations = $translation_storage->loadByProperties([
      'sid' => $sid,
      'language' => $langcode,
    ]);
    /** @var \Drupal\l10n_server\Entity\L10nServerTranslationInterface $translation */
    if ($translation = reset($translations)) {
      $translation
@@ -532,6 +550,41 @@ class L10nPo {
    static::updateStringStatus($langcode, $sid);
  }

  /**
   * Marks a translation as declined.
   *
   * @param string $langcode
   *   The language of the declined translation.
   * @param int $sid
   *   The string ID the translation belongs to.
   * @param int $tid
   *   The translation ID of the translation.
   * @param int $uid
   *   The user performing the action.
   */
  public static function declineString($langcode, $sid, $tid, $uid) {
    $database = \Drupal::database();
    $time = \Drupal::time()->getRequestTime();
    $database->update('l10n_server_translation')
      ->fields([
        'is_active' => 0,
        'time_changed' => $time,
      ])
      ->condition('tid', $tid)
      ->execute();
    // ATM we only support this through the web, so always save the web medium.
    $id = $database->insert('l10n_server_translation_history')
      ->fields([
        'tid' => $tid,
        'uid_action' => $uid,
        'type_action' => L10N_SERVER_ACTION_DECLINE,
        'time_action' => $time,
        'medium_action' => L10N_SERVER_MEDIUM_WEB,
      ])
      ->execute();
    static::updateStringStatus($langcode, $sid);
  }

  /**
   * Stores counters for status messages when modifying translations.
   *
@@ -611,6 +664,48 @@ class L10nPo {
    }
  }

  /**
   * Set a message based on the number of translations changed.
   *
   * Used by both the save and import process.
   */
  public function updateMessage() {
    $counters = $this->counter();
    $messages = [];

    if (!empty($counters[L10N_COUNT_DECLINED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_DECLINED], '1 translation declined', '@count translations declined');
    }
    if (!empty($counters[L10N_COUNT_SUGGESTION_DECLINED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_SUGGESTION_DECLINED], '1 suggestion declined', '@count suggestions declined');
    }
    if (!empty($counters[L10N_COUNT_APPROVED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_APPROVED], '1 translation approved', '@count translations approved');
    }
    if (!empty($counters[L10N_COUNT_ADDED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_ADDED], '1 translation added', '@count translations added');
    }
    if (!empty($counters[L10N_COUNT_SUGGESTED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_SUGGESTED], '1 suggestion added', '@count suggestions added');
    }
    if (!empty($counters[L10N_COUNT_UPDATED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_UPDATED], '1 translation updated', '@count translations updated');
    }
    if (!empty($counters[L10N_COUNT_DUPLICATE])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_DUPLICATE], '1 duplicate translation not saved', '@count duplicate translations not saved');
    }
    if (!empty($counters[L10N_COUNT_IGNORED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_IGNORED], '1 source string not found; its translation was ignored', '@count source strings not found; their translations were ignored');
    }
    if (!empty($counters[L10N_COUNT_UNCHANGED])) {
      $messages[] = $this->formatPlural($counters[L10N_COUNT_UNCHANGED], '1 translation unchanged', '@count translations unchanged');
    }

    if ($messages) {
      \Drupal::messenger()->addStatus(implode(', ', $messages));
    }
  }

  /**
   * Make spacing and newlines the same in translation as in the source.
   *
@@ -618,6 +713,7 @@ class L10nPo {
   *   Translation string.
   * @param string $source
   *   Source string.
   *
   * @return string
   *   Translation string with the right beginning and ending chars.
   */