xref: /plugin/deeplautotranslate/action.php (revision 5f8ab21dda11966d12b6ac874727eadc93464071)
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) {
493832d0abSNetali        $controller->register_hook('ACTION_ACT_PREPROCESS','BEFORE', $this, 'autotrans_direct');
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) {
553c636ad3SNetali        if ($event->data['view'] != 'page') return;
563c636ad3SNetali
573c636ad3SNetali        if (!$this->getConf('show_button')) return;
583c636ad3SNetali        if (!$this->check_do_translation(true)) return;
593c636ad3SNetali
603c636ad3SNetali        array_splice($event->data['items'], -1, 0, [new MenuItem()]);
613832d0abSNetali    }
623832d0abSNetali
633832d0abSNetali    public function autotrans_direct(Doku_Event $event, $param) {
643832d0abSNetali        global $ID;
653c636ad3SNetali
663c636ad3SNetali        // check if action is show or translate
673c636ad3SNetali        if ($event->data != 'show' and $event->data != 'translate') return;
683c636ad3SNetali
693c636ad3SNetali        // abort if action is translate and the translate button is disabled
703c636ad3SNetali        if ($event->data == 'translate' and !$this->getConf('show_button')) return;
713c636ad3SNetali
723c636ad3SNetali        // do nothing on show action when mode is not direct
733c636ad3SNetali        if ($event->data == 'show' and $this->get_mode() != 'direct') return;
743c636ad3SNetali
753c636ad3SNetali        // allow translation of existing pages is we are in the translate action
763c636ad3SNetali        $allow_existing = ($event->data == 'translate');
773c636ad3SNetali
783c636ad3SNetali        // reset action to show
793c636ad3SNetali        $event->data = 'show';
803c636ad3SNetali
813c636ad3SNetali        if (!$this->check_do_translation($allow_existing)) return;
823832d0abSNetali
833832d0abSNetali        $org_page_text = $this->get_org_page_text();
843832d0abSNetali        $translated_text = $this->deepl_translate($org_page_text, $this->langs[$this->get_target_lang()]);
853832d0abSNetali
86e4700ea0SNetali        if ($translated_text === '') return;
873832d0abSNetali
883832d0abSNetali        saveWikiText($ID, $translated_text, 'Automatic translation');
893832d0abSNetali
903c636ad3SNetali        // reload the page after translation
913c636ad3SNetali        send_redirect(wl($ID));
923832d0abSNetali    }
933832d0abSNetali
943832d0abSNetali    public function autotrans_editor(Doku_Event $event, $param) {
953832d0abSNetali        if ($this->get_mode() != 'editor') return;
963832d0abSNetali
973832d0abSNetali        if (!$this->check_do_translation()) return;
983832d0abSNetali
993832d0abSNetali        $org_page_text = $this->get_org_page_text();
1003832d0abSNetali
1013832d0abSNetali        $event->data['tpl'] = $this->deepl_translate($org_page_text, $this->langs[$this->get_target_lang()]);
1023832d0abSNetali    }
1033832d0abSNetali
1043832d0abSNetali    private function get_mode(): string {
1053832d0abSNetali        global $ID;
1063832d0abSNetali        if ($this->getConf('editor_regex')) {
1073832d0abSNetali            if (preg_match('/' . $this->getConf('editor_regex') . '/', $ID) === 1) return 'editor';
1083832d0abSNetali        }
1093832d0abSNetali        if ($this->getConf('direct_regex')) {
1103832d0abSNetali            if (preg_match('/' . $this->getConf('direct_regex') . '/', $ID) === 1) return 'direct';
1113832d0abSNetali        }
1123832d0abSNetali        return $this->getConf('mode');
1133832d0abSNetali    }
1143832d0abSNetali
1153832d0abSNetali    private function get_target_lang(): string {
1163832d0abSNetali        global $ID;
1173832d0abSNetali        $split_id = explode(':', $ID);
1183832d0abSNetali        return array_shift($split_id);
1193832d0abSNetali    }
1203832d0abSNetali
1213832d0abSNetali    private function get_org_page_text(): string {
1223832d0abSNetali        global $ID;
1233832d0abSNetali
1243832d0abSNetali        $split_id = explode(':', $ID);
1253832d0abSNetali        array_shift($split_id);
1263832d0abSNetali        $org_id = implode(':', $split_id);
1273832d0abSNetali
1283832d0abSNetali        return rawWiki($org_id);
1293832d0abSNetali    }
1303832d0abSNetali
1313c636ad3SNetali    private function check_do_translation($allow_existing = false): bool {
1323832d0abSNetali        global $INFO;
1333832d0abSNetali        global $ID;
1343832d0abSNetali
1353c636ad3SNetali        // only translate if the current page does not exist
1363c636ad3SNetali        if ($INFO['exists'] and !$allow_existing) return false;
1373c636ad3SNetali
1383c636ad3SNetali        // permission check
1393c636ad3SNetali        $perm = auth_quickaclcheck($ID);
1403c636ad3SNetali        if (($INFO['exists'] and $perm < AUTH_EDIT) or (!$INFO['exists'] and $perm < AUTH_CREATE)) return false;
1413c636ad3SNetali
1423832d0abSNetali        // skip blacklisted namespaces and pages
1433832d0abSNetali        if ($this->getConf('blacklist_regex')) {
1443832d0abSNetali            if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false;
1453832d0abSNetali        }
1463832d0abSNetali
1473832d0abSNetali        $split_id = explode(':', $ID);
1483832d0abSNetali        $lang_ns = array_shift($split_id);
1493832d0abSNetali        // only translate if the current page is in a language namespace
1503832d0abSNetali        if (!array_key_exists($lang_ns, $this->langs)) return false;
1513832d0abSNetali
1523832d0abSNetali        $org_id = implode(':', $split_id);
1533832d0abSNetali        // check if the original page exists
1543832d0abSNetali        if (!page_exists($org_id)) return false;
1553832d0abSNetali
1563832d0abSNetali        return true;
1573832d0abSNetali    }
1583832d0abSNetali
1593832d0abSNetali    private function deepl_translate($text, $target_lang): string {
1603832d0abSNetali        if (!$this->getConf('api_key')) return '';
1613832d0abSNetali
1623832d0abSNetali        $text = $this->insert_ignore_tags($text);
1633832d0abSNetali
1643832d0abSNetali        $data = [
1653832d0abSNetali            'auth_key' => $this->getConf('api_key'),
1663832d0abSNetali            'target_lang' => $target_lang,
1673832d0abSNetali            'tag_handling' => 'xml',
168e4700ea0SNetali            'ignore_tags' => 'ignore,code,file,php',
1693832d0abSNetali            'text' => $text
1703832d0abSNetali        ];
1713832d0abSNetali
1723832d0abSNetali        if ($this->getConf('api') == 'free') {
17381931e50SNetali            $url = 'https://api-free.deepl.com/v2/translate';
1743832d0abSNetali        } else {
17581931e50SNetali            $url = 'https://api.deepl.com/v2/translate';
1763832d0abSNetali        }
1773832d0abSNetali
17881931e50SNetali        $http = new DokuHTTPClient();
17981931e50SNetali        $raw_response = $http->post($url, $data);
1803832d0abSNetali
181*5f8ab21dSNetali
182*5f8ab21dSNetali        if ($http->status >= 400) {
183*5f8ab21dSNetali            // add error messages
184*5f8ab21dSNetali            switch ($http->status) {
185*5f8ab21dSNetali                case 403:
186*5f8ab21dSNetali                    msg($this->getLang('msg_translation_fail_bad_key'), -1);
187*5f8ab21dSNetali                    break;
188*5f8ab21dSNetali                case 456:
189*5f8ab21dSNetali                    msg($this->getLang('msg_translation_fail_quota_exceeded'), -1);
190*5f8ab21dSNetali                    break;
191*5f8ab21dSNetali                default:
192*5f8ab21dSNetali                    msg($this->getLang('msg_translation_fail'), -1);
193*5f8ab21dSNetali                    break;
194*5f8ab21dSNetali            }
195*5f8ab21dSNetali
1963832d0abSNetali            // if any error occurred return an empty string
197*5f8ab21dSNetali            return '';
198*5f8ab21dSNetali        }
1993832d0abSNetali
2003832d0abSNetali        $json_response = json_decode($raw_response, true);
2013832d0abSNetali        $translated_text = $json_response['translations'][0]['text'];
2023832d0abSNetali
2033832d0abSNetali        $translated_text = $this->remove_ignore_tags($translated_text);
2043832d0abSNetali
205*5f8ab21dSNetali        msg($this->getLang('msg_translation_success'), 1);
206*5f8ab21dSNetali
2073832d0abSNetali        return $translated_text;
2083832d0abSNetali    }
2093832d0abSNetali
2103832d0abSNetali    private function insert_ignore_tags($text): string {
2113832d0abSNetali        $text = str_replace('[[', '<ignore>[[', $text);
2123832d0abSNetali        $text = str_replace('{{', '<ignore>{{', $text);
2133832d0abSNetali        $text = str_replace(']]', ']]</ignore>', $text);
2143832d0abSNetali        $text = str_replace('}}', '}}</ignore>', $text);
215*5f8ab21dSNetali        $text = str_replace("''", "<ignore>''</ignore>", $text);
2163832d0abSNetali
2173832d0abSNetali        $ignored_expressions = explode(':', $this->getConf('ignored_expressions'));
2183832d0abSNetali
2193832d0abSNetali        foreach ($ignored_expressions as $expression) {
2203832d0abSNetali            $text = str_replace($expression, '<ignore>' . $expression . '</ignore>', $text);
2213832d0abSNetali        }
2223832d0abSNetali
2233832d0abSNetali        return $text;
2243832d0abSNetali    }
2253832d0abSNetali
2263832d0abSNetali    private function remove_ignore_tags($text): string {
2273832d0abSNetali        $text = str_replace('<ignore>[[', '[[', $text);
2283832d0abSNetali        $text = str_replace('<ignore>{{', '{{', $text);
2293832d0abSNetali        $text = str_replace(']]</ignore>', ']]', $text);
2303832d0abSNetali        $text = str_replace('}}</ignore>', '}}', $text);
231*5f8ab21dSNetali        $text = str_replace("<ignore>''</ignore>", "''", $text);
232*5f8ab21dSNetali
233*5f8ab21dSNetali        // restore < and > for example from arrows (-->) in wikitext
234*5f8ab21dSNetali        $text = str_replace('&gt;', '>', $text);
235*5f8ab21dSNetali        $text = str_replace('&lt;', '<', $text);
2363832d0abSNetali
2373832d0abSNetali        $ignored_expressions = explode(':', $this->getConf('ignored_expressions'));
2383832d0abSNetali
2393832d0abSNetali        foreach ($ignored_expressions as $expression) {
2403832d0abSNetali            $text = str_replace('<ignore>' . $expression . '</ignore>', $expression, $text);
2413832d0abSNetali        }
2423832d0abSNetali
2433832d0abSNetali        return $text;
2443832d0abSNetali    }
2453832d0abSNetali}
2463832d0abSNetali
247