*/
if(!defined('DOKU_INC')) die();
use \dokuwiki\HTTP\DokuHTTPClient;
use \dokuwiki\plugin\deeplautotranslate\MenuItem;
class action_plugin_deeplautotranslate extends DokuWiki_Action_Plugin {
// manual mapping of ISO-languages to DeepL-languages to fix inconsistent naming
private $langs = [
'bg' => 'BG',
'cs' => 'CS',
'da' => 'DA',
'de' => 'DE',
'de-informal' => 'DE',
'el' => 'EL',
'en' => 'EN-GB',
'es' => 'ES',
'et' => 'ET',
'fi' => 'FI',
'fr' => 'FR',
'hu' => 'HU',
'hu-formal' => 'HU',
'it' => 'IT',
'ja' => 'JA',
'lt' => 'LT',
'lv' => 'LV',
'nl' => 'NL',
'pl' => 'PL',
'pt' => 'PT-PT',
'ro' => 'RO',
'ru' => 'RU',
'sk' => 'SK',
'sl' => 'SL',
'sv' => 'SV',
'zh' => 'ZH'
];
/**
* Register its handlers with the DokuWiki's event controller
*/
public function register(Doku_Event_Handler $controller) {
$controller->register_hook('ACTION_ACT_PREPROCESS','BEFORE', $this, 'preprocess');
$controller->register_hook('COMMON_PAGETPL_LOAD','AFTER', $this, 'autotrans_editor');
$controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'add_menu_button');
}
public function add_menu_button(Doku_Event $event) {
global $ID;
global $ACT;
if ($ACT != 'show') return;
if ($event->data['view'] != 'page') return;
if (!$this->getConf('show_button')) return;
$split_id = explode(':', $ID);
$lang_ns = array_shift($split_id);
// check if we are in a language namespace
if (array_key_exists($lang_ns, $this->langs)) {
// in language namespace --> check if we should translate
if (!$this->check_do_translation(true)) return;
} else {
// not in language namespace --> check if we should show the push translate button
if (!$this->check_do_push_translate()) return;
}
array_splice($event->data['items'], -1, 0, [new MenuItem()]);
}
public function preprocess(Doku_Event $event, $param): void {
global $ID;
// check if action is show or translate
if ($event->data != 'show' and $event->data != 'translate') return;
$split_id = explode(':', $ID);
$lang_ns = array_shift($split_id);
// check if we are in a language namespace
if (array_key_exists($lang_ns, $this->langs)) {
// in language namespace --> autotrans_direct
$this->autotrans_direct($event);
} else {
// not in language namespace --> push translate
$this->push_translate($event);
}
}
private function autotrans_direct(Doku_Event $event): void {
global $ID;
// abort if action is translate and the translate button is disabled
if ($event->data == 'translate' and !$this->getConf('show_button')) return;
// do nothing on show action when mode is not direct
if ($event->data == 'show' and $this->get_mode() != 'direct') return;
// allow translation of existing pages is we are in the translate action
$allow_existing = ($event->data == 'translate');
// reset action to show
$event->data = 'show';
if (!$this->check_do_translation($allow_existing)) {
send_redirect(wl($ID));
return;
}
$org_page_text = $this->get_org_page_text();
$translated_text = $this->deepl_translate($org_page_text, $this->langs[$this->get_target_lang()]);
if ($translated_text === '') {
send_redirect(wl($ID));
return;
}
saveWikiText($ID, $translated_text, 'Automatic translation');
msg($this->getLang('msg_translation_success'), 1);
// reload the page after translation
send_redirect(wl($ID));
}
public function autotrans_editor(Doku_Event $event, $param): void {
if ($this->get_mode() != 'editor') return;
if (!$this->check_do_translation()) return;
$org_page_text = $this->get_org_page_text();
$event->data['tpl'] = $this->deepl_translate($org_page_text, $this->langs[$this->get_target_lang()]);
}
private function push_translate(Doku_Event $event): void {
global $ID;
// check if action is translate
if ($event->data != 'translate') return;
// check if button is enabled
if (!$this->getConf('show_button')) {
send_redirect(wl($ID));
return;
}
if (!$this->check_do_push_translate()) {
send_redirect(wl($ID));
return;
}
// push translate
$push_langs = $this->get_push_langs();
$org_page_text = rawWiki($ID);
foreach ($push_langs as $lang) {
// skip invalid languages
if (!array_key_exists($lang, $this->langs)) {
msg($this->getLang('msg_translation_fail_invalid_lang') . $lang, -1);
continue;
}
$lang_id = $lang . ':' . $ID;
// check permissions
$perm = auth_quickaclcheck($ID);
$exists = page_exists($lang_id);
if (($exists and $perm < AUTH_EDIT) or (!$exists and $perm < AUTH_CREATE)) {
msg($this->getLang('msg_translation_fail_no_permissions') . $lang_id, -1);
continue;
}
$translated_text = $this->deepl_translate($org_page_text, $this->langs[$lang]);
saveWikiText($lang_id, $translated_text, 'Automatic push translation');
}
msg($this->getLang('msg_translation_success'), 1);
// reload the page after translation to clear the action
send_redirect(wl($ID));
}
private function get_mode(): string {
global $ID;
if ($this->getConf('editor_regex')) {
if (preg_match('/' . $this->getConf('editor_regex') . '/', $ID) === 1) return 'editor';
}
if ($this->getConf('direct_regex')) {
if (preg_match('/' . $this->getConf('direct_regex') . '/', $ID) === 1) return 'direct';
}
return $this->getConf('mode');
}
private function get_target_lang(): string {
global $ID;
$split_id = explode(':', $ID);
return array_shift($split_id);
}
private function get_org_page_text(): string {
global $ID;
$split_id = explode(':', $ID);
array_shift($split_id);
$org_id = implode(':', $split_id);
return rawWiki($org_id);
}
private function check_do_translation($allow_existing = false): bool {
global $INFO;
global $ID;
// only translate if the current page does not exist
if ($INFO['exists'] and !$allow_existing) return false;
// permission check
$perm = auth_quickaclcheck($ID);
if (($INFO['exists'] and $perm < AUTH_EDIT) or (!$INFO['exists'] and $perm < AUTH_CREATE)) return false;
// skip blacklisted namespaces and pages
if ($this->getConf('blacklist_regex')) {
if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false;
}
$split_id = explode(':', $ID);
$lang_ns = array_shift($split_id);
// only translate if the current page is in a language namespace
if (!array_key_exists($lang_ns, $this->langs)) return false;
$org_id = implode(':', $split_id);
// check if the original page exists
if (!page_exists($org_id)) return false;
return true;
}
private function check_do_push_translate(): bool {
global $ID;
$push_langs = $this->get_push_langs();
// push_langs empty --> push_translate disabled --> abort
if (empty($push_langs)) return false;
// skip blacklisted namespaces and pages
if ($this->getConf('blacklist_regex')) {
// blacklist regex match --> abort
if (preg_match('/' . $this->getConf('blacklist_regex') . '/', $ID) === 1) return false;
}
return true;
}
private function deepl_translate($text, $target_lang): string {
if (!trim($this->getConf('api_key'))) return '';
$text = $this->insert_ignore_tags($text);
$data = [
'auth_key' => $this->getConf('api_key'),
'target_lang' => $target_lang,
'tag_handling' => 'xml',
'ignore_tags' => 'ignore,php',
'text' => $text
];
if ($this->getConf('api') == 'free') {
$url = 'https://api-free.deepl.com/v2/translate';
} else {
$url = 'https://api.deepl.com/v2/translate';
}
$http = new DokuHTTPClient();
$raw_response = $http->post($url, $data);
if ($http->status >= 400) {
// add error messages
switch ($http->status) {
case 403:
msg($this->getLang('msg_translation_fail_bad_key'), -1);
break;
case 456:
msg($this->getLang('msg_translation_fail_quota_exceeded'), -1);
break;
default:
msg($this->getLang('msg_translation_fail'), -1);
break;
}
// if any error occurred return an empty string
return '';
}
$json_response = json_decode($raw_response, true);
$translated_text = $json_response['translations'][0]['text'];
$translated_text = $this->remove_ignore_tags($translated_text);
return $translated_text;
}
private function get_push_langs(): array {
$push_langs = trim($this->getConf('push_langs'));
if ($push_langs === '') return array();
return explode(' ', $push_langs);
}
private function insert_ignore_tags($text): string {
$text = str_replace('[[', '[[', $text);
$text = str_replace('{{', '{{', $text);
$text = str_replace(']]', ']]', $text);
$text = str_replace('}}', '}}', $text);
$text = str_replace("''", "''", $text);
$text = preg_replace('/([\s\S]*?<\/file>)/', '${1}', $text);
$text = preg_replace('/([\s\S]*?<\/code>)/', '${1}', $text);
$ignored_expressions = explode(':', $this->getConf('ignored_expressions'));
foreach ($ignored_expressions as $expression) {
$text = str_replace($expression, '' . $expression . '', $text);
}
return $text;
}
private function remove_ignore_tags($text): string {
$text = str_replace('[[', '[[', $text);
$text = str_replace('{{', '{{', $text);
$text = str_replace(']]', ']]', $text);
$text = str_replace('}}', '}}', $text);
$text = str_replace("''", "''", $text);
$text = preg_replace('/([\s\S]*?<\/file>)<\/ignore>/', '${1}', $text);
$text = preg_replace('/([\s\S]*?<\/code>)<\/ignore>/', '${1}', $text);
// restore < and > for example from arrows (-->) in wikitext
$text = str_replace('>', '>', $text);
$text = str_replace('<', '<', $text);
$ignored_expressions = explode(':', $this->getConf('ignored_expressions'));
foreach ($ignored_expressions as $expression) {
$text = str_replace('' . $expression . '', $expression, $text);
}
return $text;
}
}