1<?php 2 3use dokuwiki\Extension\Plugin; 4use dokuwiki\ChangeLog\PageChangeLog; 5 6/** 7 * Translation Plugin: Simple multilanguage plugin 8 * 9 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 10 * @author Andreas Gohr <andi@splitbrain.org> 11 */ 12class helper_plugin_translation extends Plugin 13{ 14 public $translations = []; 15 public $translationNs = ''; 16 public $defaultlang = ''; 17 public $LN = []; // hold native names 18 public $opts = []; // display options 19 20 /** 21 * Initialize 22 */ 23 public function __construct() 24 { 25 global $conf; 26 require_once(DOKU_INC . 'inc/pageutils.php'); 27 require_once(DOKU_INC . 'inc/utf8.php'); 28 29 $this->loadTranslationNamespaces(); 30 31 // load language names 32 $this->LN = confToHash(__DIR__ . '/lang/langnames.txt'); 33 34 // display options 35 $this->opts = $this->getConf('display'); 36 $this->opts = explode(',', $this->opts); 37 $this->opts = array_map('trim', $this->opts); 38 $this->opts = array_fill_keys($this->opts, true); 39 40 // get default translation 41 if (empty($conf['lang_before_translation'])) { 42 $dfl = $conf['lang']; 43 } else { 44 $dfl = $conf['lang_before_translation']; 45 } 46 if (in_array($dfl, $this->translations)) { 47 $this->defaultlang = $dfl; 48 } else { 49 $this->defaultlang = ''; 50 array_unshift($this->translations, ''); 51 } 52 53 $this->translationNs = cleanID($this->getConf('translationns')); 54 if ($this->translationNs) $this->translationNs .= ':'; 55 } 56 57 /** 58 * Parse 'translations'-setting into $this->translations 59 */ 60 public function loadTranslationNamespaces() 61 { 62 // load wanted translation into array 63 $this->translations = strtolower(str_replace(',', ' ', $this->getConf('translations'))); 64 $this->translations = array_unique(array_filter(explode(' ', $this->translations))); 65 sort($this->translations); 66 } 67 68 /** 69 * Check if the given ID is a translation and return the language code. 70 * 71 * @param string $id 72 * @return string 73 */ 74 public function getLangPart($id) 75 { 76 [$lng] = $this->getTransParts($id); 77 return $lng; 78 } 79 80 /** 81 * Check if the given ID is a translation and return the language code and 82 * the id part. 83 * 84 * @param string $id 85 * @return array 86 */ 87 public function getTransParts($id) 88 { 89 $rx = '/^' . $this->translationNs . '(' . implode('|', $this->translations) . '):(.*)/'; 90 if (preg_match($rx, $id, $match)) { 91 return [$match[1], $match[2]]; 92 } 93 return ['', $id]; 94 } 95 96 /** 97 * Returns the browser language if it matches with one of the configured 98 * languages 99 */ 100 public function getBrowserLang() 101 { 102 global $conf; 103 $langs = $this->translations; 104 if (!in_array($conf['lang'], $langs)) { 105 $langs[] = $conf['lang']; 106 } 107 $rx = '/(^|,|:|;|-)(' . implode('|', $langs) . ')($|,|:|;|-)/i'; 108 if (preg_match($rx, $_SERVER['HTTP_ACCEPT_LANGUAGE'], $match)) { 109 return strtolower($match[2]); 110 } 111 return false; 112 } 113 114 /** 115 * Returns the ID and name to the wanted translation, empty 116 * $lng is default lang 117 * 118 * @param string $lng 119 * @param string $idpart 120 * @return array 121 */ 122 public function buildTransID($lng, $idpart) 123 { 124 if ($lng && in_array($lng, $this->translations)) { 125 $link = ':' . $this->translationNs . $lng . ':' . $idpart; 126 $name = $lng; 127 } else { 128 $link = ':' . $this->translationNs . $idpart; 129 $name = $this->realLC(''); 130 } 131 return [$link, $name]; 132 } 133 134 /** 135 * Returns the real language code, even when an empty one is given 136 * (eg. resolves th default language) 137 * 138 * @param string $lc 139 * @return string 140 */ 141 public function realLC($lc) 142 { 143 global $conf; 144 if ($lc) { 145 return $lc; 146 } elseif (empty($conf['lang_before_translation'])) { 147 return $conf['lang']; 148 } else { 149 return $conf['lang_before_translation']; 150 } 151 } 152 153 /** 154 * Check if current ID should be translated and any GUI 155 * should be shown 156 * 157 * @param string $id 158 * @param bool $checkact only return true if $ACT is 'show' 159 * @return bool 160 */ 161 public function istranslatable($id, $checkact = true) 162 { 163 global $ACT; 164 165 if ($checkact && (!isset($ACT) || act_clean($ACT) != 'show')) return false; 166 if ($this->translationNs && strpos($id, (string) $this->translationNs) !== 0) return false; 167 $skiptrans = trim($this->getConf('skiptrans')); 168 if ($skiptrans && preg_match('/' . $skiptrans . '/ui', ':' . $id)) return false; 169 $meta = p_get_metadata($id); 170 if (!empty($meta['plugin']['translation']['notrans'])) return false; 171 172 return true; 173 } 174 175 /** 176 * Return the (localized) about link 177 */ 178 public function showAbout() 179 { 180 global $ID; 181 182 $curlc = $this->getLangPart($ID); 183 184 $about = $this->getConf('about'); 185 if ($this->getConf('localabout')) { 186 [, $idpart] = $this->getTransParts($about); 187 [$about, ] = $this->buildTransID($curlc, $idpart); 188 $about = cleanID($about); 189 } 190 191 $out = '<sup>'; 192 $out .= html_wikilink($about, '?'); 193 $out .= '</sup>'; 194 195 return $out; 196 } 197 198 /** 199 * Returns a list of (lc => link) for all existing translations of a page 200 * 201 * @param $id 202 * @return array 203 */ 204 public function getAvailableTranslations($id) 205 { 206 $result = []; 207 208 [$lc, $idpart] = $this->getTransParts($id); 209 210 foreach ($this->translations as $t) { 211 if ($t == $lc) continue; //skip self 212 [$link, $name] = $this->buildTransID($t, $idpart); 213 if (page_exists($link)) { 214 $result[$name] = $link; 215 } 216 } 217 218 return $result; 219 } 220 221 /** 222 * Creates an UI for linking to the available and configured translations 223 * 224 * Can be called from the template or via the ~~TRANS~~ syntax component. 225 * 226 * @param string $checkage (note that checkAge() should be called anyway at some point) 227 */ 228 public function showTranslations($checkage = true) 229 { 230 global $INFO; 231 232 if (!$this->istranslatable($INFO['id'])) return ''; 233 if ($checkage) $this->checkage(); 234 235 [, $idpart] = $this->getTransParts($INFO['id']); 236 237 $out = '<div class="plugin_translation ' . ($this->getConf('dropdown') ? 'is-dropdown' : '') . '">'; 238 239 //show title and about 240 if (isset($this->opts['title']) || $this->getConf('about')) { 241 $out .= '<span class="title">'; 242 if (isset($this->opts['title'])) $out .= $this->getLang('translations'); 243 if ($this->getConf('about')) $out .= $this->showAbout(); 244 if (isset($this->opts['title'])) $out .= ': '; 245 $out .= '</span>'; 246 } 247 248 $out .= '<ul>'; 249 foreach ($this->translations as $t) { 250 [$type, $text, $attr] = $this->prepareLanguageSelectorItem($t, $idpart, $INFO['id']); 251 $out .= '<li class="' . $type . '">'; 252 $out .= "<$type " . buildAttributes($attr) . ">$text</$type>"; 253 $out .= '</li>'; 254 } 255 $out .= '</ul>'; 256 257 $out .= '</div>'; 258 259 return $out; 260 } 261 262 /** 263 * Return the local name 264 * 265 * @param $lang 266 * @return string 267 */ 268 public function getLocalName($lang) 269 { 270 return $this->LN[$lang] ?? $lang; 271 } 272 273 /** 274 * Create a single language selector item 275 * 276 * @param string $lc The language code of the item 277 * @param string $idpart The ID part of the item 278 * @param string $current The current ID 279 * @return array [$type, $text, $attr] 280 */ 281 protected function prepareLanguageSelectorItem($lc, $idpart, $current) 282 { 283 [$target, $lang] = $this->buildTransID($lc, $idpart); 284 $target = cleanID($target); 285 $exists = page_exists($target, '', false); 286 287 $text = ''; 288 $attr = [ 289 'class' => $exists ? 'wikilink1' : 'wikilink2', 290 'title' => $this->getLocalName($lang), 291 ]; 292 293 // no link on current page 294 if ($current === $target) { 295 $type = 'span'; 296 } else { 297 $type = 'a'; 298 $attr['href'] = wl($target); 299 } 300 301 // add flag 302 if (isset($this->opts['flag'])) { 303 $text .= '<i>' . inlineSVG(DOKU_PLUGIN . 'translation/flags/' . $lang . '.svg', 1024 * 12) . '</i>'; 304 } 305 306 // decide what to show 307 if (isset($this->opts['name'])) { 308 $text .= hsc($this->getLocalName($lang)); 309 if (isset($this->opts['langcode'])) $text .= ' (' . hsc($lang) . ')'; 310 } elseif (isset($this->opts['langcode'])) { 311 $text .= hsc($lang); 312 } 313 314 return [$type, $text, $attr]; 315 } 316 317 /** 318 * Checks if the current page is a translation of a page 319 * in the default language. Displays a notice when it is 320 * older than the original page. Tries to link to a diff 321 * with changes on the original since the translation 322 */ 323 public function checkage() 324 { 325 global $ID; 326 global $INFO; 327 if (!$this->getConf('checkage')) return; 328 if (!$INFO['exists']) return; 329 $lng = $this->getLangPart($ID); 330 if ($lng == $this->defaultlang) return; 331 332 $rx = '/^' . $this->translationNs . '((' . implode('|', $this->translations) . '):)?/'; 333 $idpart = preg_replace($rx, '', $ID); 334 335 // compare modification times 336 [$orig, ] = $this->buildTransID($this->defaultlang, $idpart); 337 $origfn = wikiFN($orig); 338 if ($INFO['lastmod'] >= @filemtime($origfn)) return; 339 340 // build the message and display it 341 $orig = cleanID($orig); 342 $msg = sprintf($this->getLang('outdated'), wl($orig)); 343 344 $difflink = $this->getOldDiffLink($orig, $INFO['lastmod']); 345 if ($difflink) { 346 $msg .= sprintf(' ' . $this->getLang('diff'), $difflink); 347 } 348 349 echo '<div class="notify">' . $msg . '</div>'; 350 } 351 352 /** 353 * Get a link to a diff with changes on the original since the translation 354 * 355 * @param string $id 356 * @param int $lastmod 357 * @return false|string false id no diff can be found, link otherwise 358 */ 359 public function getOldDiffLink($id, $lastmod) 360 { 361 // get revision from before translation 362 $orev = false; 363 $changelog = new PageChangeLog($id); 364 $revs = $changelog->getRevisions(0, 100); 365 foreach ($revs as $rev) { 366 if ($rev < $lastmod) { 367 $orev = $rev; 368 break; 369 } 370 } 371 if ($orev && !page_exists($id, $orev)) { 372 return false; 373 } 374 $id = cleanID($id); 375 return wl($id, ['do' => 'diff', 'rev' => $orev]); 376 } 377} 378