'String in Javascript', 'title' => 'Page title']; /** * List of simple tags * * @var array */ protected $_allowedTagsSimple = [ 'legend' => 'Caption for the fieldset element', 'label' => 'Label for an input element.', 'button' => 'Push button', 'a' => 'Link label', 'b' => 'Bold text', 'strong' => 'Strong emphasized text', 'i' => 'Italic text', 'em' => 'Emphasized text', 'u' => 'Underlined text', 'sup' => 'Superscript text', 'sub' => 'Subscript text', 'span' => 'Span element', 'small' => 'Smaller text', 'big' => 'Bigger text', 'address' => 'Contact information', 'blockquote' => 'Long quotation', 'q' => 'Short quotation', 'cite' => 'Citation', 'caption' => 'Table caption', 'abbr' => 'Abbreviated phrase', 'acronym' => 'An acronym', 'var' => 'Variable part of a text', 'dfn' => 'Term', 'strike' => 'Strikethrough text', 'del' => 'Deleted text', 'ins' => 'Inserted text', 'h1' => 'Heading level 1', 'h2' => 'Heading level 2', 'h3' => 'Heading level 3', 'h4' => 'Heading level 4', 'h5' => 'Heading level 5', 'h6' => 'Heading level 6', 'center' => 'Centered text', 'select' => 'List options', 'img' => 'Image', 'input' => 'Form element', ]; /** * @var \Magento\Translation\Model\ResourceModel\StringFactory */ protected $_resourceFactory; /** * @var \Magento\Store\Model\StoreManagerInterface */ protected $_storeManager; /** * @var \Zend_Filter_Interface */ protected $_inputFilter; /** * @var \Magento\Framework\App\State */ protected $_appState; /** * @var \Magento\Framework\Translate\InlineInterface */ protected $_translateInline; /** * @var \Magento\Framework\App\Cache\TypeListInterface */ protected $_appCache; /** * @var \Magento\Translation\Model\Inline\CacheManager */ private $cacheManager; /** * @var array */ private $relatedCacheTypes; /** * @return \Magento\Translation\Model\Inline\CacheManager * * @deprecated 100.1.0 */ private function getCacheManger() { if (!$this->cacheManager instanceof \Magento\Translation\Model\Inline\CacheManager) { $this->cacheManager = \Magento\Framework\App\ObjectManager::getInstance()->get( \Magento\Translation\Model\Inline\CacheManager::class ); } return $this->cacheManager; } /** * Initialize base inline translation model * * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Translation\Model\ResourceModel\StringUtilsFactory $resource * @param \Zend_Filter_Interface $inputFilter * @param \Magento\Framework\App\State $appState * @param \Magento\Framework\App\Cache\TypeListInterface $appCache * @param \Magento\Framework\Translate\InlineInterface $translateInline * @param array $relatedCacheTypes */ public function __construct( \Magento\Translation\Model\ResourceModel\StringUtilsFactory $resource, \Magento\Store\Model\StoreManagerInterface $storeManager, \Zend_Filter_Interface $inputFilter, \Magento\Framework\App\State $appState, \Magento\Framework\App\Cache\TypeListInterface $appCache, \Magento\Framework\Translate\InlineInterface $translateInline, array $relatedCacheTypes = [] ) { $this->_resourceFactory = $resource; $this->_storeManager = $storeManager; $this->_inputFilter = $inputFilter; $this->_appState = $appState; $this->_appCache = $appCache; $this->_translateInline = $translateInline; $this->relatedCacheTypes = $relatedCacheTypes; } /** * Parse and save edited translation * * @param array $translateParams * @return array */ public function processAjaxPost(array $translateParams) { if (!$this->_translateInline->isAllowed()) { return ['inline' => 'not allowed']; } if (!empty($this->relatedCacheTypes)) { $this->_appCache->invalidate($this->relatedCacheTypes); } $this->_validateTranslationParams($translateParams); $this->_filterTranslationParams($translateParams, ['custom']); /** @var $validStoreId int */ $validStoreId = $this->_storeManager->getStore()->getId(); /** @var $resource \Magento\Translation\Model\ResourceModel\StringUtils */ $resource = $this->_resourceFactory->create(); foreach ($translateParams as $param) { if ($this->_appState->getAreaCode() == \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE) { $storeId = 0; } else { if (empty($param['perstore'])) { $resource->deleteTranslate($param['original'], null, false); $storeId = 0; } else { $storeId = $validStoreId; } } $resource->saveTranslate($param['original'], $param['custom'], null, $storeId); } return $this->getCacheManger()->updateAndGetTranslations(); } /** * Validate the structure of translation parameters * * @param array $translateParams * @return void * @throws \InvalidArgumentException */ protected function _validateTranslationParams(array $translateParams) { foreach ($translateParams as $param) { if (!is_array($param) || !isset($param['original']) || !isset($param['custom'])) { throw new \InvalidArgumentException( 'Both original and custom phrases are required for inline translation.' ); } } } /** * Apply input filter to values of translation parameters * * @param array &$translateParams * @param array $fieldNames Names of fields values of which are to be filtered * @return void */ protected function _filterTranslationParams(array &$translateParams, array $fieldNames) { foreach ($translateParams as &$param) { foreach ($fieldNames as $fieldName) { $param[$fieldName] = $this->_inputFilter->filter($param[$fieldName]); } } } /** * Replace html body with translation wrapping. * * @param string $body * @return string */ public function processResponseBodyString($body) { $this->_content = $body; $this->_specialTags(); $this->_tagAttributes(); $this->_otherText(); return $this->_content; } /** * Returns the body content that is being parsed. * * @return string */ public function getContent() { return $this->_content; } /** * Sets the body content that is being parsed passed upon the passed in string. * * @param string $content * @return void */ public function setContent($content) { $this->_content = $content; } /** * Set flag about parsed content is Json * * @param bool $flag * @return $this */ public function setIsJson($flag) { $this->_isJson = $flag; return $this; } /** * Get attribute location. * * @param array $matches * @param array $options * @return string * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function _getAttributeLocation($matches, $options) { // return value should not be translated. return 'Tag attribute (ALT, TITLE, etc.)'; } /** * Get tag location * * @param array $matches * @param array $options * @return string * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function _getTagLocation($matches, $options) { $tagName = strtolower($options['tagName']); if (isset($options['tagList'][$tagName])) { return $options['tagList'][$tagName]; } return ucfirst($tagName) . ' Text'; } /** * Format translation for special tags. Adding translate mode attribute for vde requests. * * @param string $tagHtml * @param string $tagName * @param array $trArr * @return string */ protected function _applySpecialTagsFormat($tagHtml, $tagName, $trArr) { $specialTags = $tagHtml . '_getHtmlAttribute( self::DATA_TRANSLATE, '[' . htmlspecialchars(join(',', $trArr)) . ']' ); $additionalAttr = $this->_getAdditionalHtmlAttribute($tagName); if ($additionalAttr !== null) { $specialTags .= ' ' . $additionalAttr . '>'; } else { $specialTags .= '>' . strtoupper($tagName); } $specialTags .= ''; return $specialTags; } /** * Format translation for simple tags. Added translate mode attribute for vde requests. * * @param string $tagHtml * @param string $tagName * @param array $trArr * @return string */ protected function _applySimpleTagsFormat($tagHtml, $tagName, $trArr) { $simpleTags = substr( $tagHtml, 0, strlen($tagName) + 1 ) . ' ' . $this->_getHtmlAttribute( self::DATA_TRANSLATE, htmlspecialchars('[' . join(',', $trArr) . ']') ); $additionalAttr = $this->_getAdditionalHtmlAttribute($tagName); if ($additionalAttr !== null) { $simpleTags .= ' ' . $additionalAttr; } $simpleTags .= substr($tagHtml, strlen($tagName) + 1); return $simpleTags; } /** * Get translate data by regexp * * @param string $regexp * @param string &$text * @param string|array $locationCallback * @param array $options * @return array */ private function _getTranslateData($regexp, &$text, $locationCallback, $options = []) { $trArr = []; $next = 0; while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE, $next)) { $trArr[] = json_encode( [ 'shown' => htmlspecialchars_decode($matches[1][0]), 'translated' => htmlspecialchars_decode($matches[2][0]), 'original' => htmlspecialchars_decode($matches[3][0]), 'location' => htmlspecialchars_decode(call_user_func($locationCallback, $matches, $options)), ] ); $text = substr_replace($text, $matches[1][0], $matches[0][1], strlen($matches[0][0])); $next = $matches[0][1]; } return $trArr; } /** * Prepare tags inline translates * * @return void */ private function _tagAttributes() { $this->_prepareTagAttributesForContent($this->_content); } /** * Prepare tags inline translates for the content * * @param string &$content * @return void */ private function _prepareTagAttributesForContent(&$content) { $quoteHtml = $this->_getHtmlQuote(); $tagMatch = []; $nextTag = 0; $tagRegExp = '#<([a-z]+)\s*?[^>]+?((' . self::REGEXP_TOKEN . ')[^>]*?)+\\\\?/?>#iS'; while (preg_match($tagRegExp, $content, $tagMatch, PREG_OFFSET_CAPTURE, $nextTag)) { $tagHtml = $tagMatch[0][0]; $matches = []; $attrRegExp = '#' . self::REGEXP_TOKEN . '#S'; $trArr = $this->_getTranslateData($attrRegExp, $tagHtml, [$this, '_getAttributeLocation']); if ($trArr) { $transRegExp = '# ' . $this->_getHtmlAttribute( self::DATA_TRANSLATE, '\[([^' . preg_quote($quoteHtml) . ']*)]' ) . '#i'; if (preg_match($transRegExp, $tagHtml, $matches)) { $tagHtml = str_replace($matches[0], '', $tagHtml); $trAttr = ' ' . $this->_getHtmlAttribute( self::DATA_TRANSLATE, '[' . htmlspecialchars($matches[1]) . ',' . str_replace("\"", "'", join(',', $trArr)) . ']' ); } else { $trAttr = ' ' . $this->_getHtmlAttribute( self::DATA_TRANSLATE, '[' . str_replace("\"", "'", join(',', $trArr)) . ']' ); } $trAttr = $this->_addTranslateAttribute($trAttr); $tagHtml = substr_replace($tagHtml, $trAttr, strlen($tagMatch[1][0]) + 1, 1); $content = substr_replace($content, $tagHtml, $tagMatch[0][1], strlen($tagMatch[0][0])); } $nextTag = $tagMatch[0][1] + strlen($tagHtml); } } /** * Get html element attribute * * @param string $name * @param string $value * @return string */ private function _getHtmlAttribute($name, $value) { return $name . '=' . $this->_getHtmlQuote() . $value . $this->_getHtmlQuote(); } /** * Add data-translate-mode attribute * * @param string $trAttr * @return string */ private function _addTranslateAttribute($trAttr) { $translateAttr = $trAttr; $additionalAttr = $this->_getAdditionalHtmlAttribute(); if ($additionalAttr !== null) { $translateAttr .= ' ' . $additionalAttr . ' '; } return $translateAttr; } /** * Get html quote symbol * * @return string */ private function _getHtmlQuote() { if ($this->_isJson) { return '\"'; } else { return '"'; } } /** * Prepare special tags * * @return void */ private function _specialTags() { $this->_translateTags($this->_content, $this->_allowedTagsGlobal, '_applySpecialTagsFormat'); $this->_translateTags($this->_content, $this->_allowedTagsSimple, '_applySimpleTagsFormat'); } /** * Prepare simple tags * * @param string &$content * @param array $tagsList * @param string|array $formatCallback * @return void */ private function _translateTags(&$content, $tagsList, $formatCallback) { $nextTag = 0; $tagRegExpBody = '#<(body)(/?>| \s*[^>]*+/?>)#iSU'; $tags = implode('|', array_keys($tagsList)); $tagRegExp = '#<(' . $tags . ')(/?>| \s*[^>]*+/?>)#iSU'; $tagMatch = []; $headTranslateTags = ''; while (preg_match($tagRegExp, $content, $tagMatch, PREG_OFFSET_CAPTURE, $nextTag)) { $tagName = strtolower($tagMatch[1][0]); if (substr($tagMatch[0][0], -2) == '/>') { $tagClosurePos = $tagMatch[0][1] + strlen($tagMatch[0][0]); } else { $tagClosurePos = $this->_findEndOfTag($content, $tagName, $tagMatch[0][1]); } if ($tagClosurePos === false) { $nextTag += strlen($tagMatch[0][0]); continue; } $tagLength = $tagClosurePos - $tagMatch[0][1]; $tagStartLength = strlen($tagMatch[0][0]); $tagHtml = $tagMatch[0][0] . substr( $content, $tagMatch[0][1] + $tagStartLength, $tagLength - $tagStartLength ); $tagClosurePos = $tagMatch[0][1] + strlen($tagHtml); $trArr = $this->_getTranslateData( '#' . self::REGEXP_TOKEN . '#iS', $tagHtml, [$this, '_getTagLocation'], ['tagName' => $tagName, 'tagList' => $tagsList] ); if (!empty($trArr)) { $trArr = array_unique($trArr); $tagBodyMatch = []; preg_match($tagRegExpBody, $content, $tagBodyMatch, PREG_OFFSET_CAPTURE); if (!empty($tagBodyMatch)) { $tagBodyOpenStartPosition = $tagBodyMatch[0][1]; if (array_key_exists($tagName, $this->_allowedTagsGlobal) && $tagBodyOpenStartPosition > $tagMatch[0][1] ) { $tagHtmlHead = call_user_func([$this, $formatCallback], $tagHtml, $tagName, $trArr); $headTranslateTags .= substr($tagHtmlHead, strlen($tagHtml)); } else { $tagHtml = call_user_func([$this, $formatCallback], $tagHtml, $tagName, $trArr); } } $tagClosurePos = $tagMatch[0][1] + strlen($tagHtml); $content = substr_replace($content, $tagHtml, $tagMatch[0][1], $tagLength); } $nextTag = $tagClosurePos; } if ($headTranslateTags) { $tagBodyMatch = []; preg_match($tagRegExpBody, $content, $tagBodyMatch, PREG_OFFSET_CAPTURE); $tagBodyOpenStartPosition = $tagBodyMatch[0][1]; $openTagBodyEndPosition = $tagBodyOpenStartPosition + strlen($tagBodyMatch[0][0]); $content = substr($content, 0, $openTagBodyEndPosition) . $headTranslateTags . substr($content, $openTagBodyEndPosition); } } /** * Find end of tag * * @param string $body * @param string $tagName * @param int $from * @return bool|int return false if end of tag is not found */ private function _findEndOfTag($body, $tagName, $from) { $openTag = '<' . $tagName; $closeTag = ($this->_isJson ? '<\\/' : '#i', $body, $tagMatch, null, $end)) { return $end + strlen($tagMatch[0]); } else { return false; } } /** * Prepare other text inline translates * * @return void */ private function _otherText() { $next = 0; $matches = []; while (preg_match('#' . self::REGEXP_TOKEN . '#', $this->_content, $matches, PREG_OFFSET_CAPTURE, $next)) { $translateProperties = json_encode( [ 'shown' => $matches[1][0], 'translated' => $matches[2][0], 'original' => $matches[3][0], 'location' => 'Text', 'scope' => $matches[4][0], ], JSON_HEX_QUOT ); $spanHtml = $this->_getDataTranslateSpan( '[' . htmlspecialchars($translateProperties) . ']', $matches[1][0] ); $this->_content = substr_replace($this->_content, $spanHtml, $matches[0][1], strlen($matches[0][0])); $next = $matches[0][1] + strlen($spanHtml) - 1; } } /** * Returns the html span that contains the data translate attribute including vde specific translate mode attribute * * @param string $data * @param string $text * @return string */ protected function _getDataTranslateSpan($data, $text) { $translateSpan = '_getHtmlAttribute(self::DATA_TRANSLATE, $data); $additionalAttr = $this->_getAdditionalHtmlAttribute(); if ($additionalAttr !== null) { $translateSpan .= ' ' . $additionalAttr; } $translateSpan .= '>' . $text . ''; return $translateSpan; } /** * Add an additional html attribute if needed. * * @param mixed $tagName * @return string */ protected function _getAdditionalHtmlAttribute($tagName = null) { return $this->_translateInline->getAdditionalHtmlAttribute($tagName); } }