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($checkact && $ACT != 'show') return false; 188 if($this->tns && strpos($id, $this->tns) !== 0) return false; 189 $skiptrans = trim($this->getConf('skiptrans')); 190 if($skiptrans && preg_match('/' . $skiptrans . '/ui', ':' . $id)) return false; 191 $meta = p_get_metadata($id); 192 if($meta['plugin']['translation']['notrans']) return false; 193 194 return true; 195 } 196 197 /** 198 * Return the (localized) about link 199 */ 200 function showAbout() { 201 global $ID; 202 global $conf; 203 global $INFO; 204 205 $curlc = $this->getLangPart($ID); 206 207 $about = $this->getConf('about'); 208 if($this->getConf('localabout')) { 209 list($lc, $idpart) = $this->getTransParts($about); 210 list($about, $name) = $this->buildTransID($curlc, $idpart); 211 $about = cleanID($about); 212 } 213 214 $out = ''; 215 $out .= '<sup>'; 216 $out .= html_wikilink($about, '?'); 217 $out .= '</sup>'; 218 219 return $out; 220 } 221 222 /** 223 * Returns a list of (lc => link) for all existing translations of a page 224 * 225 * @param $id 226 * @return array 227 */ 228 function getAvailableTranslations($id) { 229 $result = array(); 230 231 list($lc, $idpart) = $this->getTransParts($id); 232 $lang = $this->realLC($lc); 233 234 foreach($this->trans as $t) { 235 if($t == $lc) continue; //skip self 236 list($link, $name) = $this->buildTransID($t, $idpart); 237 if(page_exists($link)) { 238 $result[$name] = $link; 239 } 240 } 241 242 return $result; 243 } 244 245 /** 246 * Creates an UI for linking to the available and configured translations 247 * 248 * Can be called from the template or via the ~~TRANS~~ syntax component. 249 */ 250 public function showTranslations() { 251 global $conf; 252 global $INFO; 253 254 if(!$this->istranslatable($INFO['id'])) return ''; 255 $this->checkage(); 256 257 list($lc, $idpart) = $this->getTransParts($INFO['id']); 258 $lang = $this->realLC($lc); 259 260 $out = '<div class="plugin_translation">'; 261 262 //show title and about 263 if(isset($this->opts['title'])) { 264 $out .= '<span>' . $this->getLang('translations'); 265 if($this->getConf('about')) $out .= $this->showAbout(); 266 $out .= ':</span> '; 267 if(isset($this->opts['twolines'])) $out .= '<br />'; 268 } 269 270 // open wrapper 271 if($this->getConf('dropdown')) { 272 // select needs its own styling 273 if($INFO['exists']) { 274 $class = 'wikilink1'; 275 } else { 276 $class = 'wikilink2'; 277 } 278 if(isset($this->opts['flag'])) { 279 $flag = DOKU_BASE . 'lib/plugins/translation/flags/' . hsc($lang) . '.gif'; 280 }else{ 281 $flag = ''; 282 } 283 284 if($conf['userewrite']) { 285 $action = wl(); 286 } else { 287 $action = script(); 288 } 289 290 $out .= '<form action="' . $action . '" id="translation__dropdown">'; 291 if($flag) $out .= '<img src="' . $flag . '" alt="' . hsc($lang) . '" height="11" class="' . $class . '" /> '; 292 $out .= '<select name="id" class="' . $class . '">'; 293 } else { 294 $out .= '<ul>'; 295 } 296 297 // insert items 298 foreach($this->trans as $t) { 299 $out .= $this->getTransItem($t, $idpart); 300 } 301 302 // close wrapper 303 if($this->getConf('dropdown')) { 304 $out .= '</select>'; 305 $out .= '<input name="go" type="submit" value="→" />'; 306 $out .= '</form>'; 307 } else { 308 $out .= '</ul>'; 309 } 310 311 // show about if not already shown 312 if(!isset($this->opts['title']) && $this->getConf('about')) { 313 $out .= ' '; 314 $out .= $this->showAbout(); 315 } 316 317 $out .= '</div>'; 318 319 return $out; 320 } 321 322 /** 323 * Return the local name 324 * 325 * @param $lang 326 * @return string 327 */ 328 function getLocalName($lang) { 329 if($this->LN[$lang]) { 330 return $this->LN[$lang]; 331 } 332 return $lang; 333 } 334 335 /** 336 * Create the link or option for a single translation 337 * 338 * @param $lc string The language code 339 * @param $idpart string The ID of the translated page 340 * @returns string The item 341 */ 342 function getTransItem($lc, $idpart) { 343 global $ID; 344 global $conf; 345 346 list($link, $lang) = $this->buildTransID($lc, $idpart); 347 $link = cleanID($link); 348 349 // class 350 if(page_exists($link, '', false)) { 351 $class = 'wikilink1'; 352 } else { 353 $class = 'wikilink2'; 354 } 355 356 // local language name 357 $localname = $this->getLocalName($lang); 358 359 // current? 360 if($ID == $link) { 361 $sel = ' selected="selected"'; 362 $class .= ' cur'; 363 } else { 364 $sel = ''; 365 } 366 367 // flag 368 if(isset($this->opts['flag'])) { 369 $flag = DOKU_BASE . 'lib/plugins/translation/flags/' . hsc($lang) . '.gif'; 370 $style = ' style="background-image: url(\'' . $flag . '\')"'; 371 $class .= ' flag'; 372 } 373 374 // what to display as name 375 if(isset($this->opts['name'])) { 376 $display = hsc($localname); 377 if(isset($this->opts['langcode'])) $display .= ' (' . hsc($lang) . ')'; 378 } elseif(isset($this->opts['langcode'])) { 379 $display = hsc($lang); 380 } else { 381 $display = ' '; 382 } 383 384 // prepare output 385 $out = ''; 386 if($this->getConf('dropdown')) { 387 if($conf['useslash']) $link = str_replace(':', '/', $link); 388 389 $out .= '<option class="' . $class . '" title="' . hsc($localname) . '" value="' . $link . '"' . $sel . $style . '>'; 390 $out .= $display; 391 $out .= '</option>'; 392 } else { 393 $out .= '<li><div class="li">'; 394 $out .= '<a href="' . wl($link, 'tns') . '" class="' . $class . '" title="' . hsc($localname) . '">'; 395 if($flag) $out .= '<img src="' . $flag . '" alt="' . hsc($lang) . '" height="11" />'; 396 $out .= $display; 397 $out .= '</a>'; 398 $out .= '</div></li>'; 399 } 400 401 return $out; 402 } 403 404 /** 405 * Checks if the current page is a translation of a page 406 * in the default language. Displays a notice when it is 407 * older than the original page. Tries to lin to a diff 408 * with changes on the original since the translation 409 */ 410 function checkage() { 411 global $ID; 412 global $INFO; 413 if(!$this->getConf('checkage')) return; 414 if(!$INFO['exists']) return; 415 $lng = $this->getLangPart($ID); 416 if($lng == $this->defaultlang) return; 417 418 $rx = '/^' . $this->tns . '((' . join('|', $this->trans) . '):)?/'; 419 $idpart = preg_replace($rx, '', $ID); 420 421 // compare modification times 422 list($orig, $name) = $this->buildTransID($this->defaultlang, $idpart); 423 $origfn = wikiFN($orig); 424 if($INFO['lastmod'] >= @filemtime($origfn)) return; 425 426 // get revision from before translation 427 $orev = 0; 428 $revs = getRevisions($orig, 0, 100); 429 foreach($revs as $rev) { 430 if($rev < $INFO['lastmod']) { 431 $orev = $rev; 432 break; 433 } 434 } 435 436 // see if the found revision still exists 437 if($orev && !page_exists($orig, $orev)) $orev = 0; 438 439 // build the message and display it 440 $orig = cleanID($orig); 441 $msg = sprintf($this->getLang('outdated'), wl($orig)); 442 if($orev) { 443 $msg .= sprintf( 444 ' ' . $this->getLang('diff'), 445 wl($orig, array('do' => 'diff', 'rev' => $orev)) 446 ); 447 } 448 449 echo '<div class="notify">' . $msg . '</div>'; 450 } 451 452 /** 453 * Checks if the current ID has a translated page 454 */ 455 function hasTranslation($inputID = null) { 456 global $ID, $INFO, $conf; 457 458 if ( empty($inputID) ) 459 { 460 $inputID = $ID; 461 } 462 463 if ( !$this->istranslatable($id) ) return false; 464 465 $idpart = $this->getIDPart($inputID); 466 467 foreach($this->trans as $t) 468 { 469 list($link,$name) = $this->buildTransID($t,$idpart,false); 470 $link = cleanID($link); 471 472 if( $inputID != $link && page_exists($link,'',false) ){ 473 return true; 474 } 475 } 476 477 return false; 478 } 479} 480