13832d0abSNetali<?php 23832d0abSNetali/** 33832d0abSNetali * Deepl Autotranslate Plugin 43832d0abSNetali * 53832d0abSNetali * @author Jennifer Graul <me@netali.de> 63832d0abSNetali */ 73832d0abSNetali 83832d0abSNetaliif(!defined('DOKU_INC')) die(); 93832d0abSNetali 1081931e50SNetaliuse \dokuwiki\HTTP\DokuHTTPClient; 113c636ad3SNetaliuse \dokuwiki\plugin\deeplautotranslate\MenuItem; 1281931e50SNetali 133832d0abSNetaliclass action_plugin_deeplautotranslate extends DokuWiki_Action_Plugin { 143832d0abSNetali 153832d0abSNetali // manual mapping of ISO-languages to DeepL-languages to fix inconsistent naming 163832d0abSNetali private $langs = [ 173832d0abSNetali 'bg' => 'BG', 183832d0abSNetali 'cs' => 'CS', 193832d0abSNetali 'da' => 'DA', 203832d0abSNetali 'de' => 'DE', 213832d0abSNetali 'de-informal' => 'DE', 223832d0abSNetali 'el' => 'EL', 233832d0abSNetali 'en' => 'EN-GB', 243832d0abSNetali 'es' => 'ES', 253832d0abSNetali 'et' => 'ET', 263832d0abSNetali 'fi' => 'FI', 273832d0abSNetali 'fr' => 'FR', 283832d0abSNetali 'hu' => 'HU', 293832d0abSNetali 'hu-formal' => 'HU', 303832d0abSNetali 'it' => 'IT', 313832d0abSNetali 'ja' => 'JA', 323832d0abSNetali 'lt' => 'LT', 333832d0abSNetali 'lv' => 'LV', 343832d0abSNetali 'nl' => 'NL', 353832d0abSNetali 'pl' => 'PL', 363832d0abSNetali 'pt' => 'PT-PT', 373832d0abSNetali 'ro' => 'RO', 383832d0abSNetali 'ru' => 'RU', 393832d0abSNetali 'sk' => 'SK', 403832d0abSNetali 'sl' => 'SL', 413832d0abSNetali 'sv' => 'SV', 423832d0abSNetali 'zh' => 'ZH' 433832d0abSNetali ]; 443832d0abSNetali 453832d0abSNetali /** 463832d0abSNetali * Register its handlers with the DokuWiki's event controller 473832d0abSNetali */ 483832d0abSNetali public function register(Doku_Event_Handler $controller) { 49153e4498SNetali $controller->register_hook('ACTION_ACT_PREPROCESS','BEFORE', $this, 'preprocess'); 503832d0abSNetali $controller->register_hook('COMMON_PAGETPL_LOAD','AFTER', $this, 'autotrans_editor'); 513c636ad3SNetali $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'add_menu_button'); 523c636ad3SNetali } 533c636ad3SNetali 543c636ad3SNetali public function add_menu_button(Doku_Event $event) { 55153e4498SNetali global $ID; 56*bbb1fba9SNetali global $ACT; 57*bbb1fba9SNetali 58*bbb1fba9SNetali if ($ACT != 'show') return; 59153e4498SNetali 603c636ad3SNetali if ($event->data['view'] != 'page') return; 613c636ad3SNetali 623c636ad3SNetali if (!$this->getConf('show_button')) return; 63153e4498SNetali 64153e4498SNetali $split_id = explode(':', $ID); 65153e4498SNetali $lang_ns = array_shift($split_id); 66153e4498SNetali // check if we are in a language namespace 67153e4498SNetali if (array_key_exists($lang_ns, $this->langs)) { 68153e4498SNetali // in language namespace --> check if we should translate 693c636ad3SNetali if (!$this->check_do_translation(true)) return; 70153e4498SNetali } else { 71153e4498SNetali // not in language namespace --> check if we should show the push translate button 72153e4498SNetali if (!$this->check_do_push_translate()) return; 73153e4498SNetali } 743c636ad3SNetali 753c636ad3SNetali array_splice($event->data['items'], -1, 0, [new MenuItem()]); 763832d0abSNetali } 773832d0abSNetali 78153e4498SNetali public function preprocess(Doku_Event $event, $param): void { 793832d0abSNetali global $ID; 803c636ad3SNetali 813c636ad3SNetali // check if action is show or translate 823c636ad3SNetali if ($event->data != 'show' and $event->data != 'translate') return; 833c636ad3SNetali 84153e4498SNetali $split_id = explode(':', $ID); 85153e4498SNetali $lang_ns = array_shift($split_id); 86153e4498SNetali // check if we are in a language namespace 87153e4498SNetali if (array_key_exists($lang_ns, $this->langs)) { 88153e4498SNetali // in language namespace --> autotrans_direct 89153e4498SNetali $this->autotrans_direct($event); 90153e4498SNetali } else { 91153e4498SNetali // not in language namespace --> push translate 92153e4498SNetali $this->push_translate($event); 93153e4498SNetali } 94153e4498SNetali } 95153e4498SNetali 96153e4498SNetali private function autotrans_direct(Doku_Event $event): void { 97153e4498SNetali global $ID; 98153e4498SNetali 993c636ad3SNetali // abort if action is translate and the translate button is disabled 1003c636ad3SNetali if ($event->data == 'translate' and !$this->getConf('show_button')) return; 1013c636ad3SNetali 1023c636ad3SNetali // do nothing on show action when mode is not direct 1033c636ad3SNetali if ($event->data == 'show' and $this->get_mode() != 'direct') return; 1043c636ad3SNetali 1053c636ad3SNetali // allow translation of existing pages is we are in the translate action 1063c636ad3SNetali $allow_existing = ($event->data == 'translate'); 1073c636ad3SNetali 1083c636ad3SNetali // reset action to show 1093c636ad3SNetali $event->data = 'show'; 1103c636ad3SNetali 111153e4498SNetali if (!$this->check_do_translation($allow_existing)) { 112153e4498SNetali send_redirect(wl($ID)); 113153e4498SNetali return; 114153e4498SNetali } 1153832d0abSNetali 1163832d0abSNetali $org_page_text = $this->get_org_page_text(); 1173832d0abSNetali $translated_text = $this->deepl_translate($org_page_text, $this->langs[$this->get_target_lang()]); 1183832d0abSNetali 119153e4498SNetali if ($translated_text === '') { 120153e4498SNetali send_redirect(wl($ID)); 121153e4498SNetali return; 122153e4498SNetali } 1233832d0abSNetali 1243832d0abSNetali saveWikiText($ID, $translated_text, 'Automatic translation'); 1253832d0abSNetali 126153e4498SNetali msg($this->getLang('msg_translation_success'), 1); 127153e4498SNetali 1283c636ad3SNetali // reload the page after translation 1293c636ad3SNetali send_redirect(wl($ID)); 1303832d0abSNetali } 1313832d0abSNetali 132153e4498SNetali public function autotrans_editor(Doku_Event $event, $param): void { 1333832d0abSNetali if ($this->get_mode() != 'editor') return; 1343832d0abSNetali 1353832d0abSNetali if (!$this->check_do_translation()) return; 1363832d0abSNetali 1373832d0abSNetali $org_page_text = $this->get_org_page_text(); 1383832d0abSNetali 1393832d0abSNetali $event->data['tpl'] = $this->deepl_translate($org_page_text, $this->langs[$this->get_target_lang()]); 1403832d0abSNetali } 1413832d0abSNetali 142153e4498SNetali private function push_translate(Doku_Event $event): void { 143153e4498SNetali global $ID; 144153e4498SNetali 145153e4498SNetali // check if action is translate 146153e4498SNetali if ($event->data != 'translate') return; 147153e4498SNetali 148153e4498SNetali // check if button is enabled 149153e4498SNetali if (!$this->getConf('show_button')) { 150153e4498SNetali send_redirect(wl($ID)); 151153e4498SNetali return; 152153e4498SNetali } 153153e4498SNetali 154153e4498SNetali if (!$this->check_do_push_translate()) { 155153e4498SNetali send_redirect(wl($ID)); 156153e4498SNetali return; 157153e4498SNetali } 158153e4498SNetali 159153e4498SNetali // push translate 160153e4498SNetali $push_langs = $this->get_push_langs(); 161153e4498SNetali $org_page_text = rawWiki($ID); 162153e4498SNetali foreach ($push_langs as $lang) { 163153e4498SNetali // skip invalid languages 164153e4498SNetali if (!array_key_exists($lang, $this->langs)) { 165153e4498SNetali msg($this->getLang('msg_translation_fail_invalid_lang') . $lang, -1); 166153e4498SNetali continue; 167153e4498SNetali } 168153e4498SNetali 169153e4498SNetali $lang_id = $lang . ':' . $ID; 170153e4498SNetali 171153e4498SNetali // check permissions 172153e4498SNetali $perm = auth_quickaclcheck($ID); 173153e4498SNetali $exists = page_exists($lang_id); 174153e4498SNetali if (($exists and $perm < AUTH_EDIT) or (!$exists and $perm < AUTH_CREATE)) { 175153e4498SNetali msg($this->getLang('msg_translation_fail_no_permissions') . $lang_id, -1); 176153e4498SNetali continue; 177153e4498SNetali } 178153e4498SNetali 179153e4498SNetali $translated_text = $this->deepl_translate($org_page_text, $this->langs[$lang]); 180153e4498SNetali saveWikiText($lang_id, $translated_text, 'Automatic push translation'); 181153e4498SNetali } 182153e4498SNetali 183153e4498SNetali msg($this->getLang('msg_translation_success'), 1); 184153e4498SNetali 185153e4498SNetali // reload the page after translation to clear the action 186153e4498SNetali send_redirect(wl($ID)); 187153e4498SNetali } 188153e4498SNetali 1893832d0abSNetali private function get_mode(): string { 1903832d0abSNetali global $ID; 1913832d0abSNetali if ($this->getConf('editor_regex')) { 1923832d0abSNetali if (preg_match('/' . $this->getConf('editor_regex') . '/', $ID) === 1) return 'editor'; 1933832d0abSNetali } 1943832d0abSNetali if ($this->getConf('direct_regex')) { 1953832d0abSNetali if (preg_match('/' . $this->getConf('direct_regex') . '/', $ID) === 1) return 'direct'; 1963832d0abSNetali } 1973832d0abSNetali return $this->getConf('mode'); 1983832d0abSNetali } 1993832d0abSNetali 2003832d0abSNetali private function get_target_lang(): string { 2013832d0abSNetali global $ID; 2023832d0abSNetali $split_id = explode(':', $ID); 2033832d0abSNetali return array_shift($split_id); 2043832d0abSNetali } 2053832d0abSNetali 2063832d0abSNetali private function get_org_page_text(): string { 2073832d0abSNetali global $ID; 2083832d0abSNetali 2093832d0abSNetali $split_id = explode(':', $ID); 2103832d0abSNetali array_shift($split_id); 2113832d0abSNetali $org_id = implode(':', $split_id); 2123832d0abSNetali 2133832d0abSNetali return rawWiki($org_id); 2143832d0abSNetali } 2153832d0abSNetali 2163c636ad3SNetali private function check_do_translation($allow_existing = false): bool { 2173832d0abSNetali global $INFO; 2183832d0abSNetali global $ID; 2193832d0abSNetali 2203c636ad3SNetali // only translate if the current page does not exist 2213c636ad3SNetali if ($INFO['exists'] and !$allow_existing) return false; 2223c636ad3SNetali 2233c636ad3SNetali // permission check 2243c636ad3SNetali $perm = auth_quickaclcheck($ID); 2253c636ad3SNetali if (($INFO['exists'] and $perm < AUTH_EDIT) or (!$INFO['exists'] and $perm < AUTH_CREATE)) return false; 2263c636ad3SNetali 2273832d0abSNetali // skip blacklisted namespaces and pages 2283832d0abSNetali if ($this->getConf('blacklist_regex')) { 2293832d0abSNetali if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false; 2303832d0abSNetali } 2313832d0abSNetali 2323832d0abSNetali $split_id = explode(':', $ID); 2333832d0abSNetali $lang_ns = array_shift($split_id); 2343832d0abSNetali // only translate if the current page is in a language namespace 2353832d0abSNetali if (!array_key_exists($lang_ns, $this->langs)) return false; 2363832d0abSNetali 2373832d0abSNetali $org_id = implode(':', $split_id); 2383832d0abSNetali // check if the original page exists 2393832d0abSNetali if (!page_exists($org_id)) return false; 2403832d0abSNetali 2413832d0abSNetali return true; 2423832d0abSNetali } 2433832d0abSNetali 244153e4498SNetali private function check_do_push_translate(): bool { 245153e4498SNetali global $ID; 246153e4498SNetali 247153e4498SNetali $push_langs = $this->get_push_langs(); 248153e4498SNetali // push_langs empty --> push_translate disabled --> abort 249153e4498SNetali if (empty($push_langs)) return false; 250153e4498SNetali 251153e4498SNetali // skip blacklisted namespaces and pages 252153e4498SNetali if ($this->getConf('blacklist_regex')) { 253153e4498SNetali // blacklist regex match --> abort 254153e4498SNetali if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false; 255153e4498SNetali } 256153e4498SNetali 257153e4498SNetali return true; 258153e4498SNetali } 259153e4498SNetali 2603832d0abSNetali private function deepl_translate($text, $target_lang): string { 261153e4498SNetali if (!trim($this->getConf('api_key'))) return ''; 2623832d0abSNetali 2633832d0abSNetali $text = $this->insert_ignore_tags($text); 2643832d0abSNetali 2653832d0abSNetali $data = [ 2663832d0abSNetali 'auth_key' => $this->getConf('api_key'), 2673832d0abSNetali 'target_lang' => $target_lang, 2683832d0abSNetali 'tag_handling' => 'xml', 269e4700ea0SNetali 'ignore_tags' => 'ignore,code,file,php', 2703832d0abSNetali 'text' => $text 2713832d0abSNetali ]; 2723832d0abSNetali 2733832d0abSNetali if ($this->getConf('api') == 'free') { 27481931e50SNetali $url = 'https://api-free.deepl.com/v2/translate'; 2753832d0abSNetali } else { 27681931e50SNetali $url = 'https://api.deepl.com/v2/translate'; 2773832d0abSNetali } 2783832d0abSNetali 27981931e50SNetali $http = new DokuHTTPClient(); 28081931e50SNetali $raw_response = $http->post($url, $data); 2813832d0abSNetali 2825f8ab21dSNetali if ($http->status >= 400) { 2835f8ab21dSNetali // add error messages 2845f8ab21dSNetali switch ($http->status) { 2855f8ab21dSNetali case 403: 2865f8ab21dSNetali msg($this->getLang('msg_translation_fail_bad_key'), -1); 2875f8ab21dSNetali break; 2885f8ab21dSNetali case 456: 2895f8ab21dSNetali msg($this->getLang('msg_translation_fail_quota_exceeded'), -1); 2905f8ab21dSNetali break; 2915f8ab21dSNetali default: 2925f8ab21dSNetali msg($this->getLang('msg_translation_fail'), -1); 2935f8ab21dSNetali break; 2945f8ab21dSNetali } 2955f8ab21dSNetali 2963832d0abSNetali // if any error occurred return an empty string 2975f8ab21dSNetali return ''; 2985f8ab21dSNetali } 2993832d0abSNetali 3003832d0abSNetali $json_response = json_decode($raw_response, true); 3013832d0abSNetali $translated_text = $json_response['translations'][0]['text']; 3023832d0abSNetali 3033832d0abSNetali $translated_text = $this->remove_ignore_tags($translated_text); 3043832d0abSNetali 3053832d0abSNetali return $translated_text; 3063832d0abSNetali } 3073832d0abSNetali 308153e4498SNetali private function get_push_langs(): array { 309153e4498SNetali $push_langs = trim($this->getConf('push_langs')); 310153e4498SNetali 311153e4498SNetali if ($push_langs === '') return array(); 312153e4498SNetali 313153e4498SNetali return explode(' ', $push_langs); 314153e4498SNetali } 315153e4498SNetali 3163832d0abSNetali private function insert_ignore_tags($text): string { 3173832d0abSNetali $text = str_replace('[[', '<ignore>[[', $text); 3183832d0abSNetali $text = str_replace('{{', '<ignore>{{', $text); 3193832d0abSNetali $text = str_replace(']]', ']]</ignore>', $text); 3203832d0abSNetali $text = str_replace('}}', '}}</ignore>', $text); 3215f8ab21dSNetali $text = str_replace("''", "<ignore>''</ignore>", $text); 3223832d0abSNetali 3233832d0abSNetali $ignored_expressions = explode(':', $this->getConf('ignored_expressions')); 3243832d0abSNetali 3253832d0abSNetali foreach ($ignored_expressions as $expression) { 3263832d0abSNetali $text = str_replace($expression, '<ignore>' . $expression . '</ignore>', $text); 3273832d0abSNetali } 3283832d0abSNetali 3293832d0abSNetali return $text; 3303832d0abSNetali } 3313832d0abSNetali 3323832d0abSNetali private function remove_ignore_tags($text): string { 3333832d0abSNetali $text = str_replace('<ignore>[[', '[[', $text); 3343832d0abSNetali $text = str_replace('<ignore>{{', '{{', $text); 3353832d0abSNetali $text = str_replace(']]</ignore>', ']]', $text); 3363832d0abSNetali $text = str_replace('}}</ignore>', '}}', $text); 3375f8ab21dSNetali $text = str_replace("<ignore>''</ignore>", "''", $text); 3385f8ab21dSNetali 3395f8ab21dSNetali // restore < and > for example from arrows (-->) in wikitext 3405f8ab21dSNetali $text = str_replace('>', '>', $text); 3415f8ab21dSNetali $text = str_replace('<', '<', $text); 3423832d0abSNetali 3433832d0abSNetali $ignored_expressions = explode(':', $this->getConf('ignored_expressions')); 3443832d0abSNetali 3453832d0abSNetali foreach ($ignored_expressions as $expression) { 3463832d0abSNetali $text = str_replace('<ignore>' . $expression . '</ignore>', $expression, $text); 3473832d0abSNetali } 3483832d0abSNetali 3493832d0abSNetali return $text; 3503832d0abSNetali } 3513832d0abSNetali} 3523832d0abSNetali 353