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 16b33135efSNetali private $langs = array( 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', 428311ddaaSnetali 'uk' => 'UK', 433832d0abSNetali 'zh' => 'ZH' 44b33135efSNetali ); 453832d0abSNetali 463832d0abSNetali /** 473832d0abSNetali * Register its handlers with the DokuWiki's event controller 483832d0abSNetali */ 493832d0abSNetali public function register(Doku_Event_Handler $controller) { 50153e4498SNetali $controller->register_hook('ACTION_ACT_PREPROCESS','BEFORE', $this, 'preprocess'); 51b33135efSNetali $controller->register_hook('COMMON_PAGETPL_LOAD','AFTER', $this, 'pagetpl_load'); 52b33135efSNetali $controller->register_hook('COMMON_WIKIPAGE_SAVE','AFTER', $this, 'update_glossary'); 533c636ad3SNetali $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'add_menu_button'); 543c636ad3SNetali } 553c636ad3SNetali 560180404cSNetali public function add_menu_button(Doku_Event $event): void { 57153e4498SNetali global $ID; 58bbb1fba9SNetali global $ACT; 59bbb1fba9SNetali 60bbb1fba9SNetali if ($ACT != 'show') return; 61153e4498SNetali 623c636ad3SNetali if ($event->data['view'] != 'page') return; 633c636ad3SNetali 643c636ad3SNetali if (!$this->getConf('show_button')) return; 65153e4498SNetali 66b33135efSNetali // no translations for the glossary namespace 67b33135efSNetali if ($this->check_in_glossary_ns()) return; 68b33135efSNetali 69153e4498SNetali $split_id = explode(':', $ID); 70153e4498SNetali $lang_ns = array_shift($split_id); 71153e4498SNetali // check if we are in a language namespace 72153e4498SNetali if (array_key_exists($lang_ns, $this->langs)) { 73ff327fe6SNetali if($this->getConf('default_lang_in_ns') and $lang_ns === $this->get_default_lang()) { 7499da9a08SNetali // if the default lang is in a namespace and we are in that namespace --> check for push translation 7599da9a08SNetali if (!$this->check_do_push_translate()) return; 7699da9a08SNetali } else { 77153e4498SNetali // in language namespace --> check if we should translate 783c636ad3SNetali if (!$this->check_do_translation(true)) return; 7999da9a08SNetali } 80153e4498SNetali } else { 8199da9a08SNetali // do not show the button if we are not in a language namespace and the default language is in a namespace 8299da9a08SNetali if($this->getConf('default_lang_in_ns')) return; 83b33135efSNetali // not in language namespace and default language is not in a namespace --> check if we should show the push translate button 84153e4498SNetali if (!$this->check_do_push_translate()) return; 85153e4498SNetali } 863c636ad3SNetali 873c636ad3SNetali array_splice($event->data['items'], -1, 0, [new MenuItem()]); 883832d0abSNetali } 893832d0abSNetali 90153e4498SNetali public function preprocess(Doku_Event $event, $param): void { 913832d0abSNetali global $ID; 923c636ad3SNetali 933c636ad3SNetali // check if action is show or translate 943c636ad3SNetali if ($event->data != 'show' and $event->data != 'translate') return; 953c636ad3SNetali 96b33135efSNetali // redirect to glossary ns start if glossary ns is called 97b33135efSNetali if ($this->check_in_glossary_ns() and $event->data == 'show' and $ID == $this->get_glossary_ns()) { 98b33135efSNetali send_redirect(wl($this->get_glossary_ns() . ':start')); 99b33135efSNetali } 100b33135efSNetali 101153e4498SNetali $split_id = explode(':', $ID); 102153e4498SNetali $lang_ns = array_shift($split_id); 103153e4498SNetali // check if we are in a language namespace 104153e4498SNetali if (array_key_exists($lang_ns, $this->langs)) { 105ff327fe6SNetali if($this->getConf('default_lang_in_ns') and $lang_ns === $this->get_default_lang()) { 10699da9a08SNetali // if the default lang is in a namespace and we are in that namespace --> push translate 1073e2a3564SAndreas Gohr $this->push_translate_event($event); 10899da9a08SNetali } else { 10999da9a08SNetali // in language namespace --> autotrans direct 110153e4498SNetali $this->autotrans_direct($event); 11199da9a08SNetali } 112153e4498SNetali } else { 113153e4498SNetali // not in language namespace --> push translate 1143e2a3564SAndreas Gohr $this->push_translate_event($event); 115153e4498SNetali } 116153e4498SNetali } 117153e4498SNetali 118b33135efSNetali public function pagetpl_load(Doku_Event $event, $param): void { 119b33135efSNetali // handle glossary namespace init when we are in it 120b33135efSNetali if ($this->check_in_glossary_ns()) { 121b33135efSNetali $this->handle_glossary_init($event); 122b33135efSNetali return; 123b33135efSNetali } 124b33135efSNetali 125b33135efSNetali $this->autotrans_editor($event); 126b33135efSNetali } 127b33135efSNetali 128b33135efSNetali public function update_glossary(Doku_Event $event, $param): void { 129b33135efSNetali global $ID; 130b33135efSNetali // this also checks if the glossary feature is enabled 131b33135efSNetali if (!$this->check_in_glossary_ns()) return; 132b33135efSNetali 133b33135efSNetali $glossary_ns = $this->get_glossary_ns(); 134b33135efSNetali 135b33135efSNetali // check if we are in a glossary definition 136b33135efSNetali if(preg_match('/^' . $glossary_ns . ':(\w{2})_(\w{2})$/', $ID, $id_match)) { 137b33135efSNetali $old_glossary_id = $this->get_glossary_id($id_match[1], $id_match[2]); 138b33135efSNetali if ($event->data['changeType'] == DOKU_CHANGE_TYPE_DELETE) { 139b33135efSNetali // page deleted --> delete glossary 140b33135efSNetali if ($old_glossary_id) { 141b33135efSNetali $result = $this->delete_glossary($old_glossary_id); 142b33135efSNetali if ($result) { 143b33135efSNetali msg($this->getLang('msg_glossary_delete_success'), 1); 144b33135efSNetali $this->unset_glossary_id($id_match[1], $id_match[2]); 145b33135efSNetali } 146b33135efSNetali } 147b33135efSNetali return; 148b33135efSNetali } 149b33135efSNetali 150b33135efSNetali $entries = ''; 151b33135efSNetali 152b33135efSNetali // grep entries from definition table 153b33135efSNetali preg_match_all('/[ \t]*\|(.*?)\|(.*?)\|/', $event->data['newContent'], $matches, PREG_SET_ORDER); 154b33135efSNetali foreach ($matches as $match) { 155b33135efSNetali $src = trim($match[1]); 156b33135efSNetali $target = trim($match[2]); 157b33135efSNetali if ($src == '' or $target == '') { 158b33135efSNetali msg($this->getLang('msg_glossary_empty_key'), -1); 159b33135efSNetali return; 160b33135efSNetali } 161b33135efSNetali $entries .= $src . "\t" . $target . "\n"; 162b33135efSNetali } 163b33135efSNetali 164b33135efSNetali if (empty($matches)) { 165b33135efSNetali // no matches --> delete glossary 166b33135efSNetali if ($old_glossary_id) { 167b33135efSNetali $result = $this->delete_glossary($old_glossary_id); 168b33135efSNetali if ($result) { 169b33135efSNetali msg($this->getLang('msg_glossary_delete_success'), 1); 170b33135efSNetali $this->unset_glossary_id($id_match[1], $id_match[2]); 171b33135efSNetali } 172b33135efSNetali } 173b33135efSNetali return; 174b33135efSNetali } 175b33135efSNetali 176b33135efSNetali $new_glossary_id = $this->create_glossary($id_match[1], $id_match[2], $entries); 177b33135efSNetali 178b33135efSNetali if ($new_glossary_id) { 179b33135efSNetali msg($this->getLang('msg_glossary_create_success'), 1); 180b33135efSNetali $this->set_glossary_id($id_match[1], $id_match[2], $new_glossary_id); 181b33135efSNetali } else { 182b33135efSNetali return; 183b33135efSNetali } 184b33135efSNetali 185b33135efSNetali if ($old_glossary_id) $this->delete_glossary($old_glossary_id); 186b33135efSNetali } 187b33135efSNetali } 188b33135efSNetali 189153e4498SNetali private function autotrans_direct(Doku_Event $event): void { 190153e4498SNetali global $ID; 191153e4498SNetali 1923c636ad3SNetali // abort if action is translate and the translate button is disabled 1933c636ad3SNetali if ($event->data == 'translate' and !$this->getConf('show_button')) return; 1943c636ad3SNetali 1953c636ad3SNetali // do nothing on show action when mode is not direct 1963c636ad3SNetali if ($event->data == 'show' and $this->get_mode() != 'direct') return; 1973c636ad3SNetali 1983c636ad3SNetali // allow translation of existing pages is we are in the translate action 1993c636ad3SNetali $allow_existing = ($event->data == 'translate'); 2003c636ad3SNetali 2013c636ad3SNetali // reset action to show 2023c636ad3SNetali $event->data = 'show'; 2033c636ad3SNetali 204153e4498SNetali if (!$this->check_do_translation($allow_existing)) { 205153e4498SNetali return; 206153e4498SNetali } 2073832d0abSNetali 2080180404cSNetali $org_page_info = $this->get_org_page_info(); 2093e2a3564SAndreas Gohr try { 2100180404cSNetali $translated_text = $this->deepl_translate($org_page_info["text"], $this->get_target_lang(), $org_page_info["ns"]); 2113e2a3564SAndreas Gohr } catch (\Exception $e) { 2123e2a3564SAndreas Gohr msg($e->getMessage(), -1); 213153e4498SNetali return; 214153e4498SNetali } 2153832d0abSNetali 2163832d0abSNetali saveWikiText($ID, $translated_text, 'Automatic translation'); 2173832d0abSNetali 218153e4498SNetali msg($this->getLang('msg_translation_success'), 1); 219153e4498SNetali 2203c636ad3SNetali // reload the page after translation 2213c636ad3SNetali send_redirect(wl($ID)); 2223832d0abSNetali } 2233832d0abSNetali 224b33135efSNetali private function autotrans_editor(Doku_Event $event): void { 2253832d0abSNetali if ($this->get_mode() != 'editor') return; 2263832d0abSNetali 2273832d0abSNetali if (!$this->check_do_translation()) return; 2283832d0abSNetali 2290180404cSNetali $org_page_info = $this->get_org_page_info(); 2303832d0abSNetali 2313e2a3564SAndreas Gohr try { 2320180404cSNetali $event->data['tpl'] = $this->deepl_translate($org_page_info["text"], $this->get_target_lang(), $org_page_info["ns"]); 2333e2a3564SAndreas Gohr } catch (\Exception $e) { 2343e2a3564SAndreas Gohr msg($e->getMessage(), -1); 2353e2a3564SAndreas Gohr return; 2363e2a3564SAndreas Gohr } 2373832d0abSNetali } 2383832d0abSNetali 2393e2a3564SAndreas Gohr private function push_translate_event(Doku_Event $event): void { 240153e4498SNetali global $ID; 241153e4498SNetali 242153e4498SNetali // check if action is translate 243153e4498SNetali if ($event->data != 'translate') return; 244153e4498SNetali 245153e4498SNetali // check if button is enabled 246153e4498SNetali if (!$this->getConf('show_button')) { 247153e4498SNetali send_redirect(wl($ID)); 248153e4498SNetali return; 249153e4498SNetali } 250153e4498SNetali 251153e4498SNetali // push translate 252153e4498SNetali $push_langs = $this->get_push_langs(); 253153e4498SNetali $org_page_text = rawWiki($ID); 254153e4498SNetali foreach ($push_langs as $lang) { 2553e2a3564SAndreas Gohr try { 2563e2a3564SAndreas Gohr $this->push_translate($ID, $org_page_text, $lang); 2573e2a3564SAndreas Gohr } catch (\Exception $e) { 2583e2a3564SAndreas Gohr msg($e->getMessage(), -1); 259153e4498SNetali } 260153e4498SNetali } 261153e4498SNetali 262153e4498SNetali msg($this->getLang('msg_translation_success'), 1); 263153e4498SNetali 264153e4498SNetali // reload the page after translation to clear the action 265153e4498SNetali send_redirect(wl($ID)); 266153e4498SNetali } 267153e4498SNetali 2683e2a3564SAndreas Gohr public function push_translate($id, $org_page_text, $lang): string { 2693e2a3564SAndreas Gohr if (!$this->check_do_push_translate()) { 2703e2a3564SAndreas Gohr throw new \Exception('Failed push translate checks', 400); 2713e2a3564SAndreas Gohr } 2723e2a3564SAndreas Gohr 2733e2a3564SAndreas Gohr // skip invalid languages 2743e2a3564SAndreas Gohr if (!array_key_exists($lang, $this->langs)) { 2753e2a3564SAndreas Gohr throw new \Exception($this->getLang('msg_translation_fail_invalid_lang') . $lang, 404); 2763e2a3564SAndreas Gohr } 2773e2a3564SAndreas Gohr 2783e2a3564SAndreas Gohr if ($this->getConf('default_lang_in_ns')) { 2793e2a3564SAndreas Gohr // if default lang is in ns: replace language namespace in ID 2803e2a3564SAndreas Gohr $split_id = explode(':', $id); 2813e2a3564SAndreas Gohr array_shift($split_id); 2823e2a3564SAndreas Gohr $lang_id = implode(':', $split_id); 2833e2a3564SAndreas Gohr $lang_id = $lang . ':' . $lang_id; 2843e2a3564SAndreas Gohr } else { 2853e2a3564SAndreas Gohr // if default lang is not in ns: add language namespace to ID 2863e2a3564SAndreas Gohr $lang_id = $lang . ':' . $id; 2873e2a3564SAndreas Gohr } 2883e2a3564SAndreas Gohr 2893e2a3564SAndreas Gohr // check permissions 2903e2a3564SAndreas Gohr $perm = auth_quickaclcheck($lang_id); 2913e2a3564SAndreas Gohr $exists = page_exists($lang_id); 2923e2a3564SAndreas Gohr if (($exists and $perm < AUTH_EDIT) or (!$exists and $perm < AUTH_CREATE)) { 2933e2a3564SAndreas Gohr throw new \Exception($this->getLang('msg_translation_fail_no_permissions') . $lang_id, 403); 2943e2a3564SAndreas Gohr } 2953e2a3564SAndreas Gohr 2963e2a3564SAndreas Gohr $translated_text = $this->deepl_translate($org_page_text, $lang, getNS($id)); 2973e2a3564SAndreas Gohr saveWikiText($lang_id, $translated_text, 'Automatic push translation'); 2983e2a3564SAndreas Gohr 2993e2a3564SAndreas Gohr return $lang_id; 3003e2a3564SAndreas Gohr } 3013e2a3564SAndreas Gohr 302b33135efSNetali private function handle_glossary_init(Doku_Event $event): void { 303b33135efSNetali global $ID; 304b33135efSNetali 305b33135efSNetali $glossary_ns = $this->get_glossary_ns(); 306b33135efSNetali 307b33135efSNetali // create glossary landing page 308b33135efSNetali if ($ID == $glossary_ns . ':start') { 309b33135efSNetali $landing_page_text = '====== ' . $this->getLang('glossary_landing_heading') . ' ======' . "\n"; 310b33135efSNetali $landing_page_text .= $this->getLang('glossary_landing_info_msg') . "\n"; 311b33135efSNetali 312b33135efSNetali $src_lang = substr($this->get_default_lang(), 0, 2); 313b33135efSNetali 314b33135efSNetali $available_glossaries = $this->get_available_glossaries(); 315b33135efSNetali foreach ($available_glossaries as $glossary) { 316b33135efSNetali if ($glossary['source_lang'] != $src_lang) continue; 317b33135efSNetali // generate links to the available glossary pages 318b33135efSNetali $landing_page_text .= ' * [[.:' . $glossary['source_lang'] . '_' . $glossary['target_lang'] . '|' . strtoupper($glossary['source_lang']) . ' -> ' . strtoupper($glossary['target_lang']) . ']]' . "\n"; 319b33135efSNetali } 320b33135efSNetali $event->data['tpl'] = $landing_page_text; 321b33135efSNetali return; 322b33135efSNetali } 323b33135efSNetali 324b33135efSNetali if (preg_match('/^' . $glossary_ns . ':(\w{2})_(\w{2})$/', $ID, $match)) { 325b33135efSNetali // check if glossaries are supported for this language pair 326b33135efSNetali if (!$this->check_glossary_supported($match[1], $match[2])) { 327b33135efSNetali msg($this->getLang('msg_glossary_unsupported'), -1); 328b33135efSNetali return; 329b33135efSNetali } 330b33135efSNetali 331b33135efSNetali $page_text = '====== ' . $this->getLang('glossary_definition_heading') . ': ' . strtoupper($match[1]) . ' -> ' . strtoupper($match[2]) . ' ======' . "\n"; 332b33135efSNetali $page_text .= $this->getLang('glossary_definition_help') . "\n\n"; 333b33135efSNetali $page_text .= '^ ' . strtoupper($match[1]) . ' ^ ' . strtoupper($match[2]) . ' ^' . "\n"; 334b33135efSNetali 335b33135efSNetali $event->data['tpl'] = $page_text; 336b33135efSNetali return; 337b33135efSNetali } 338b33135efSNetali } 339b33135efSNetali 340b33135efSNetali private function get_glossary_ns(): string { 341b33135efSNetali return trim(strtolower($this->getConf('glossary_ns'))); 342b33135efSNetali } 343b33135efSNetali 3443832d0abSNetali private function get_mode(): string { 3453832d0abSNetali global $ID; 3463832d0abSNetali if ($this->getConf('editor_regex')) { 3473832d0abSNetali if (preg_match('/' . $this->getConf('editor_regex') . '/', $ID) === 1) return 'editor'; 3483832d0abSNetali } 3493832d0abSNetali if ($this->getConf('direct_regex')) { 3503832d0abSNetali if (preg_match('/' . $this->getConf('direct_regex') . '/', $ID) === 1) return 'direct'; 3513832d0abSNetali } 3523832d0abSNetali return $this->getConf('mode'); 3533832d0abSNetali } 3543832d0abSNetali 3553832d0abSNetali private function get_target_lang(): string { 3563832d0abSNetali global $ID; 3573832d0abSNetali $split_id = explode(':', $ID); 3583832d0abSNetali return array_shift($split_id); 3593832d0abSNetali } 3603832d0abSNetali 361ff327fe6SNetali private function get_default_lang(): string { 362ff327fe6SNetali global $conf; 363ff327fe6SNetali 364ff327fe6SNetali if (empty($conf['lang_before_translation'])) { 365ff327fe6SNetali $default_lang = $conf['lang']; 366ff327fe6SNetali } else { 367ff327fe6SNetali $default_lang = $conf['lang_before_translation']; 368ff327fe6SNetali } 369ff327fe6SNetali 370ff327fe6SNetali return $default_lang; 371ff327fe6SNetali } 372ff327fe6SNetali 3730180404cSNetali private function get_org_page_info(): array { 3743832d0abSNetali global $ID; 3753832d0abSNetali 3763832d0abSNetali $split_id = explode(':', $ID); 3773832d0abSNetali array_shift($split_id); 3783832d0abSNetali $org_id = implode(':', $split_id); 3793832d0abSNetali 38099da9a08SNetali // if default lang is in ns: add default ns in front of org id 38199da9a08SNetali if ($this->getConf('default_lang_in_ns')) { 382ff327fe6SNetali $org_id = $this->get_default_lang() . ':' . $org_id; 38399da9a08SNetali } 38499da9a08SNetali 3850180404cSNetali return array("ns" => getNS($org_id), "text" => rawWiki($org_id)); 3863832d0abSNetali } 3873832d0abSNetali 388b33135efSNetali private function get_available_glossaries(): array { 389b33135efSNetali if (!trim($this->getConf('api_key'))) { 390b33135efSNetali msg($this->getLang('msg_bad_key'), -1); 391b33135efSNetali return array(); 392b33135efSNetali } 393b33135efSNetali 394b33135efSNetali if ($this->getConf('api') == 'free') { 395b33135efSNetali $url = 'https://api-free.deepl.com/v2/glossary-language-pairs'; 396b33135efSNetali } else { 397b33135efSNetali $url = 'https://api.deepl.com/v2/glossary-language-pairs'; 398b33135efSNetali } 399b33135efSNetali 400b33135efSNetali $http = new DokuHTTPClient(); 401b33135efSNetali 402b33135efSNetali $http->headers = array('Authorization' => 'DeepL-Auth-Key ' . $this->getConf('api_key')); 403b33135efSNetali 404b33135efSNetali $raw_response = $http->get($url); 405b33135efSNetali 406b33135efSNetali if ($http->status >= 400) { 407b33135efSNetali // add error messages 408b33135efSNetali switch ($http->status) { 409b33135efSNetali case 403: 410b33135efSNetali msg($this->getLang('msg_bad_key'), -1); 411b33135efSNetali break; 412b33135efSNetali default: 413b33135efSNetali msg($this->getLang('msg_glossary_fetch_fail'), -1); 414b33135efSNetali break; 415b33135efSNetali } 416b33135efSNetali 417b33135efSNetali // if any error occurred return an empty array 418b33135efSNetali return array(); 419b33135efSNetali } 420b33135efSNetali 421b33135efSNetali $json_response = json_decode($raw_response, true); 422b33135efSNetali 423b33135efSNetali return $json_response['supported_languages']; 424b33135efSNetali } 425b33135efSNetali 426b33135efSNetali private function get_glossary_id($src, $target): string { 427b33135efSNetali if (!file_exists(DOKU_CONF . 'deepl-glossaries.json')) return ''; 428b33135efSNetali 429b33135efSNetali $key = $src . "_" . $target; 430b33135efSNetali 431b33135efSNetali $raw_json = file_get_contents(DOKU_CONF . 'deepl-glossaries.json'); 432b33135efSNetali $content = json_decode($raw_json, true); 433b33135efSNetali 434b33135efSNetali if (array_key_exists($key, $content)) { 435b33135efSNetali return $content[$key]; 436b33135efSNetali } else { 437b33135efSNetali return ''; 438b33135efSNetali } 439b33135efSNetali } 440b33135efSNetali 441b33135efSNetali private function set_glossary_id($src, $target, $glossary_id): void { 442b33135efSNetali if (file_exists(DOKU_CONF . 'deepl-glossaries.json')) { 443b33135efSNetali $raw_json = file_get_contents(DOKU_CONF . 'deepl-glossaries.json'); 444b33135efSNetali $content = json_decode($raw_json, true); 445b33135efSNetali } else { 446b33135efSNetali $content = array(); 447b33135efSNetali } 448b33135efSNetali 449b33135efSNetali $key = $src . "_" . $target; 450b33135efSNetali 451b33135efSNetali $content[$key] = $glossary_id; 452b33135efSNetali 453b33135efSNetali $raw_json = json_encode($content); 454b33135efSNetali file_put_contents(DOKU_CONF . 'deepl-glossaries.json', $raw_json); 455b33135efSNetali } 456b33135efSNetali 457b33135efSNetali private function unset_glossary_id($src, $target): void { 458b33135efSNetali if (file_exists(DOKU_CONF . 'deepl-glossaries.json')) { 459b33135efSNetali $raw_json = file_get_contents(DOKU_CONF . 'deepl-glossaries.json'); 460b33135efSNetali $content = json_decode($raw_json, true); 461b33135efSNetali } else { 462b33135efSNetali return; 463b33135efSNetali } 464b33135efSNetali 465b33135efSNetali $key = $src . "_" . $target; 466b33135efSNetali 467b33135efSNetali unset($content[$key]); 468b33135efSNetali 469b33135efSNetali $raw_json = json_encode($content); 470b33135efSNetali file_put_contents(DOKU_CONF . 'deepl-glossaries.json', $raw_json); 471b33135efSNetali } 472b33135efSNetali 473b33135efSNetali private function check_in_glossary_ns(): bool { 474b33135efSNetali global $ID; 475b33135efSNetali 476b33135efSNetali $glossary_ns = $this->get_glossary_ns(); 477b33135efSNetali 478b33135efSNetali // check if the glossary namespace is defined 479b33135efSNetali if (!$glossary_ns) return false; 480b33135efSNetali 481b33135efSNetali // check if we are in the glossary namespace 482b33135efSNetali if (substr($ID, 0, strlen($glossary_ns)) == $glossary_ns) { 483b33135efSNetali return true; 484b33135efSNetali } else { 485b33135efSNetali return false; 486b33135efSNetali } 487b33135efSNetali } 488b33135efSNetali 489b33135efSNetali private function check_glossary_supported($src, $target): bool { 490b33135efSNetali if(strlen($src) != 2 or strlen($target) != 2) return false; 491b33135efSNetali $available_glossaries = $this->get_available_glossaries(); 492b33135efSNetali foreach ($available_glossaries as $glossary) { 493b33135efSNetali if ($src == $glossary['source_lang'] and $target == $glossary['target_lang']) return true; 494b33135efSNetali } 495b33135efSNetali return false; 496b33135efSNetali } 497b33135efSNetali 4983c636ad3SNetali private function check_do_translation($allow_existing = false): bool { 4993832d0abSNetali global $INFO; 5003832d0abSNetali global $ID; 5013832d0abSNetali 5023c636ad3SNetali // only translate if the current page does not exist 5033c636ad3SNetali if ($INFO['exists'] and !$allow_existing) return false; 5043c636ad3SNetali 5053c636ad3SNetali // permission check 5063c636ad3SNetali $perm = auth_quickaclcheck($ID); 5073c636ad3SNetali if (($INFO['exists'] and $perm < AUTH_EDIT) or (!$INFO['exists'] and $perm < AUTH_CREATE)) return false; 5083c636ad3SNetali 5093832d0abSNetali // skip blacklisted namespaces and pages 5103832d0abSNetali if ($this->getConf('blacklist_regex')) { 5113832d0abSNetali if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false; 5123832d0abSNetali } 5133832d0abSNetali 5143832d0abSNetali $split_id = explode(':', $ID); 5153832d0abSNetali $lang_ns = array_shift($split_id); 5163832d0abSNetali // only translate if the current page is in a language namespace 5173832d0abSNetali if (!array_key_exists($lang_ns, $this->langs)) return false; 5183832d0abSNetali 5193832d0abSNetali $org_id = implode(':', $split_id); 52099da9a08SNetali 52199da9a08SNetali // if default lang is in ns: add default ns in front of org id 52299da9a08SNetali if ($this->getConf('default_lang_in_ns')) { 523ff327fe6SNetali $org_id = $this->get_default_lang() . ':' . $org_id; 52499da9a08SNetali } 52599da9a08SNetali 526b33135efSNetali // no translations for the glossary namespace 527b33135efSNetali $glossary_ns = $this->get_glossary_ns(); 528b33135efSNetali if ($glossary_ns and substr($org_id, 0, strlen($glossary_ns)) == $glossary_ns) return false; 529b33135efSNetali 5303832d0abSNetali // check if the original page exists 5313832d0abSNetali if (!page_exists($org_id)) return false; 5323832d0abSNetali 5333832d0abSNetali return true; 5343832d0abSNetali } 5353832d0abSNetali 536153e4498SNetali private function check_do_push_translate(): bool { 537153e4498SNetali global $ID; 53899da9a08SNetali global $INFO; 53999da9a08SNetali 54099da9a08SNetali if (!$INFO['exists']) return false; 54199da9a08SNetali 542a3a51507SNetali // only allow push translation if the user can edit this page 543a3a51507SNetali $perm = auth_quickaclcheck($ID); 544a3a51507SNetali if ($perm < AUTH_EDIT) return false; 545a3a51507SNetali 54699da9a08SNetali // if default language is in namespace: only allow push translation from that namespace 54799da9a08SNetali if($this->getConf('default_lang_in_ns')) { 54899da9a08SNetali $split_id = explode(':', $ID); 54999da9a08SNetali $lang_ns = array_shift($split_id); 55099da9a08SNetali 551ff327fe6SNetali if ($lang_ns !== $this->get_default_lang()) return false; 55299da9a08SNetali } 553153e4498SNetali 554b33135efSNetali // no translations for the glossary namespace 555b33135efSNetali if ($this->check_in_glossary_ns()) return false; 556b33135efSNetali 557153e4498SNetali $push_langs = $this->get_push_langs(); 558153e4498SNetali // push_langs empty --> push_translate disabled --> abort 559153e4498SNetali if (empty($push_langs)) return false; 560153e4498SNetali 561153e4498SNetali // skip blacklisted namespaces and pages 562153e4498SNetali if ($this->getConf('blacklist_regex')) { 563153e4498SNetali // blacklist regex match --> abort 564153e4498SNetali if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false; 565153e4498SNetali } 566153e4498SNetali 567153e4498SNetali return true; 568153e4498SNetali } 569153e4498SNetali 570b33135efSNetali private function create_glossary($src, $target, $entries): string { 571b33135efSNetali if (!trim($this->getConf('api_key'))) { 572b33135efSNetali msg($this->getLang('msg_bad_key'), -1); 573b33135efSNetali return ''; 574b33135efSNetali } 575b33135efSNetali 576b33135efSNetali if ($this->getConf('api') == 'free') { 577b33135efSNetali $url = 'https://api-free.deepl.com/v2/glossaries'; 578b33135efSNetali } else { 579b33135efSNetali $url = 'https://api.deepl.com/v2/glossaries'; 580b33135efSNetali } 581b33135efSNetali 582b33135efSNetali $data = array( 583b33135efSNetali 'name' => 'DokuWiki-Autotranslate-' . $src . '_' . $target, 584b33135efSNetali 'source_lang' => $src, 585b33135efSNetali 'target_lang' => $target, 586b33135efSNetali 'entries' => $entries, 587b33135efSNetali 'entries_format' => 'tsv' 588b33135efSNetali ); 589b33135efSNetali 590b33135efSNetali $http = new DokuHTTPClient(); 591b33135efSNetali 592b33135efSNetali $http->headers = array('Authorization' => 'DeepL-Auth-Key ' . $this->getConf('api_key')); 593b33135efSNetali 594b33135efSNetali $raw_response = $http->post($url, $data); 595b33135efSNetali 596b33135efSNetali if ($http->status >= 400) { 597b33135efSNetali // add error messages 598b33135efSNetali switch ($http->status) { 599b33135efSNetali case 403: 600b33135efSNetali msg($this->getLang('msg_bad_key'), -1); 601b33135efSNetali break; 602b33135efSNetali case 400: 603b33135efSNetali msg($this->getLang('msg_glossary_content_invalid'), -1); 604b33135efSNetali break; 605b33135efSNetali default: 606b33135efSNetali msg($this->getLang('msg_glossary_create_fail'), -1); 607b33135efSNetali break; 608b33135efSNetali } 609b33135efSNetali 610b33135efSNetali // if any error occurred return an empty string 611b33135efSNetali return ''; 612b33135efSNetali } 613b33135efSNetali 614b33135efSNetali $json_response = json_decode($raw_response, true); 615b33135efSNetali 616b33135efSNetali return $json_response['glossary_id']; 617b33135efSNetali } 618b33135efSNetali 619b33135efSNetali private function delete_glossary($glossary_id): bool { 620b33135efSNetali if (!trim($this->getConf('api_key'))) { 621b33135efSNetali msg($this->getLang('msg_bad_key'), -1); 622b33135efSNetali return false; 623b33135efSNetali } 624b33135efSNetali 625b33135efSNetali if ($this->getConf('api') == 'free') { 626b33135efSNetali $url = 'https://api-free.deepl.com/v2/glossaries'; 627b33135efSNetali } else { 628b33135efSNetali $url = 'https://api.deepl.com/v2/glossaries'; 629b33135efSNetali } 630b33135efSNetali 631b33135efSNetali $url .= '/' . $glossary_id; 632b33135efSNetali 633b33135efSNetali $http = new DokuHTTPClient(); 634b33135efSNetali 635b33135efSNetali $http->headers = array('Authorization' => 'DeepL-Auth-Key ' . $this->getConf('api_key')); 636b33135efSNetali 637b33135efSNetali $http->sendRequest($url, '', 'DELETE'); 638b33135efSNetali 639b33135efSNetali if ($http->status >= 400) { 640b33135efSNetali // add error messages 641b33135efSNetali switch ($http->status) { 642b33135efSNetali case 403: 643b33135efSNetali msg($this->getLang('msg_bad_key'), -1); 644b33135efSNetali break; 645b33135efSNetali default: 646b33135efSNetali msg($this->getLang('msg_glossary_delete_fail'), -1); 647b33135efSNetali break; 648b33135efSNetali } 649b33135efSNetali 650b33135efSNetali // if any error occurred return false 651b33135efSNetali return false; 652b33135efSNetali } 653b33135efSNetali 654b33135efSNetali return true; 655b33135efSNetali } 656b33135efSNetali 6570180404cSNetali private function deepl_translate($text, $target_lang, $org_ns): string { 658b33135efSNetali if (!trim($this->getConf('api_key'))) { 6593e2a3564SAndreas Gohr throw new \Exception($this->getLang('msg_translation_fail_bad_key'), 400); 660b33135efSNetali } 6613832d0abSNetali 6620180404cSNetali $text = $this->patch_links($text, $target_lang, $org_ns); 6630180404cSNetali 6643832d0abSNetali $text = $this->insert_ignore_tags($text); 6653832d0abSNetali 666b33135efSNetali $data = array( 667b33135efSNetali 'source_lang' => strtoupper(substr($this->get_default_lang(), 0, 2)), // cut of things like "-informal" 6680180404cSNetali 'target_lang' => $this->langs[$target_lang], 6693832d0abSNetali 'tag_handling' => 'xml', 6700180404cSNetali 'ignore_tags' => 'ignore', 6713832d0abSNetali 'text' => $text 672b33135efSNetali ); 673b33135efSNetali 674b33135efSNetali // check if glossaries are enabled 675b33135efSNetali if ($this->get_glossary_ns()) { 676b33135efSNetali $src = substr($this->get_default_lang(), 0, 2); 677b33135efSNetali $target = substr($target_lang, 0, 2); 678b33135efSNetali $glossary_id = $this->get_glossary_id($src, $target); 679b33135efSNetali if ($glossary_id) { 680b33135efSNetali // use glossary if it is defined 681b33135efSNetali $data['glossary_id'] = $glossary_id; 682b33135efSNetali } 683b33135efSNetali } 6843832d0abSNetali 6853832d0abSNetali if ($this->getConf('api') == 'free') { 68681931e50SNetali $url = 'https://api-free.deepl.com/v2/translate'; 6873832d0abSNetali } else { 68881931e50SNetali $url = 'https://api.deepl.com/v2/translate'; 6893832d0abSNetali } 6903832d0abSNetali 69181931e50SNetali $http = new DokuHTTPClient(); 692*858956d2SAndreas Gohr $http->keep_alive = false; 693b33135efSNetali 694b33135efSNetali $http->headers = array('Authorization' => 'DeepL-Auth-Key ' . $this->getConf('api_key')); 695b33135efSNetali 69681931e50SNetali $raw_response = $http->post($url, $data); 6973832d0abSNetali 698*858956d2SAndreas Gohr if ($http->status >= 400 || $http->status < 200) { 6995f8ab21dSNetali // add error messages 7005f8ab21dSNetali switch ($http->status) { 7015f8ab21dSNetali case 403: 7023e2a3564SAndreas Gohr throw new \Exception($this->getLang('msg_translation_fail_bad_key'), 403); 703b33135efSNetali case 404: 7043e2a3564SAndreas Gohr throw new \Exception($this->getLang('msg_translation_fail_invalid_glossary'), 404); 7055f8ab21dSNetali case 456: 7063e2a3564SAndreas Gohr throw new \Exception($this->getLang('msg_translation_fail_quota_exceeded'), 456); 7075f8ab21dSNetali default: 708*858956d2SAndreas Gohr throw new \Exception($this->getLang('msg_translation_fail'), $http->status ?: 500); 7093e2a3564SAndreas Gohr } 7105f8ab21dSNetali } 7115f8ab21dSNetali 7123e2a3564SAndreas Gohr $json_response = json_decode($raw_response, true, JSON_THROW_ON_ERROR); 7133832d0abSNetali $translated_text = $json_response['translations'][0]['text']; 7143832d0abSNetali 7153832d0abSNetali $translated_text = $this->remove_ignore_tags($translated_text); 7163832d0abSNetali 7173832d0abSNetali return $translated_text; 7183832d0abSNetali } 7193832d0abSNetali 720153e4498SNetali private function get_push_langs(): array { 721153e4498SNetali $push_langs = trim($this->getConf('push_langs')); 722153e4498SNetali 723153e4498SNetali if ($push_langs === '') return array(); 724153e4498SNetali 725153e4498SNetali return explode(' ', $push_langs); 726153e4498SNetali } 727153e4498SNetali 72824d1dd2fSAndreas Gohr /** 72924d1dd2fSAndreas Gohr * Is the given ID a relative path? 73024d1dd2fSAndreas Gohr * 73124d1dd2fSAndreas Gohr * Always returns false if keep_relative is disabled. 73224d1dd2fSAndreas Gohr * 73324d1dd2fSAndreas Gohr * @param string $id 73424d1dd2fSAndreas Gohr * @return bool 73524d1dd2fSAndreas Gohr */ 73624d1dd2fSAndreas Gohr private function isRelativeLink($id) 73724d1dd2fSAndreas Gohr { 73824d1dd2fSAndreas Gohr if (!$this->getConf('keep_relative')) return false; 73924d1dd2fSAndreas Gohr if ($id === '') return false; 74024d1dd2fSAndreas Gohr if (strpos($id, ':') === false) return true; 74124d1dd2fSAndreas Gohr if ($id[0] === '.') return true; 74224d1dd2fSAndreas Gohr if ($id[0] === '~') return true; 74324d1dd2fSAndreas Gohr return false; 74424d1dd2fSAndreas Gohr } 74524d1dd2fSAndreas Gohr 7460180404cSNetali private function patch_links($text, $target_lang, $ns): string { 7470180404cSNetali /* 7480180404cSNetali * 1. Find links in [[ aa:bb ]] or [[ aa:bb | cc ]] 7490180404cSNetali * 2. Extract aa:bb 7500180404cSNetali * 3. Check if lang:aa:bb exists 7510180404cSNetali * 3.1. --> Yes --> replace 7520180404cSNetali * 3.2. --> No --> leave it as it is 7530180404cSNetali */ 7543832d0abSNetali 7550180404cSNetali 7560180404cSNetali /* 7570180404cSNetali * LINKS 7580180404cSNetali */ 7590180404cSNetali 7604b84d3cfSNetali preg_match_all('/\[\[([\s\S]*?)(#[\s\S]*?)?((\|)([\s\S]*?))?]]/', $text, $matches, PREG_SET_ORDER); 7610180404cSNetali 7620180404cSNetali foreach ($matches as $match) { 7630180404cSNetali 7640180404cSNetali // external link --> skip 765a3a51507SNetali if (strpos($match[1], '://') !== false) continue; 7660180404cSNetali 76784cda41fSNetali // skip interwiki links 76884cda41fSNetali if (strpos($match[1], '>') !== false) continue; 76984cda41fSNetali 7702a12605eSnetali // skip mail addresses 7712a12605eSnetali if (strpos($match[1], '@') !== false) continue; 7722a12605eSnetali 77384cda41fSNetali // skip windows share links 77484cda41fSNetali if (strpos($match[1], '\\\\') !== false) continue; 77584cda41fSNetali 77684cda41fSNetali $resolved_id = trim($match[1]); 77724d1dd2fSAndreas Gohr if($this->isRelativeLink($resolved_id)) continue; 7780180404cSNetali 7790180404cSNetali resolve_pageid($ns, $resolved_id, $exists); 7800180404cSNetali 78153f3766cSNetali $resolved_id_full = $resolved_id; 7820180404cSNetali 7836663bcb5SNetali // if the link already points to a target in a language namespace drop it and add the new language namespace 7846663bcb5SNetali $split_id = explode(':', $resolved_id); 7856663bcb5SNetali $lang_ns = array_shift($split_id); 7866663bcb5SNetali if (array_key_exists($lang_ns, $this->langs)) { 7876663bcb5SNetali $resolved_id = implode(':', $split_id); 7886663bcb5SNetali } 7896663bcb5SNetali 7900180404cSNetali $lang_id = $target_lang . ':' . $resolved_id; 7910180404cSNetali 7920180404cSNetali if (!page_exists($lang_id)) { 79353f3766cSNetali // Page in target lang does not exist --> replace with absolute ID in case it was a relative ID 79453f3766cSNetali $new_link = '[[' . $resolved_id_full . $match[2] . $match[3] . ']]'; 79553f3766cSNetali } else { 79653f3766cSNetali // Page in target lang exists --> replace link 7974b84d3cfSNetali $new_link = '[[' . $lang_id . $match[2] . $match[3] . ']]'; 79853f3766cSNetali } 7990180404cSNetali 8000180404cSNetali $text = str_replace($match[0], $new_link, $text); 8010180404cSNetali 8020180404cSNetali } 8030180404cSNetali 8040180404cSNetali /* 8050180404cSNetali * MEDIA 8060180404cSNetali */ 8070180404cSNetali 80884cda41fSNetali preg_match_all('/\{\{(([\s\S]*?)(\?[\s\S]*?)?)(\|([\s\S]*?))?}}/', $text, $matches, PREG_SET_ORDER); 8090180404cSNetali 8100180404cSNetali foreach ($matches as $match) { 8110180404cSNetali 8120180404cSNetali // external image --> skip 813a3a51507SNetali if (strpos($match[1], '://') !== false) continue; 814a3a51507SNetali 815a3a51507SNetali // skip things like {{tag>...}} 816a3a51507SNetali if (strpos($match[1], '>') !== false) continue; 8170180404cSNetali 81884cda41fSNetali // keep alignment 81984cda41fSNetali $align_left = ""; 82084cda41fSNetali $align_right = ""; 82184cda41fSNetali 82284cda41fSNetali // align left --> space in front of ID 82384cda41fSNetali if (substr($match[1], 0, 1) == " ") $align_left = " "; 82484cda41fSNetali // align right --> space behind id 82584cda41fSNetali if (substr($match[1], -1) == " ") $align_right = " "; 82684cda41fSNetali 82784cda41fSNetali $resolved_id = trim($match[2]); 82884cda41fSNetali $params = trim($match[3]); 8290180404cSNetali 83024d1dd2fSAndreas Gohr if($this->isRelativeLink($resolved_id)) continue; 83124d1dd2fSAndreas Gohr 8320180404cSNetali resolve_mediaid($ns, $resolved_id, $exists); 8330180404cSNetali 83453f3766cSNetali $resolved_id_full = $resolved_id; 8350180404cSNetali 8366663bcb5SNetali // if the link already points to a target in a language namespace drop it and add the new language namespace 8376663bcb5SNetali $split_id = explode(':', $resolved_id); 8386663bcb5SNetali $lang_ns = array_shift($split_id); 8396663bcb5SNetali if (array_key_exists($lang_ns, $this->langs)) { 8406663bcb5SNetali $resolved_id = implode(':', $split_id); 8416663bcb5SNetali } 8426663bcb5SNetali 8430180404cSNetali $lang_id = $target_lang . ':' . $resolved_id; 8440180404cSNetali 8450180404cSNetali $lang_id_fn = mediaFN($lang_id); 8460180404cSNetali 8470180404cSNetali if (!file_exists($lang_id_fn)) { 84853f3766cSNetali // media in target lang does not exist --> replace with absolute ID in case it was a relative ID 84984cda41fSNetali $new_link = '{{' . $align_left . $resolved_id_full . $params . $align_right . $match[4] . '}}'; 85053f3766cSNetali } else { 85153f3766cSNetali // media in target lang exists --> replace it 85284cda41fSNetali $new_link = '{{' . $align_left . $lang_id . $params . $align_right . $match[4] . '}}'; 85353f3766cSNetali } 8540180404cSNetali 8550180404cSNetali $text = str_replace($match[0], $new_link, $text); 8560180404cSNetali 8570180404cSNetali } 8580180404cSNetali 8590180404cSNetali return $text; 8600180404cSNetali } 8610180404cSNetali 8620180404cSNetali private function insert_ignore_tags($text): string { 8630180404cSNetali // ignore every other xml-like tags (the tags themselves, not their content), otherwise deepl would break the formatting 8640180404cSNetali $text = preg_replace('/<[\s\S]+?>/', '<ignore>${0}</ignore>', $text); 8650180404cSNetali 8661cd781c4SNetali // prevent deepl from breaking headings 8671cd781c4SNetali $text = preg_replace('/={1,6}/', '<ignore>${0}</ignore>', $text); 8681cd781c4SNetali 86943d62a6bSnetali // prevent deepl from messing with nocache-instructions 87043d62a6bSnetali $text = str_replace("~~NOCACHE~~", "<ignore>~~NOCACHE~~</ignore>", $text); 87143d62a6bSnetali 872a3a51507SNetali // fix for plugins like tag or template 873a3a51507SNetali $text = preg_replace('/\{\{[\s\w]+?>[\s\S]*?}}/', '<ignore>${0}</ignore>', $text); 8740180404cSNetali 8753b1ff295SNetali // ignore links in wikitext (outside of dokuwiki-links) 8763b1ff295SNetali $text = preg_replace('/\S+:\/\/\S+/', '<ignore>${0}</ignore>', $text); 8773b1ff295SNetali 8780180404cSNetali // ignore link/media ids but translate the text (if existing) 8794b84d3cfSNetali $text = preg_replace('/\[\[([\s\S]*?)(#[\s\S]*?)?((\|)([\s\S]*?))?]]/', '<ignore>[[${1}${2}${4}</ignore>${5}<ignore>]]</ignore>', $text); 8800180404cSNetali $text = preg_replace('/\{\{([\s\S]*?)(\?[\s\S]*?)?((\|)([\s\S]*?))?}}/', '<ignore>{{${1}${2}${4}</ignore>${5}<ignore>}}</ignore>', $text); 8810180404cSNetali 8823b1ff295SNetali // prevent deepl from messing with tables 8833b1ff295SNetali $text = str_replace(" ^ ", "<ignore> ^ </ignore>", $text); 8847c99a9b0Sextrasec $text = str_replace(" ^ ", "<ignore> ^ </ignore>", $text); 8857c99a9b0Sextrasec $text = str_replace(" ^ ", "<ignore> ^ </ignore>", $text); 8867c99a9b0Sextrasec $text = str_replace("^ ", "<ignore>^ </ignore>", $text); 8877c99a9b0Sextrasec $text = str_replace(" ^", "<ignore> ^</ignore>", $text); 8887c99a9b0Sextrasec $text = str_replace("^", "<ignore>^</ignore>", $text); 8897c99a9b0Sextrasec $text = str_replace(" | ", "<ignore> | </ignore>", $text); 8907c99a9b0Sextrasec $text = str_replace(" | ", "<ignore> | </ignore>", $text); 8917c99a9b0Sextrasec $text = str_replace(" | ", "<ignore> | </ignore>", $text); 8927c99a9b0Sextrasec $text = str_replace("| ", "<ignore>| </ignore>", $text); 8937c99a9b0Sextrasec $text = str_replace(" |", "<ignore> |</ignore>", $text); 8943b1ff295SNetali $text = str_replace("|", "<ignore>|</ignore>", $text); 8953b1ff295SNetali 8960180404cSNetali // prevent deepl from doing strange things with dokuwiki syntax 8976301ba6eSNetali // if a full line is formatted, we have to double-ignore for some reason 8986301ba6eSNetali $text = str_replace("''", "<ignore><ignore>''</ignore></ignore>", $text); 8996301ba6eSNetali $text = str_replace("//", "<ignore><ignore>//</ignore></ignore>", $text); 9006301ba6eSNetali $text = str_replace("**", "<ignore><ignore>**</ignore></ignore>", $text); 9016301ba6eSNetali $text = str_replace("__", "<ignore><ignore>__</ignore></ignore>", $text); 9026301ba6eSNetali $text = str_replace("\\\\", "<ignore><ignore>\\\\</ignore></ignore>", $text); 9030180404cSNetali 90413221d46SNetali // prevent deepl from messing with smileys 90513221d46SNetali $smileys = array_keys(getSmileys()); 90613221d46SNetali foreach ($smileys as $smiley) { 90713221d46SNetali $text = str_replace($smiley, "<ignore>" . $smiley . "</ignore>", $text); 90813221d46SNetali } 90913221d46SNetali 9100180404cSNetali // ignore code tags 9110180404cSNetali $text = preg_replace('/(<php[\s\S]*?>[\s\S]*?<\/php>)/', '<ignore>${1}</ignore>', $text); 912057940f7SNetali $text = preg_replace('/(<file[\s\S]*?>[\s\S]*?<\/file>)/', '<ignore>${1}</ignore>', $text); 913057940f7SNetali $text = preg_replace('/(<code[\s\S]*?>[\s\S]*?<\/code>)/', '<ignore>${1}</ignore>', $text); 914057940f7SNetali 9150180404cSNetali // ignore the expressions from the ignore list 9163832d0abSNetali $ignored_expressions = explode(':', $this->getConf('ignored_expressions')); 9173832d0abSNetali 9183832d0abSNetali foreach ($ignored_expressions as $expression) { 9193832d0abSNetali $text = str_replace($expression, '<ignore>' . $expression . '</ignore>', $text); 9203832d0abSNetali } 9213832d0abSNetali 9223832d0abSNetali return $text; 9233832d0abSNetali } 9243832d0abSNetali 9253832d0abSNetali private function remove_ignore_tags($text): string { 9263832d0abSNetali $ignored_expressions = explode(':', $this->getConf('ignored_expressions')); 9273832d0abSNetali 9283832d0abSNetali foreach ($ignored_expressions as $expression) { 9293832d0abSNetali $text = str_replace('<ignore>' . $expression . '</ignore>', $expression, $text); 9303832d0abSNetali } 9313832d0abSNetali 93243d62a6bSnetali // prevent deepl from messing with nocache-instructions 93343d62a6bSnetali $text = str_replace("<ignore>~~NOCACHE~~</ignore>", "~~NOCACHE~~", $text); 93443d62a6bSnetali 9353b1ff295SNetali // prevent deepl from messing with tables 9363b1ff295SNetali $text = str_replace("<ignore>^</ignore>", "^", $text); 9377c99a9b0Sextrasec $text = str_replace("<ignore>^ </ignore>", "^ ", $text); 9387c99a9b0Sextrasec $text = str_replace("<ignore> ^</ignore>", " ^", $text); 9397c99a9b0Sextrasec $text = str_replace("<ignore> ^ </ignore>", " ^ ", $text); 9407c99a9b0Sextrasec $text = str_replace("<ignore> ^ </ignore>", " ^ ", $text); 9417c99a9b0Sextrasec $text = str_replace("<ignore> ^ </ignore>", " ^ ", $text); 9427c99a9b0Sextrasec $text = str_replace("<ignore>|</ignore>", "|", $text); 9437c99a9b0Sextrasec $text = str_replace("<ignore>| </ignore>", "| ", $text); 9447c99a9b0Sextrasec $text = str_replace("<ignore> |</ignore>", " |", $text); 9457c99a9b0Sextrasec $text = str_replace("<ignore> | </ignore>", " | ", $text); 9467c99a9b0Sextrasec $text = str_replace("<ignore> | </ignore>", " | ", $text); 9473b1ff295SNetali $text = str_replace("<ignore> | </ignore>", " | ", $text); 9483b1ff295SNetali 9496301ba6eSNetali $text = str_replace("<ignore><ignore>''</ignore></ignore>", "''", $text); 9506301ba6eSNetali $text = str_replace("<ignore><ignore>//</ignore></ignore>", "//", $text); 9516301ba6eSNetali $text = str_replace("<ignore><ignore>**</ignore></ignore>", "**", $text); 9526301ba6eSNetali $text = str_replace("<ignore><ignore>__</ignore></ignore>", "__", $text); 9536301ba6eSNetali $text = str_replace("<ignore><ignore>\\\\</ignore></ignore>", "\\\\", $text); 9540180404cSNetali 9553b1ff295SNetali // ignore links in wikitext (outside of dokuwiki-links) 9563b1ff295SNetali $text = preg_replace('/<ignore>(\S+:\/\/\S+)<\/ignore>/', '${1}', $text); 9573b1ff295SNetali 9584b84d3cfSNetali $text = preg_replace('/<ignore>\[\[([\s\S]*?)(\|)?(<\/ignore>)([\s\S]*?)?<ignore>]]<\/ignore>/', '[[${1}${2}${4}]]', $text); 9594b84d3cfSNetali $text = preg_replace('/<ignore>\{\{([\s\S]*?)(\|)?(<\/ignore>)([\s\S]*?)?<ignore>}}<\/ignore>/', '{{${1}${2}${4}}}', $text); 9604b84d3cfSNetali 96113221d46SNetali // prevent deepl from messing with smileys 96213221d46SNetali $smileys = array_keys(getSmileys()); 96313221d46SNetali foreach ($smileys as $smiley) { 96413221d46SNetali $text = str_replace("<ignore>" . $smiley . "</ignore>", $smiley, $text); 96513221d46SNetali } 96613221d46SNetali 9670180404cSNetali $text = preg_replace('/<ignore>(<php[\s\S]*?>[\s\S]*?<\/php>)<\/ignore>/', '${1}', $text); 9680180404cSNetali $text = preg_replace('/<ignore>(<file[\s\S]*?>[\s\S]*?<\/file>)<\/ignore>/', '${1}', $text); 9690180404cSNetali $text = preg_replace('/<ignore>(<code[\s\S]*?>[\s\S]*?<\/code>)<\/ignore>/', '${1}', $text); 9700180404cSNetali 971a3a51507SNetali // fix for plugins like tag or template 972a3a51507SNetali $text = preg_replace('/<ignore>(\{\{[\s\w]+?>[\s\S]*?}})<\/ignore>/', '${1}', $text); 9730180404cSNetali 9741cd781c4SNetali // prevent deepl from breaking headings 9751cd781c4SNetali $text = preg_replace('/<ignore>(={1,6})<\/ignore>/','${1}', $text); 9761cd781c4SNetali 9771cd781c4SNetali // ignore every other xml-like tags (the tags themselves, not their content), otherwise deepl would break the formatting 9781cd781c4SNetali $text = preg_replace('/<ignore>(<[\s\S]+?>)<\/ignore>/', '${1}', $text); 9791cd781c4SNetali 9800180404cSNetali // restore < and > for example from arrows (-->) in wikitext 9810180404cSNetali $text = str_replace('>', '>', $text); 9820180404cSNetali $text = str_replace('<', '<', $text); 9830180404cSNetali 9843b1ff295SNetali // restore & in wikitext 9853b1ff295SNetali $text = str_replace('&', '&', $text); 9863b1ff295SNetali 9873832d0abSNetali return $text; 9883832d0abSNetali } 9893832d0abSNetali} 9903832d0abSNetali 991