l10n_server-2.x-dev/l10n_server/src/L10nPo.php

l10n_server/src/L10nPo.php
<?php

namespace Drupal\l10n_server;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Link;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\file\FileInterface;
use Drupal\l10n_server\Entity\L10nServerTranslationHistory;

/**
 * Service description.
 */
class L10nPo {

  use StringTranslationTrait;

  /**
   * The file handler.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected FileSystemInterface $fileSystem;

  /**
   * Date formatter service.
   *
   * @var \Drupal\Core\Datetime\DateFormatterInterface
   */
  protected $dateFormatter;

  /**
   * Time service.
   *
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected $time;

  /**
   * Constructs a L10nPo object.
   *
   * @param \Drupal\Core\File\FileSystemInterface $file_system
   *   The file handler.
   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
   *   Date formatter service.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   Time service.
   */
  public function __construct(
    FileSystemInterface $file_system,
    DateFormatterInterface $date_formatter,
    TimeInterface $time,
  ) {
    $this->fileSystem = $file_system;
    $this->dateFormatter = $date_formatter;
    $this->time = $time;
  }

  /**
   * Parses a Gettext Portable Object file and saves strings.
   *
   * Modified version of Drupal 7's _locale_import_read_po():
   *   - 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.
   *
   * @param \Drupal\file\FileInterface $file
   *   Drupal file object corresponding to the PO file to import.
   * @param string $string_callback
   *   Callback invoked to save a string.
   * @param array $callback_arguments
   *   Array of arguments to pass on to the callback after the string found.
   */
  public function parse(FileInterface $file, string $string_callback, array $callback_arguments) {
    // 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(),
      ]));
      return FALSE;
    }

    // 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)) {
      // 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);
      }
      $lineno++;
      $line = trim(strtr($line, ["\\\n" => ""]));

      // 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.
          call_user_func_array($string_callback, array_merge([$current], $callback_arguments));
          $current = [];
          $current["#"][] = substr($line, 1);
          $context = "COMMENT";
        }
        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,
          ]));
          return FALSE;
        }
      }
      elseif (!strncmp("msgid_plural", $line, 12)) {
        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,
          ]));
          return FALSE;
        }
        $line = trim(substr($line, 12));
        $quoted = self::parseQuoted($line);
        if ($quoted === FALSE) {
          \Drupal::messenger()->addError($this->t('%filename contains a syntax error on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
          ]));
          return FALSE;
        }
        $current["msgid"] = $current["msgid"] . "\0" . $quoted;
        $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([$current], $callback_arguments));
          $current = [];
        }
        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,
          ]));
          return FALSE;
        }
        $line = trim(substr($line, 5));
        $quoted = self::parseQuoted($line);
        if ($quoted === FALSE) {
          \Drupal::messenger()->addError($this->t('%filename contains a syntax error on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
          ]));
          return FALSE;
        }
        $current["msgid"] = $quoted;
        $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([$current], $callback_arguments));
          $current = [];
        }
        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,
          ]));
          return FALSE;
        }
        $line = trim(substr($line, 7));
        $quoted = self::parseQuoted($line);
        if ($quoted === FALSE) {
          \Drupal::messenger()->addError($this->t('%filename contains a syntax error on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
          ]));
          return FALSE;
        }
        $current["msgctxt"] = $quoted;
        $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[].
          \Drupal::messenger()->addError($this->t('%filename contains an error: "msgstr[]" is unexpected on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
          ]));
          return FALSE;
        }
        if (strpos($line, "]") === FALSE) {
          \Drupal::messenger()->addError($this->t('%filename contains a syntax error on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
          ]));
          return FALSE;
        }
        $frombracket = strstr($line, "[");
        $plural = substr($frombracket, 1, strpos($frombracket, "]") - 1);
        $line = trim(strstr($line, " "));
        $quoted = self::parseQuoted($line);
        if ($quoted === FALSE) {
          \Drupal::messenger()->addError($this->t('%filename contains a syntax error on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
          ]));
          return FALSE;
        }
        $current["msgstr"][$plural] = $quoted;
        $context = "MSGSTR_ARR";
      }
      elseif (!strncmp("msgstr", $line, 6)) {
        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,
          ]));
          return FALSE;
        }
        $line = trim(substr($line, 6));
        $quoted = self::parseQuoted($line);
        if ($quoted === FALSE) {
          \Drupal::messenger()->addError($this->t('%filename contains a syntax error on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
          ]));
          return FALSE;
        }
        $current["msgstr"] = $quoted;
        $context = "MSGSTR";
      }
      elseif ($line != "") {
        $quoted = self::parseQuoted($line);
        if ($quoted === FALSE) {
          \Drupal::messenger()->addError($this->t('%filename contains a syntax error on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
          ]));
          return FALSE;
        }
        if (($context == "MSGID") || ($context == "MSGID_PLURAL")) {
          $current["msgid"] .= $quoted;
        }
        elseif ($context == "MSGCTXT") {
          $current["msgctxt"] .= $quoted;
        }
        elseif ($context == "MSGSTR") {
          $current["msgstr"] .= $quoted;
        }
        elseif ($context == "MSGSTR_ARR") {
          $current["msgstr"][$plural] .= $quoted;
        }
        else {
          \Drupal::messenger()->addError($this->t('%filename contains an error: there is an unexpected string on line %line.', [
            '%filename' => $file->getFileUri(),
            '%line' => $lineno,
          ]));
          return FALSE;
        }
      }
    }

    // End of PO file, flush last entry.
    if (!empty($current)) {
      call_user_func_array($string_callback, array_merge([$current], $callback_arguments));
    }
    elseif ($context != "COMMENT") {
      \Drupal::messenger()->addError($this->t('filename ended unexpectedly at line %line.', [
        '%filename' => $file->getFileUri(),
        '%line' => $lineno,
      ]));
      return FALSE;
    }

    return TRUE;
  }

  /**
   * Parses a string in quotes.
   *
   * @param string $string
   *   A string specified with enclosing quotes.
   *
   * @return string|false
   *   The string parsed from inside the quotes.
   */
  private static function parseQuoted(string $string) {
    if (substr($string, 0, 1) != substr($string, -1, 1)) {
      return FALSE;

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

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

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

      // Unrecognized quote.
    }
  }

  /**
   * Imports a string into the database.
   *
   * @param array $value
   *   Details of the string stored.
   * @param string|null $langcode
   *   Language to store the string in.
   * @param bool $is_suggestion
   *   TRUE if the string to store is a suggestion, FALSE otherwise.
   * @param int|null $uid
   *   User id used to save attribution information.
   *
   * @throws \Exception
   */
  public static function importOneString(array $value, ?string $langcode = NULL, bool $is_suggestion = FALSE, ?int $uid = NULL) {
    $user = \Drupal::currentUser()->getAccount();

    // Trim translation (we will apply source string based whitespace later).
    if (is_string($value['msgstr'])) {
      $value['msgstr'] = trim($value['msgstr']);
    }

    if (!empty($value['msgid']) && !empty($value['msgstr'])) {
      // We only save non-empty translations/suggestions.
      if (empty($uid)) {
        $uid = $user->id();
      }

      // If the comment array for this value contains the ', fuzzy' flag, then
      // mark this as a suggestion import in all cases.
      if (!empty($value['#'])) {
        $is_suggestion = ($is_suggestion ? TRUE : in_array(', fuzzy', $value['#']));
      }

      // If context was not set, set to empty.
      $value['msgctxt'] = !empty($value['msgctxt']) ? $value['msgctxt'] : '';

      $sid = \Drupal::database()
        ->query("SELECT sid FROM {l10n_server_string} WHERE hashkey = :hashkey", [
          ':hashkey' => md5($value['msgid'] . $value['msgctxt']),
        ])->fetchField();
      if ($sid) {
        // Merge plural versions into one for saving values.
        $value['msgstr'] = is_array($value['msgstr']) ? implode("\0", $value['msgstr']) : $value['msgstr'];

        // Add this as a suggestion first.
        $tid = static::addSuggestion($sid, $value['msgstr'], $langcode, $uid, $user->id(), L10nServerTranslationHistory::MEDIUM_IMPORT, !$is_suggestion);

        if ($tid) {
          if ($is_suggestion) {
            static::counter(L10nServerTranslationHistory::COUNT_SUGGESTED);
          }
          else {
            static::approveString($langcode, $sid, $tid);
            static::counter(L10nServerTranslationHistory::COUNT_ADDED);
          }
        }
        elseif ($tid === FALSE) {
          static::counter(L10nServerTranslationHistory::COUNT_DUPLICATE);
        }

      }
      else {
        // Source string not found, string ignored.
        static::counter(L10nServerTranslationHistory::COUNT_IGNORED);
      }
    }
  }

  /**
   * Adds a suggestion to a language/string.
   *
   * @param int $sid
   *   The string ID for which a new translation should be added.
   * @param string $translation
   *   String representing the new translation.
   * @param string $langcode
   *   The language of the new translation.
   * @param int $uid_attribution
   *   User ID to use to save the string.
   * @param int $uid_user
   *   User ID to use to keep history of.
   * @param string $medium
   *   Medium type constant L10N_SERVER_MEDIUM_*.
   * @param bool $force
   *   Force replacing a suggestion if it already exists.
   *
   * @return int
   *   Translation ID.
   *
   * @throws \Exception
   */
  public static function addSuggestion(int $sid, string $translation, string $langcode, int $uid_attribution, int $uid_user, string $medium, bool $force = FALSE) {
    $database = \Drupal::database();
    $translation_storage = \Drupal::entityTypeManager()
      ->getStorage('l10n_server_translation');
    $translation_history_storage = \Drupal::entityTypeManager()
      ->getStorage('l10n_server_translation_history');

    // Load source string and adjust translation whitespace based on source.
    $source_string = $database
      ->query('SELECT value FROM {l10n_server_string} WHERE sid = :sid', [
        ':sid' => $sid,
      ])
      ->fetchField();
    $translation = static::trim($translation, $source_string);

    // Don't store empty translations.
    if ($translation === '') {
      return NULL;
    }

    $time = \Drupal::time()->getRequestTime();

    // Look for an existing active translation, if any.
    // Use BINARY matching to avoid marking case-corrections as duplicate.
    $existing = $database
      ->select('l10n_server_translation', 't')
      ->fields('t', ['tid', 'status', 'suggestion'])
      ->condition('sid', $sid)
      ->condition('language', $langcode)
      ->condition('translation', $translation)
      ->execute()
      ->fetchObject();

    if (!empty($existing)) {
      if ($existing->status == 0
          || ($existing->suggestion == 1 && $force)) {
        // If the existing item is not active, make it an active suggestion and
        // clean up its possible previous approval information.
        $database
          ->update('l10n_server_translation')
          ->fields([
            'suggestion' => 1,
            'status' => 1,
            'time_changed' => $time,
          ])
          ->condition('tid', $existing->tid)
          ->execute();
        $tid = $existing->tid;
        $type = L10nServerTranslationhistory::ACTION_READD;
      }
      else {
        return FALSE;
      }
    }
    else {
      // Insert the new suggestion.
      $translation = $translation_storage
        ->create([
          'sid' => $sid,
          'translation' => $translation,
          'language' => $langcode,
          'uid' => $uid_attribution,
          'created' => $time,
          'changed' => $time,
          'suggestion' => 1,
          'status' => 1,
        ]);
      $translation->save();
      $type = L10N_SERVER_ACTION_ADD;
    }
    $translation_history = $translation_history_storage
      ->create([
        'tid' => $translation->id(),
        'uid_action' => $uid_attribution,
        'time_action' => $time,
        'type_action' => $type,
        'medium_action' => $medium,
      ]);
    $translation_history->save();

    // Mark the existing or mock translation as having suggestions.
    static::updateStringStatus($langcode, $sid);

    return $translation->id();
  }

  /**
   * Mark a translation as approved.
   *
   * @param string $langcode
   *   The language of the approved translation.
   * @param int $sid
   *   The string ID the translation belongs to.
   * @param int $tid
   *   The translation ID of the translation.
   *
   * @throws \Exception
   */
  public static function approveString(string $langcode, int $sid, int $tid) {
    $database = \Drupal::database();
    $translation_storage = \Drupal::entityTypeManager()
      ->getStorage('l10n_server_translation');
    $translation_history_storage = \Drupal::entityTypeManager()
      ->getStorage('l10n_server_translation_history');

    $user = \Drupal::currentUser()->getAccount();
    $time = \Drupal::time()->getRequestTime();

    // Make the existing approved string a suggestion (if applicable).
    // There should only ever be one string like this, but you know, errors
    // happen.
    $translations = $translation_storage->loadByProperties([
      'sid' => $sid,
      'language' => $langcode,
      'suggestion' => 0,
      'status' => 1,
    ]);
    /** @var \Drupal\l10n_server\Entity\L10nServerTranslationInterface $translation */
    foreach ($translations as $translation) {
      $translation
        ->set('suggestion', 1)
        ->set('changed', $time);
      $translation->save();

      // ATM we only support this through the web, so always save the web
      // medium.
      $translation_history = $translation_history_storage->create([
        'tid' => $translation->id(),
        'uid_action' => $user->id(),
        'type_action' => L10nServerTranslationHistory::ACTION_DEMOTE,
        'time_action' => $time,
        'medium_action' => L10nServerTranslationHistory::MEDIUM_WEB,
      ]);
      $translation_history->save();
    }

    // Mark this exact suggestion as active translation, and set approval time.
    $database
      ->update('l10n_server_translation')
      ->fields([
        'changed' => $time,
        'suggestion' => 0,
        'status' => 1,
      ])
      ->condition('tid', $tid)
      ->execute();
    // ATM we only support this through the web, so always save the web medium.
    $translation_history = $translation_history_storage->create([
      'tid' => $tid,
      'uid_action' => $user->id(),
      'type_action' => L10nServerTranslationHistory::ACTION_APPROVE,
      'time_action' => $time,
      'medium_action' => L10nServerTranslationHistory::MEDIUM_WEB,
    ]);
    $translation_history->save();
    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.
   *
   * @param string $field
   *   The field to increment. Can be one of L10N_COUNT_*.
   *   If not specified, the counters are returned and reset afterwards.
   * @param int $increment
   *   Optional increment for the counter. Defaults to 1.
   */
  public static function counter($field = NULL, int $increment = 1) {
    static $counters = [];

    if (isset($field)) {
      if (!isset($counters[$field])) {
        $counters[$field] = 0;
      }
      $counters[$field] += $increment;
    }
    else {
      $return = $counters;
      $counters = [];
      return $return;
    }
  }

  /**
   * Updates the status flags for the given source string.
   *
   * @param string $langcode
   *   The language of the string.
   * @param int $sid
   *   The string ID that should be updated.
   *
   * @throws \Exception
   */
  public static function updateStringStatus(string $langcode, int $sid) {
    $database = \Drupal::database();
    $translation_storage = \Drupal::entityTypeManager()
      ->getStorage('l10n_server_translation');
    $status_flag_storage = \Drupal::entityTypeManager()
      ->getStorage('l10n_server_status_flag');

    // Let's see if we have any suggestions remaining in this language.
    $has_suggestion = $database
      ->query("SELECT 1 FROM {l10n_server_translation} WHERE sid = :sid AND suggestion = 1 AND status = 1 AND language = :language", [
        ':sid' => $sid,
        ':language' => $langcode,
      ])->fetchField();
    $has_translation = $database
      ->query("SELECT 1 FROM {l10n_server_translation} WHERE sid = :sid AND suggestion = 0 AND status = 1 AND language = :language", [
        ':sid' => $sid,
        ':language' => $langcode,
      ])->fetchField();

    $database
      ->delete('l10n_server_status_flag')
      ->condition('sid', $sid)
      ->condition('language', $langcode)
      ->execute();
    $status_flags = $status_flag_storage
      ->loadByProperties([
        'sid' => $sid,
        'language' => $langcode,
      ]);
    if ($status_flag = reset($status_flags)) {
      $status_flag->delete();
    }

    if ($has_suggestion || $has_translation) {
      $status_flag = $status_flag_storage->create([
        'sid' => $sid,
        'language' => $langcode,
        'has_suggestion' => (int) $has_suggestion,
        'has_translation' => (int) $has_translation,
      ]);
      $status_flag->save();
    }
  }

  /**
   * 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_COMMUNITY_COUNT_DECLINED])) {
      $messages[] = $this->formatPlural($counters[L10N_COMMUNITY_COUNT_DECLINED], '1 translation declined', '@count translations declined');
    }
    if (!empty($counters[L10N_COMMUNITY_COUNT_SUGGESTION_DECLINED])) {
      $messages[] = $this->formatPlural($counters[L10N_COMMUNITY_COUNT_SUGGESTION_DECLINED], '1 suggestion declined', '@count suggestions declined');
    }
    if (!empty($counters[L10N_COMMUNITY_COUNT_APPROVED])) {
      $messages[] = $this->formatPlural($counters[L10N_COMMUNITY_COUNT_APPROVED], '1 translation approved', '@count translations approved');
    }
    if (!empty($counters[L10N_COMMUNITY_COUNT_ADDED])) {
      $messages[] = $this->formatPlural($counters[L10N_COMMUNITY_COUNT_ADDED], '1 translation added', '@count translations added');
    }
    if (!empty($counters[L10N_COMMUNITY_COUNT_SUGGESTED])) {
      $messages[] = $this->formatPlural($counters[L10N_COMMUNITY_COUNT_SUGGESTED], '1 suggestion added', '@count suggestions added');
    }
    if (!empty($counters[L10N_COMMUNITY_COUNT_UPDATED])) {
      $messages[] = $this->formatPlural($counters[L10N_COMMUNITY_COUNT_UPDATED], '1 translation updated', '@count translations updated');
    }
    if (!empty($counters[L10N_COMMUNITY_COUNT_DUPLICATE])) {
      $messages[] = $this->formatPlural($counters[L10N_COMMUNITY_COUNT_DUPLICATE], '1 duplicate translation not saved', '@count duplicate translations not saved');
    }
    if (!empty($counters[L10N_COMMUNITY_COUNT_IGNORED])) {
      $messages[] = $this->formatPlural($counters[L10N_COMMUNITY_COUNT_IGNORED], '1 source string not found; its translation was ignored', '@count source strings not found; their translations were ignored');
    }
    if (!empty($counters[L10N_COMMUNITY_COUNT_UNCHANGED])) {
      $messages[] = $this->formatPlural($counters[L10N_COMMUNITY_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.
   *
   * @param string $translation
   *   Translation string.
   * @param string $source
   *   Source string.
   *
   * @return string
   *   Translation string with the right beginning and ending chars.
   */
  public static function trim(string $translation, string $source) {
    if (is_string($translation) && is_string($source)) {
      $matches = [];
      preg_match("/^(\s*).*\S(\s*)\$/s", $source, $matches);
      return $matches[1] . trim($translation) . $matches[2];
    }
    return $translation;
  }

  /**
   * Generates the byline containing meta information about a string.
   *
   * @param string $name
   *   Name of user who submitted the suggestion.
   * @param int $uid
   *   ID of user who submitted the suggestion.
   * @param int $time
   *   Timestamp of the suggestion.
   * @param int $medium
   *   Medium type constant L10N_SERVER_MEDIUM_*.
   * @param int $type
   *   Type of action.
   * @param bool $link_user
   *   Whether user must be displayed as a link or not.
   *
   * @return null|string
   *   Formatted line.
   *
   * @throws \Exception
   */
  public function translateByline($name, $uid, $time, $medium, $type, $link_user = TRUE) {
    $params = [
      // Avoid loading user for performance reasons.
      '@author' => $uid ? ($link_user ? Link::fromTextAndUrl($name, Url::fromRoute('entity.user.canonical', ['user' => $uid]))->toString() : $name) : $this->t('unknown user'),
      // Also skip handling time if uid was not specified (for decline entries
      // in the update, which have time for ordering reasons, but no uid).
      '@date' => $time && $uid ? $this->dateFormatter->format($time) : $this->t('unknown time'),
      '@ago' => $time ? $this->t('@time ago', ['@time' => $this->dateFormatter->formatInterval($this->time->getRequestTime() - $time)]) : $this->t('no time record available'),
    ];
    switch ($type) {
      case L10N_SERVER_ACTION_ADD:
        switch ($medium) {
          case L10N_SERVER_MEDIUM_IMPORT:
            return $this->t('imported by @author <span title="@ago">on @date</span>', $params);

          case L10N_SERVER_MEDIUM_REMOTE:
            return $this->t('remotely submitted by @author <span title="@ago">on @date</span>', $params);

          case L10N_SERVER_MEDIUM_WEB:
            return $this->t('suggested on the web by @author <span title="@ago">on @date</span>', $params);

          case L10N_SERVER_MEDIUM_UNKNOWN:
            return $this->t('suggested by @author <span title="@ago">on @date</span> (source unknown)', $params);
        }
        break;

      case L10N_SERVER_ACTION_READD:
        switch ($medium) {
          case L10N_SERVER_MEDIUM_IMPORT:
            return $this->t('re-imported by @author <span title="@ago">on @date</span>', $params);

          case L10N_SERVER_MEDIUM_REMOTE:
            return $this->t('remotely re-submitted by @author <span title="@ago">on @date</span>', $params);

          case L10N_SERVER_MEDIUM_WEB:
            return $this->t('re-suggested on the web by @author <span title="@ago">on @date</span>', $params);

          // L10N_SERVER_MEDIUM_UNKNOWN does not apply, because we only have
          // that for backwards compatibility and L10N_SERVER_ACTION_READD did
          // not happen with data migrated (at least we did not know about it).
        }
        break;

      case L10N_SERVER_ACTION_APPROVE:
        return $this->t('approved by @author <span title="@ago">on @date</span>', $params);

      case L10N_SERVER_ACTION_DECLINE:
        return $this->t('declined by @author <span title="@ago">on @date</span>', $params);

      case L10N_SERVER_ACTION_DEMOTE:
        return $this->t('demoted by @author <span title="@ago">on @date</span>', $params);

      default:
        // Default byline that work as a click-target to get more information.
        return $this->t('by @author <span title="@ago">on @date</span>', $params);
    }
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc