1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25:
26:
27:
28: 29: 30: 31: 32: 33: 34:
35: class Mage_Core_Model_Translate_Inline
36: {
37: 38: 39: 40: 41:
42: protected $_tokenRegex = '\{\{\{(.*?)\}\}\{\{(.*?)\}\}\{\{(.*?)\}\}\{\{(.*?)\}\}\}';
43:
44: 45: 46: 47: 48:
49: protected $_content;
50:
51: 52: 53: 54: 55:
56: protected $_isAllowed;
57:
58: 59: 60: 61: 62:
63: protected $_isScriptInserted = false;
64:
65: 66: 67: 68: 69:
70: protected $_isJson = false;
71:
72: 73: 74: 75: 76:
77: protected $_maxTranslateBlocks = 7;
78:
79: 80: 81: 82: 83:
84: protected $_allowedTagsGlobal = array(
85: 'script' => 'String in Javascript',
86: 'title' => 'Page title',
87: );
88:
89: 90: 91: 92: 93:
94: protected $_allowedTagsSimple = array(
95: 'legend' => 'Caption for the fieldset element',
96: 'label' => 'Label for an input element.',
97: 'button' => 'Push button',
98: 'a' => 'Link label',
99: 'b' => 'Bold text',
100: 'strong' => 'Strong emphasized text',
101: 'i' => 'Italic text',
102: 'em' => 'Emphasized text',
103: 'u' => 'Underlined text',
104: 'sup' => 'Superscript text',
105: 'sub' => 'Subscript text',
106: 'span' => 'Span element',
107: 'small' => 'Smaller text',
108: 'big' => 'Bigger text',
109: 'address' => 'Contact information',
110: 'blockquote' => 'Long quotation',
111: 'q' => 'Short quotation',
112: 'cite' => 'Citation',
113: 'dt' => 'Item in a definition list',
114: 'dd' => 'Item description in a definition list.',
115: 'caption' => 'Table caption',
116: 'th' => 'Header cell in a table',
117: 'abbr' => 'Abbreviated phrase',
118: 'acronym' => 'An acronym',
119: 'var' => 'Variable part of a text',
120: 'dfn' => 'Term',
121: 'strike' => 'Strikethrough text',
122: 'del' => 'Deleted text',
123: 'ins' => 'Inserted text',
124: 'h1' => 'Heading level 1',
125: 'h2' => 'Heading level 2',
126: 'h3' => 'Heading level 3',
127: 'h4' => 'Heading level 4',
128: 'h5' => 'Heading level 5',
129: 'h6' => 'Heading level 6',
130: 'p' => 'Paragraph',
131: 'pre' => 'Preformatted text',
132: 'center' => 'Centered text',
133: 'select' => 'List options',
134: );
135:
136: 137: 138: 139: 140: 141:
142: public function isAllowed($store = null)
143: {
144: if (is_null($store)) {
145: $store = Mage::app()->getStore();
146: }
147: if (!$store instanceof Mage_Core_Model_Store) {
148: $store = Mage::app()->getStore($store);
149: }
150:
151: if (is_null($this->_isAllowed)) {
152: if (Mage::getDesign()->getArea() == 'adminhtml') {
153: $active = Mage::getStoreConfigFlag('dev/translate_inline/active_admin', $store);
154: } else {
155: $active = Mage::getStoreConfigFlag('dev/translate_inline/active', $store);
156: }
157:
158: $this->_isAllowed = $active && Mage::helper('core')->isDevAllowed($store);
159: }
160:
161: $translate = Mage::getSingleton('core/translate');
162:
163:
164: return $translate->getTranslateInline() && $this->_isAllowed;
165: }
166:
167: 168: 169: 170: 171: 172:
173: public function processAjaxPost($translate)
174: {
175: if (!$this->isAllowed()) {
176: return $this;
177: }
178:
179:
180: $resource = Mage::getResourceModel('core/translate_string');
181: foreach ($translate as $t) {
182: if (Mage::getDesign()->getArea() == 'adminhtml') {
183: $storeId = 0;
184: } else if (empty($t['perstore'])) {
185: $resource->deleteTranslate($t['original'], null, false);
186: $storeId = 0;
187: } else {
188: $storeId = Mage::app()->getStore()->getId();
189: }
190:
191: $resource->saveTranslate($t['original'], $t['custom'], null, $storeId);
192: }
193:
194: return $this;
195: }
196:
197: 198: 199: 200: 201: 202:
203: public function stripInlineTranslations(&$body)
204: {
205: if (is_array($body)) {
206: foreach ($body as &$part) {
207: $this->stripInlineTranslations($part);
208: }
209: } else if (is_string($body)) {
210: $body = preg_replace('#' . $this->_tokenRegex . '#', '$1', $body);
211: }
212: return $this;
213: }
214:
215: 216: 217: 218: 219: 220:
221: public function processResponseBody(&$body)
222: {
223: if (!$this->isAllowed()) {
224: if (Mage::getDesign()->getArea() == 'adminhtml') {
225: $this->stripInlineTranslations($body);
226: }
227: return $this;
228: }
229:
230: if (is_array($body)) {
231: foreach ($body as &$part) {
232: $this->processResponseBody($part);
233: }
234: } else if (is_string($body)) {
235: $this->_content = $body;
236:
237: $this->_specialTags();
238: $this->_tagAttributes();
239: $this->_otherText();
240: $this->_insertInlineScriptsHtml();
241:
242: $body = $this->_content;
243: }
244:
245: return $this;
246: }
247:
248: 249: 250:
251: protected function _insertInlineScriptsHtml()
252: {
253: if ($this->_isScriptInserted || stripos($this->_content, '</body>')===false) {
254: return;
255: }
256:
257: $baseJsUrl = Mage::getBaseUrl('js');
258: $url_prefix = Mage::app()->getStore()->isAdmin() ? 'adminhtml' : 'core';
259: $ajaxUrl = Mage::getUrl($url_prefix . '/ajax/translate',
260: array('_secure'=>Mage::app()->getStore()->isCurrentlySecure()));
261: $trigImg = Mage::getDesign()->getSkinUrl('images/fam_book_open.png');
262:
263: ob_start();
264: $magentoSkinUrl = Mage::getDesign()->getSkinUrl('lib/prototype/windows/themes/magento.css');
265: ?>
266: <!-- script type="text/javascript" src="<?php echo $baseJsUrl ?>prototype/effects.js"></script -->
267: <script type="text/javascript" src="<?php echo $baseJsUrl ?>prototype/window.js"></script>
268: <link rel="stylesheet" type="text/css" href="<?php echo $baseJsUrl ?>prototype/windows/themes/default.css"/>
269: <link rel="stylesheet" type="text/css" href="<?php echo $magentoSkinUrl; ?>"/>
270:
271: <script type="text/javascript" src="<?php echo $baseJsUrl ?>mage/translate_inline.js"></script>
272: <link rel="stylesheet" type="text/css" href="<?php echo $baseJsUrl ?>mage/translate_inline.css"/>
273:
274: <div id="translate-inline-trig"><img src="<?php echo $trigImg ?>" alt="[TR]"/></div>
275: <script type="text/javascript">
276: new TranslateInline('translate-inline-trig', '<?php echo $ajaxUrl ?>', '<?php
277: echo Mage::getDesign()->getArea() ?>');
278: </script>
279: <?php
280: $html = ob_get_clean();
281:
282: $this->_content = str_ireplace('</body>', $html . '</body>', $this->_content);
283:
284: $this->_isScriptInserted = true;
285: }
286:
287: 288: 289: 290: 291: 292:
293: protected function _escape($string)
294: {
295: return str_replace("'", "\\'", htmlspecialchars($string));
296: }
297:
298: 299: 300: 301: 302: 303: 304:
305: protected function _getAttributeLocation($matches, $options)
306: {
307: return 'Tag attribute (ALT, TITLE, etc.)';
308: }
309:
310: 311: 312: 313: 314: 315: 316:
317: protected function _getTagLocation($matches, $options)
318: {
319: $tagName = strtolower($options['tagName']);
320:
321: if (isset($options['tagList'][$tagName])) {
322: return $options['tagList'][$tagName];
323: }
324:
325: return ucfirst($tagName) . ' Text';
326: }
327:
328: 329: 330: 331: 332: 333: 334: 335: 336:
337: protected function _getTranslateData($regexp, &$text, $locationCallback, $options = array())
338: {
339: $trArr = array();
340: $next = 0;
341: while (preg_match($regexp, $text, $m, PREG_OFFSET_CAPTURE, $next)) {
342: $trArr[] = json_encode(array(
343: 'shown' => $m[1][0],
344: 'translated' => $m[2][0],
345: 'original' => $m[3][0],
346: 'location' => call_user_func($locationCallback, $m, $options),
347: 'scope' => $m[4][0],
348: ));
349: $text = substr_replace($text, $m[1][0], $m[0][1], strlen($m[0][0]));
350: $next = $m[0][1];
351: }
352: return $trArr;
353: }
354:
355:
356: 357: 358: 359:
360: protected function _tagAttributes()
361: {
362: $this->_prepareTagAttributesForContent($this->_content);
363: }
364:
365: 366: 367: 368: 369:
370: protected function _prepareTagAttributesForContent(&$content)
371: {
372: if ($this->getIsJson()) {
373: $quoteHtml = '\"';
374: } else {
375: $quoteHtml = '"';
376: }
377:
378: $tagMatch = array();
379: $nextTag = 0;
380: $tagRegExp = '#<([a-z]+)\s*?[^>]+?((' . $this->_tokenRegex . ')[^>]*?)+/?>#i';
381: while (preg_match($tagRegExp, $content, $tagMatch, PREG_OFFSET_CAPTURE, $nextTag)) {
382: $next = 0;
383: $tagHtml = $tagMatch[0][0];
384: $m = array();
385: $attrRegExp = '#' . $this->_tokenRegex . '#';
386: $trArr = $this->_getTranslateData($attrRegExp, $tagHtml, array($this, '_getAttributeLocation'));
387: if ($trArr) {
388: $transRegExp = '# translate=' . $quoteHtml . '\[([^'.preg_quote($quoteHtml).']*)]' . $quoteHtml . '#i';
389: if (preg_match($transRegExp, $tagHtml, $m)) {
390: $tagHtml = str_replace($m[0], '', $tagHtml);
391: $trAttr = ' translate=' . $quoteHtml
392: . htmlspecialchars('[' . $m[1] . ',' . join(',', $trArr) . ']') . $quoteHtml;
393: } else {
394: $trAttr = ' translate=' . $quoteHtml
395: . htmlspecialchars('[' . join(',', $trArr) . ']') . $quoteHtml;
396: }
397: $content = substr_replace($content, $tagHtml, $tagMatch[0][1], strlen($tagMatch[0][0]));
398: }
399: $nextTag = $tagMatch[0][1] + strlen($tagHtml);
400: }
401: }
402:
403: 404: 405: 406: 407:
408: protected function _getHtmlQuote()
409: {
410: if ($this->getIsJson()) {
411: return '\"';
412: } else {
413: return '"';
414: }
415: }
416:
417: 418: 419:
420: protected function _specialTags() {
421: $this->_translateTags($this->_content, $this->_allowedTagsGlobal, '_applySpecialTagsFormat', false);
422: $this->_translateTags($this->_content, $this->_allowedTagsSimple, '_applySimpleTagsFormat', true);
423: }
424:
425: 426: 427: 428: 429: 430: 431: 432:
433: protected function _applySpecialTagsFormat($tagHtml, $tagName, $trArr)
434: {
435: return $tagHtml . '<span class="translate-inline-' . $tagName
436: . '" translate='
437: . $this->_getHtmlQuote()
438: . htmlspecialchars('[' . join(',', $trArr) . ']')
439: . $this->_getHtmlQuote() . '>'
440: . strtoupper($tagName) . '</span>';
441: }
442:
443: 444: 445: 446: 447: 448: 449: 450:
451: protected function _applySimpleTagsFormat($tagHtml, $tagName, $trArr)
452: {
453: return substr($tagHtml, 0, strlen($tagName) + 1)
454: . ' translate='
455: . $this->_getHtmlQuote() . htmlspecialchars( '[' . join(',', $trArr) . ']')
456: . $this->_getHtmlQuote()
457: . substr($tagHtml, strlen($tagName) + 1);
458: }
459:
460: 461: 462: 463: 464: 465: 466: 467:
468: protected function _translateTags(&$body, $tagsList, $formatCallback, $isNeedTranslateAttributes)
469: {
470: if ($this->getIsJson()) {
471: $quoteHtml = '\"';
472: } else {
473: $quoteHtml = '"';
474: }
475:
476: $nextTag = 0;
477:
478: $tags = implode('|', array_keys($tagsList));
479: $tagRegExp = '#<(' . $tags . ')(\s*[^>]*>)#i';
480:
481: $tagMatch = array();
482: while (preg_match($tagRegExp, $body, $tagMatch, PREG_OFFSET_CAPTURE, $nextTag)) {
483: $tagName = strtolower($tagMatch[1][0]);
484: $tagClosurePos = $this->findEndOfTag($body, $tagName, $tagMatch[0][1]);
485: if ($tagClosurePos === false) {
486: $nextTag += strlen($tagMatch[0][0]);
487: continue;
488: }
489:
490: $tagLength = $tagClosurePos - $tagMatch[0][1];
491:
492: $tagStartLength = strlen($tagMatch[0][0]);
493:
494: $tagHtml = $tagMatch[0][0] ;
495: $tagEnd = substr($body, $tagMatch[0][1] + $tagStartLength, $tagLength - $tagStartLength);
496:
497: if ($isNeedTranslateAttributes
498: && preg_match_all('#' . $this->_tokenRegex . '#', $tagEnd, $m)
499: && count($m[0]) > $this->_maxTranslateBlocks
500: ) {
501: $this->_translateTags($tagEnd, $tagsList, $formatCallback, $isNeedTranslateAttributes);
502: }
503:
504: if ($isNeedTranslateAttributes) {
505: $this->_prepareTagAttributesForContent($tagEnd);
506: }
507: $tagHtml .= $tagEnd;
508:
509: $trArr = $this->_getTranslateData(
510: '#' . $this->_tokenRegex . '#i',
511: $tagHtml,
512: array($this, '_getTagLocation'),
513: array(
514: 'tagName' => $tagName,
515: 'tagList' => $tagsList
516: )
517: );
518:
519: if (!empty($trArr)) {
520: $trArr = array_unique($trArr);
521: $tagHtml = call_user_func(array($this, $formatCallback), $tagHtml, $tagName, $trArr);
522:
523: $body = substr_replace($body, $tagHtml, $tagMatch[0][1], $tagLength);
524: }
525: $nextTag = $tagClosurePos;
526: }
527: }
528:
529: 530: 531: 532: 533: 534: 535: 536:
537: private function findEndOfTag($body, $tagName, $from)
538: {
539: $openTag = '<' . $tagName;
540: $closeTag = '</' . $tagName;
541: $end = $from + strlen($openTag);
542: $length = $end - $from;
543: while (substr_count($body, $openTag, $from, $length) != substr_count($body, $closeTag, $from, $length)) {
544: $end = strpos($body, $closeTag, $end + strlen($closeTag) - 1);
545: if ($end === false) {
546: return false;
547: }
548: $length = $end - $from + strlen($closeTag);
549: }
550: if (preg_match('#<\/' . $tagName .'\s*?>#i', $body, $tagMatch, null, $end)) {
551: return $end + strlen($tagMatch[0]);
552: } else {
553: return false;
554: }
555: }
556:
557: 558: 559:
560: protected function _otherText()
561: {
562: if ($this->getIsJson()) {
563: $quoteHtml = '\"';
564: } else {
565: $quoteHtml = '"';
566: }
567:
568: $next = 0;
569: $m = array();
570: while (preg_match('#' . $this->_tokenRegex . '#', $this->_content, $m, PREG_OFFSET_CAPTURE, $next)) {
571: $tr = json_encode(array(
572: 'shown' => $m[1][0],
573: 'translated' => $m[2][0],
574: 'original' => $m[3][0],
575: 'location' => 'Text',
576: 'scope' => $m[4][0],
577: ));
578:
579: $spanHtml = '<span translate=' . $quoteHtml . htmlspecialchars('[' . $tr . ']') . $quoteHtml
580: . '>' . $m[1][0] . '</span>';
581: $this->_content = substr_replace($this->_content, $spanHtml, $m[0][1], strlen($m[0][0]));
582: $next = $m[0][1] + strlen($spanHtml) - 1;
583: }
584:
585: }
586:
587: 588: 589: 590: 591: 592:
593: public function getIsAjaxRequest()
594: {
595: return (bool)Mage::app()->getRequest()->getQuery('isAjax');
596: }
597:
598: 599: 600: 601: 602: 603: 604:
605: public function setIsAjaxRequest($flag)
606: {
607: Mage::app()->getRequest()->setQuery('isAjax', intval((bool)$flag));
608: return $this;
609: }
610:
611: 612: 613: 614: 615:
616: public function getIsJson()
617: {
618: return $this->_isJson;
619: }
620:
621: 622: 623: 624: 625: 626:
627: public function setIsJson($flag)
628: {
629: $this->_isJson = (bool)$flag;
630: return $this;
631: }
632: }
633: