xref: /plugin/deeplautotranslate/action.php (revision d33a2f201081611ec062d674eda63eb47532ebba)
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',
35*d33a2f20SJennifer Graul        'no' => 'NB',
363832d0abSNetali        'pl' => 'PL',
373832d0abSNetali        'pt' => 'PT-PT',
383832d0abSNetali        'ro' => 'RO',
393832d0abSNetali        'ru' => 'RU',
403832d0abSNetali        'sk' => 'SK',
413832d0abSNetali        'sl' => 'SL',
423832d0abSNetali        'sv' => 'SV',
438311ddaaSnetali        'uk' => 'UK',
443832d0abSNetali        'zh' => 'ZH'
45b33135efSNetali    );
463832d0abSNetali
473832d0abSNetali    /**
483832d0abSNetali     * Register its handlers with the DokuWiki's event controller
493832d0abSNetali     */
503832d0abSNetali    public function register(Doku_Event_Handler $controller) {
51153e4498SNetali        $controller->register_hook('ACTION_ACT_PREPROCESS','BEFORE', $this, 'preprocess');
52b33135efSNetali        $controller->register_hook('COMMON_PAGETPL_LOAD','AFTER', $this, 'pagetpl_load');
53b33135efSNetali        $controller->register_hook('COMMON_WIKIPAGE_SAVE','AFTER', $this, 'update_glossary');
543c636ad3SNetali        $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'add_menu_button');
553c636ad3SNetali    }
563c636ad3SNetali
570180404cSNetali    public function add_menu_button(Doku_Event $event): void {
58153e4498SNetali        global $ID;
59bbb1fba9SNetali        global $ACT;
60bbb1fba9SNetali
61bbb1fba9SNetali        if ($ACT != 'show') return;
62153e4498SNetali
633c636ad3SNetali        if ($event->data['view'] != 'page') return;
643c636ad3SNetali
653c636ad3SNetali        if (!$this->getConf('show_button')) return;
66153e4498SNetali
67b33135efSNetali        // no translations for the glossary namespace
68b33135efSNetali        if ($this->check_in_glossary_ns()) return;
69b33135efSNetali
70153e4498SNetali        $split_id = explode(':', $ID);
71153e4498SNetali        $lang_ns = array_shift($split_id);
72153e4498SNetali        // check if we are in a language namespace
73153e4498SNetali        if (array_key_exists($lang_ns, $this->langs)) {
74ff327fe6SNetali            if($this->getConf('default_lang_in_ns') and $lang_ns === $this->get_default_lang()) {
7599da9a08SNetali                // if the default lang is in a namespace and we are in that namespace --> check for push translation
7699da9a08SNetali                if (!$this->check_do_push_translate()) return;
7799da9a08SNetali            } else {
78153e4498SNetali                // in language namespace --> check if we should translate
793c636ad3SNetali                if (!$this->check_do_translation(true)) return;
8099da9a08SNetali            }
81153e4498SNetali        } else {
8299da9a08SNetali            // do not show the button if we are not in a language namespace and the default language is in a namespace
8399da9a08SNetali            if($this->getConf('default_lang_in_ns')) return;
84b33135efSNetali            // not in language namespace and default language is not in a namespace --> check if we should show the push translate button
85153e4498SNetali            if (!$this->check_do_push_translate()) return;
86153e4498SNetali        }
873c636ad3SNetali
883c636ad3SNetali        array_splice($event->data['items'], -1, 0, [new MenuItem()]);
893832d0abSNetali    }
903832d0abSNetali
91153e4498SNetali    public function preprocess(Doku_Event $event, $param): void {
923832d0abSNetali        global $ID;
933c636ad3SNetali
943c636ad3SNetali        // check if action is show or translate
953c636ad3SNetali        if ($event->data != 'show' and $event->data != 'translate') return;
963c636ad3SNetali
97b33135efSNetali        // redirect to glossary ns start if glossary ns is called
98b33135efSNetali        if ($this->check_in_glossary_ns() and $event->data == 'show' and $ID == $this->get_glossary_ns()) {
99b33135efSNetali            send_redirect(wl($this->get_glossary_ns() . ':start'));
100b33135efSNetali        }
101b33135efSNetali
102153e4498SNetali        $split_id = explode(':', $ID);
103153e4498SNetali        $lang_ns = array_shift($split_id);
104153e4498SNetali        // check if we are in a language namespace
105153e4498SNetali        if (array_key_exists($lang_ns, $this->langs)) {
106ff327fe6SNetali            if($this->getConf('default_lang_in_ns') and $lang_ns === $this->get_default_lang()) {
10799da9a08SNetali                // if the default lang is in a namespace and we are in that namespace --> push translate
1083e2a3564SAndreas Gohr                $this->push_translate_event($event);
10999da9a08SNetali            } else {
11099da9a08SNetali                // in language namespace --> autotrans direct
111153e4498SNetali                $this->autotrans_direct($event);
11299da9a08SNetali            }
113153e4498SNetali        } else {
114153e4498SNetali            // not in language namespace --> push translate
1153e2a3564SAndreas Gohr            $this->push_translate_event($event);
116153e4498SNetali        }
117153e4498SNetali    }
118153e4498SNetali
119b33135efSNetali    public function pagetpl_load(Doku_Event $event, $param): void {
120b33135efSNetali        // handle glossary namespace init when we are in it
121b33135efSNetali        if ($this->check_in_glossary_ns()) {
122b33135efSNetali            $this->handle_glossary_init($event);
123b33135efSNetali            return;
124b33135efSNetali        }
125b33135efSNetali
126b33135efSNetali        $this->autotrans_editor($event);
127b33135efSNetali    }
128b33135efSNetali
129b33135efSNetali    public function update_glossary(Doku_Event $event, $param): void {
130b33135efSNetali        global $ID;
131b33135efSNetali        // this also checks if the glossary feature is enabled
132b33135efSNetali        if (!$this->check_in_glossary_ns()) return;
133b33135efSNetali
134b33135efSNetali        $glossary_ns = $this->get_glossary_ns();
135b33135efSNetali
136b33135efSNetali        // check if we are in a glossary definition
137b33135efSNetali        if(preg_match('/^' . $glossary_ns . ':(\w{2})_(\w{2})$/', $ID, $id_match)) {
138b33135efSNetali            $old_glossary_id = $this->get_glossary_id($id_match[1], $id_match[2]);
139b33135efSNetali            if ($event->data['changeType'] == DOKU_CHANGE_TYPE_DELETE) {
140b33135efSNetali                // page deleted --> delete glossary
141b33135efSNetali                if ($old_glossary_id) {
142b33135efSNetali                    $result = $this->delete_glossary($old_glossary_id);
143b33135efSNetali                    if ($result) {
144b33135efSNetali                        msg($this->getLang('msg_glossary_delete_success'), 1);
145b33135efSNetali                        $this->unset_glossary_id($id_match[1], $id_match[2]);
146b33135efSNetali                    }
147b33135efSNetali                }
148b33135efSNetali                return;
149b33135efSNetali            }
150b33135efSNetali
151b33135efSNetali            $entries = '';
152b33135efSNetali
153b33135efSNetali            // grep entries from definition table
154b33135efSNetali            preg_match_all('/[ \t]*\|(.*?)\|(.*?)\|/', $event->data['newContent'], $matches, PREG_SET_ORDER);
155b33135efSNetali            foreach ($matches as $match) {
156b33135efSNetali                $src = trim($match[1]);
157b33135efSNetali                $target = trim($match[2]);
158b33135efSNetali                if ($src == '' or $target == '') {
159b33135efSNetali                    msg($this->getLang('msg_glossary_empty_key'), -1);
160b33135efSNetali                    return;
161b33135efSNetali                }
162b33135efSNetali                $entries .=  $src . "\t" . $target . "\n";
163b33135efSNetali            }
164b33135efSNetali
165b33135efSNetali            if (empty($matches)) {
166b33135efSNetali                // no matches --> delete glossary
167b33135efSNetali                if ($old_glossary_id) {
168b33135efSNetali                    $result = $this->delete_glossary($old_glossary_id);
169b33135efSNetali                    if ($result) {
170b33135efSNetali                        msg($this->getLang('msg_glossary_delete_success'), 1);
171b33135efSNetali                        $this->unset_glossary_id($id_match[1], $id_match[2]);
172b33135efSNetali                    }
173b33135efSNetali                }
174b33135efSNetali                return;
175b33135efSNetali            }
176b33135efSNetali
177b33135efSNetali            $new_glossary_id = $this->create_glossary($id_match[1], $id_match[2], $entries);
178b33135efSNetali
179b33135efSNetali            if ($new_glossary_id) {
180b33135efSNetali                msg($this->getLang('msg_glossary_create_success'), 1);
181b33135efSNetali                $this->set_glossary_id($id_match[1], $id_match[2], $new_glossary_id);
182b33135efSNetali            } else {
183b33135efSNetali                return;
184b33135efSNetali            }
185b33135efSNetali
186b33135efSNetali            if ($old_glossary_id) $this->delete_glossary($old_glossary_id);
187b33135efSNetali        }
188b33135efSNetali    }
189b33135efSNetali
190153e4498SNetali    private function autotrans_direct(Doku_Event $event): void {
191153e4498SNetali        global $ID;
192153e4498SNetali
1933c636ad3SNetali        // abort if action is translate and the translate button is disabled
1943c636ad3SNetali        if ($event->data == 'translate' and !$this->getConf('show_button')) return;
1953c636ad3SNetali
1963c636ad3SNetali        // do nothing on show action when mode is not direct
1973c636ad3SNetali        if ($event->data == 'show' and $this->get_mode() != 'direct') return;
1983c636ad3SNetali
1993c636ad3SNetali        // allow translation of existing pages is we are in the translate action
2003c636ad3SNetali        $allow_existing = ($event->data == 'translate');
2013c636ad3SNetali
2023c636ad3SNetali        // reset action to show
2033c636ad3SNetali        $event->data = 'show';
2043c636ad3SNetali
205153e4498SNetali        if (!$this->check_do_translation($allow_existing)) {
206153e4498SNetali            return;
207153e4498SNetali        }
2083832d0abSNetali
2090180404cSNetali        $org_page_info = $this->get_org_page_info();
2103e2a3564SAndreas Gohr        try {
2110180404cSNetali            $translated_text = $this->deepl_translate($org_page_info["text"], $this->get_target_lang(), $org_page_info["ns"]);
2123e2a3564SAndreas Gohr        } catch (\Exception $e) {
2133e2a3564SAndreas Gohr            msg($e->getMessage(), -1);
214153e4498SNetali            return;
215153e4498SNetali        }
2163832d0abSNetali
2173832d0abSNetali        saveWikiText($ID, $translated_text, 'Automatic translation');
2183832d0abSNetali
219153e4498SNetali        msg($this->getLang('msg_translation_success'), 1);
220153e4498SNetali
2213c636ad3SNetali        // reload the page after translation
2223c636ad3SNetali        send_redirect(wl($ID));
2233832d0abSNetali    }
2243832d0abSNetali
225b33135efSNetali    private function autotrans_editor(Doku_Event $event): void {
2263832d0abSNetali        if ($this->get_mode() != 'editor') return;
2273832d0abSNetali
2283832d0abSNetali        if (!$this->check_do_translation()) return;
2293832d0abSNetali
2300180404cSNetali        $org_page_info = $this->get_org_page_info();
2313832d0abSNetali
2323e2a3564SAndreas Gohr        try {
2330180404cSNetali            $event->data['tpl'] = $this->deepl_translate($org_page_info["text"], $this->get_target_lang(), $org_page_info["ns"]);
2343e2a3564SAndreas Gohr        } catch (\Exception $e) {
2353e2a3564SAndreas Gohr            msg($e->getMessage(), -1);
2363e2a3564SAndreas Gohr            return;
2373e2a3564SAndreas Gohr        }
2383832d0abSNetali    }
2393832d0abSNetali
2403e2a3564SAndreas Gohr    private function push_translate_event(Doku_Event $event): void {
241153e4498SNetali        global $ID;
242153e4498SNetali
243153e4498SNetali        // check if action is translate
244153e4498SNetali        if ($event->data != 'translate') return;
245153e4498SNetali
246153e4498SNetali        // check if button is enabled
247153e4498SNetali        if (!$this->getConf('show_button')) {
248153e4498SNetali            send_redirect(wl($ID));
249153e4498SNetali            return;
250153e4498SNetali        }
251153e4498SNetali
252153e4498SNetali        // push translate
253153e4498SNetali        $push_langs = $this->get_push_langs();
254153e4498SNetali        $org_page_text = rawWiki($ID);
255153e4498SNetali        foreach ($push_langs as $lang) {
2563e2a3564SAndreas Gohr            try {
2573e2a3564SAndreas Gohr                $this->push_translate($ID, $org_page_text, $lang);
2583e2a3564SAndreas Gohr            } catch (\Exception $e) {
2593e2a3564SAndreas Gohr                msg($e->getMessage(), -1);
260153e4498SNetali            }
261153e4498SNetali        }
262153e4498SNetali
263153e4498SNetali        msg($this->getLang('msg_translation_success'), 1);
264153e4498SNetali
265153e4498SNetali        // reload the page after translation to clear the action
266153e4498SNetali        send_redirect(wl($ID));
267153e4498SNetali    }
268153e4498SNetali
2693e2a3564SAndreas Gohr    public function push_translate($id, $org_page_text, $lang): string {
2703e2a3564SAndreas Gohr        if (!$this->check_do_push_translate()) {
2713e2a3564SAndreas Gohr            throw new \Exception('Failed push translate checks', 400);
2723e2a3564SAndreas Gohr        }
2733e2a3564SAndreas Gohr
2743e2a3564SAndreas Gohr        // skip invalid languages
2753e2a3564SAndreas Gohr        if (!array_key_exists($lang, $this->langs)) {
2763e2a3564SAndreas Gohr            throw new \Exception($this->getLang('msg_translation_fail_invalid_lang') . $lang, 404);
2773e2a3564SAndreas Gohr        }
2783e2a3564SAndreas Gohr
2793e2a3564SAndreas Gohr        if ($this->getConf('default_lang_in_ns')) {
2803e2a3564SAndreas Gohr            // if default lang is in ns: replace language namespace in ID
2813e2a3564SAndreas Gohr            $split_id = explode(':', $id);
2823e2a3564SAndreas Gohr            array_shift($split_id);
2833e2a3564SAndreas Gohr            $lang_id = implode(':', $split_id);
2843e2a3564SAndreas Gohr            $lang_id = $lang . ':' . $lang_id;
2853e2a3564SAndreas Gohr        } else {
2863e2a3564SAndreas Gohr            // if default lang is not in ns: add language namespace to ID
2873e2a3564SAndreas Gohr            $lang_id = $lang . ':' . $id;
2883e2a3564SAndreas Gohr        }
2893e2a3564SAndreas Gohr
2903e2a3564SAndreas Gohr        // check permissions
2913e2a3564SAndreas Gohr        $perm = auth_quickaclcheck($lang_id);
2923e2a3564SAndreas Gohr        $exists = page_exists($lang_id);
2933e2a3564SAndreas Gohr        if (($exists and $perm < AUTH_EDIT) or (!$exists and $perm < AUTH_CREATE)) {
2943e2a3564SAndreas Gohr            throw new \Exception($this->getLang('msg_translation_fail_no_permissions') . $lang_id, 403);
2953e2a3564SAndreas Gohr        }
2963e2a3564SAndreas Gohr
2973e2a3564SAndreas Gohr        $translated_text = $this->deepl_translate($org_page_text, $lang, getNS($id));
2983e2a3564SAndreas Gohr        saveWikiText($lang_id, $translated_text, 'Automatic push translation');
2993e2a3564SAndreas Gohr
3003e2a3564SAndreas Gohr        return $lang_id;
3013e2a3564SAndreas Gohr    }
3023e2a3564SAndreas Gohr
303b33135efSNetali    private function handle_glossary_init(Doku_Event $event): void {
304b33135efSNetali        global $ID;
305b33135efSNetali
306b33135efSNetali        $glossary_ns = $this->get_glossary_ns();
307b33135efSNetali
308b33135efSNetali        // create glossary landing page
309b33135efSNetali        if ($ID == $glossary_ns . ':start') {
310b33135efSNetali            $landing_page_text = '====== ' . $this->getLang('glossary_landing_heading') . ' ======' . "\n";
311b33135efSNetali            $landing_page_text .= $this->getLang('glossary_landing_info_msg') . "\n";
312b33135efSNetali
313b33135efSNetali            $src_lang = substr($this->get_default_lang(), 0, 2);
314b33135efSNetali
315b33135efSNetali            $available_glossaries = $this->get_available_glossaries();
316b33135efSNetali            foreach ($available_glossaries as $glossary) {
317b33135efSNetali                if ($glossary['source_lang'] != $src_lang) continue;
318b33135efSNetali                // generate links to the available glossary pages
319b33135efSNetali                $landing_page_text .= '  * [[.:' . $glossary['source_lang'] . '_' . $glossary['target_lang'] . '|' . strtoupper($glossary['source_lang']) . ' -> ' . strtoupper($glossary['target_lang']) . ']]' . "\n";
320b33135efSNetali            }
321b33135efSNetali            $event->data['tpl'] = $landing_page_text;
322b33135efSNetali            return;
323b33135efSNetali        }
324b33135efSNetali
325b33135efSNetali        if (preg_match('/^' . $glossary_ns . ':(\w{2})_(\w{2})$/', $ID, $match)) {
326b33135efSNetali            // check if glossaries are supported for this language pair
327b33135efSNetali            if (!$this->check_glossary_supported($match[1], $match[2])) {
328b33135efSNetali                msg($this->getLang('msg_glossary_unsupported'), -1);
329b33135efSNetali                return;
330b33135efSNetali            }
331b33135efSNetali
332b33135efSNetali            $page_text = '====== ' . $this->getLang('glossary_definition_heading') . ': ' . strtoupper($match[1]) . ' -> ' . strtoupper($match[2]) . ' ======' . "\n";
333b33135efSNetali            $page_text .= $this->getLang('glossary_definition_help') . "\n\n";
334b33135efSNetali            $page_text .= '^ ' . strtoupper($match[1]) . ' ^ ' . strtoupper($match[2]) . ' ^' . "\n";
335b33135efSNetali
336b33135efSNetali            $event->data['tpl'] = $page_text;
337b33135efSNetali            return;
338b33135efSNetali        }
339b33135efSNetali    }
340b33135efSNetali
341b33135efSNetali    private function get_glossary_ns(): string {
342b33135efSNetali        return trim(strtolower($this->getConf('glossary_ns')));
343b33135efSNetali    }
344b33135efSNetali
3453832d0abSNetali    private function get_mode(): string {
3463832d0abSNetali        global $ID;
3473832d0abSNetali        if ($this->getConf('editor_regex')) {
3483832d0abSNetali            if (preg_match('/' . $this->getConf('editor_regex') . '/', $ID) === 1) return 'editor';
3493832d0abSNetali        }
3503832d0abSNetali        if ($this->getConf('direct_regex')) {
3513832d0abSNetali            if (preg_match('/' . $this->getConf('direct_regex') . '/', $ID) === 1) return 'direct';
3523832d0abSNetali        }
3533832d0abSNetali        return $this->getConf('mode');
3543832d0abSNetali    }
3553832d0abSNetali
3563832d0abSNetali    private function get_target_lang(): string {
3573832d0abSNetali        global $ID;
3583832d0abSNetali        $split_id = explode(':', $ID);
3593832d0abSNetali        return array_shift($split_id);
3603832d0abSNetali    }
3613832d0abSNetali
362ff327fe6SNetali    private function get_default_lang(): string {
363ff327fe6SNetali        global $conf;
364ff327fe6SNetali
365ff327fe6SNetali        if (empty($conf['lang_before_translation'])) {
366ff327fe6SNetali            $default_lang = $conf['lang'];
367ff327fe6SNetali        } else {
368ff327fe6SNetali            $default_lang = $conf['lang_before_translation'];
369ff327fe6SNetali        }
370ff327fe6SNetali
371ff327fe6SNetali        return $default_lang;
372ff327fe6SNetali    }
373ff327fe6SNetali
3740180404cSNetali    private function get_org_page_info(): array {
3753832d0abSNetali        global $ID;
3763832d0abSNetali
3773832d0abSNetali        $split_id = explode(':', $ID);
3783832d0abSNetali        array_shift($split_id);
3793832d0abSNetali        $org_id = implode(':', $split_id);
3803832d0abSNetali
38199da9a08SNetali        // if default lang is in ns: add default ns in front of org id
38299da9a08SNetali        if ($this->getConf('default_lang_in_ns')) {
383ff327fe6SNetali            $org_id = $this->get_default_lang() . ':' . $org_id;
38499da9a08SNetali        }
38599da9a08SNetali
3860180404cSNetali        return array("ns" => getNS($org_id), "text" => rawWiki($org_id));
3873832d0abSNetali    }
3883832d0abSNetali
389b33135efSNetali    private function get_available_glossaries(): array {
390b33135efSNetali        if (!trim($this->getConf('api_key'))) {
391b33135efSNetali            msg($this->getLang('msg_bad_key'), -1);
392b33135efSNetali            return array();
393b33135efSNetali        }
394b33135efSNetali
395b33135efSNetali        if ($this->getConf('api') == 'free') {
396b33135efSNetali            $url = 'https://api-free.deepl.com/v2/glossary-language-pairs';
397b33135efSNetali        } else {
398b33135efSNetali            $url = 'https://api.deepl.com/v2/glossary-language-pairs';
399b33135efSNetali        }
400b33135efSNetali
401b33135efSNetali        $http = new DokuHTTPClient();
402b33135efSNetali
403b33135efSNetali        $http->headers = array('Authorization' => 'DeepL-Auth-Key ' . $this->getConf('api_key'));
404b33135efSNetali
405b33135efSNetali        $raw_response = $http->get($url);
406b33135efSNetali
407b33135efSNetali        if ($http->status >= 400) {
408b33135efSNetali            // add error messages
409b33135efSNetali            switch ($http->status) {
410b33135efSNetali                case 403:
411b33135efSNetali                    msg($this->getLang('msg_bad_key'), -1);
412b33135efSNetali                    break;
413b33135efSNetali                default:
414b33135efSNetali                    msg($this->getLang('msg_glossary_fetch_fail'), -1);
415b33135efSNetali                    break;
416b33135efSNetali            }
417b33135efSNetali
418b33135efSNetali            // if any error occurred return an empty array
419b33135efSNetali            return array();
420b33135efSNetali        }
421b33135efSNetali
422b33135efSNetali        $json_response = json_decode($raw_response, true);
423b33135efSNetali
424b33135efSNetali        return $json_response['supported_languages'];
425b33135efSNetali    }
426b33135efSNetali
427b33135efSNetali    private function get_glossary_id($src, $target): string {
428b33135efSNetali        if (!file_exists(DOKU_CONF . 'deepl-glossaries.json')) return '';
429b33135efSNetali
430b33135efSNetali        $key = $src . "_" . $target;
431b33135efSNetali
432b33135efSNetali        $raw_json = file_get_contents(DOKU_CONF . 'deepl-glossaries.json');
433b33135efSNetali        $content = json_decode($raw_json, true);
434b33135efSNetali
435b33135efSNetali        if (array_key_exists($key, $content)) {
436b33135efSNetali            return $content[$key];
437b33135efSNetali        } else {
438b33135efSNetali            return '';
439b33135efSNetali        }
440b33135efSNetali    }
441b33135efSNetali
442b33135efSNetali    private function set_glossary_id($src, $target, $glossary_id): void {
443b33135efSNetali        if (file_exists(DOKU_CONF . 'deepl-glossaries.json')) {
444b33135efSNetali            $raw_json = file_get_contents(DOKU_CONF . 'deepl-glossaries.json');
445b33135efSNetali            $content = json_decode($raw_json, true);
446b33135efSNetali        } else {
447b33135efSNetali            $content = array();
448b33135efSNetali        }
449b33135efSNetali
450b33135efSNetali        $key = $src . "_" . $target;
451b33135efSNetali
452b33135efSNetali        $content[$key] = $glossary_id;
453b33135efSNetali
454b33135efSNetali        $raw_json = json_encode($content);
455b33135efSNetali        file_put_contents(DOKU_CONF . 'deepl-glossaries.json', $raw_json);
456b33135efSNetali    }
457b33135efSNetali
458b33135efSNetali    private function unset_glossary_id($src, $target): void {
459b33135efSNetali        if (file_exists(DOKU_CONF . 'deepl-glossaries.json')) {
460b33135efSNetali            $raw_json = file_get_contents(DOKU_CONF . 'deepl-glossaries.json');
461b33135efSNetali            $content = json_decode($raw_json, true);
462b33135efSNetali        } else {
463b33135efSNetali            return;
464b33135efSNetali        }
465b33135efSNetali
466b33135efSNetali        $key = $src . "_" . $target;
467b33135efSNetali
468b33135efSNetali        unset($content[$key]);
469b33135efSNetali
470b33135efSNetali        $raw_json = json_encode($content);
471b33135efSNetali        file_put_contents(DOKU_CONF . 'deepl-glossaries.json', $raw_json);
472b33135efSNetali    }
473b33135efSNetali
474b33135efSNetali    private function check_in_glossary_ns(): bool {
475b33135efSNetali        global $ID;
476b33135efSNetali
477b33135efSNetali        $glossary_ns = $this->get_glossary_ns();
478b33135efSNetali
479b33135efSNetali        // check if the glossary namespace is defined
480b33135efSNetali        if (!$glossary_ns) return false;
481b33135efSNetali
482b33135efSNetali        // check if we are in the glossary namespace
483b33135efSNetali        if (substr($ID, 0, strlen($glossary_ns)) == $glossary_ns) {
484b33135efSNetali            return true;
485b33135efSNetali        } else {
486b33135efSNetali            return false;
487b33135efSNetali        }
488b33135efSNetali    }
489b33135efSNetali
490b33135efSNetali    private function check_glossary_supported($src, $target): bool {
491b33135efSNetali        if(strlen($src) != 2 or strlen($target) != 2) return false;
492b33135efSNetali        $available_glossaries = $this->get_available_glossaries();
493b33135efSNetali        foreach ($available_glossaries as $glossary) {
494b33135efSNetali            if ($src == $glossary['source_lang'] and $target == $glossary['target_lang']) return true;
495b33135efSNetali        }
496b33135efSNetali        return false;
497b33135efSNetali    }
498b33135efSNetali
4993c636ad3SNetali    private function check_do_translation($allow_existing = false): bool {
5003832d0abSNetali        global $INFO;
5013832d0abSNetali        global $ID;
5023832d0abSNetali
5033c636ad3SNetali        // only translate if the current page does not exist
5043c636ad3SNetali        if ($INFO['exists'] and !$allow_existing) return false;
5053c636ad3SNetali
5063c636ad3SNetali        // permission check
5073c636ad3SNetali        $perm = auth_quickaclcheck($ID);
5083c636ad3SNetali        if (($INFO['exists'] and $perm < AUTH_EDIT) or (!$INFO['exists'] and $perm < AUTH_CREATE)) return false;
5093c636ad3SNetali
5103832d0abSNetali        // skip blacklisted namespaces and pages
5113832d0abSNetali        if ($this->getConf('blacklist_regex')) {
5123832d0abSNetali            if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false;
5133832d0abSNetali        }
5143832d0abSNetali
5153832d0abSNetali        $split_id = explode(':', $ID);
5163832d0abSNetali        $lang_ns = array_shift($split_id);
5173832d0abSNetali        // only translate if the current page is in a language namespace
5183832d0abSNetali        if (!array_key_exists($lang_ns, $this->langs)) return false;
5193832d0abSNetali
5203832d0abSNetali        $org_id = implode(':', $split_id);
52199da9a08SNetali
52299da9a08SNetali        // if default lang is in ns: add default ns in front of org id
52399da9a08SNetali        if ($this->getConf('default_lang_in_ns')) {
524ff327fe6SNetali            $org_id = $this->get_default_lang() . ':' . $org_id;
52599da9a08SNetali        }
52699da9a08SNetali
527b33135efSNetali        // no translations for the glossary namespace
528b33135efSNetali        $glossary_ns = $this->get_glossary_ns();
529b33135efSNetali        if ($glossary_ns and substr($org_id, 0, strlen($glossary_ns)) == $glossary_ns) return false;
530b33135efSNetali
5313832d0abSNetali        // check if the original page exists
5323832d0abSNetali        if (!page_exists($org_id)) return false;
5333832d0abSNetali
5343832d0abSNetali        return true;
5353832d0abSNetali    }
5363832d0abSNetali
537153e4498SNetali    private function check_do_push_translate(): bool {
538153e4498SNetali        global $ID;
53999da9a08SNetali        global $INFO;
54099da9a08SNetali
54199da9a08SNetali        if (!$INFO['exists']) return false;
54299da9a08SNetali
543a3a51507SNetali        // only allow push translation if the user can edit this page
544a3a51507SNetali        $perm = auth_quickaclcheck($ID);
545a3a51507SNetali        if ($perm < AUTH_EDIT) return false;
546a3a51507SNetali
54799da9a08SNetali        // if default language is in namespace: only allow push translation from that namespace
54899da9a08SNetali        if($this->getConf('default_lang_in_ns')) {
54999da9a08SNetali            $split_id = explode(':', $ID);
55099da9a08SNetali            $lang_ns = array_shift($split_id);
55199da9a08SNetali
552ff327fe6SNetali            if ($lang_ns !== $this->get_default_lang()) return false;
55399da9a08SNetali        }
554153e4498SNetali
555b33135efSNetali        // no translations for the glossary namespace
556b33135efSNetali        if ($this->check_in_glossary_ns()) return false;
557b33135efSNetali
558153e4498SNetali        $push_langs = $this->get_push_langs();
559153e4498SNetali        // push_langs empty --> push_translate disabled --> abort
560153e4498SNetali        if (empty($push_langs)) return false;
561153e4498SNetali
562153e4498SNetali        // skip blacklisted namespaces and pages
563153e4498SNetali        if ($this->getConf('blacklist_regex')) {
564153e4498SNetali            // blacklist regex match --> abort
565153e4498SNetali            if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false;
566153e4498SNetali        }
567153e4498SNetali
568153e4498SNetali        return true;
569153e4498SNetali    }
570153e4498SNetali
571b33135efSNetali    private function create_glossary($src, $target, $entries): string {
572b33135efSNetali        if (!trim($this->getConf('api_key'))) {
573b33135efSNetali            msg($this->getLang('msg_bad_key'), -1);
574b33135efSNetali            return '';
575b33135efSNetali        }
576b33135efSNetali
577b33135efSNetali        if ($this->getConf('api') == 'free') {
578b33135efSNetali            $url = 'https://api-free.deepl.com/v2/glossaries';
579b33135efSNetali        } else {
580b33135efSNetali            $url = 'https://api.deepl.com/v2/glossaries';
581b33135efSNetali        }
582b33135efSNetali
583b33135efSNetali        $data = array(
584b33135efSNetali            'name' => 'DokuWiki-Autotranslate-' . $src . '_' . $target,
585b33135efSNetali            'source_lang' => $src,
586b33135efSNetali            'target_lang' => $target,
587b33135efSNetali            'entries' => $entries,
588b33135efSNetali            'entries_format' => 'tsv'
589b33135efSNetali        );
590b33135efSNetali
591b33135efSNetali        $http = new DokuHTTPClient();
592b33135efSNetali
593b33135efSNetali        $http->headers = array('Authorization' => 'DeepL-Auth-Key ' . $this->getConf('api_key'));
594b33135efSNetali
595b33135efSNetali        $raw_response = $http->post($url, $data);
596b33135efSNetali
597b33135efSNetali        if ($http->status >= 400) {
598b33135efSNetali            // add error messages
599b33135efSNetali            switch ($http->status) {
600b33135efSNetali                case 403:
601b33135efSNetali                    msg($this->getLang('msg_bad_key'), -1);
602b33135efSNetali                    break;
603b33135efSNetali                case 400:
604b33135efSNetali                    msg($this->getLang('msg_glossary_content_invalid'), -1);
605b33135efSNetali                    break;
606b33135efSNetali                default:
607b33135efSNetali                    msg($this->getLang('msg_glossary_create_fail'), -1);
608b33135efSNetali                    break;
609b33135efSNetali            }
610b33135efSNetali
611b33135efSNetali            // if any error occurred return an empty string
612b33135efSNetali            return '';
613b33135efSNetali        }
614b33135efSNetali
615b33135efSNetali        $json_response = json_decode($raw_response, true);
616b33135efSNetali
617b33135efSNetali        return $json_response['glossary_id'];
618b33135efSNetali    }
619b33135efSNetali
620b33135efSNetali    private function delete_glossary($glossary_id): bool {
621b33135efSNetali        if (!trim($this->getConf('api_key'))) {
622b33135efSNetali            msg($this->getLang('msg_bad_key'), -1);
623b33135efSNetali            return false;
624b33135efSNetali        }
625b33135efSNetali
626b33135efSNetali        if ($this->getConf('api') == 'free') {
627b33135efSNetali            $url = 'https://api-free.deepl.com/v2/glossaries';
628b33135efSNetali        } else {
629b33135efSNetali            $url = 'https://api.deepl.com/v2/glossaries';
630b33135efSNetali        }
631b33135efSNetali
632b33135efSNetali        $url .= '/' . $glossary_id;
633b33135efSNetali
634b33135efSNetali        $http = new DokuHTTPClient();
635b33135efSNetali
636b33135efSNetali        $http->headers = array('Authorization' => 'DeepL-Auth-Key ' . $this->getConf('api_key'));
637b33135efSNetali
638b33135efSNetali        $http->sendRequest($url, '', 'DELETE');
639b33135efSNetali
640b33135efSNetali        if ($http->status >= 400) {
641b33135efSNetali            // add error messages
642b33135efSNetali            switch ($http->status) {
643b33135efSNetali                case 403:
644b33135efSNetali                    msg($this->getLang('msg_bad_key'), -1);
645b33135efSNetali                    break;
646b33135efSNetali                default:
647b33135efSNetali                    msg($this->getLang('msg_glossary_delete_fail'), -1);
648b33135efSNetali                    break;
649b33135efSNetali            }
650b33135efSNetali
651b33135efSNetali            // if any error occurred return false
652b33135efSNetali            return false;
653b33135efSNetali        }
654b33135efSNetali
655b33135efSNetali        return true;
656b33135efSNetali    }
657b33135efSNetali
6580180404cSNetali    private function deepl_translate($text, $target_lang, $org_ns): string {
659b33135efSNetali        if (!trim($this->getConf('api_key'))) {
6603e2a3564SAndreas Gohr            throw new \Exception($this->getLang('msg_translation_fail_bad_key'), 400);
661b33135efSNetali        }
6623832d0abSNetali
6630180404cSNetali        $text = $this->patch_links($text, $target_lang, $org_ns);
6640180404cSNetali
6653832d0abSNetali        $text = $this->insert_ignore_tags($text);
6663832d0abSNetali
667b33135efSNetali        $data = array(
668b33135efSNetali            'source_lang' => strtoupper(substr($this->get_default_lang(), 0, 2)), // cut of things like "-informal"
6690180404cSNetali            'target_lang' => $this->langs[$target_lang],
6703832d0abSNetali            'tag_handling' => 'xml',
6710180404cSNetali            'ignore_tags' => 'ignore',
6723832d0abSNetali            'text' => $text
673b33135efSNetali        );
674b33135efSNetali
6756a17ed8dSAnna Dabrowska        // use v1 of tag handling (not as strict XML parsing as default v2 - in 2026)
6766a17ed8dSAnna Dabrowska        if ($this->getConf('tag_handling_v1')) {
6776a17ed8dSAnna Dabrowska            $data['tag_handling_version'] = 'v1';
6786a17ed8dSAnna Dabrowska        }
6796a17ed8dSAnna Dabrowska
680b33135efSNetali        // check if glossaries are enabled
681b33135efSNetali        if ($this->get_glossary_ns()) {
682b33135efSNetali            $src = substr($this->get_default_lang(), 0, 2);
683b33135efSNetali            $target = substr($target_lang, 0, 2);
684b33135efSNetali            $glossary_id = $this->get_glossary_id($src, $target);
685b33135efSNetali            if ($glossary_id) {
686b33135efSNetali                // use glossary if it is defined
687b33135efSNetali                $data['glossary_id'] = $glossary_id;
688b33135efSNetali            }
689b33135efSNetali        }
6903832d0abSNetali
6913832d0abSNetali        if ($this->getConf('api') == 'free') {
69281931e50SNetali            $url = 'https://api-free.deepl.com/v2/translate';
6933832d0abSNetali        } else {
69481931e50SNetali            $url = 'https://api.deepl.com/v2/translate';
6953832d0abSNetali        }
6963832d0abSNetali
69781931e50SNetali        $http = new DokuHTTPClient();
698858956d2SAndreas Gohr        $http->keep_alive = false;
699b33135efSNetali
700b33135efSNetali        $http->headers = array('Authorization' => 'DeepL-Auth-Key ' . $this->getConf('api_key'));
701b33135efSNetali
70281931e50SNetali        $raw_response = $http->post($url, $data);
7033832d0abSNetali
704858956d2SAndreas Gohr        if ($http->status >= 400 || $http->status < 200) {
7055f8ab21dSNetali            // add error messages
7065f8ab21dSNetali            switch ($http->status) {
7075f8ab21dSNetali                case 403:
7083e2a3564SAndreas Gohr                    throw new \Exception($this->getLang('msg_translation_fail_bad_key'), 403);
709b33135efSNetali                case 404:
7103e2a3564SAndreas Gohr                    throw new \Exception($this->getLang('msg_translation_fail_invalid_glossary'), 404);
7115f8ab21dSNetali                case 456:
7123e2a3564SAndreas Gohr                    throw new \Exception($this->getLang('msg_translation_fail_quota_exceeded'), 456);
7135f8ab21dSNetali                default:
7146a17ed8dSAnna Dabrowska                    if ($this->getConf('api_log_errors')) {
7156a17ed8dSAnna Dabrowska                        $logger = \dokuwiki\Logger::getInstance('deeplautotranslate');
7166a17ed8dSAnna Dabrowska                        $logger->log("$http->status " . $http->resp_body, $data['text']);
7176a17ed8dSAnna Dabrowska                    }
718858956d2SAndreas Gohr                    throw new \Exception($this->getLang('msg_translation_fail'), $http->status ?: 500);
7193e2a3564SAndreas Gohr            }
7205f8ab21dSNetali        }
7215f8ab21dSNetali
7223e2a3564SAndreas Gohr        $json_response = json_decode($raw_response, true, JSON_THROW_ON_ERROR);
7233832d0abSNetali        $translated_text = $json_response['translations'][0]['text'];
7243832d0abSNetali
7253832d0abSNetali        $translated_text = $this->remove_ignore_tags($translated_text);
7263832d0abSNetali
7273832d0abSNetali        return $translated_text;
7283832d0abSNetali    }
7293832d0abSNetali
730153e4498SNetali    private function get_push_langs(): array {
731153e4498SNetali        $push_langs = trim($this->getConf('push_langs'));
732153e4498SNetali
733153e4498SNetali        if ($push_langs === '') return array();
734153e4498SNetali
735153e4498SNetali        return explode(' ', $push_langs);
736153e4498SNetali    }
737153e4498SNetali
73824d1dd2fSAndreas Gohr    /**
73924d1dd2fSAndreas Gohr     * Is the given ID a relative path?
74024d1dd2fSAndreas Gohr     *
74124d1dd2fSAndreas Gohr     * Always returns false if keep_relative is disabled.
74224d1dd2fSAndreas Gohr     *
74324d1dd2fSAndreas Gohr     * @param string $id
74424d1dd2fSAndreas Gohr     * @return bool
74524d1dd2fSAndreas Gohr     */
746522d0814SJennifer Graul    private function is_relative_link($id): bool {
74724d1dd2fSAndreas Gohr        if (!$this->getConf('keep_relative')) return false;
74824d1dd2fSAndreas Gohr        if ($id === '') return false;
74924d1dd2fSAndreas Gohr        if (strpos($id, ':') === false) return true;
75024d1dd2fSAndreas Gohr        if ($id[0] === '.') return true;
75124d1dd2fSAndreas Gohr        if ($id[0] === '~') return true;
75224d1dd2fSAndreas Gohr        return false;
75324d1dd2fSAndreas Gohr    }
75424d1dd2fSAndreas Gohr
7550180404cSNetali    private function patch_links($text, $target_lang, $ns): string {
7560180404cSNetali        /*
7570180404cSNetali         * 1. Find links in [[ aa:bb ]] or [[ aa:bb | cc ]]
7580180404cSNetali         * 2. Extract aa:bb
7590180404cSNetali         * 3. Check if lang:aa:bb exists
7600180404cSNetali         * 3.1. --> Yes --> replace
7610180404cSNetali         * 3.2. --> No --> leave it as it is
7620180404cSNetali         */
7633832d0abSNetali
7640180404cSNetali
7650180404cSNetali        /*
7660180404cSNetali         * LINKS
7670180404cSNetali         */
7680180404cSNetali
7694b84d3cfSNetali        preg_match_all('/\[\[([\s\S]*?)(#[\s\S]*?)?((\|)([\s\S]*?))?]]/', $text, $matches, PREG_SET_ORDER);
7700180404cSNetali
7710180404cSNetali        foreach ($matches as $match) {
7720180404cSNetali
7730180404cSNetali            // external link --> skip
774a3a51507SNetali            if (strpos($match[1], '://') !== false) continue;
7750180404cSNetali
77684cda41fSNetali            // skip interwiki links
77784cda41fSNetali            if (strpos($match[1], '>') !== false) continue;
77884cda41fSNetali
7792a12605eSnetali            // skip mail addresses
7802a12605eSnetali            if (strpos($match[1], '@') !== false) continue;
7812a12605eSnetali
78284cda41fSNetali            // skip windows share links
78384cda41fSNetali            if (strpos($match[1], '\\\\') !== false) continue;
78484cda41fSNetali
78584cda41fSNetali            $resolved_id = trim($match[1]);
786522d0814SJennifer Graul            if($this->is_relative_link($resolved_id)) continue;
7870180404cSNetali
7880180404cSNetali            resolve_pageid($ns, $resolved_id, $exists);
7890180404cSNetali
79053f3766cSNetali            $resolved_id_full = $resolved_id;
7910180404cSNetali
7926663bcb5SNetali            // if the link already points to a target in a language namespace drop it and add the new language namespace
7936663bcb5SNetali            $split_id = explode(':', $resolved_id);
7946663bcb5SNetali            $lang_ns = array_shift($split_id);
7956663bcb5SNetali            if (array_key_exists($lang_ns, $this->langs)) {
7966663bcb5SNetali                $resolved_id = implode(':', $split_id);
7976663bcb5SNetali            }
7986663bcb5SNetali
7990180404cSNetali            $lang_id = $target_lang . ':' . $resolved_id;
8000180404cSNetali
8010180404cSNetali            if (!page_exists($lang_id)) {
80253f3766cSNetali                // Page in target lang does not exist --> replace with absolute ID in case it was a relative ID
80353f3766cSNetali                $new_link = '[[' . $resolved_id_full . $match[2] . $match[3] . ']]';
80453f3766cSNetali            } else {
80553f3766cSNetali                // Page in target lang exists --> replace link
8064b84d3cfSNetali                $new_link = '[[' . $lang_id . $match[2] . $match[3] . ']]';
80753f3766cSNetali            }
8080180404cSNetali
8090180404cSNetali            $text = str_replace($match[0], $new_link, $text);
8100180404cSNetali
8110180404cSNetali        }
8120180404cSNetali
8130180404cSNetali        /*
8140180404cSNetali         * MEDIA
8150180404cSNetali         */
8160180404cSNetali
81784cda41fSNetali        preg_match_all('/\{\{(([\s\S]*?)(\?[\s\S]*?)?)(\|([\s\S]*?))?}}/', $text, $matches, PREG_SET_ORDER);
8180180404cSNetali
8190180404cSNetali        foreach ($matches as $match) {
8200180404cSNetali
8210180404cSNetali            // external image --> skip
822a3a51507SNetali            if (strpos($match[1], '://') !== false) continue;
823a3a51507SNetali
824a3a51507SNetali            // skip things like {{tag>...}}
825a3a51507SNetali            if (strpos($match[1], '>') !== false) continue;
8260180404cSNetali
82784cda41fSNetali            // keep alignment
82884cda41fSNetali            $align_left = "";
82984cda41fSNetali            $align_right = "";
83084cda41fSNetali
83184cda41fSNetali            // align left --> space in front of ID
83284cda41fSNetali            if (substr($match[1], 0, 1) == " ") $align_left = " ";
83384cda41fSNetali            // align right --> space behind id
83484cda41fSNetali            if (substr($match[1], -1) == " ") $align_right = " ";
83584cda41fSNetali
83684cda41fSNetali            $resolved_id = trim($match[2]);
83784cda41fSNetali            $params = trim($match[3]);
8380180404cSNetali
839522d0814SJennifer Graul            if($this->is_relative_link($resolved_id)) continue;
84024d1dd2fSAndreas Gohr
8410180404cSNetali            resolve_mediaid($ns, $resolved_id, $exists);
8420180404cSNetali
84353f3766cSNetali            $resolved_id_full = $resolved_id;
8440180404cSNetali
8456663bcb5SNetali            // if the link already points to a target in a language namespace drop it and add the new language namespace
8466663bcb5SNetali            $split_id = explode(':', $resolved_id);
8476663bcb5SNetali            $lang_ns = array_shift($split_id);
8486663bcb5SNetali            if (array_key_exists($lang_ns, $this->langs)) {
8496663bcb5SNetali                $resolved_id = implode(':', $split_id);
8506663bcb5SNetali            }
8516663bcb5SNetali
8520180404cSNetali            $lang_id = $target_lang . ':' . $resolved_id;
8530180404cSNetali
8540180404cSNetali            $lang_id_fn = mediaFN($lang_id);
8550180404cSNetali
8560180404cSNetali            if (!file_exists($lang_id_fn)) {
85753f3766cSNetali                // media in target lang does not exist --> replace with absolute ID in case it was a relative ID
85884cda41fSNetali                $new_link = '{{' . $align_left . $resolved_id_full . $params . $align_right . $match[4] . '}}';
85953f3766cSNetali            } else {
86053f3766cSNetali                // media in target lang exists --> replace it
86184cda41fSNetali                $new_link = '{{' . $align_left . $lang_id . $params . $align_right . $match[4] . '}}';
86253f3766cSNetali            }
8630180404cSNetali
8640180404cSNetali            $text = str_replace($match[0], $new_link, $text);
8650180404cSNetali
8660180404cSNetali        }
8670180404cSNetali
8680180404cSNetali        return $text;
8690180404cSNetali    }
8700180404cSNetali
8710180404cSNetali    private function insert_ignore_tags($text): string {
8720180404cSNetali        // ignore every other xml-like tags (the tags themselves, not their content), otherwise deepl would break the formatting
8730180404cSNetali        $text = preg_replace('/<[\s\S]+?>/', '<ignore>${0}</ignore>', $text);
8740180404cSNetali
8751cd781c4SNetali        // prevent deepl from breaking headings
8761cd781c4SNetali        $text = preg_replace('/={1,6}/', '<ignore>${0}</ignore>', $text);
8771cd781c4SNetali
878087c645bSJennifer Graul        // prevent deepl from with some page lists
879087c645bSJennifer Graul        $text = str_replace("{{top}}", "<ignore>{{top}}</ignore>", $text);
880087c645bSJennifer Graul        $text = str_replace("{{rating}}", "<ignore>{{rating}}</ignore>", $text);
881087c645bSJennifer Graul
88243d62a6bSnetali        // prevent deepl from messing with nocache-instructions
88343d62a6bSnetali        $text = str_replace("~~NOCACHE~~", "<ignore>~~NOCACHE~~</ignore>", $text);
88443d62a6bSnetali
885a3a51507SNetali        // fix for plugins like tag or template
886a3a51507SNetali        $text = preg_replace('/\{\{[\s\w]+?>[\s\S]*?}}/', '<ignore>${0}</ignore>', $text);
8870180404cSNetali
8883b1ff295SNetali        // ignore links in wikitext (outside of dokuwiki-links)
8893b1ff295SNetali        $text = preg_replace('/\S+:\/\/\S+/', '<ignore>${0}</ignore>', $text);
8903b1ff295SNetali
8910180404cSNetali        // ignore link/media ids but translate the text (if existing)
8924b84d3cfSNetali        $text = preg_replace('/\[\[([\s\S]*?)(#[\s\S]*?)?((\|)([\s\S]*?))?]]/', '<ignore>[[${1}${2}${4}</ignore>${5}<ignore>]]</ignore>', $text);
8930180404cSNetali        $text = preg_replace('/\{\{([\s\S]*?)(\?[\s\S]*?)?((\|)([\s\S]*?))?}}/', '<ignore>{{${1}${2}${4}</ignore>${5}<ignore>}}</ignore>', $text);
8940180404cSNetali
8953b1ff295SNetali        // prevent deepl from messing with tables
8963b1ff295SNetali        $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);
9067c99a9b0Sextrasec        $text = str_replace("  |", "<ignore>  |</ignore>", $text);
9073b1ff295SNetali        $text = str_replace("|", "<ignore>|</ignore>", $text);
9083b1ff295SNetali
9090180404cSNetali        // prevent deepl from doing strange things with dokuwiki syntax
9106301ba6eSNetali        // if a full line is formatted, we have to double-ignore for some reason
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);
9156301ba6eSNetali        $text = str_replace("\\\\", "<ignore><ignore>\\\\</ignore></ignore>", $text);
9160180404cSNetali
91713221d46SNetali        // prevent deepl from messing with smileys
91813221d46SNetali        $smileys = array_keys(getSmileys());
91913221d46SNetali        foreach ($smileys as $smiley) {
92013221d46SNetali            $text = str_replace($smiley, "<ignore>" . $smiley . "</ignore>", $text);
92113221d46SNetali        }
92213221d46SNetali
9230180404cSNetali        // ignore code tags
9240180404cSNetali        $text = preg_replace('/(<php[\s\S]*?>[\s\S]*?<\/php>)/', '<ignore>${1}</ignore>', $text);
925057940f7SNetali        $text = preg_replace('/(<file[\s\S]*?>[\s\S]*?<\/file>)/', '<ignore>${1}</ignore>', $text);
926057940f7SNetali        $text = preg_replace('/(<code[\s\S]*?>[\s\S]*?<\/code>)/', '<ignore>${1}</ignore>', $text);
927057940f7SNetali
9280180404cSNetali        // ignore the expressions from the ignore list
9293832d0abSNetali        $ignored_expressions = explode(':', $this->getConf('ignored_expressions'));
9303832d0abSNetali
9313832d0abSNetali        foreach ($ignored_expressions as $expression) {
9323832d0abSNetali            $text = str_replace($expression, '<ignore>' . $expression . '</ignore>', $text);
9333832d0abSNetali        }
9343832d0abSNetali
9353832d0abSNetali        return $text;
9363832d0abSNetali    }
9373832d0abSNetali
9383832d0abSNetali    private function remove_ignore_tags($text): string {
9393832d0abSNetali        $ignored_expressions = explode(':', $this->getConf('ignored_expressions'));
9403832d0abSNetali
9413832d0abSNetali        foreach ($ignored_expressions as $expression) {
9423832d0abSNetali            $text = str_replace('<ignore>' . $expression . '</ignore>', $expression, $text);
9433832d0abSNetali        }
9443832d0abSNetali
94543d62a6bSnetali        // prevent deepl from messing with nocache-instructions
94643d62a6bSnetali        $text = str_replace("<ignore>~~NOCACHE~~</ignore>", "~~NOCACHE~~", $text);
94743d62a6bSnetali
948d72397a2SJennifer Graul        // prevent deepl from breaking headings
949d72397a2SJennifer Graul        $text = preg_replace('/<ignore>(={1,6})<\/ignore>/','${1}', $text);
950d72397a2SJennifer Graul
9513b1ff295SNetali        // prevent deepl from messing with tables
9523b1ff295SNetali        $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);
9597c99a9b0Sextrasec        $text = str_replace("<ignore>|  </ignore>", "|  ", $text);
9607c99a9b0Sextrasec        $text = str_replace("<ignore>  |</ignore>", "  |", $text);
9617c99a9b0Sextrasec        $text = str_replace("<ignore> |  </ignore>", " |  ", $text);
9627c99a9b0Sextrasec        $text = str_replace("<ignore>  | </ignore>", "  | ", $text);
9633b1ff295SNetali        $text = str_replace("<ignore>  |  </ignore>", "  |  ", $text);
9643b1ff295SNetali
9656301ba6eSNetali        $text = str_replace("<ignore><ignore>''</ignore></ignore>", "''", $text);
9666301ba6eSNetali        $text = str_replace("<ignore><ignore>//</ignore></ignore>", "//", $text);
9676301ba6eSNetali        $text = str_replace("<ignore><ignore>**</ignore></ignore>", "**", $text);
9686301ba6eSNetali        $text = str_replace("<ignore><ignore>__</ignore></ignore>", "__", $text);
9696301ba6eSNetali        $text = str_replace("<ignore><ignore>\\\\</ignore></ignore>", "\\\\", $text);
9700180404cSNetali
9713b1ff295SNetali        // ignore links in wikitext (outside of dokuwiki-links)
9723b1ff295SNetali        $text = preg_replace('/<ignore>(\S+:\/\/\S+)<\/ignore>/', '${1}', $text);
9733b1ff295SNetali
9744b84d3cfSNetali        $text = preg_replace('/<ignore>\[\[([\s\S]*?)(\|)?(<\/ignore>)([\s\S]*?)?<ignore>]]<\/ignore>/', '[[${1}${2}${4}]]', $text);
9754b84d3cfSNetali        $text = preg_replace('/<ignore>\{\{([\s\S]*?)(\|)?(<\/ignore>)([\s\S]*?)?<ignore>}}<\/ignore>/', '{{${1}${2}${4}}}', $text);
9764b84d3cfSNetali
977087c645bSJennifer Graul        // prevent deepl from with some page lists
978087c645bSJennifer Graul        $text = str_replace("<ignore>{{top}}</ignore>", "{{top}}", $text);
979087c645bSJennifer Graul        $text = str_replace("<ignore>{{rating}}</ignore>", "{{rating}}", $text);
980087c645bSJennifer Graul
98113221d46SNetali        // prevent deepl from messing with smileys
98213221d46SNetali        $smileys = array_keys(getSmileys());
98313221d46SNetali        foreach ($smileys as $smiley) {
98413221d46SNetali            $text = str_replace("<ignore>" . $smiley . "</ignore>", $smiley, $text);
98513221d46SNetali        }
98613221d46SNetali
9870180404cSNetali        $text = preg_replace('/<ignore>(<php[\s\S]*?>[\s\S]*?<\/php>)<\/ignore>/', '${1}', $text);
9880180404cSNetali        $text = preg_replace('/<ignore>(<file[\s\S]*?>[\s\S]*?<\/file>)<\/ignore>/', '${1}', $text);
9890180404cSNetali        $text = preg_replace('/<ignore>(<code[\s\S]*?>[\s\S]*?<\/code>)<\/ignore>/', '${1}', $text);
9900180404cSNetali
991a3a51507SNetali        // fix for plugins like tag or template
992a3a51507SNetali        $text = preg_replace('/<ignore>(\{\{[\s\w]+?>[\s\S]*?}})<\/ignore>/', '${1}', $text);
9930180404cSNetali
9941cd781c4SNetali        // ignore every other xml-like tags (the tags themselves, not their content), otherwise deepl would break the formatting
9951cd781c4SNetali        $text = preg_replace('/<ignore>(<[\s\S]+?>)<\/ignore>/', '${1}', $text);
9961cd781c4SNetali
9970180404cSNetali        // restore < and > for example from arrows (-->) in wikitext
9980180404cSNetali        $text = str_replace('&gt;', '>', $text);
9990180404cSNetali        $text = str_replace('&lt;', '<', $text);
10000180404cSNetali
10013b1ff295SNetali        // restore & in wikitext
10023b1ff295SNetali        $text = str_replace('&amp;', '&', $text);
10033b1ff295SNetali
10043832d0abSNetali        return $text;
10053832d0abSNetali    }
10063832d0abSNetali}
10073832d0abSNetali
1008