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 674*6a17ed8dSAnna Dabrowska // use v1 of tag handling (not as strict XML parsing as default v2 - in 2026) 675*6a17ed8dSAnna Dabrowska if ($this->getConf('tag_handling_v1')) { 676*6a17ed8dSAnna Dabrowska $data['tag_handling_version'] = 'v1'; 677*6a17ed8dSAnna Dabrowska } 678*6a17ed8dSAnna Dabrowska 679b33135efSNetali // check if glossaries are enabled 680b33135efSNetali if ($this->get_glossary_ns()) { 681b33135efSNetali $src = substr($this->get_default_lang(), 0, 2); 682b33135efSNetali $target = substr($target_lang, 0, 2); 683b33135efSNetali $glossary_id = $this->get_glossary_id($src, $target); 684b33135efSNetali if ($glossary_id) { 685b33135efSNetali // use glossary if it is defined 686b33135efSNetali $data['glossary_id'] = $glossary_id; 687b33135efSNetali } 688b33135efSNetali } 6893832d0abSNetali 6903832d0abSNetali if ($this->getConf('api') == 'free') { 69181931e50SNetali $url = 'https://api-free.deepl.com/v2/translate'; 6923832d0abSNetali } else { 69381931e50SNetali $url = 'https://api.deepl.com/v2/translate'; 6943832d0abSNetali } 6953832d0abSNetali 69681931e50SNetali $http = new DokuHTTPClient(); 697858956d2SAndreas Gohr $http->keep_alive = false; 698b33135efSNetali 699b33135efSNetali $http->headers = array('Authorization' => 'DeepL-Auth-Key ' . $this->getConf('api_key')); 700b33135efSNetali 70181931e50SNetali $raw_response = $http->post($url, $data); 7023832d0abSNetali 703858956d2SAndreas Gohr if ($http->status >= 400 || $http->status < 200) { 7045f8ab21dSNetali // add error messages 7055f8ab21dSNetali switch ($http->status) { 7065f8ab21dSNetali case 403: 7073e2a3564SAndreas Gohr throw new \Exception($this->getLang('msg_translation_fail_bad_key'), 403); 708b33135efSNetali case 404: 7093e2a3564SAndreas Gohr throw new \Exception($this->getLang('msg_translation_fail_invalid_glossary'), 404); 7105f8ab21dSNetali case 456: 7113e2a3564SAndreas Gohr throw new \Exception($this->getLang('msg_translation_fail_quota_exceeded'), 456); 7125f8ab21dSNetali default: 713*6a17ed8dSAnna Dabrowska if ($this->getConf('api_log_errors')) { 714*6a17ed8dSAnna Dabrowska $logger = \dokuwiki\Logger::getInstance('deeplautotranslate'); 715*6a17ed8dSAnna Dabrowska $logger->log("$http->status " . $http->resp_body, $data['text']); 716*6a17ed8dSAnna Dabrowska } 717858956d2SAndreas Gohr throw new \Exception($this->getLang('msg_translation_fail'), $http->status ?: 500); 7183e2a3564SAndreas Gohr } 7195f8ab21dSNetali } 7205f8ab21dSNetali 7213e2a3564SAndreas Gohr $json_response = json_decode($raw_response, true, JSON_THROW_ON_ERROR); 7223832d0abSNetali $translated_text = $json_response['translations'][0]['text']; 7233832d0abSNetali 7243832d0abSNetali $translated_text = $this->remove_ignore_tags($translated_text); 7253832d0abSNetali 7263832d0abSNetali return $translated_text; 7273832d0abSNetali } 7283832d0abSNetali 729153e4498SNetali private function get_push_langs(): array { 730153e4498SNetali $push_langs = trim($this->getConf('push_langs')); 731153e4498SNetali 732153e4498SNetali if ($push_langs === '') return array(); 733153e4498SNetali 734153e4498SNetali return explode(' ', $push_langs); 735153e4498SNetali } 736153e4498SNetali 73724d1dd2fSAndreas Gohr /** 73824d1dd2fSAndreas Gohr * Is the given ID a relative path? 73924d1dd2fSAndreas Gohr * 74024d1dd2fSAndreas Gohr * Always returns false if keep_relative is disabled. 74124d1dd2fSAndreas Gohr * 74224d1dd2fSAndreas Gohr * @param string $id 74324d1dd2fSAndreas Gohr * @return bool 74424d1dd2fSAndreas Gohr */ 745522d0814SJennifer Graul private function is_relative_link($id): bool { 74624d1dd2fSAndreas Gohr if (!$this->getConf('keep_relative')) return false; 74724d1dd2fSAndreas Gohr if ($id === '') return false; 74824d1dd2fSAndreas Gohr if (strpos($id, ':') === false) return true; 74924d1dd2fSAndreas Gohr if ($id[0] === '.') return true; 75024d1dd2fSAndreas Gohr if ($id[0] === '~') return true; 75124d1dd2fSAndreas Gohr return false; 75224d1dd2fSAndreas Gohr } 75324d1dd2fSAndreas Gohr 7540180404cSNetali private function patch_links($text, $target_lang, $ns): string { 7550180404cSNetali /* 7560180404cSNetali * 1. Find links in [[ aa:bb ]] or [[ aa:bb | cc ]] 7570180404cSNetali * 2. Extract aa:bb 7580180404cSNetali * 3. Check if lang:aa:bb exists 7590180404cSNetali * 3.1. --> Yes --> replace 7600180404cSNetali * 3.2. --> No --> leave it as it is 7610180404cSNetali */ 7623832d0abSNetali 7630180404cSNetali 7640180404cSNetali /* 7650180404cSNetali * LINKS 7660180404cSNetali */ 7670180404cSNetali 7684b84d3cfSNetali preg_match_all('/\[\[([\s\S]*?)(#[\s\S]*?)?((\|)([\s\S]*?))?]]/', $text, $matches, PREG_SET_ORDER); 7690180404cSNetali 7700180404cSNetali foreach ($matches as $match) { 7710180404cSNetali 7720180404cSNetali // external link --> skip 773a3a51507SNetali if (strpos($match[1], '://') !== false) continue; 7740180404cSNetali 77584cda41fSNetali // skip interwiki links 77684cda41fSNetali if (strpos($match[1], '>') !== false) continue; 77784cda41fSNetali 7782a12605eSnetali // skip mail addresses 7792a12605eSnetali if (strpos($match[1], '@') !== false) continue; 7802a12605eSnetali 78184cda41fSNetali // skip windows share links 78284cda41fSNetali if (strpos($match[1], '\\\\') !== false) continue; 78384cda41fSNetali 78484cda41fSNetali $resolved_id = trim($match[1]); 785522d0814SJennifer Graul if($this->is_relative_link($resolved_id)) continue; 7860180404cSNetali 7870180404cSNetali resolve_pageid($ns, $resolved_id, $exists); 7880180404cSNetali 78953f3766cSNetali $resolved_id_full = $resolved_id; 7900180404cSNetali 7916663bcb5SNetali // if the link already points to a target in a language namespace drop it and add the new language namespace 7926663bcb5SNetali $split_id = explode(':', $resolved_id); 7936663bcb5SNetali $lang_ns = array_shift($split_id); 7946663bcb5SNetali if (array_key_exists($lang_ns, $this->langs)) { 7956663bcb5SNetali $resolved_id = implode(':', $split_id); 7966663bcb5SNetali } 7976663bcb5SNetali 7980180404cSNetali $lang_id = $target_lang . ':' . $resolved_id; 7990180404cSNetali 8000180404cSNetali if (!page_exists($lang_id)) { 80153f3766cSNetali // Page in target lang does not exist --> replace with absolute ID in case it was a relative ID 80253f3766cSNetali $new_link = '[[' . $resolved_id_full . $match[2] . $match[3] . ']]'; 80353f3766cSNetali } else { 80453f3766cSNetali // Page in target lang exists --> replace link 8054b84d3cfSNetali $new_link = '[[' . $lang_id . $match[2] . $match[3] . ']]'; 80653f3766cSNetali } 8070180404cSNetali 8080180404cSNetali $text = str_replace($match[0], $new_link, $text); 8090180404cSNetali 8100180404cSNetali } 8110180404cSNetali 8120180404cSNetali /* 8130180404cSNetali * MEDIA 8140180404cSNetali */ 8150180404cSNetali 81684cda41fSNetali preg_match_all('/\{\{(([\s\S]*?)(\?[\s\S]*?)?)(\|([\s\S]*?))?}}/', $text, $matches, PREG_SET_ORDER); 8170180404cSNetali 8180180404cSNetali foreach ($matches as $match) { 8190180404cSNetali 8200180404cSNetali // external image --> skip 821a3a51507SNetali if (strpos($match[1], '://') !== false) continue; 822a3a51507SNetali 823a3a51507SNetali // skip things like {{tag>...}} 824a3a51507SNetali if (strpos($match[1], '>') !== false) continue; 8250180404cSNetali 82684cda41fSNetali // keep alignment 82784cda41fSNetali $align_left = ""; 82884cda41fSNetali $align_right = ""; 82984cda41fSNetali 83084cda41fSNetali // align left --> space in front of ID 83184cda41fSNetali if (substr($match[1], 0, 1) == " ") $align_left = " "; 83284cda41fSNetali // align right --> space behind id 83384cda41fSNetali if (substr($match[1], -1) == " ") $align_right = " "; 83484cda41fSNetali 83584cda41fSNetali $resolved_id = trim($match[2]); 83684cda41fSNetali $params = trim($match[3]); 8370180404cSNetali 838522d0814SJennifer Graul if($this->is_relative_link($resolved_id)) continue; 83924d1dd2fSAndreas Gohr 8400180404cSNetali resolve_mediaid($ns, $resolved_id, $exists); 8410180404cSNetali 84253f3766cSNetali $resolved_id_full = $resolved_id; 8430180404cSNetali 8446663bcb5SNetali // if the link already points to a target in a language namespace drop it and add the new language namespace 8456663bcb5SNetali $split_id = explode(':', $resolved_id); 8466663bcb5SNetali $lang_ns = array_shift($split_id); 8476663bcb5SNetali if (array_key_exists($lang_ns, $this->langs)) { 8486663bcb5SNetali $resolved_id = implode(':', $split_id); 8496663bcb5SNetali } 8506663bcb5SNetali 8510180404cSNetali $lang_id = $target_lang . ':' . $resolved_id; 8520180404cSNetali 8530180404cSNetali $lang_id_fn = mediaFN($lang_id); 8540180404cSNetali 8550180404cSNetali if (!file_exists($lang_id_fn)) { 85653f3766cSNetali // media in target lang does not exist --> replace with absolute ID in case it was a relative ID 85784cda41fSNetali $new_link = '{{' . $align_left . $resolved_id_full . $params . $align_right . $match[4] . '}}'; 85853f3766cSNetali } else { 85953f3766cSNetali // media in target lang exists --> replace it 86084cda41fSNetali $new_link = '{{' . $align_left . $lang_id . $params . $align_right . $match[4] . '}}'; 86153f3766cSNetali } 8620180404cSNetali 8630180404cSNetali $text = str_replace($match[0], $new_link, $text); 8640180404cSNetali 8650180404cSNetali } 8660180404cSNetali 8670180404cSNetali return $text; 8680180404cSNetali } 8690180404cSNetali 8700180404cSNetali private function insert_ignore_tags($text): string { 8710180404cSNetali // ignore every other xml-like tags (the tags themselves, not their content), otherwise deepl would break the formatting 8720180404cSNetali $text = preg_replace('/<[\s\S]+?>/', '<ignore>${0}</ignore>', $text); 8730180404cSNetali 8741cd781c4SNetali // prevent deepl from breaking headings 8751cd781c4SNetali $text = preg_replace('/={1,6}/', '<ignore>${0}</ignore>', $text); 8761cd781c4SNetali 877087c645bSJennifer Graul // prevent deepl from with some page lists 878087c645bSJennifer Graul $text = str_replace("{{top}}", "<ignore>{{top}}</ignore>", $text); 879087c645bSJennifer Graul $text = str_replace("{{rating}}", "<ignore>{{rating}}</ignore>", $text); 880087c645bSJennifer Graul 88143d62a6bSnetali // prevent deepl from messing with nocache-instructions 88243d62a6bSnetali $text = str_replace("~~NOCACHE~~", "<ignore>~~NOCACHE~~</ignore>", $text); 88343d62a6bSnetali 884a3a51507SNetali // fix for plugins like tag or template 885a3a51507SNetali $text = preg_replace('/\{\{[\s\w]+?>[\s\S]*?}}/', '<ignore>${0}</ignore>', $text); 8860180404cSNetali 8873b1ff295SNetali // ignore links in wikitext (outside of dokuwiki-links) 8883b1ff295SNetali $text = preg_replace('/\S+:\/\/\S+/', '<ignore>${0}</ignore>', $text); 8893b1ff295SNetali 8900180404cSNetali // ignore link/media ids but translate the text (if existing) 8914b84d3cfSNetali $text = preg_replace('/\[\[([\s\S]*?)(#[\s\S]*?)?((\|)([\s\S]*?))?]]/', '<ignore>[[${1}${2}${4}</ignore>${5}<ignore>]]</ignore>', $text); 8920180404cSNetali $text = preg_replace('/\{\{([\s\S]*?)(\?[\s\S]*?)?((\|)([\s\S]*?))?}}/', '<ignore>{{${1}${2}${4}</ignore>${5}<ignore>}}</ignore>', $text); 8930180404cSNetali 8943b1ff295SNetali // prevent deepl from messing with tables 8953b1ff295SNetali $text = str_replace(" ^ ", "<ignore> ^ </ignore>", $text); 8967c99a9b0Sextrasec $text = str_replace(" ^ ", "<ignore> ^ </ignore>", $text); 8977c99a9b0Sextrasec $text = str_replace(" ^ ", "<ignore> ^ </ignore>", $text); 8987c99a9b0Sextrasec $text = str_replace("^ ", "<ignore>^ </ignore>", $text); 8997c99a9b0Sextrasec $text = str_replace(" ^", "<ignore> ^</ignore>", $text); 9007c99a9b0Sextrasec $text = str_replace("^", "<ignore>^</ignore>", $text); 9017c99a9b0Sextrasec $text = str_replace(" | ", "<ignore> | </ignore>", $text); 9027c99a9b0Sextrasec $text = str_replace(" | ", "<ignore> | </ignore>", $text); 9037c99a9b0Sextrasec $text = str_replace(" | ", "<ignore> | </ignore>", $text); 9047c99a9b0Sextrasec $text = str_replace("| ", "<ignore>| </ignore>", $text); 9057c99a9b0Sextrasec $text = str_replace(" |", "<ignore> |</ignore>", $text); 9063b1ff295SNetali $text = str_replace("|", "<ignore>|</ignore>", $text); 9073b1ff295SNetali 9080180404cSNetali // prevent deepl from doing strange things with dokuwiki syntax 9096301ba6eSNetali // if a full line is formatted, we have to double-ignore for some reason 9106301ba6eSNetali $text = str_replace("''", "<ignore><ignore>''</ignore></ignore>", $text); 9116301ba6eSNetali $text = str_replace("//", "<ignore><ignore>//</ignore></ignore>", $text); 9126301ba6eSNetali $text = str_replace("**", "<ignore><ignore>**</ignore></ignore>", $text); 9136301ba6eSNetali $text = str_replace("__", "<ignore><ignore>__</ignore></ignore>", $text); 9146301ba6eSNetali $text = str_replace("\\\\", "<ignore><ignore>\\\\</ignore></ignore>", $text); 9150180404cSNetali 91613221d46SNetali // prevent deepl from messing with smileys 91713221d46SNetali $smileys = array_keys(getSmileys()); 91813221d46SNetali foreach ($smileys as $smiley) { 91913221d46SNetali $text = str_replace($smiley, "<ignore>" . $smiley . "</ignore>", $text); 92013221d46SNetali } 92113221d46SNetali 9220180404cSNetali // ignore code tags 9230180404cSNetali $text = preg_replace('/(<php[\s\S]*?>[\s\S]*?<\/php>)/', '<ignore>${1}</ignore>', $text); 924057940f7SNetali $text = preg_replace('/(<file[\s\S]*?>[\s\S]*?<\/file>)/', '<ignore>${1}</ignore>', $text); 925057940f7SNetali $text = preg_replace('/(<code[\s\S]*?>[\s\S]*?<\/code>)/', '<ignore>${1}</ignore>', $text); 926057940f7SNetali 9270180404cSNetali // ignore the expressions from the ignore list 9283832d0abSNetali $ignored_expressions = explode(':', $this->getConf('ignored_expressions')); 9293832d0abSNetali 9303832d0abSNetali foreach ($ignored_expressions as $expression) { 9313832d0abSNetali $text = str_replace($expression, '<ignore>' . $expression . '</ignore>', $text); 9323832d0abSNetali } 9333832d0abSNetali 9343832d0abSNetali return $text; 9353832d0abSNetali } 9363832d0abSNetali 9373832d0abSNetali private function remove_ignore_tags($text): string { 9383832d0abSNetali $ignored_expressions = explode(':', $this->getConf('ignored_expressions')); 9393832d0abSNetali 9403832d0abSNetali foreach ($ignored_expressions as $expression) { 9413832d0abSNetali $text = str_replace('<ignore>' . $expression . '</ignore>', $expression, $text); 9423832d0abSNetali } 9433832d0abSNetali 94443d62a6bSnetali // prevent deepl from messing with nocache-instructions 94543d62a6bSnetali $text = str_replace("<ignore>~~NOCACHE~~</ignore>", "~~NOCACHE~~", $text); 94643d62a6bSnetali 9473b1ff295SNetali // prevent deepl from messing with tables 9483b1ff295SNetali $text = str_replace("<ignore>^</ignore>", "^", $text); 9497c99a9b0Sextrasec $text = str_replace("<ignore>^ </ignore>", "^ ", $text); 9507c99a9b0Sextrasec $text = str_replace("<ignore> ^</ignore>", " ^", $text); 9517c99a9b0Sextrasec $text = str_replace("<ignore> ^ </ignore>", " ^ ", $text); 9527c99a9b0Sextrasec $text = str_replace("<ignore> ^ </ignore>", " ^ ", $text); 9537c99a9b0Sextrasec $text = str_replace("<ignore> ^ </ignore>", " ^ ", $text); 9547c99a9b0Sextrasec $text = str_replace("<ignore>|</ignore>", "|", $text); 9557c99a9b0Sextrasec $text = str_replace("<ignore>| </ignore>", "| ", $text); 9567c99a9b0Sextrasec $text = str_replace("<ignore> |</ignore>", " |", $text); 9577c99a9b0Sextrasec $text = str_replace("<ignore> | </ignore>", " | ", $text); 9587c99a9b0Sextrasec $text = str_replace("<ignore> | </ignore>", " | ", $text); 9593b1ff295SNetali $text = str_replace("<ignore> | </ignore>", " | ", $text); 9603b1ff295SNetali 9616301ba6eSNetali $text = str_replace("<ignore><ignore>''</ignore></ignore>", "''", $text); 9626301ba6eSNetali $text = str_replace("<ignore><ignore>//</ignore></ignore>", "//", $text); 9636301ba6eSNetali $text = str_replace("<ignore><ignore>**</ignore></ignore>", "**", $text); 9646301ba6eSNetali $text = str_replace("<ignore><ignore>__</ignore></ignore>", "__", $text); 9656301ba6eSNetali $text = str_replace("<ignore><ignore>\\\\</ignore></ignore>", "\\\\", $text); 9660180404cSNetali 9673b1ff295SNetali // ignore links in wikitext (outside of dokuwiki-links) 9683b1ff295SNetali $text = preg_replace('/<ignore>(\S+:\/\/\S+)<\/ignore>/', '${1}', $text); 9693b1ff295SNetali 9704b84d3cfSNetali $text = preg_replace('/<ignore>\[\[([\s\S]*?)(\|)?(<\/ignore>)([\s\S]*?)?<ignore>]]<\/ignore>/', '[[${1}${2}${4}]]', $text); 9714b84d3cfSNetali $text = preg_replace('/<ignore>\{\{([\s\S]*?)(\|)?(<\/ignore>)([\s\S]*?)?<ignore>}}<\/ignore>/', '{{${1}${2}${4}}}', $text); 9724b84d3cfSNetali 973087c645bSJennifer Graul // prevent deepl from with some page lists 974087c645bSJennifer Graul $text = str_replace("<ignore>{{top}}</ignore>", "{{top}}", $text); 975087c645bSJennifer Graul $text = str_replace("<ignore>{{rating}}</ignore>", "{{rating}}", $text); 976087c645bSJennifer Graul 97713221d46SNetali // prevent deepl from messing with smileys 97813221d46SNetali $smileys = array_keys(getSmileys()); 97913221d46SNetali foreach ($smileys as $smiley) { 98013221d46SNetali $text = str_replace("<ignore>" . $smiley . "</ignore>", $smiley, $text); 98113221d46SNetali } 98213221d46SNetali 9830180404cSNetali $text = preg_replace('/<ignore>(<php[\s\S]*?>[\s\S]*?<\/php>)<\/ignore>/', '${1}', $text); 9840180404cSNetali $text = preg_replace('/<ignore>(<file[\s\S]*?>[\s\S]*?<\/file>)<\/ignore>/', '${1}', $text); 9850180404cSNetali $text = preg_replace('/<ignore>(<code[\s\S]*?>[\s\S]*?<\/code>)<\/ignore>/', '${1}', $text); 9860180404cSNetali 987a3a51507SNetali // fix for plugins like tag or template 988a3a51507SNetali $text = preg_replace('/<ignore>(\{\{[\s\w]+?>[\s\S]*?}})<\/ignore>/', '${1}', $text); 9890180404cSNetali 9901cd781c4SNetali // prevent deepl from breaking headings 9911cd781c4SNetali $text = preg_replace('/<ignore>(={1,6})<\/ignore>/','${1}', $text); 9921cd781c4SNetali 9931cd781c4SNetali // ignore every other xml-like tags (the tags themselves, not their content), otherwise deepl would break the formatting 9941cd781c4SNetali $text = preg_replace('/<ignore>(<[\s\S]+?>)<\/ignore>/', '${1}', $text); 9951cd781c4SNetali 9960180404cSNetali // restore < and > for example from arrows (-->) in wikitext 9970180404cSNetali $text = str_replace('>', '>', $text); 9980180404cSNetali $text = str_replace('<', '<', $text); 9990180404cSNetali 10003b1ff295SNetali // restore & in wikitext 10013b1ff295SNetali $text = str_replace('&', '&', $text); 10023b1ff295SNetali 10033832d0abSNetali return $text; 10043832d0abSNetali } 10053832d0abSNetali} 10063832d0abSNetali 1007