1<?php 2/** 3 * Translation Plugin: Simple multilanguage plugin 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <andi@splitbrain.org> 7 */ 8 9// must be run within Dokuwiki 10if(!defined('DOKU_INC')) die(); 11 12class helper_plugin_autotranslation extends DokuWiki_Plugin { 13 14 public $translations = array(); 15 public $translationNs = ''; 16 public $defaultlang = ''; 17 18 private $opts = array(); // display options 19 private $LN = array(); // hold native names 20 21 /** 22 * Initialize 23 */ 24 function __construct() { 25 global $conf; 26 require_once(DOKU_INC . 'inc/pageutils.php'); 27 require_once(DOKU_INC . 'inc/utf8.php'); 28 29 // load wanted translation into array 30 $this->translations = strtolower(str_replace(',', ' ', $this->getConf('translations'))); 31 $this->translations = array_unique(array_filter(explode(' ', $this->translations))); 32 sort($this->translations); 33 34 // load language names 35 $this->LN = confToHash(dirname(__FILE__) . '/lang/langnames.txt'); 36 37 // display options 38 $this->opts = $this->getConf('display'); 39 $this->opts = explode(',', $this->opts); 40 $this->opts = array_map('trim', $this->opts); 41 $this->opts = array_fill_keys($this->opts, true); 42 43 // get default translation 44 if(empty($conf['lang_before_translation'])) { 45 $dfl = $conf['lang']; 46 } else { 47 $dfl = $conf['lang_before_translation']; 48 } 49 if(in_array($dfl, $this->translations)) { 50 $this->defaultlang = $dfl; 51 } else { 52 $this->defaultlang = ''; 53 array_unshift($this->translations, ''); 54 } 55 56 $this->translationsNs = $this->setupTNS(); 57 $JSINFO['conf']['lang'] = $dfl; 58 } 59 60 /** 61 * Find the current translation namespace 62 * This may be detected automatically or defined by the config option 63 **/ 64 function setupTNS($ID="", $forceAutotranslation=false) { 65 global $conf; 66 67 if ( !empty( $this->translationsNs) ) { return $this->translationsNs; } 68 if ( empty($ID) ) { $ID = getID(); } 69 70 // autodetect? 71 // this will only work for namespaces other than the root and default language 72 if ( $forceAutotranslation || $this->getConf('autodetectnamespace') ) 73 { 74 $lang = explode(':', $ID); 75 foreach( array_reverse($lang) as $tns ) 76 { 77 array_pop($lang); 78 if ( in_array($tns, $this->translations) ) 79 { 80 // Found 81 $tns = implode(":", $lang) . ':'; 82 if($tns == ':' ) { $tns = ''; } 83 return $tns; 84 } 85 } 86 } 87 88 // Array of translations can be givven 89 $tnsA = explode(' ', $this->getConf('translationns')); 90 if ( empty($tnsA) ) return ''; // there is just this one - and translation is active. 91 92 usort($tnsA,array($this, 'lensort') ); 93 foreach ( $tnsA as $tns ) { 94 $tns = cleanID(trim($tns)); 95 if($tns && substr($tns, -1) != ':') { $tns .= ':'; } 96 if($tns && strpos($ID,$tns) === false) continue; 97 if($tns == ':' ) { $tns = ''; } 98 99 return $tns; 100 } 101 102 return false; 103 } 104 105 // Inner function for sorting 106 private function lensort($a,$b){ 107 return strlen($b)-strlen($a); 108 } 109 110 /** 111 * Check if the given ID is a translation and return the language code. 112 */ 113 function getLangPart($id) { 114 list($lng) = $this->getTransParts($id); 115 return $lng; 116 } 117 118 /** 119 * Check if the given ID is a translation and return the ID up the translation root. 120 */ 121 function getIDPart($id) { 122 list($lng, $idpart) = $this->getTransParts($id); 123 return $idpart; 124 } 125 126 /** 127 * Check if the given ID is a translation and return the language code and 128 * the id part. 129 */ 130 function getTransParts($id) { 131 $rx = '/^' . $this->translationsNs . '(' . join('|', $this->translations) . '):(.*)/'; 132 if(preg_match($rx, $id, $match)) { 133 return array($match[1], $match[2]); 134 } 135 return array('', $id); 136 } 137 138 /** 139 * Returns the browser language if it matches with one of the configured 140 * languages 141 */ 142 function getBrowserLang() { 143 $rx = '/(^|,|:|;|-)(' . join('|', $this->translations) . ')($|,|:|;|-)/i'; 144 if(preg_match($rx, $_SERVER['HTTP_ACCEPT_LANGUAGE'], $match)) { 145 return strtolower($match[2]); 146 } 147 return false; 148 } 149 150 /** 151 * Returns the ID and name to the wanted translation, empty 152 * $lng is default lang 153 */ 154 function buildTransID($lng, $idpart) { 155 global $conf; 156 if($lng) { 157 $link = ':' . $this->translationsNs . $lng . ':' . $idpart; 158 $name = $lng; 159 } else { 160 $link = ':' . $this->translationsNs . $idpart; 161 $name = $this->realLC(''); 162 } 163 return array($link, $name); 164 } 165 166 /** 167 * Returns the real language code, even when an empty one is given 168 * (eg. resolves th default language) 169 */ 170 function realLC($lc) { 171 global $conf; 172 if($lc) { 173 return $lc; 174 } elseif(empty($conf['lang_before_translation'])) { 175 return $conf['lang']; 176 } else { 177 return $conf['lang_before_translation']; 178 } 179 } 180 181 /** 182 * Check if current ID should be translated and any GUI 183 * should be shown 184 */ 185 function istranslatable($id, $checkact = true) { 186 global $ACT; 187 188 if(auth_isAdmin()) return true; 189 190 if($checkact && $ACT != 'show') return false; 191 if($this->translationsNs && strpos($id, $this->translationsNs) !== 0) return false; 192 $skiptrans = trim($this->getConf('skiptrans')); 193 if($skiptrans && preg_match('/' . $skiptrans . '/ui', ':' . $id)) return false; 194 $meta = p_get_metadata($id); 195 if(!empty($meta['plugin']['autotranslation']['notrans'])) return false; 196 197 return true; 198 } 199 200 /** 201 * Return the (localized) about link 202 */ 203 function showAbout() { 204 global $ID; 205 global $conf; 206 global $INFO; 207 208 $curlc = $this->getLangPart($ID); 209 210 $about = $this->getConf('about'); 211 if($this->getConf('localabout')) { 212 list($lc, $idpart) = $this->getTransParts($about); 213 list($about, $name) = $this->buildTransID($curlc, $idpart); 214 $about = cleanID($about); 215 } 216 217 $out = ''; 218 $out .= '<sup>'; 219 $out .= html_wikilink($about, '?'); 220 $out .= '</sup>'; 221 222 return $out; 223 } 224 225 /** 226 * Returns a list of (lc => link) for all existing translations of a page 227 * 228 * @param $id 229 * @return array 230 */ 231 function getAvailableTranslations($id) { 232 $result = array(); 233 234 list($lc, $idpart) = $this->getTransParts($id); 235 $lang = $this->realLC($lc); 236 237 foreach($this->translations as $t) { 238 if($t == $lc) continue; //skip self 239 list($link, $name) = $this->buildTransID($t, $idpart); 240 if(page_exists($link)) { 241 $result[$name] = $link; 242 } 243 } 244 245 return $result; 246 } 247 248 /** 249 * Creates an UI for linking to the available and configured translations 250 * 251 * Can be called from the template or via the ~~TRANS~~ syntax component. 252 */ 253 public function showTranslations() { 254 global $conf; 255 global $INFO; 256 257 if(!$this->istranslatable($INFO['id'])) return ''; 258 $this->checkage(); 259 260 list($lc, $idpart) = $this->getTransParts($INFO['id']); 261 $lang = $this->realLC($lc); 262 263 $out = '<div class="plugin_autotranslation">'; 264 265 //show title and about 266 if(isset($this->opts['title'])) { 267 $out .= '<span>' . $this->getLang('translations'); 268 if($this->getConf('about')) $out .= $this->showAbout(); 269 $out .= ':</span> '; 270 if(isset($this->opts['twolines'])) $out .= '<br />'; 271 } 272 273 // open wrapper 274 if($this->getConf('dropdown')) { 275 // select needs its own styling 276 if($INFO['exists']) { 277 $class = 'wikilink1'; 278 } else { 279 $class = 'wikilink2'; 280 } 281 if(isset($this->opts['flag'])) { 282 $flag = DOKU_BASE . 'lib/plugins/translation/flags/' . hsc($lang) . '.gif'; 283 }else{ 284 $flag = ''; 285 } 286 287 if($conf['userewrite']) { 288 $action = wl(); 289 } else { 290 $action = script(); 291 } 292 293 $out .= '<form action="' . $action . '" id="translation__dropdown">'; 294 if($flag) $out .= '<img src="' . $flag . '" alt="' . hsc($lang) . '" height="11" class="' . $class . '" /> '; 295 $out .= '<select name="id" class="' . $class . '">'; 296 } else { 297 $out .= '<ul>'; 298 } 299 300 // insert items 301 foreach($this->translations as $t) { 302 $out .= $this->getTransItem($t, $idpart); 303 } 304 305 // close wrapper 306 if($this->getConf('dropdown')) { 307 $out .= '</select>'; 308 $out .= '<input name="go" type="submit" value="→" />'; 309 $out .= '</form>'; 310 } else { 311 $out .= '</ul>'; 312 } 313 314 // show about if not already shown 315 if(!isset($this->opts['title']) && $this->getConf('about')) { 316 $out .= ' '; 317 $out .= $this->showAbout(); 318 } 319 320 $out .= '</div>'; 321 322 return $out; 323 } 324 325 /** 326 * Return the local name 327 * 328 * @param $lang 329 * @return string 330 */ 331 function getLocalName($lang) { 332 if($this->LN[$lang]) { 333 return $this->LN[$lang]; 334 } 335 return $lang; 336 } 337 338 /** 339 * Create the link or option for a single translation 340 * 341 * @param $lc string The language code 342 * @param $idpart string The ID of the translated page 343 * @returns string The item 344 */ 345 function getTransItem($lc, $idpart) { 346 global $ID; 347 global $conf; 348 349 list($link, $lang) = $this->buildTransID($lc, $idpart); 350 $link = cleanID($link); 351 352 // class 353 if(page_exists($link, '', false)) { 354 $class = 'wikilink1'; 355 } else { 356 $class = 'wikilink2'; 357 } 358 359 // local language name 360 $localname = $this->getLocalName($lang); 361 362 // current? 363 if($ID == $link) { 364 $sel = ' selected="selected"'; 365 $class .= ' cur'; 366 } else { 367 $sel = ''; 368 } 369 370 // flag 371 $flag = $style = ''; 372 if(isset($this->opts['flag'])) { 373 $flag = DOKU_BASE . 'lib/plugins/translation/flags/' . hsc($lang) . '.gif'; 374 $style = ' style="background-image: url(\'' . $flag . '\')"'; 375 $class .= ' flag'; 376 } 377 378 // what to display as name 379 if(isset($this->opts['name'])) { 380 $display = hsc($localname); 381 if(isset($this->opts['langcode'])) $display .= ' (' . hsc($lang) . ')'; 382 } elseif(isset($this->opts['langcode'])) { 383 $display = hsc($lang); 384 } else { 385 $display = ' '; 386 } 387 388 // prepare output 389 $out = ''; 390 if($this->getConf('dropdown')) { 391 if($conf['useslash']) $link = str_replace(':', '/', $link); 392 393 $out .= '<option class="' . $class . '" title="' . hsc($localname) . '" value="' . $link . '"' . $sel . $style . '>'; 394 $out .= $display; 395 $out .= '</option>'; 396 } else { 397 $out .= '<li><div class="li">'; 398 $out .= '<a href="' . wl($link, 'tns') . '" class="' . $class . '" title="' . hsc($localname) . '">'; 399 if($flag) $out .= '<img src="' . $flag . '" alt="' . hsc($lang) . '" height="11" />'; 400 $out .= $display; 401 $out .= '</a>'; 402 $out .= '</div></li>'; 403 } 404 405 return $out; 406 } 407 408 /** 409 * Checks if the current page is a translation of a page 410 * in the default language. Displays a notice when it is 411 * older than the original page. Tries to link to a diff 412 * with changes on the original since the translation 413 */ 414 function checkage() { 415 global $ID; 416 global $INFO; 417 if(!$this->getConf('checkage')) return; 418 if(!$INFO['exists']) return; 419 $lng = $this->getLangPart($ID); 420 if($lng == $this->defaultlang) return; 421 422 $rx = '/^' . $this->translationsNs . '((' . join('|', $this->translations) . '):)?/'; 423 $idpart = preg_replace($rx, '', $ID); 424 425 // compare modification times 426 list($orig, $name) = $this->buildTransID($this->defaultlang, $idpart); 427 $origfn = wikiFN($orig); 428 if($INFO['lastmod'] >= @filemtime($origfn)) return; 429 430 // get revision from before translation 431 $orev = 0; 432 $changelog = new PageChangelog($orig); 433 $revs = $changelog->getRevisions(0, 100); 434 foreach($revs as $rev) { 435 if($rev < $INFO['lastmod']) { 436 $orev = $rev; 437 break; 438 } 439 } 440 441 // see if the found revision still exists 442 if($orev && !page_exists($orig, $orev)) $orev = 0; 443 444 // build the message and display it 445 $orig = cleanID($orig); 446 $msg = sprintf($this->getLang('outdated'), wl($orig)); 447 448 $difflink = $this->getOldDiffLink($orig, $INFO['lastmod']); 449 if ($difflink) { 450 $msg .= sprintf(' ' . $this->getLang('diff'), $difflink); 451 } 452 453 echo '<div class="notify">' . $msg . '</div>'; 454 } 455 456 function getOldDiffLink($id, $lastmod) { 457 // get revision from before translation 458 $orev = false; 459 $changelog = new PageChangelog($id); 460 $revs = $changelog->getRevisions(0, 100); 461 foreach($revs as $rev) { 462 if($rev < $lastmod) { 463 $orev = $rev; 464 break; 465 } 466 } 467 if($orev && !page_exists($id, $orev)) { 468 return false; 469 } 470 $id = cleanID($id); 471 return wl($id, array('do' => 'diff', 'rev' => $orev)); 472 473 } 474 475 /** 476 * Checks if the current ID has a translated page 477 */ 478 function hasTranslation($inputID = null) { 479 global $ID, $INFO, $conf; 480 481 if ( empty($inputID) ) 482 { 483 $inputID = $ID; 484 } 485 486 if ( !$this->istranslatable($id) ) return false; 487 488 $idpart = $this->getIDPart($inputID); 489 490 foreach($this->translations as $t) 491 { 492 list($link,$name) = $this->buildTransID($t,$idpart,false); 493 $link = cleanID($link); 494 495 if( $inputID != $link && page_exists($link,'',false) ){ 496 return true; 497 } 498 } 499 500 return false; 501 } 502} 503