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; 56bbb1fba9SNetali global $ACT; 57*99da9a08SNetali global $conf; 58bbb1fba9SNetali 59bbb1fba9SNetali if ($ACT != 'show') return; 60153e4498SNetali 613c636ad3SNetali if ($event->data['view'] != 'page') return; 623c636ad3SNetali 633c636ad3SNetali if (!$this->getConf('show_button')) return; 64153e4498SNetali 65153e4498SNetali $split_id = explode(':', $ID); 66153e4498SNetali $lang_ns = array_shift($split_id); 67153e4498SNetali // check if we are in a language namespace 68153e4498SNetali if (array_key_exists($lang_ns, $this->langs)) { 69*99da9a08SNetali if($this->getConf('default_lang_in_ns') and $lang_ns === $conf['lang']) { 70*99da9a08SNetali // if the default lang is in a namespace and we are in that namespace --> check for push translation 71*99da9a08SNetali if (!$this->check_do_push_translate()) return; 72*99da9a08SNetali } else { 73153e4498SNetali // in language namespace --> check if we should translate 743c636ad3SNetali if (!$this->check_do_translation(true)) return; 75*99da9a08SNetali } 76153e4498SNetali } else { 77*99da9a08SNetali // do not show the button if we are not in a language namespace and the default language is in a namespace 78*99da9a08SNetali if($this->getConf('default_lang_in_ns')) return; 79*99da9a08SNetali // not in language namespace and default language is npt in a namespace --> check if we should show the push translate button 80153e4498SNetali if (!$this->check_do_push_translate()) return; 81153e4498SNetali } 823c636ad3SNetali 833c636ad3SNetali array_splice($event->data['items'], -1, 0, [new MenuItem()]); 843832d0abSNetali } 853832d0abSNetali 86153e4498SNetali public function preprocess(Doku_Event $event, $param): void { 873832d0abSNetali global $ID; 88*99da9a08SNetali global $conf; 893c636ad3SNetali 903c636ad3SNetali // check if action is show or translate 913c636ad3SNetali if ($event->data != 'show' and $event->data != 'translate') return; 923c636ad3SNetali 93153e4498SNetali $split_id = explode(':', $ID); 94153e4498SNetali $lang_ns = array_shift($split_id); 95153e4498SNetali // check if we are in a language namespace 96153e4498SNetali if (array_key_exists($lang_ns, $this->langs)) { 97*99da9a08SNetali if($this->getConf('default_lang_in_ns') and $lang_ns === $conf['lang']) { 98*99da9a08SNetali // if the default lang is in a namespace and we are in that namespace --> push translate 99*99da9a08SNetali $this->push_translate($event); 100*99da9a08SNetali } else { 101*99da9a08SNetali // in language namespace --> autotrans direct 102153e4498SNetali $this->autotrans_direct($event); 103*99da9a08SNetali } 104153e4498SNetali } else { 105153e4498SNetali // not in language namespace --> push translate 106153e4498SNetali $this->push_translate($event); 107153e4498SNetali } 108153e4498SNetali } 109153e4498SNetali 110153e4498SNetali private function autotrans_direct(Doku_Event $event): void { 111153e4498SNetali global $ID; 112153e4498SNetali 1133c636ad3SNetali // abort if action is translate and the translate button is disabled 1143c636ad3SNetali if ($event->data == 'translate' and !$this->getConf('show_button')) return; 1153c636ad3SNetali 1163c636ad3SNetali // do nothing on show action when mode is not direct 1173c636ad3SNetali if ($event->data == 'show' and $this->get_mode() != 'direct') return; 1183c636ad3SNetali 1193c636ad3SNetali // allow translation of existing pages is we are in the translate action 1203c636ad3SNetali $allow_existing = ($event->data == 'translate'); 1213c636ad3SNetali 1223c636ad3SNetali // reset action to show 1233c636ad3SNetali $event->data = 'show'; 1243c636ad3SNetali 125153e4498SNetali if (!$this->check_do_translation($allow_existing)) { 126153e4498SNetali send_redirect(wl($ID)); 127153e4498SNetali return; 128153e4498SNetali } 1293832d0abSNetali 1303832d0abSNetali $org_page_text = $this->get_org_page_text(); 1313832d0abSNetali $translated_text = $this->deepl_translate($org_page_text, $this->langs[$this->get_target_lang()]); 1323832d0abSNetali 133153e4498SNetali if ($translated_text === '') { 134153e4498SNetali send_redirect(wl($ID)); 135153e4498SNetali return; 136153e4498SNetali } 1373832d0abSNetali 1383832d0abSNetali saveWikiText($ID, $translated_text, 'Automatic translation'); 1393832d0abSNetali 140153e4498SNetali msg($this->getLang('msg_translation_success'), 1); 141153e4498SNetali 1423c636ad3SNetali // reload the page after translation 1433c636ad3SNetali send_redirect(wl($ID)); 1443832d0abSNetali } 1453832d0abSNetali 146153e4498SNetali public function autotrans_editor(Doku_Event $event, $param): void { 1473832d0abSNetali if ($this->get_mode() != 'editor') return; 1483832d0abSNetali 1493832d0abSNetali if (!$this->check_do_translation()) return; 1503832d0abSNetali 1513832d0abSNetali $org_page_text = $this->get_org_page_text(); 1523832d0abSNetali 1533832d0abSNetali $event->data['tpl'] = $this->deepl_translate($org_page_text, $this->langs[$this->get_target_lang()]); 1543832d0abSNetali } 1553832d0abSNetali 156153e4498SNetali private function push_translate(Doku_Event $event): void { 157153e4498SNetali global $ID; 158153e4498SNetali 159153e4498SNetali // check if action is translate 160153e4498SNetali if ($event->data != 'translate') return; 161153e4498SNetali 162153e4498SNetali // check if button is enabled 163153e4498SNetali if (!$this->getConf('show_button')) { 164153e4498SNetali send_redirect(wl($ID)); 165153e4498SNetali return; 166153e4498SNetali } 167153e4498SNetali 168153e4498SNetali if (!$this->check_do_push_translate()) { 169153e4498SNetali send_redirect(wl($ID)); 170153e4498SNetali return; 171153e4498SNetali } 172153e4498SNetali 173153e4498SNetali // push translate 174153e4498SNetali $push_langs = $this->get_push_langs(); 175153e4498SNetali $org_page_text = rawWiki($ID); 176153e4498SNetali foreach ($push_langs as $lang) { 177153e4498SNetali // skip invalid languages 178153e4498SNetali if (!array_key_exists($lang, $this->langs)) { 179153e4498SNetali msg($this->getLang('msg_translation_fail_invalid_lang') . $lang, -1); 180153e4498SNetali continue; 181153e4498SNetali } 182153e4498SNetali 183*99da9a08SNetali if ($this->getConf('default_lang_in_ns')) { 184*99da9a08SNetali // if default lang is in ns: replace language namespace in ID 185*99da9a08SNetali $split_id = explode(':', $ID); 186*99da9a08SNetali array_shift($split_id); 187*99da9a08SNetali $lang_id = implode(':', $split_id); 188*99da9a08SNetali $lang_id = $lang . ':' . $lang_id; 189*99da9a08SNetali } else { 190*99da9a08SNetali // if default lang is not in ns: add language namespace to ID 191153e4498SNetali $lang_id = $lang . ':' . $ID; 192*99da9a08SNetali } 193153e4498SNetali 194153e4498SNetali // check permissions 195153e4498SNetali $perm = auth_quickaclcheck($ID); 196153e4498SNetali $exists = page_exists($lang_id); 197153e4498SNetali if (($exists and $perm < AUTH_EDIT) or (!$exists and $perm < AUTH_CREATE)) { 198153e4498SNetali msg($this->getLang('msg_translation_fail_no_permissions') . $lang_id, -1); 199153e4498SNetali continue; 200153e4498SNetali } 201153e4498SNetali 202153e4498SNetali $translated_text = $this->deepl_translate($org_page_text, $this->langs[$lang]); 203153e4498SNetali saveWikiText($lang_id, $translated_text, 'Automatic push translation'); 204153e4498SNetali } 205153e4498SNetali 206153e4498SNetali msg($this->getLang('msg_translation_success'), 1); 207153e4498SNetali 208153e4498SNetali // reload the page after translation to clear the action 209153e4498SNetali send_redirect(wl($ID)); 210153e4498SNetali } 211153e4498SNetali 2123832d0abSNetali private function get_mode(): string { 2133832d0abSNetali global $ID; 2143832d0abSNetali if ($this->getConf('editor_regex')) { 2153832d0abSNetali if (preg_match('/' . $this->getConf('editor_regex') . '/', $ID) === 1) return 'editor'; 2163832d0abSNetali } 2173832d0abSNetali if ($this->getConf('direct_regex')) { 2183832d0abSNetali if (preg_match('/' . $this->getConf('direct_regex') . '/', $ID) === 1) return 'direct'; 2193832d0abSNetali } 2203832d0abSNetali return $this->getConf('mode'); 2213832d0abSNetali } 2223832d0abSNetali 2233832d0abSNetali private function get_target_lang(): string { 2243832d0abSNetali global $ID; 2253832d0abSNetali $split_id = explode(':', $ID); 2263832d0abSNetali return array_shift($split_id); 2273832d0abSNetali } 2283832d0abSNetali 2293832d0abSNetali private function get_org_page_text(): string { 2303832d0abSNetali global $ID; 231*99da9a08SNetali global $conf; 2323832d0abSNetali 2333832d0abSNetali $split_id = explode(':', $ID); 2343832d0abSNetali array_shift($split_id); 2353832d0abSNetali $org_id = implode(':', $split_id); 2363832d0abSNetali 237*99da9a08SNetali // if default lang is in ns: add default ns in front of org id 238*99da9a08SNetali if ($this->getConf('default_lang_in_ns')) { 239*99da9a08SNetali $org_id = $conf['lang'] . ':' . $org_id; 240*99da9a08SNetali } 241*99da9a08SNetali 2423832d0abSNetali return rawWiki($org_id); 2433832d0abSNetali } 2443832d0abSNetali 2453c636ad3SNetali private function check_do_translation($allow_existing = false): bool { 2463832d0abSNetali global $INFO; 2473832d0abSNetali global $ID; 248*99da9a08SNetali global $conf; 2493832d0abSNetali 2503c636ad3SNetali // only translate if the current page does not exist 2513c636ad3SNetali if ($INFO['exists'] and !$allow_existing) return false; 2523c636ad3SNetali 2533c636ad3SNetali // permission check 2543c636ad3SNetali $perm = auth_quickaclcheck($ID); 2553c636ad3SNetali if (($INFO['exists'] and $perm < AUTH_EDIT) or (!$INFO['exists'] and $perm < AUTH_CREATE)) return false; 2563c636ad3SNetali 2573832d0abSNetali // skip blacklisted namespaces and pages 2583832d0abSNetali if ($this->getConf('blacklist_regex')) { 2593832d0abSNetali if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false; 2603832d0abSNetali } 2613832d0abSNetali 2623832d0abSNetali $split_id = explode(':', $ID); 2633832d0abSNetali $lang_ns = array_shift($split_id); 2643832d0abSNetali // only translate if the current page is in a language namespace 2653832d0abSNetali if (!array_key_exists($lang_ns, $this->langs)) return false; 2663832d0abSNetali 2673832d0abSNetali $org_id = implode(':', $split_id); 268*99da9a08SNetali 269*99da9a08SNetali // if default lang is in ns: add default ns in front of org id 270*99da9a08SNetali if ($this->getConf('default_lang_in_ns')) { 271*99da9a08SNetali $org_id = $conf['lang'] . ':' . $org_id; 272*99da9a08SNetali } 273*99da9a08SNetali 2743832d0abSNetali // check if the original page exists 2753832d0abSNetali if (!page_exists($org_id)) return false; 2763832d0abSNetali 2773832d0abSNetali return true; 2783832d0abSNetali } 2793832d0abSNetali 280153e4498SNetali private function check_do_push_translate(): bool { 281153e4498SNetali global $ID; 282*99da9a08SNetali global $INFO; 283*99da9a08SNetali global $conf; 284*99da9a08SNetali 285*99da9a08SNetali if (!$INFO['exists']) return false; 286*99da9a08SNetali 287*99da9a08SNetali // if default language is in namespace: only allow push translation from that namespace 288*99da9a08SNetali if($this->getConf('default_lang_in_ns')) { 289*99da9a08SNetali $split_id = explode(':', $ID); 290*99da9a08SNetali $lang_ns = array_shift($split_id); 291*99da9a08SNetali 292*99da9a08SNetali if ($lang_ns !== $conf['lang']) return false; 293*99da9a08SNetali } 294153e4498SNetali 295153e4498SNetali $push_langs = $this->get_push_langs(); 296153e4498SNetali // push_langs empty --> push_translate disabled --> abort 297153e4498SNetali if (empty($push_langs)) return false; 298153e4498SNetali 299153e4498SNetali // skip blacklisted namespaces and pages 300153e4498SNetali if ($this->getConf('blacklist_regex')) { 301153e4498SNetali // blacklist regex match --> abort 302153e4498SNetali if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false; 303153e4498SNetali } 304153e4498SNetali 305153e4498SNetali return true; 306153e4498SNetali } 307153e4498SNetali 3083832d0abSNetali private function deepl_translate($text, $target_lang): string { 309153e4498SNetali if (!trim($this->getConf('api_key'))) return ''; 3103832d0abSNetali 3113832d0abSNetali $text = $this->insert_ignore_tags($text); 3123832d0abSNetali 3133832d0abSNetali $data = [ 3143832d0abSNetali 'auth_key' => $this->getConf('api_key'), 3153832d0abSNetali 'target_lang' => $target_lang, 3163832d0abSNetali 'tag_handling' => 'xml', 317057940f7SNetali 'ignore_tags' => 'ignore,php', 3183832d0abSNetali 'text' => $text 3193832d0abSNetali ]; 3203832d0abSNetali 3213832d0abSNetali if ($this->getConf('api') == 'free') { 32281931e50SNetali $url = 'https://api-free.deepl.com/v2/translate'; 3233832d0abSNetali } else { 32481931e50SNetali $url = 'https://api.deepl.com/v2/translate'; 3253832d0abSNetali } 3263832d0abSNetali 32781931e50SNetali $http = new DokuHTTPClient(); 32881931e50SNetali $raw_response = $http->post($url, $data); 3293832d0abSNetali 3305f8ab21dSNetali if ($http->status >= 400) { 3315f8ab21dSNetali // add error messages 3325f8ab21dSNetali switch ($http->status) { 3335f8ab21dSNetali case 403: 3345f8ab21dSNetali msg($this->getLang('msg_translation_fail_bad_key'), -1); 3355f8ab21dSNetali break; 3365f8ab21dSNetali case 456: 3375f8ab21dSNetali msg($this->getLang('msg_translation_fail_quota_exceeded'), -1); 3385f8ab21dSNetali break; 3395f8ab21dSNetali default: 3405f8ab21dSNetali msg($this->getLang('msg_translation_fail'), -1); 3415f8ab21dSNetali break; 3425f8ab21dSNetali } 3435f8ab21dSNetali 3443832d0abSNetali // if any error occurred return an empty string 3455f8ab21dSNetali return ''; 3465f8ab21dSNetali } 3473832d0abSNetali 3483832d0abSNetali $json_response = json_decode($raw_response, true); 3493832d0abSNetali $translated_text = $json_response['translations'][0]['text']; 3503832d0abSNetali 3513832d0abSNetali $translated_text = $this->remove_ignore_tags($translated_text); 3523832d0abSNetali 3533832d0abSNetali return $translated_text; 3543832d0abSNetali } 3553832d0abSNetali 356153e4498SNetali private function get_push_langs(): array { 357153e4498SNetali $push_langs = trim($this->getConf('push_langs')); 358153e4498SNetali 359153e4498SNetali if ($push_langs === '') return array(); 360153e4498SNetali 361153e4498SNetali return explode(' ', $push_langs); 362153e4498SNetali } 363153e4498SNetali 3643832d0abSNetali private function insert_ignore_tags($text): string { 3653832d0abSNetali $text = str_replace('[[', '<ignore>[[', $text); 3663832d0abSNetali $text = str_replace('{{', '<ignore>{{', $text); 3673832d0abSNetali $text = str_replace(']]', ']]</ignore>', $text); 3683832d0abSNetali $text = str_replace('}}', '}}</ignore>', $text); 3695f8ab21dSNetali $text = str_replace("''", "<ignore>''</ignore>", $text); 3703832d0abSNetali 371057940f7SNetali $text = preg_replace('/(<file[\s\S]*?>[\s\S]*?<\/file>)/', '<ignore>${1}</ignore>', $text); 372057940f7SNetali $text = preg_replace('/(<code[\s\S]*?>[\s\S]*?<\/code>)/', '<ignore>${1}</ignore>', $text); 373057940f7SNetali 3743832d0abSNetali $ignored_expressions = explode(':', $this->getConf('ignored_expressions')); 3753832d0abSNetali 3763832d0abSNetali foreach ($ignored_expressions as $expression) { 3773832d0abSNetali $text = str_replace($expression, '<ignore>' . $expression . '</ignore>', $text); 3783832d0abSNetali } 3793832d0abSNetali 3803832d0abSNetali return $text; 3813832d0abSNetali } 3823832d0abSNetali 3833832d0abSNetali private function remove_ignore_tags($text): string { 3843832d0abSNetali $text = str_replace('<ignore>[[', '[[', $text); 3853832d0abSNetali $text = str_replace('<ignore>{{', '{{', $text); 3863832d0abSNetali $text = str_replace(']]</ignore>', ']]', $text); 3873832d0abSNetali $text = str_replace('}}</ignore>', '}}', $text); 3885f8ab21dSNetali $text = str_replace("<ignore>''</ignore>", "''", $text); 3895f8ab21dSNetali 390057940f7SNetali $text = preg_replace('/<ignore>(<file[\s\S]*?>[\s\S]*?<\/file>)<\/ignore>/', '${1}', $text); 391057940f7SNetali $text = preg_replace('/<ignore>(<code[\s\S]*?>[\s\S]*?<\/code>)<\/ignore>/', '${1}', $text); 392057940f7SNetali 3935f8ab21dSNetali // restore < and > for example from arrows (-->) in wikitext 3945f8ab21dSNetali $text = str_replace('>', '>', $text); 3955f8ab21dSNetali $text = str_replace('<', '<', $text); 3963832d0abSNetali 3973832d0abSNetali $ignored_expressions = explode(':', $this->getConf('ignored_expressions')); 3983832d0abSNetali 3993832d0abSNetali foreach ($ignored_expressions as $expression) { 4003832d0abSNetali $text = str_replace('<ignore>' . $expression . '</ignore>', $expression, $text); 4013832d0abSNetali } 4023832d0abSNetali 4033832d0abSNetali return $text; 4043832d0abSNetali } 4053832d0abSNetali} 4063832d0abSNetali 407