1<?php 2 3/** 4 * Translation Plugin: Simple multilanguage plugin 5 * 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 * @author Andreas Gohr <andi@splitbrain.org> 8 * @author Guy Brand <gb@isis.u-strasbg.fr> 9 */ 10class action_plugin_translation extends DokuWiki_Action_Plugin 11{ 12 13 /** 14 * For the helper plugin 15 * @var helper_plugin_translation 16 */ 17 protected $helper = null; 18 19 /** 20 * Constructor. Load helper plugin 21 */ 22 public function __construct() 23 { 24 $this->helper = plugin_load('helper', 'translation'); 25 } 26 27 /** 28 * Registers a callback function for a given event 29 * 30 * @param Doku_Event_Handler $controller 31 */ 32 public function register(Doku_Event_Handler $controller) 33 { 34 if ($this->getConf('translateui')) { 35 $controller->register_hook('INIT_LANG_LOAD', 'BEFORE', $this, 'translateUI'); 36 $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'translateJS'); 37 $controller->register_hook('JS_CACHE_USE', 'BEFORE', $this, 'translateJSCache'); 38 } else { 39 $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'addLanguageAttributes'); 40 } 41 42 if ($this->getConf('redirectstart')) { 43 $controller->register_hook('DOKUWIKI_STARTED', 'BEFORE', $this, 'redirectStartPage'); 44 } 45 46 $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addHrefLangAttributes'); 47 $controller->register_hook('COMMON_PAGETPL_LOAD', 'AFTER', $this, 'handlePageTemplates'); 48 $controller->register_hook('SEARCH_QUERY_PAGELOOKUP', 'AFTER', $this, 'sortSearchResults'); 49 } 50 51 /** 52 * Hook Callback. Set the language for the UI 53 * 54 * @param Doku_Event $event INIT_LANG_LOAD 55 */ 56 public function translateUI(Doku_Event $event) 57 { 58 global $conf; 59 global $ACT; 60 global $INPUT; 61 62 // $ID is not set yet, so we have to get it ourselves 63 $id = getID(); 64 65 // For ID based access, get the language from the ID, try request param or session otherwise 66 if ( 67 isset($ACT) && 68 in_array(act_clean($ACT), ['show', 'recent', 'diff', 'edit', 'preview', 'source', 'subscribe']) 69 ) { 70 $locale = $this->helper->getLangPart($id ?? ''); 71 $_SESSION[DOKU_COOKIE]['translationlc'] = $locale; // IDs always reset the language 72 } elseif ($INPUT->has('lang')) { 73 $locale = $INPUT->str('lang'); 74 } else { 75 $locale = $_SESSION[DOKU_COOKIE]['translationlc'] ?? ''; 76 } 77 78 // if the language is not the default language, set the language 79 if ($locale && $locale !== $conf['lang']) { 80 $conf['lang_before_translation'] = $conf['lang']; //store for later access in syntax plugin 81 $event->data = $locale; 82 $conf['lang'] = $locale; 83 } 84 } 85 86 /** 87 * Hook Callback. Pass language code to JavaScript dispatcher 88 * 89 * @param Doku_Event $event TPL_METAHEADER_OUTPUT 90 */ 91 public function translateJS(Doku_Event $event) 92 { 93 global $conf; 94 95 $count = count($event->data['script']); 96 for ($i = 0; $i < $count; $i++) { 97 if (strpos($event->data['script'][$i]['src'], '/lib/exe/js.php') !== false) { 98 $event->data['script'][$i]['src'] .= '&lang=' . hsc($conf['lang']); 99 } 100 } 101 } 102 103 /** 104 * Hook Callback. Cache JavaScript per language 105 * 106 * @param Doku_Event $event JS_CACHE_USE 107 */ 108 public function translateJSCache(Doku_Event $event) 109 { 110 global $conf; 111 112 // reuse the constructor to reinitialize the cache key 113 $event->data->__construct( 114 $event->data->key . $conf['lang'], 115 $event->data->ext 116 ); 117 } 118 119 /** 120 * Hook Callback. Add lang and dir attributes when UI isn't translated 121 * 122 * @param Doku_Event $event TPL_CONTENT_DISPLAY 123 */ 124 public function addLanguageAttributes(Doku_Event $event) 125 { 126 global $ID; 127 global $conf; 128 129 if (!$this->helper->istranslatable($ID)) return; 130 $locale = $this->helper->getLangPart($ID ?? ''); 131 132 if ($locale && $locale !== $conf['lang']) { 133 if (file_exists(DOKU_INC . 'inc/lang/' . $locale . '/lang.php')) { 134 $lang = []; 135 include(DOKU_INC . 'inc/lang/' . $locale . '/lang.php'); 136 $direction = $lang['direction'] ?? 'ltr'; 137 138 $event->data = '<div lang="' . hsc($locale) . '" dir="' . hsc($direction) . '">' . 139 $event->data . 140 '</div>'; 141 } 142 } 143 } 144 145 /** 146 * Hook Callback. Redirect to translated start page 147 * 148 * @param Doku_Event $event DOKUWIKI_STARTED 149 */ 150 public function redirectStartPage(Doku_Event $event) 151 { 152 global $ID; 153 global $ACT; 154 global $conf; 155 156 if ($ID == $conf['start'] && $ACT == 'show') { 157 $lc = $this->helper->getBrowserLang(); 158 159 list($translatedStartpage,) = $this->helper->buildTransID($lc, $conf['start']); 160 if (cleanID($translatedStartpage) !== cleanID($ID)) { 161 send_redirect(wl(cleanID($translatedStartpage), '', true)); 162 } 163 } 164 } 165 166 /** 167 * Hook Callback. Add hreflang attributes to the page header 168 * 169 * @param Doku_Event $event TPL_METAHEADER_OUTPUT 170 */ 171 public function addHrefLangAttributes(Doku_Event $event) 172 { 173 global $ID; 174 global $conf; 175 176 if (!$this->helper->isTranslatable($ID)) return; 177 178 $translations = $this->helper->getAvailableTranslations($ID); 179 if ($translations) { 180 foreach ($translations as $lc => $translation) { 181 $event->data['link'][] = [ 182 'rel' => 'alternate', 183 'hreflang' => $lc, 184 'href' => wl(cleanID($translation), '', true), 185 ]; 186 } 187 } 188 189 $default = $conf['lang_before_translation'] ?? $conf['lang']; 190 $defaultlink = $this->helper->buildTransID($default, ($this->helper->getTransParts($ID))[1])[0]; 191 $event->data['link'][] = [ 192 'rel' => 'alternate', 193 'hreflang' => 'x-default', 194 'href' => wl(cleanID($defaultlink), '', true), 195 ]; 196 } 197 198 /** 199 * Hook Callback. Make current language available as page template placeholder and handle 200 * original language copying 201 * 202 * @param Doku_Event $event COMMON_PAGETPL_LOAD 203 */ 204 public function handlePageTemplates(Doku_Event $event) 205 { 206 global $ID; 207 208 // load orginal content as template? 209 if ($this->getConf('copytrans') && $this->helper->istranslatable($ID, false)) { 210 // look for existing translations 211 $translations = $this->helper->getAvailableTranslations($ID); 212 if ($translations) { 213 // find original language (might've been provided via parameter or use first translation) 214 $orig = (string)$_REQUEST['fromlang']; 215 if (!$orig) $orig = array_key_first($translations); 216 217 // load file 218 $origfile = $translations[$orig]; 219 $event->data['tpl'] = io_readFile(wikiFN($origfile)); 220 221 // prefix with warning 222 $warn = io_readFile($this->localFN('totranslate')); 223 if ($warn) $warn .= "\n\n"; 224 $event->data['tpl'] = $warn . $event->data['tpl']; 225 226 // show user a choice of translations if any 227 if (count($translations) > 1) { 228 $links = array(); 229 foreach ($translations as $t => $l) { 230 $links[] = '<a href="' . wl($ID, array( 231 'do' => 'edit', 232 'fromlang' => $t, 233 )) . '">' . $this->helper->getLocalName($t) . '</a>'; 234 } 235 236 msg( 237 sprintf( 238 $this->getLang('transloaded'), 239 $this->helper->getLocalName($orig), 240 join(', ', $links) 241 ) 242 ); 243 } 244 245 } 246 } 247 248 // apply placeholders 249 $event->data['tpl'] = str_replace('@LANG@', $this->helper->realLC(''), $event->data['tpl']); 250 $event->data['tpl'] = str_replace('@TRANS@', $this->helper->getLangPart($ID), $event->data['tpl']); 251 } 252 253 /** 254 * Hook Callback. Resort page match results so that results are ordered by translation, having the 255 * default language first 256 * 257 * @param Doku_Event $event SEARCH_QUERY_PAGELOOKUP 258 */ 259 public function sortSearchResults(Doku_Event $event) 260 { 261 // sort into translation slots 262 $res = []; 263 foreach ($event->result as $r => $t) { 264 $tr = $this->helper->getLangPart($r); 265 if (!is_array($res["x$tr"])) $res["x$tr"] = []; 266 $res["x$tr"][] = array($r, $t); 267 } 268 // sort by translations 269 ksort($res); 270 // combine 271 $event->result = []; 272 foreach ($res as $r) { 273 foreach ($r as $l) { 274 $event->result[$l[0]] = $l[1]; 275 } 276 } 277 } 278} 279