xref: /plugin/deeplautotranslate/action.php (revision 99da9a08000bf2cdebe2d11fb86a29d103c1e83c)
13832d0abSNetali<?php
23832d0abSNetali/**
33832d0abSNetali * Deepl Autotranslate Plugin
43832d0abSNetali *
53832d0abSNetali * @author     Jennifer Graul <me@netali.de>
63832d0abSNetali */
73832d0abSNetali
83832d0abSNetaliif(!defined('DOKU_INC')) die();
93832d0abSNetali
1081931e50SNetaliuse \dokuwiki\HTTP\DokuHTTPClient;
113c636ad3SNetaliuse \dokuwiki\plugin\deeplautotranslate\MenuItem;
1281931e50SNetali
133832d0abSNetaliclass action_plugin_deeplautotranslate extends DokuWiki_Action_Plugin {
143832d0abSNetali
153832d0abSNetali    // manual mapping of ISO-languages to DeepL-languages to fix inconsistent naming
163832d0abSNetali    private $langs = [
173832d0abSNetali        'bg' => 'BG',
183832d0abSNetali        'cs' => 'CS',
193832d0abSNetali        'da' => 'DA',
203832d0abSNetali        'de' => 'DE',
213832d0abSNetali        'de-informal' => 'DE',
223832d0abSNetali        'el' => 'EL',
233832d0abSNetali        'en' => 'EN-GB',
243832d0abSNetali        'es' => 'ES',
253832d0abSNetali        'et' => 'ET',
263832d0abSNetali        'fi' => 'FI',
273832d0abSNetali        'fr' => 'FR',
283832d0abSNetali        'hu' => 'HU',
293832d0abSNetali        'hu-formal' => 'HU',
303832d0abSNetali        'it' => 'IT',
313832d0abSNetali        'ja' => 'JA',
323832d0abSNetali        'lt' => 'LT',
333832d0abSNetali        'lv' => 'LV',
343832d0abSNetali        'nl' => 'NL',
353832d0abSNetali        'pl' => 'PL',
363832d0abSNetali        'pt' => 'PT-PT',
373832d0abSNetali        'ro' => 'RO',
383832d0abSNetali        'ru' => 'RU',
393832d0abSNetali        'sk' => 'SK',
403832d0abSNetali        'sl' => 'SL',
413832d0abSNetali        'sv' => 'SV',
423832d0abSNetali        'zh' => 'ZH'
433832d0abSNetali    ];
443832d0abSNetali
453832d0abSNetali    /**
463832d0abSNetali     * Register its handlers with the DokuWiki's event controller
473832d0abSNetali     */
483832d0abSNetali    public function register(Doku_Event_Handler $controller) {
49153e4498SNetali        $controller->register_hook('ACTION_ACT_PREPROCESS','BEFORE', $this, 'preprocess');
503832d0abSNetali        $controller->register_hook('COMMON_PAGETPL_LOAD','AFTER', $this, 'autotrans_editor');
513c636ad3SNetali        $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'add_menu_button');
523c636ad3SNetali    }
533c636ad3SNetali
543c636ad3SNetali    public function add_menu_button(Doku_Event $event) {
55153e4498SNetali        global $ID;
56bbb1fba9SNetali        global $ACT;
57*99da9a08SNetali        global $conf;
58bbb1fba9SNetali
59bbb1fba9SNetali        if ($ACT != 'show') return;
60153e4498SNetali
613c636ad3SNetali        if ($event->data['view'] != 'page') return;
623c636ad3SNetali
633c636ad3SNetali        if (!$this->getConf('show_button')) return;
64153e4498SNetali
65153e4498SNetali        $split_id = explode(':', $ID);
66153e4498SNetali        $lang_ns = array_shift($split_id);
67153e4498SNetali        // check if we are in a language namespace
68153e4498SNetali        if (array_key_exists($lang_ns, $this->langs)) {
69*99da9a08SNetali            if($this->getConf('default_lang_in_ns') and $lang_ns === $conf['lang']) {
70*99da9a08SNetali                // if the default lang is in a namespace and we are in that namespace --> check for push translation
71*99da9a08SNetali                if (!$this->check_do_push_translate()) return;
72*99da9a08SNetali            } else {
73153e4498SNetali                // in language namespace --> check if we should translate
743c636ad3SNetali                if (!$this->check_do_translation(true)) return;
75*99da9a08SNetali            }
76153e4498SNetali        } else {
77*99da9a08SNetali            // do not show the button if we are not in a language namespace and the default language is in a namespace
78*99da9a08SNetali            if($this->getConf('default_lang_in_ns')) return;
79*99da9a08SNetali            // not in language namespace and default language is npt in a namespace --> check if we should show the push translate button
80153e4498SNetali            if (!$this->check_do_push_translate()) return;
81153e4498SNetali        }
823c636ad3SNetali
833c636ad3SNetali        array_splice($event->data['items'], -1, 0, [new MenuItem()]);
843832d0abSNetali    }
853832d0abSNetali
86153e4498SNetali    public function preprocess(Doku_Event  $event, $param): void {
873832d0abSNetali        global $ID;
88*99da9a08SNetali        global $conf;
893c636ad3SNetali
903c636ad3SNetali        // check if action is show or translate
913c636ad3SNetali        if ($event->data != 'show' and $event->data != 'translate') return;
923c636ad3SNetali
93153e4498SNetali        $split_id = explode(':', $ID);
94153e4498SNetali        $lang_ns = array_shift($split_id);
95153e4498SNetali        // check if we are in a language namespace
96153e4498SNetali        if (array_key_exists($lang_ns, $this->langs)) {
97*99da9a08SNetali            if($this->getConf('default_lang_in_ns') and $lang_ns === $conf['lang']) {
98*99da9a08SNetali                // if the default lang is in a namespace and we are in that namespace --> push translate
99*99da9a08SNetali                $this->push_translate($event);
100*99da9a08SNetali            } else {
101*99da9a08SNetali                // in language namespace --> autotrans direct
102153e4498SNetali                $this->autotrans_direct($event);
103*99da9a08SNetali            }
104153e4498SNetali        } else {
105153e4498SNetali            // not in language namespace --> push translate
106153e4498SNetali            $this->push_translate($event);
107153e4498SNetali        }
108153e4498SNetali    }
109153e4498SNetali
110153e4498SNetali    private function autotrans_direct(Doku_Event $event): void {
111153e4498SNetali        global $ID;
112153e4498SNetali
1133c636ad3SNetali        // abort if action is translate and the translate button is disabled
1143c636ad3SNetali        if ($event->data == 'translate' and !$this->getConf('show_button')) return;
1153c636ad3SNetali
1163c636ad3SNetali        // do nothing on show action when mode is not direct
1173c636ad3SNetali        if ($event->data == 'show' and $this->get_mode() != 'direct') return;
1183c636ad3SNetali
1193c636ad3SNetali        // allow translation of existing pages is we are in the translate action
1203c636ad3SNetali        $allow_existing = ($event->data == 'translate');
1213c636ad3SNetali
1223c636ad3SNetali        // reset action to show
1233c636ad3SNetali        $event->data = 'show';
1243c636ad3SNetali
125153e4498SNetali        if (!$this->check_do_translation($allow_existing)) {
126153e4498SNetali            send_redirect(wl($ID));
127153e4498SNetali            return;
128153e4498SNetali        }
1293832d0abSNetali
1303832d0abSNetali        $org_page_text = $this->get_org_page_text();
1313832d0abSNetali        $translated_text = $this->deepl_translate($org_page_text, $this->langs[$this->get_target_lang()]);
1323832d0abSNetali
133153e4498SNetali        if ($translated_text === '') {
134153e4498SNetali            send_redirect(wl($ID));
135153e4498SNetali            return;
136153e4498SNetali        }
1373832d0abSNetali
1383832d0abSNetali        saveWikiText($ID, $translated_text, 'Automatic translation');
1393832d0abSNetali
140153e4498SNetali        msg($this->getLang('msg_translation_success'), 1);
141153e4498SNetali
1423c636ad3SNetali        // reload the page after translation
1433c636ad3SNetali        send_redirect(wl($ID));
1443832d0abSNetali    }
1453832d0abSNetali
146153e4498SNetali    public function autotrans_editor(Doku_Event $event, $param): void {
1473832d0abSNetali        if ($this->get_mode() != 'editor') return;
1483832d0abSNetali
1493832d0abSNetali        if (!$this->check_do_translation()) return;
1503832d0abSNetali
1513832d0abSNetali        $org_page_text = $this->get_org_page_text();
1523832d0abSNetali
1533832d0abSNetali        $event->data['tpl'] = $this->deepl_translate($org_page_text, $this->langs[$this->get_target_lang()]);
1543832d0abSNetali    }
1553832d0abSNetali
156153e4498SNetali    private function push_translate(Doku_Event $event): void {
157153e4498SNetali        global $ID;
158153e4498SNetali
159153e4498SNetali        // check if action is translate
160153e4498SNetali        if ($event->data != 'translate') return;
161153e4498SNetali
162153e4498SNetali        // check if button is enabled
163153e4498SNetali        if (!$this->getConf('show_button')) {
164153e4498SNetali            send_redirect(wl($ID));
165153e4498SNetali            return;
166153e4498SNetali        }
167153e4498SNetali
168153e4498SNetali        if (!$this->check_do_push_translate()) {
169153e4498SNetali            send_redirect(wl($ID));
170153e4498SNetali            return;
171153e4498SNetali        }
172153e4498SNetali
173153e4498SNetali        // push translate
174153e4498SNetali        $push_langs = $this->get_push_langs();
175153e4498SNetali        $org_page_text = rawWiki($ID);
176153e4498SNetali        foreach ($push_langs as $lang) {
177153e4498SNetali            // skip invalid languages
178153e4498SNetali            if (!array_key_exists($lang, $this->langs)) {
179153e4498SNetali                msg($this->getLang('msg_translation_fail_invalid_lang') . $lang, -1);
180153e4498SNetali                continue;
181153e4498SNetali            }
182153e4498SNetali
183*99da9a08SNetali            if ($this->getConf('default_lang_in_ns')) {
184*99da9a08SNetali                // if default lang is in ns: replace language namespace in ID
185*99da9a08SNetali                $split_id = explode(':', $ID);
186*99da9a08SNetali                array_shift($split_id);
187*99da9a08SNetali                $lang_id = implode(':', $split_id);
188*99da9a08SNetali                $lang_id = $lang . ':' . $lang_id;
189*99da9a08SNetali            } else {
190*99da9a08SNetali                // if default lang is not in ns: add language namespace to ID
191153e4498SNetali                $lang_id = $lang . ':' . $ID;
192*99da9a08SNetali            }
193153e4498SNetali
194153e4498SNetali            // check permissions
195153e4498SNetali            $perm = auth_quickaclcheck($ID);
196153e4498SNetali            $exists = page_exists($lang_id);
197153e4498SNetali            if (($exists and $perm < AUTH_EDIT) or (!$exists and $perm < AUTH_CREATE)) {
198153e4498SNetali                msg($this->getLang('msg_translation_fail_no_permissions') . $lang_id, -1);
199153e4498SNetali                continue;
200153e4498SNetali            }
201153e4498SNetali
202153e4498SNetali            $translated_text = $this->deepl_translate($org_page_text, $this->langs[$lang]);
203153e4498SNetali            saveWikiText($lang_id, $translated_text, 'Automatic push translation');
204153e4498SNetali        }
205153e4498SNetali
206153e4498SNetali        msg($this->getLang('msg_translation_success'), 1);
207153e4498SNetali
208153e4498SNetali        // reload the page after translation to clear the action
209153e4498SNetali        send_redirect(wl($ID));
210153e4498SNetali    }
211153e4498SNetali
2123832d0abSNetali    private function get_mode(): string {
2133832d0abSNetali        global $ID;
2143832d0abSNetali        if ($this->getConf('editor_regex')) {
2153832d0abSNetali            if (preg_match('/' . $this->getConf('editor_regex') . '/', $ID) === 1) return 'editor';
2163832d0abSNetali        }
2173832d0abSNetali        if ($this->getConf('direct_regex')) {
2183832d0abSNetali            if (preg_match('/' . $this->getConf('direct_regex') . '/', $ID) === 1) return 'direct';
2193832d0abSNetali        }
2203832d0abSNetali        return $this->getConf('mode');
2213832d0abSNetali    }
2223832d0abSNetali
2233832d0abSNetali    private function get_target_lang(): string {
2243832d0abSNetali        global $ID;
2253832d0abSNetali        $split_id = explode(':', $ID);
2263832d0abSNetali        return array_shift($split_id);
2273832d0abSNetali    }
2283832d0abSNetali
2293832d0abSNetali    private function get_org_page_text(): string {
2303832d0abSNetali        global $ID;
231*99da9a08SNetali        global $conf;
2323832d0abSNetali
2333832d0abSNetali        $split_id = explode(':', $ID);
2343832d0abSNetali        array_shift($split_id);
2353832d0abSNetali        $org_id = implode(':', $split_id);
2363832d0abSNetali
237*99da9a08SNetali        // if default lang is in ns: add default ns in front of org id
238*99da9a08SNetali        if ($this->getConf('default_lang_in_ns')) {
239*99da9a08SNetali            $org_id = $conf['lang'] . ':' . $org_id;
240*99da9a08SNetali        }
241*99da9a08SNetali
2423832d0abSNetali        return rawWiki($org_id);
2433832d0abSNetali    }
2443832d0abSNetali
2453c636ad3SNetali    private function check_do_translation($allow_existing = false): bool {
2463832d0abSNetali        global $INFO;
2473832d0abSNetali        global $ID;
248*99da9a08SNetali        global $conf;
2493832d0abSNetali
2503c636ad3SNetali        // only translate if the current page does not exist
2513c636ad3SNetali        if ($INFO['exists'] and !$allow_existing) return false;
2523c636ad3SNetali
2533c636ad3SNetali        // permission check
2543c636ad3SNetali        $perm = auth_quickaclcheck($ID);
2553c636ad3SNetali        if (($INFO['exists'] and $perm < AUTH_EDIT) or (!$INFO['exists'] and $perm < AUTH_CREATE)) return false;
2563c636ad3SNetali
2573832d0abSNetali        // skip blacklisted namespaces and pages
2583832d0abSNetali        if ($this->getConf('blacklist_regex')) {
2593832d0abSNetali            if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false;
2603832d0abSNetali        }
2613832d0abSNetali
2623832d0abSNetali        $split_id = explode(':', $ID);
2633832d0abSNetali        $lang_ns = array_shift($split_id);
2643832d0abSNetali        // only translate if the current page is in a language namespace
2653832d0abSNetali        if (!array_key_exists($lang_ns, $this->langs)) return false;
2663832d0abSNetali
2673832d0abSNetali        $org_id = implode(':', $split_id);
268*99da9a08SNetali
269*99da9a08SNetali        // if default lang is in ns: add default ns in front of org id
270*99da9a08SNetali        if ($this->getConf('default_lang_in_ns')) {
271*99da9a08SNetali            $org_id = $conf['lang'] . ':' . $org_id;
272*99da9a08SNetali        }
273*99da9a08SNetali
2743832d0abSNetali        // check if the original page exists
2753832d0abSNetali        if (!page_exists($org_id)) return false;
2763832d0abSNetali
2773832d0abSNetali        return true;
2783832d0abSNetali    }
2793832d0abSNetali
280153e4498SNetali    private function check_do_push_translate(): bool {
281153e4498SNetali        global $ID;
282*99da9a08SNetali        global $INFO;
283*99da9a08SNetali        global $conf;
284*99da9a08SNetali
285*99da9a08SNetali        if (!$INFO['exists']) return false;
286*99da9a08SNetali
287*99da9a08SNetali        // if default language is in namespace: only allow push translation from that namespace
288*99da9a08SNetali        if($this->getConf('default_lang_in_ns')) {
289*99da9a08SNetali            $split_id = explode(':', $ID);
290*99da9a08SNetali            $lang_ns = array_shift($split_id);
291*99da9a08SNetali
292*99da9a08SNetali            if ($lang_ns !== $conf['lang']) return false;
293*99da9a08SNetali        }
294153e4498SNetali
295153e4498SNetali        $push_langs = $this->get_push_langs();
296153e4498SNetali        // push_langs empty --> push_translate disabled --> abort
297153e4498SNetali        if (empty($push_langs)) return false;
298153e4498SNetali
299153e4498SNetali        // skip blacklisted namespaces and pages
300153e4498SNetali        if ($this->getConf('blacklist_regex')) {
301153e4498SNetali            // blacklist regex match --> abort
302153e4498SNetali            if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false;
303153e4498SNetali        }
304153e4498SNetali
305153e4498SNetali        return true;
306153e4498SNetali    }
307153e4498SNetali
3083832d0abSNetali    private function deepl_translate($text, $target_lang): string {
309153e4498SNetali        if (!trim($this->getConf('api_key'))) return '';
3103832d0abSNetali
3113832d0abSNetali        $text = $this->insert_ignore_tags($text);
3123832d0abSNetali
3133832d0abSNetali        $data = [
3143832d0abSNetali            'auth_key' => $this->getConf('api_key'),
3153832d0abSNetali            'target_lang' => $target_lang,
3163832d0abSNetali            'tag_handling' => 'xml',
317057940f7SNetali            'ignore_tags' => 'ignore,php',
3183832d0abSNetali            'text' => $text
3193832d0abSNetali        ];
3203832d0abSNetali
3213832d0abSNetali        if ($this->getConf('api') == 'free') {
32281931e50SNetali            $url = 'https://api-free.deepl.com/v2/translate';
3233832d0abSNetali        } else {
32481931e50SNetali            $url = 'https://api.deepl.com/v2/translate';
3253832d0abSNetali        }
3263832d0abSNetali
32781931e50SNetali        $http = new DokuHTTPClient();
32881931e50SNetali        $raw_response = $http->post($url, $data);
3293832d0abSNetali
3305f8ab21dSNetali        if ($http->status >= 400) {
3315f8ab21dSNetali            // add error messages
3325f8ab21dSNetali            switch ($http->status) {
3335f8ab21dSNetali                case 403:
3345f8ab21dSNetali                    msg($this->getLang('msg_translation_fail_bad_key'), -1);
3355f8ab21dSNetali                    break;
3365f8ab21dSNetali                case 456:
3375f8ab21dSNetali                    msg($this->getLang('msg_translation_fail_quota_exceeded'), -1);
3385f8ab21dSNetali                    break;
3395f8ab21dSNetali                default:
3405f8ab21dSNetali                    msg($this->getLang('msg_translation_fail'), -1);
3415f8ab21dSNetali                    break;
3425f8ab21dSNetali            }
3435f8ab21dSNetali
3443832d0abSNetali            // if any error occurred return an empty string
3455f8ab21dSNetali            return '';
3465f8ab21dSNetali        }
3473832d0abSNetali
3483832d0abSNetali        $json_response = json_decode($raw_response, true);
3493832d0abSNetali        $translated_text = $json_response['translations'][0]['text'];
3503832d0abSNetali
3513832d0abSNetali        $translated_text = $this->remove_ignore_tags($translated_text);
3523832d0abSNetali
3533832d0abSNetali        return $translated_text;
3543832d0abSNetali    }
3553832d0abSNetali
356153e4498SNetali    private function get_push_langs(): array {
357153e4498SNetali        $push_langs = trim($this->getConf('push_langs'));
358153e4498SNetali
359153e4498SNetali        if ($push_langs === '') return array();
360153e4498SNetali
361153e4498SNetali        return explode(' ', $push_langs);
362153e4498SNetali    }
363153e4498SNetali
3643832d0abSNetali    private function insert_ignore_tags($text): string {
3653832d0abSNetali        $text = str_replace('[[', '<ignore>[[', $text);
3663832d0abSNetali        $text = str_replace('{{', '<ignore>{{', $text);
3673832d0abSNetali        $text = str_replace(']]', ']]</ignore>', $text);
3683832d0abSNetali        $text = str_replace('}}', '}}</ignore>', $text);
3695f8ab21dSNetali        $text = str_replace("''", "<ignore>''</ignore>", $text);
3703832d0abSNetali
371057940f7SNetali        $text = preg_replace('/(<file[\s\S]*?>[\s\S]*?<\/file>)/', '<ignore>${1}</ignore>', $text);
372057940f7SNetali        $text = preg_replace('/(<code[\s\S]*?>[\s\S]*?<\/code>)/', '<ignore>${1}</ignore>', $text);
373057940f7SNetali
3743832d0abSNetali        $ignored_expressions = explode(':', $this->getConf('ignored_expressions'));
3753832d0abSNetali
3763832d0abSNetali        foreach ($ignored_expressions as $expression) {
3773832d0abSNetali            $text = str_replace($expression, '<ignore>' . $expression . '</ignore>', $text);
3783832d0abSNetali        }
3793832d0abSNetali
3803832d0abSNetali        return $text;
3813832d0abSNetali    }
3823832d0abSNetali
3833832d0abSNetali    private function remove_ignore_tags($text): string {
3843832d0abSNetali        $text = str_replace('<ignore>[[', '[[', $text);
3853832d0abSNetali        $text = str_replace('<ignore>{{', '{{', $text);
3863832d0abSNetali        $text = str_replace(']]</ignore>', ']]', $text);
3873832d0abSNetali        $text = str_replace('}}</ignore>', '}}', $text);
3885f8ab21dSNetali        $text = str_replace("<ignore>''</ignore>", "''", $text);
3895f8ab21dSNetali
390057940f7SNetali        $text = preg_replace('/<ignore>(<file[\s\S]*?>[\s\S]*?<\/file>)<\/ignore>/', '${1}', $text);
391057940f7SNetali        $text = preg_replace('/<ignore>(<code[\s\S]*?>[\s\S]*?<\/code>)<\/ignore>/', '${1}', $text);
392057940f7SNetali
3935f8ab21dSNetali        // restore < and > for example from arrows (-->) in wikitext
3945f8ab21dSNetali        $text = str_replace('&gt;', '>', $text);
3955f8ab21dSNetali        $text = str_replace('&lt;', '<', $text);
3963832d0abSNetali
3973832d0abSNetali        $ignored_expressions = explode(':', $this->getConf('ignored_expressions'));
3983832d0abSNetali
3993832d0abSNetali        foreach ($ignored_expressions as $expression) {
4003832d0abSNetali            $text = str_replace('<ignore>' . $expression . '</ignore>', $expression, $text);
4013832d0abSNetali        }
4023832d0abSNetali
4033832d0abSNetali        return $text;
4043832d0abSNetali    }
4053832d0abSNetali}
4063832d0abSNetali
407