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