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