1<?php 2/** 3 * Deepl Autotranslate Plugin 4 * 5 * @author Jennifer Graul <me@netali.de> 6 */ 7 8if(!defined('DOKU_INC')) die(); 9 10use \dokuwiki\HTTP\DokuHTTPClient; 11use \dokuwiki\plugin\deeplautotranslate\MenuItem; 12 13class action_plugin_deeplautotranslate extends DokuWiki_Action_Plugin { 14 15 // manual mapping of ISO-languages to DeepL-languages to fix inconsistent naming 16 private $langs = [ 17 'bg' => 'BG', 18 'cs' => 'CS', 19 'da' => 'DA', 20 'de' => 'DE', 21 'de-informal' => 'DE', 22 'el' => 'EL', 23 'en' => 'EN-GB', 24 'es' => 'ES', 25 'et' => 'ET', 26 'fi' => 'FI', 27 'fr' => 'FR', 28 'hu' => 'HU', 29 'hu-formal' => 'HU', 30 'it' => 'IT', 31 'ja' => 'JA', 32 'lt' => 'LT', 33 'lv' => 'LV', 34 'nl' => 'NL', 35 'pl' => 'PL', 36 'pt' => 'PT-PT', 37 'ro' => 'RO', 38 'ru' => 'RU', 39 'sk' => 'SK', 40 'sl' => 'SL', 41 'sv' => 'SV', 42 'zh' => 'ZH' 43 ]; 44 45 /** 46 * Register its handlers with the DokuWiki's event controller 47 */ 48 public function register(Doku_Event_Handler $controller) { 49 $controller->register_hook('ACTION_ACT_PREPROCESS','BEFORE', $this, 'autotrans_direct'); 50 $controller->register_hook('COMMON_PAGETPL_LOAD','AFTER', $this, 'autotrans_editor'); 51 $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'add_menu_button'); 52 } 53 54 public function add_menu_button(Doku_Event $event) { 55 if ($event->data['view'] != 'page') return; 56 57 if (!$this->getConf('show_button')) return; 58 if (!$this->check_do_translation(true)) return; 59 60 array_splice($event->data['items'], -1, 0, [new MenuItem()]); 61 } 62 63 public function autotrans_direct(Doku_Event $event, $param) { 64 global $ID; 65 66 // check if action is show or translate 67 if ($event->data != 'show' and $event->data != 'translate') return; 68 69 // abort if action is translate and the translate button is disabled 70 if ($event->data == 'translate' and !$this->getConf('show_button')) return; 71 72 // do nothing on show action when mode is not direct 73 if ($event->data == 'show' and $this->get_mode() != 'direct') return; 74 75 // allow translation of existing pages is we are in the translate action 76 $allow_existing = ($event->data == 'translate'); 77 78 // reset action to show 79 $event->data = 'show'; 80 81 if (!$this->check_do_translation($allow_existing)) return; 82 83 $org_page_text = $this->get_org_page_text(); 84 $translated_text = $this->deepl_translate($org_page_text, $this->langs[$this->get_target_lang()]); 85 86 if ($translated_text === '') return; 87 88 saveWikiText($ID, $translated_text, 'Automatic translation'); 89 90 // reload the page after translation 91 send_redirect(wl($ID)); 92 } 93 94 public function autotrans_editor(Doku_Event $event, $param) { 95 if ($this->get_mode() != 'editor') return; 96 97 if (!$this->check_do_translation()) return; 98 99 $org_page_text = $this->get_org_page_text(); 100 101 $event->data['tpl'] = $this->deepl_translate($org_page_text, $this->langs[$this->get_target_lang()]); 102 } 103 104 private function get_mode(): string { 105 global $ID; 106 if ($this->getConf('editor_regex')) { 107 if (preg_match('/' . $this->getConf('editor_regex') . '/', $ID) === 1) return 'editor'; 108 } 109 if ($this->getConf('direct_regex')) { 110 if (preg_match('/' . $this->getConf('direct_regex') . '/', $ID) === 1) return 'direct'; 111 } 112 return $this->getConf('mode'); 113 } 114 115 private function get_target_lang(): string { 116 global $ID; 117 $split_id = explode(':', $ID); 118 return array_shift($split_id); 119 } 120 121 private function get_org_page_text(): string { 122 global $ID; 123 124 $split_id = explode(':', $ID); 125 array_shift($split_id); 126 $org_id = implode(':', $split_id); 127 128 return rawWiki($org_id); 129 } 130 131 private function check_do_translation($allow_existing = false): bool { 132 global $INFO; 133 global $ID; 134 135 // only translate if the current page does not exist 136 if ($INFO['exists'] and !$allow_existing) return false; 137 138 // permission check 139 $perm = auth_quickaclcheck($ID); 140 if (($INFO['exists'] and $perm < AUTH_EDIT) or (!$INFO['exists'] and $perm < AUTH_CREATE)) return false; 141 142 // skip blacklisted namespaces and pages 143 if ($this->getConf('blacklist_regex')) { 144 if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false; 145 } 146 147 $split_id = explode(':', $ID); 148 $lang_ns = array_shift($split_id); 149 // only translate if the current page is in a language namespace 150 if (!array_key_exists($lang_ns, $this->langs)) return false; 151 152 $org_id = implode(':', $split_id); 153 // check if the original page exists 154 if (!page_exists($org_id)) return false; 155 156 return true; 157 } 158 159 private function deepl_translate($text, $target_lang): string { 160 if (!$this->getConf('api_key')) return ''; 161 162 $text = $this->insert_ignore_tags($text); 163 164 $data = [ 165 'auth_key' => $this->getConf('api_key'), 166 'target_lang' => $target_lang, 167 'tag_handling' => 'xml', 168 'ignore_tags' => 'ignore,code,file,php', 169 'text' => $text 170 ]; 171 172 if ($this->getConf('api') == 'free') { 173 $url = 'https://api-free.deepl.com/v2/translate'; 174 } else { 175 $url = 'https://api.deepl.com/v2/translate'; 176 } 177 178 $http = new DokuHTTPClient(); 179 $raw_response = $http->post($url, $data); 180 181 // if any error occurred return an empty string 182 if ($http->status >= 400) return ''; 183 184 $json_response = json_decode($raw_response, true); 185 $translated_text = $json_response['translations'][0]['text']; 186 187 $translated_text = $this->remove_ignore_tags($translated_text); 188 189 return $translated_text; 190 } 191 192 private function insert_ignore_tags($text): string { 193 $text = str_replace('[[', '<ignore>[[', $text); 194 $text = str_replace('{{', '<ignore>{{', $text); 195 $text = str_replace(']]', ']]</ignore>', $text); 196 $text = str_replace('}}', '}}</ignore>', $text); 197 198 $ignored_expressions = explode(':', $this->getConf('ignored_expressions')); 199 200 foreach ($ignored_expressions as $expression) { 201 $text = str_replace($expression, '<ignore>' . $expression . '</ignore>', $text); 202 } 203 204 return $text; 205 } 206 207 private function remove_ignore_tags($text): string { 208 $text = str_replace('<ignore>[[', '[[', $text); 209 $text = str_replace('<ignore>{{', '{{', $text); 210 $text = str_replace(']]</ignore>', ']]', $text); 211 $text = str_replace('}}</ignore>', '}}', $text); 212 213 $ignored_expressions = explode(':', $this->getConf('ignored_expressions')); 214 215 foreach ($ignored_expressions as $expression) { 216 $text = str_replace('<ignore>' . $expression . '</ignore>', $expression, $text); 217 } 218 219 return $text; 220 } 221} 222 223