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 $translations = array(); 14 var $translationNs = ''; 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->translations = strtolower(str_replace(',', ' ', $this->getConf('translations'))); 29 $this->translations = array_unique(array_filter(explode(' ', $this->translations))); 30 sort($this->translations); 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->translations)) { 48 $this->defaultlang = $dfl; 49 } else { 50 $this->defaultlang = ''; 51 array_unshift($this->translations, ''); 52 } 53 54 $this->translationNs = cleanID($this->getConf('translationns')); 55 if($this->translationNs) $this->translationNs .= ':'; 56 } 57 58 /** 59 * Check if the given ID is a translation and return the language code. 60 */ 61 function getLangPart($id) { 62 list($lng) = $this->getTransParts($id); 63 return $lng; 64 } 65 66 /** 67 * Check if the given ID is a translation and return the language code and 68 * the id part. 69 */ 70 function getTransParts($id) { 71 $rx = '/^' . $this->translationNs . '(' . join('|', $this->translations) . '):(.*)/'; 72 if(preg_match($rx, $id, $match)) { 73 return array($match[1], $match[2]); 74 } 75 return array('', $id); 76 } 77 78 /** 79 * Returns the browser language if it matches with one of the configured 80 * languages 81 */ 82 function getBrowserLang() { 83 $rx = '/(^|,|:|;|-)(' . join('|', $this->translations) . ')($|,|:|;|-)/i'; 84 if(preg_match($rx, $_SERVER['HTTP_ACCEPT_LANGUAGE'], $match)) { 85 return strtolower($match[2]); 86 } 87 return false; 88 } 89 90 /** 91 * Returns the ID and name to the wanted translation, empty 92 * $lng is default lang 93 */ 94 function buildTransID($lng, $idpart) { 95 global $conf; 96 if($lng) { 97 $link = ':' . $this->translationNs . $lng . ':' . $idpart; 98 $name = $lng; 99 } else { 100 $link = ':' . $this->translationNs . $idpart; 101 $name = $this->realLC(''); 102 } 103 return array($link, $name); 104 } 105 106 /** 107 * Returns the real language code, even when an empty one is given 108 * (eg. resolves th default language) 109 */ 110 function realLC($lc) { 111 global $conf; 112 if($lc) { 113 return $lc; 114 } elseif(!$conf['lang_before_translation']) { 115 return $conf['lang']; 116 } else { 117 return $conf['lang_before_translation']; 118 } 119 } 120 121 /** 122 * Check if current ID should be translated and any GUI 123 * should be shown 124 */ 125 function istranslatable($id, $checkact = true) { 126 global $ACT; 127 128 if($checkact && $ACT != 'show') return false; 129 if($this->translationNs && strpos($id, $this->translationNs) !== 0) return false; 130 $skiptrans = trim($this->getConf('skiptrans')); 131 if($skiptrans && preg_match('/' . $skiptrans . '/ui', ':' . $id)) return false; 132 $meta = p_get_metadata($id); 133 if($meta['plugin']['translation']['notrans']) return false; 134 135 return true; 136 } 137 138 /** 139 * Return the (localized) about link 140 */ 141 function showAbout() { 142 global $ID; 143 global $conf; 144 global $INFO; 145 146 $curlc = $this->getLangPart($ID); 147 148 $about = $this->getConf('about'); 149 if($this->getConf('localabout')) { 150 list($lc, $idpart) = $this->getTransParts($about); 151 list($about, $name) = $this->buildTransID($curlc, $idpart); 152 $about = cleanID($about); 153 } 154 155 $out = ''; 156 $out .= '<sup>'; 157 $out .= html_wikilink($about, '?'); 158 $out .= '</sup>'; 159 160 return $out; 161 } 162 163 /** 164 * Returns a list of (lc => link) for all existing translations of a page 165 * 166 * @param $id 167 * @return array 168 */ 169 function getAvailableTranslations($id) { 170 $result = array(); 171 172 list($lc, $idpart) = $this->getTransParts($id); 173 $lang = $this->realLC($lc); 174 175 foreach($this->translations as $t) { 176 if($t == $lc) continue; //skip self 177 list($link, $name) = $this->buildTransID($t, $idpart); 178 if(page_exists($link)) { 179 $result[$name] = $link; 180 } 181 } 182 183 return $result; 184 } 185 186 /** 187 * Creates an UI for linking to the available and configured translations 188 * 189 * Can be called from the template or via the ~~TRANS~~ syntax component. 190 */ 191 public function showTranslations() { 192 global $conf; 193 global $INFO; 194 195 if(!$this->istranslatable($INFO['id'])) return ''; 196 $this->checkage(); 197 198 list($lc, $idpart) = $this->getTransParts($INFO['id']); 199 $lang = $this->realLC($lc); 200 201 $out = '<div class="plugin_translation">'; 202 203 //show title and about 204 if(isset($this->opts['title'])) { 205 $out .= '<span>' . $this->getLang('translations'); 206 if($this->getConf('about')) $out .= $this->showAbout(); 207 $out .= ':</span> '; 208 if(isset($this->opts['twolines'])) $out .= '<br />'; 209 } 210 211 // open wrapper 212 if($this->getConf('dropdown')) { 213 // select needs its own styling 214 if($INFO['exists']) { 215 $class = 'wikilink1'; 216 } else { 217 $class = 'wikilink2'; 218 } 219 if(isset($this->opts['flag'])) { 220 $flag = DOKU_BASE . 'lib/plugins/translation/flags/' . hsc($lang) . '.gif'; 221 }else{ 222 $flag = ''; 223 } 224 225 if($conf['userewrite']) { 226 $action = wl(); 227 } else { 228 $action = script(); 229 } 230 231 $out .= '<form action="' . $action . '" id="translation__dropdown">'; 232 if($flag) $out .= '<img src="' . $flag . '" alt="' . hsc($lang) . '" height="11" class="' . $class . '" /> '; 233 $out .= '<select name="id" class="' . $class . '">'; 234 } else { 235 $out .= '<ul>'; 236 } 237 238 // insert items 239 foreach($this->translations as $t) { 240 $out .= $this->getTransItem($t, $idpart); 241 } 242 243 // close wrapper 244 if($this->getConf('dropdown')) { 245 $out .= '</select>'; 246 $out .= '<input name="go" type="submit" value="→" />'; 247 $out .= '</form>'; 248 } else { 249 $out .= '</ul>'; 250 } 251 252 // show about if not already shown 253 if(!isset($this->opts['title']) && $this->getConf('about')) { 254 $out .= ' '; 255 $out .= $this->showAbout(); 256 } 257 258 $out .= '</div>'; 259 260 return $out; 261 } 262 263 /** 264 * Return the local name 265 * 266 * @param $lang 267 * @return string 268 */ 269 function getLocalName($lang) { 270 if($this->LN[$lang]) { 271 return $this->LN[$lang]; 272 } 273 return $lang; 274 } 275 276 /** 277 * Create the link or option for a single translation 278 * 279 * @param $lc string The language code 280 * @param $idpart string The ID of the translated page 281 * @returns string The item 282 */ 283 function getTransItem($lc, $idpart) { 284 global $ID; 285 global $conf; 286 287 list($link, $lang) = $this->buildTransID($lc, $idpart); 288 $link = cleanID($link); 289 290 // class 291 if(page_exists($link, '', false)) { 292 $class = 'wikilink1'; 293 } else { 294 $class = 'wikilink2'; 295 } 296 297 // local language name 298 $localname = $this->getLocalName($lang); 299 300 // current? 301 if($ID == $link) { 302 $sel = ' selected="selected"'; 303 $class .= ' cur'; 304 } else { 305 $sel = ''; 306 } 307 308 // flag 309 if(isset($this->opts['flag'])) { 310 $flag = DOKU_BASE . 'lib/plugins/translation/flags/' . hsc($lang) . '.gif'; 311 $style = ' style="background-image: url(\'' . $flag . '\')"'; 312 $class .= ' flag'; 313 } 314 315 // what to display as name 316 if(isset($this->opts['name'])) { 317 $display = hsc($localname); 318 if(isset($this->opts['langcode'])) $display .= ' (' . hsc($lang) . ')'; 319 } elseif(isset($this->opts['langcode'])) { 320 $display = hsc($lang); 321 } else { 322 $display = ' '; 323 } 324 325 // prepare output 326 $out = ''; 327 if($this->getConf('dropdown')) { 328 if($conf['useslash']) $link = str_replace(':', '/', $link); 329 330 $out .= '<option class="' . $class . '" title="' . hsc($localname) . '" value="' . $link . '"' . $sel . $style . '>'; 331 $out .= $display; 332 $out .= '</option>'; 333 } else { 334 $out .= '<li><div class="li">'; 335 $out .= '<a href="' . wl($link) . '" class="' . $class . '" title="' . hsc($localname) . '">'; 336 if($flag) $out .= '<img src="' . $flag . '" alt="' . hsc($lang) . '" height="11" />'; 337 $out .= $display; 338 $out .= '</a>'; 339 $out .= '</div></li>'; 340 } 341 342 return $out; 343 } 344 345 /** 346 * Checks if the current page is a translation of a page 347 * in the default language. Displays a notice when it is 348 * older than the original page. Tries to lin to a diff 349 * with changes on the original since the translation 350 */ 351 function checkage() { 352 global $ID; 353 global $INFO; 354 if(!$this->getConf('checkage')) return; 355 if(!$INFO['exists']) return; 356 $lng = $this->getLangPart($ID); 357 if($lng == $this->defaultlang) return; 358 359 $rx = '/^' . $this->translationNs . '((' . join('|', $this->translations) . '):)?/'; 360 $idpart = preg_replace($rx, '', $ID); 361 362 // compare modification times 363 list($orig, $name) = $this->buildTransID($this->defaultlang, $idpart); 364 $origfn = wikiFN($orig); 365 if($INFO['lastmod'] >= @filemtime($origfn)) return; 366 367 // get revision from before translation 368 $orev = 0; 369 $revs = getRevisions($orig, 0, 100); 370 foreach($revs as $rev) { 371 if($rev < $INFO['lastmod']) { 372 $orev = $rev; 373 break; 374 } 375 } 376 377 // see if the found revision still exists 378 if($orev && !page_exists($orig, $orev)) $orev = 0; 379 380 // build the message and display it 381 $orig = cleanID($orig); 382 $msg = sprintf($this->getLang('outdated'), wl($orig)); 383 if($orev) { 384 $msg .= sprintf( 385 ' ' . $this->getLang('diff'), 386 wl($orig, array('do' => 'diff', 'rev' => $orev)) 387 ); 388 } 389 390 echo '<div class="notify">' . $msg . '</div>'; 391 } 392} 393