xref: /plugin/deeplautotranslate/action.php (revision 3c636ad3694828386d1f441be3cce0488f6ef01a)
1<?php
2/**
3 * Deepl Autotranslate Plugin
4 *
5 * @author     Jennifer Graul <me@netali.de>
6 */
7
8if(!defined('DOKU_INC')) die();
9
10use \dokuwiki\HTTP\DokuHTTPClient;
11use \dokuwiki\plugin\deeplautotranslate\MenuItem;
12
13class action_plugin_deeplautotranslate extends DokuWiki_Action_Plugin {
14
15    // manual mapping of ISO-languages to DeepL-languages to fix inconsistent naming
16    private $langs = [
17        'bg' => 'BG',
18        'cs' => 'CS',
19        'da' => 'DA',
20        'de' => 'DE',
21        'de-informal' => 'DE',
22        'el' => 'EL',
23        'en' => 'EN-GB',
24        'es' => 'ES',
25        'et' => 'ET',
26        'fi' => 'FI',
27        'fr' => 'FR',
28        'hu' => 'HU',
29        'hu-formal' => 'HU',
30        'it' => 'IT',
31        'ja' => 'JA',
32        'lt' => 'LT',
33        'lv' => 'LV',
34        'nl' => 'NL',
35        'pl' => 'PL',
36        'pt' => 'PT-PT',
37        'ro' => 'RO',
38        'ru' => 'RU',
39        'sk' => 'SK',
40        'sl' => 'SL',
41        'sv' => 'SV',
42        'zh' => 'ZH'
43    ];
44
45    /**
46     * Register its handlers with the DokuWiki's event controller
47     */
48    public function register(Doku_Event_Handler $controller) {
49        $controller->register_hook('ACTION_ACT_PREPROCESS','BEFORE', $this, 'autotrans_direct');
50        $controller->register_hook('COMMON_PAGETPL_LOAD','AFTER', $this, 'autotrans_editor');
51        $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'add_menu_button');
52    }
53
54    public function add_menu_button(Doku_Event $event) {
55        if ($event->data['view'] != 'page') return;
56
57        if (!$this->getConf('show_button')) return;
58        if (!$this->check_do_translation(true)) return;
59
60        array_splice($event->data['items'], -1, 0, [new MenuItem()]);
61    }
62
63    public function autotrans_direct(Doku_Event $event, $param) {
64        global $ID;
65
66        // check if action is show or translate
67        if ($event->data != 'show' and $event->data != 'translate') return;
68
69        // abort if action is translate and the translate button is disabled
70        if ($event->data == 'translate' and !$this->getConf('show_button')) return;
71
72        // do nothing on show action when mode is not direct
73        if ($event->data == 'show' and $this->get_mode() != 'direct') return;
74
75        // allow translation of existing pages is we are in the translate action
76        $allow_existing = ($event->data == 'translate');
77
78        // reset action to show
79        $event->data = 'show';
80
81        if (!$this->check_do_translation($allow_existing)) return;
82
83        $org_page_text = $this->get_org_page_text();
84        $translated_text = $this->deepl_translate($org_page_text, $this->langs[$this->get_target_lang()]);
85
86        if ($translated_text === '') return;
87
88        saveWikiText($ID, $translated_text, 'Automatic translation');
89
90        // reload the page after translation
91        send_redirect(wl($ID));
92    }
93
94    public function autotrans_editor(Doku_Event $event, $param) {
95        if ($this->get_mode() != 'editor') return;
96
97        if (!$this->check_do_translation()) return;
98
99        $org_page_text = $this->get_org_page_text();
100
101        $event->data['tpl'] = $this->deepl_translate($org_page_text, $this->langs[$this->get_target_lang()]);
102    }
103
104    private function get_mode(): string {
105        global $ID;
106        if ($this->getConf('editor_regex')) {
107            if (preg_match('/' . $this->getConf('editor_regex') . '/', $ID) === 1) return 'editor';
108        }
109        if ($this->getConf('direct_regex')) {
110            if (preg_match('/' . $this->getConf('direct_regex') . '/', $ID) === 1) return 'direct';
111        }
112        return $this->getConf('mode');
113    }
114
115    private function get_target_lang(): string {
116        global $ID;
117        $split_id = explode(':', $ID);
118        return array_shift($split_id);
119    }
120
121    private function get_org_page_text(): string {
122        global $ID;
123
124        $split_id = explode(':', $ID);
125        array_shift($split_id);
126        $org_id = implode(':', $split_id);
127
128        return rawWiki($org_id);
129    }
130
131    private function check_do_translation($allow_existing = false): bool {
132        global $INFO;
133        global $ID;
134
135        // only translate if the current page does not exist
136        if ($INFO['exists'] and !$allow_existing) return false;
137
138        // permission check
139        $perm = auth_quickaclcheck($ID);
140        if (($INFO['exists'] and $perm < AUTH_EDIT) or (!$INFO['exists'] and $perm < AUTH_CREATE)) return false;
141
142        // skip blacklisted namespaces and pages
143        if ($this->getConf('blacklist_regex')) {
144            if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false;
145        }
146
147        $split_id = explode(':', $ID);
148        $lang_ns = array_shift($split_id);
149        // only translate if the current page is in a language namespace
150        if (!array_key_exists($lang_ns, $this->langs)) return false;
151
152        $org_id = implode(':', $split_id);
153        // check if the original page exists
154        if (!page_exists($org_id)) return false;
155
156        return true;
157    }
158
159    private function deepl_translate($text, $target_lang): string {
160        if (!$this->getConf('api_key')) return '';
161
162        $text = $this->insert_ignore_tags($text);
163
164        $data = [
165            'auth_key' => $this->getConf('api_key'),
166            'target_lang' => $target_lang,
167            'tag_handling' => 'xml',
168            'ignore_tags' => 'ignore,code,file,php',
169            'text' => $text
170        ];
171
172        if ($this->getConf('api') == 'free') {
173            $url = 'https://api-free.deepl.com/v2/translate';
174        } else {
175            $url = 'https://api.deepl.com/v2/translate';
176        }
177
178        $http = new DokuHTTPClient();
179        $raw_response = $http->post($url, $data);
180
181        // if any error occurred return an empty string
182        if ($http->status >= 400) return '';
183
184        $json_response = json_decode($raw_response, true);
185        $translated_text = $json_response['translations'][0]['text'];
186
187        $translated_text = $this->remove_ignore_tags($translated_text);
188
189        return $translated_text;
190    }
191
192    private function insert_ignore_tags($text): string {
193        $text = str_replace('[[', '<ignore>[[', $text);
194        $text = str_replace('{{', '<ignore>{{', $text);
195        $text = str_replace(']]', ']]</ignore>', $text);
196        $text = str_replace('}}', '}}</ignore>', $text);
197
198        $ignored_expressions = explode(':', $this->getConf('ignored_expressions'));
199
200        foreach ($ignored_expressions as $expression) {
201            $text = str_replace($expression, '<ignore>' . $expression . '</ignore>', $text);
202        }
203
204        return $text;
205    }
206
207    private function remove_ignore_tags($text): string {
208        $text = str_replace('<ignore>[[', '[[', $text);
209        $text = str_replace('<ignore>{{', '{{', $text);
210        $text = str_replace(']]</ignore>', ']]', $text);
211        $text = str_replace('}}</ignore>', '}}', $text);
212
213        $ignored_expressions = explode(':', $this->getConf('ignored_expressions'));
214
215        foreach ($ignored_expressions as $expression) {
216            $text = str_replace('<ignore>' . $expression . '</ignore>', $expression, $text);
217        }
218
219        return $text;
220    }
221}
222
223