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(empty($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 $JSINFO['conf']['lang'] = $dfl; 56 } 57 58 /** 59 * Find the current translation namespace 60 * This may be detected automatically or defined by the config option 61 **/ 62 function setupTNS($ID="") { 63 global $conf; 64 65 if ( !empty( $this->tns) ) { return $this->tns; } 66 if ( empty($ID) ) { $ID = getID(); } 67 68 // autodetect? 69 // this will only work for namespaces other than the root and default language 70 if ( $this->getConf('autodetectnamespace') ) 71 { 72 $lang = explode(':', $ID); 73 foreach( array_reverse($lang) as $tns ) 74 { 75 array_pop($lang); 76 if ( in_array($tns, $this->trans) ) 77 { 78 // Found 79 $tns = implode(":", $lang) . ':'; 80 if($tns == ':' ) { $tns = ''; } 81 return $tns; 82 } 83 } 84 } 85 86 // Array of translations can be givven 87 $tnsA = explode(' ', $this->getConf('translationns')); 88 if ( empty($tnsA) ) return ''; // there is just this one - and translation is active. 89 90 usort($tnsA,array($this, 'lensort') ); 91 foreach ( $tnsA as $tns ) { 92 $tns = cleanID(trim($tns)); 93 if($tns && substr($tns, -1) != ':') { $tns .= ':'; } 94 if($tns && strpos($ID,$tns) === false) continue; 95 if($tns == ':' ) { $tns = ''; } 96 97 return $tns; 98 } 99 100 return false; 101 } 102 103 // Inner function for sorting 104 private function lensort($a,$b){ 105 return strlen($b)-strlen($a); 106 } 107 108 /** 109 * Check if the given ID is a translation and return the language code. 110 */ 111 function getLangPart($id) { 112 list($lng) = $this->getTransParts($id); 113 return $lng; 114 } 115 116 /** 117 * Check if the given ID is a translation and return the ID up the translation root. 118 */ 119 function getIDPart($id) { 120 list($lng, $idpart) = $this->getTransParts($id); 121 return $idpart; 122 } 123 124 /** 125 * Check if the given ID is a translation and return the language code and 126 * the id part. 127 */ 128 function getTransParts($id) { 129 $rx = '/^' . $this->tns . '(' . join('|', $this->trans) . '):(.*)/'; 130 if(preg_match($rx, $id, $match)) { 131 return array($match[1], $match[2]); 132 } 133 return array('', $id); 134 } 135 136 /** 137 * Returns the browser language if it matches with one of the configured 138 * languages 139 */ 140 function getBrowserLang() { 141 $rx = '/(^|,|:|;|-)(' . join('|', $this->trans) . ')($|,|:|;|-)/i'; 142 if(preg_match($rx, $_SERVER['HTTP_ACCEPT_LANGUAGE'], $match)) { 143 return strtolower($match[2]); 144 } 145 return false; 146 } 147 148 /** 149 * Returns the ID and name to the wanted translation, empty 150 * $lng is default lang 151 */ 152 function buildTransID($lng, $idpart) { 153 global $conf; 154 if($lng) { 155 $link = ':' . $this->tns . $lng . ':' . $idpart; 156 $name = $lng; 157 } else { 158 $link = ':' . $this->tns . $idpart; 159 $name = $this->realLC(''); 160 } 161 return array($link, $name); 162 } 163 164 /** 165 * Returns the real language code, even when an empty one is given 166 * (eg. resolves th default language) 167 */ 168 function realLC($lc) { 169 global $conf; 170 if($lc) { 171 return $lc; 172 } elseif(empty($conf['lang_before_translation'])) { 173 return $conf['lang']; 174 } else { 175 return $conf['lang_before_translation']; 176 } 177 } 178 179 /** 180 * Check if current ID should be translated and any GUI 181 * should be shown 182 */ 183 function istranslatable($id, $checkact = true) { 184 global $ACT; 185 186 if(auth_isAdmin()) return true; 187 188 if($checkact && $ACT != 'show') return false; 189 if($this->tns && strpos($id, $this->tns) !== 0) return false; 190 $skiptrans = trim($this->getConf('skiptrans')); 191 if($skiptrans && preg_match('/' . $skiptrans . '/ui', ':' . $id)) return false; 192 $meta = p_get_metadata($id); 193 if($meta['plugin']['translation']['notrans']) return false; 194 195 return true; 196 } 197 198 /** 199 * Return the (localized) about link 200 */ 201 function showAbout() { 202 global $ID; 203 global $conf; 204 global $INFO; 205 206 $curlc = $this->getLangPart($ID); 207 208 $about = $this->getConf('about'); 209 if($this->getConf('localabout')) { 210 list($lc, $idpart) = $this->getTransParts($about); 211 list($about, $name) = $this->buildTransID($curlc, $idpart); 212 $about = cleanID($about); 213 } 214 215 $out = ''; 216 $out .= '<sup>'; 217 $out .= html_wikilink($about, '?'); 218 $out .= '</sup>'; 219 220 return $out; 221 } 222 223 /** 224 * Returns a list of (lc => link) for all existing translations of a page 225 * 226 * @param $id 227 * @return array 228 */ 229 function getAvailableTranslations($id) { 230 $result = array(); 231 232 list($lc, $idpart) = $this->getTransParts($id); 233 $lang = $this->realLC($lc); 234 235 foreach($this->trans as $t) { 236 if($t == $lc) continue; //skip self 237 list($link, $name) = $this->buildTransID($t, $idpart); 238 if(page_exists($link)) { 239 $result[$name] = $link; 240 } 241 } 242 243 return $result; 244 } 245 246 /** 247 * Creates an UI for linking to the available and configured translations 248 * 249 * Can be called from the template or via the ~~TRANS~~ syntax component. 250 */ 251 public function showTranslations() { 252 global $conf; 253 global $INFO; 254 255 if(!$this->istranslatable($INFO['id'])) return ''; 256 $this->checkage(); 257 258 list($lc, $idpart) = $this->getTransParts($INFO['id']); 259 $lang = $this->realLC($lc); 260 261 $out = '<div class="plugin_translation">'; 262 263 //show title and about 264 if(isset($this->opts['title'])) { 265 $out .= '<span>' . $this->getLang('translations'); 266 if($this->getConf('about')) $out .= $this->showAbout(); 267 $out .= ':</span> '; 268 if(isset($this->opts['twolines'])) $out .= '<br />'; 269 } 270 271 // open wrapper 272 if($this->getConf('dropdown')) { 273 // select needs its own styling 274 if($INFO['exists']) { 275 $class = 'wikilink1'; 276 } else { 277 $class = 'wikilink2'; 278 } 279 if(isset($this->opts['flag'])) { 280 $flag = DOKU_BASE . 'lib/plugins/translation/flags/' . hsc($lang) . '.gif'; 281 }else{ 282 $flag = ''; 283 } 284 285 if($conf['userewrite']) { 286 $action = wl(); 287 } else { 288 $action = script(); 289 } 290 291 $out .= '<form action="' . $action . '" id="translation__dropdown">'; 292 if($flag) $out .= '<img src="' . $flag . '" alt="' . hsc($lang) . '" height="11" class="' . $class . '" /> '; 293 $out .= '<select name="id" class="' . $class . '">'; 294 } else { 295 $out .= '<ul>'; 296 } 297 298 // insert items 299 foreach($this->trans as $t) { 300 $out .= $this->getTransItem($t, $idpart); 301 } 302 303 // close wrapper 304 if($this->getConf('dropdown')) { 305 $out .= '</select>'; 306 $out .= '<input name="go" type="submit" value="→" />'; 307 $out .= '</form>'; 308 } else { 309 $out .= '</ul>'; 310 } 311 312 // show about if not already shown 313 if(!isset($this->opts['title']) && $this->getConf('about')) { 314 $out .= ' '; 315 $out .= $this->showAbout(); 316 } 317 318 $out .= '</div>'; 319 320 return $out; 321 } 322 323 /** 324 * Return the local name 325 * 326 * @param $lang 327 * @return string 328 */ 329 function getLocalName($lang) { 330 if($this->LN[$lang]) { 331 return $this->LN[$lang]; 332 } 333 return $lang; 334 } 335 336 /** 337 * Create the link or option for a single translation 338 * 339 * @param $lc string The language code 340 * @param $idpart string The ID of the translated page 341 * @returns string The item 342 */ 343 function getTransItem($lc, $idpart) { 344 global $ID; 345 global $conf; 346 347 list($link, $lang) = $this->buildTransID($lc, $idpart); 348 $link = cleanID($link); 349 350 // class 351 if(page_exists($link, '', false)) { 352 $class = 'wikilink1'; 353 } else { 354 $class = 'wikilink2'; 355 } 356 357 // local language name 358 $localname = $this->getLocalName($lang); 359 360 // current? 361 if($ID == $link) { 362 $sel = ' selected="selected"'; 363 $class .= ' cur'; 364 } else { 365 $sel = ''; 366 } 367 368 // flag 369 $flag = $style = ''; 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