1<?php 2/** 3 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 4 * @author Viktor Söderqvist <viktor@zuiderkwast.se> 5 */ 6 7// must be run within Dokuwiki 8if (!defined('DOKU_INC')) die(); 9if (!defined('DOKU_LF')) define ('DOKU_LF',"\n"); 10 11class helper_plugin_translate extends DokuWiki_Plugin { 12 13 // vars used for caching / lazy initialization 14 private $enabledLangs; 15 private $langnames; 16 private $langflags; 17 private $page_language; 18 private $curPageIsTranslatable; 19 20 public function getMethods() { 21 return array( 22 array( 23 'name' => 'getPageLanguage', 24 'desc' => 'Returns a language code or null if failed to detect', 25 'return' => array('language' => 'string'), 26 ), 27 array( 28 'name' => 'languageExists', 29 'desc' => 'Checks if a language exists by checking the translations of dokuwiki itself', 30 'param' => array('language' => 'string'), 31 'return' => array('exists' => 'boolean'), 32 ), 33 array( 34 'name' => 'translationLookup', 35 'desc' => 'Returns the ID of a page (default the current page) translated in another language, or null if not existing.', 36 'param' => array('language' => 'string', 'id (optional)' => 'sting'), 37 'return' => array('translation_id' => 'string'), 38 ), 39 array( 40 'name' => 'suggestTranslationId', 41 'desc' => 'Returns an suggested ID of a translation of the current page, '. 42 'in the same or another namespace, according to the configuration', 43 'param' => array('title' => 'string', 'language' => 'sting', 'from_language' => 'string'), 44 'return' => array('translation_id' => 'string'), 45 ), 46 array( 47 'name' => 'getLanguageName', 48 'desc' => 'Returns the name of a language in the native language and English', 49 'param' => array('code' => 'string'), 50 'return' => array('name' => 'string'), 51 ), 52 // FIXME add the rest of them here..... 53 ); 54 } 55 56 /** using guessing rules set in configuration */ 57 public function getPageLanguage($id=null) { 58 global $INFO, $ID, $conf; 59 if (is_null($id)) $id = $ID; 60 $meta = $id!==$ID ? p_get_metadata($id) : $INFO['meta']; 61 if (!isset($this->page_language)) $this->page_language = array(); 62 if (!isset($this->page_language[$id])) { 63 // Detect the language of the page 64 if (isset($meta['language'])) { 65 $lang = $meta['language']; 66 } 67 if (!isset($lang) && $this->getConf('guess_lang_by_ns')) { 68 // If the first level of namespace is a language code, use that 69 list($ns1) = explode(':',$id,2); 70 if ($this->languageExists($ns1)) $lang = $ns1; 71 } 72 if (!isset($lang) && $this->getConf('guess_lang_by_ui_lang')) { 73 // Use the UI language 74 $lang = $conf['lang']; 75 } 76 if (!isset($lang) && ($default = $this->getConf('default_language')) && 77 $this->languageExists($default)) { 78 // Use default language 79 $lang = $default; 80 } 81 $this->page_language[$id] = $lang; 82 } 83 return $this->page_language[$id]; 84 } 85 86 /** checks if a language exists, i.e. is enabled. */ 87 public function languageExists($lang) { 88 return preg_match('/^\w{2,3}(?:-\w+)?$/', $lang) && 89 is_dir(DOKU_INC . 'inc/lang/' . $lang); 90 } 91 92 public function getLanguageName($code) { 93 $this->loadLangnames(); 94 list ($key, $subkey) = explode('-',$code,2); // Locale style code, i.e. en-GB 95 $name = isset($this->langnames[$key]) ? $this->langnames[$key] : $code; 96 list ($name) = explode(',',$name,2); 97 if ($subkey) $name .= '-'.strtoupper($subkey); 98 return $name; 99 } 100 101 private function loadLangnames() { 102 if (isset($this->langnames)) return; 103 // A list where the first letter is capitalized only if 104 // the local language has this convention 105 if (@include(dirname(__FILE__).'/langinfo.php')) { 106 foreach ($langinfo as $code => $info) { 107 list ($local_name, $english_name) = $info; 108 $this->langnames[$code] = $local_name; 109 } 110 } 111 else { 112 $this->langnames = array(); // failed to load 113 } 114 } 115 116 /** Returns true if page is allowed to be translated */ 117 public function isTranslatable($id=null) { 118 global $ID; 119 if (is_null($id)) $id = $ID; 120 if ($id===$ID && isset($this->curPageIsTranslatable)) // cached 121 return $this->curPageIsTranslatable; 122 $ret = $this->checkIsTranslatable($id); 123 if ($id===$ID) $this->curPageIsTranslatable = $ret; // cache 124 return $ret; 125 } 126 127 /** helper used by isTranslatable */ 128 private function checkIsTranslatable($id) { 129 $str = trim($this->getConf('include_namespaces')); 130 if ($str == '') return false; // nothing to include 131 if ($str != '*') { 132 $inc_nss = array_map('trim',explode(',',$str)); 133 $any = false; 134 foreach ($inc_nss as $ns) { 135 if (self::isIdInNamespace($id,$ns)) { 136 $any = true; 137 break; 138 } 139 } 140 if (!$any) return false; 141 } 142 $str = $this->getConf('exclude_namespaces'); 143 if ($str != '') { 144 $exc_nss = array_map('trim',explode(',',$str)); 145 foreach ($exc_nss as $ns) 146 if (self::isIdInNamespace($id,$ns)) 147 return false; 148 } 149 $str = $this->getConf('exclude_pagenames'); 150 if ($str != '') { 151 $exc_pages = array_map('trim',explode(',',$str)); 152 $p = noNS($id); 153 foreach ($exc_pages as $exc) 154 if ($p == $exc) 155 return false; 156 } 157 // Finally check if the language can be detected 158 $lang = $this->getPageLanguage($id); 159 return !empty($lang); 160 } 161 162 private static function isIdInNamespace($id, $ns) { 163 return substr($id, 0, strlen($ns) + 1) === "$ns:"; 164 } 165 166 /** Returns true if the current user is may translate the current page, false otherwise */ 167 private function hasPermissionTranslate() { 168 global $INFO; 169 // Assume that if we have EDIT on the current page, we may also create translation pages. 170 if ($INFO['perm'] >= AUTH_EDIT) return true; 171 $grp = $this->getConf('translator_group'); 172 return $grp && in_array($grp, $INFO['userinfo']['grps']); 173 } 174 175 /** Returns true if the current page is a translation, false otherwise. */ 176 public function isTranslation($id=null) { 177 global $ID; 178 if (is_null($id)) $id = $ID; 179 return $this->getOriginal($id) != $id; 180 } 181 182 /** Returns the id of the original page */ 183 public function getOriginal($id=null) { 184 global $INFO, $ID; 185 if (is_null($id)) $id = $ID; 186 $meta = $id!==$ID ? p_get_metadata($id) : $INFO['meta']; 187 if (empty($meta['relation']['istranslationof'])) return $id; 188 list ($orig) = array_keys($meta['relation']['istranslationof']); 189 return $orig; 190 } 191 192 /** 193 * Returns the ID of the current page translated in another language, or null if not existing. 194 */ 195 public function translationLookup($language, $id=null) { 196 global $INFO, $ID; 197 if (is_null($id)) $id = $ID; 198 $id = $this->getOriginal($id); 199 $orig_lang = $this->getPageLanguage($id); 200 if ($language == $orig_lang) return $id; 201 $meta = $id!==$ID ? p_get_metadata($id) : $INFO['meta']; 202 if (!isset($meta['relation']['translations'])) return null; 203 foreach ($meta['relation']['translations'] as $tid => $tlang) { 204 if ($tlang == $language && page_exists($tid)) return $tid; 205 } 206 return null; 207 } 208 209 /** returns HTML for a link to a translation or a page where it can be created */ 210 public function translationLink($language,$text='') { 211 $langname = $this->getLanguageName($language); 212 if ($text=='') { 213 $text = $this->getConf('link_style') == 'langname' ? $langname : $language; 214 } 215 $current_lang = $this->getPageLanguage(); 216 $original_id = $this->getOriginal(); 217 $id = $this->translationLookup($language); 218 if (isset($id)) { 219 $url = wl($id); 220 $class = 'wikilink1'; 221 $more = ''; 222 } 223 else { 224 $url = wl($original_id, array('do'=>'translate', 'to'=>$language)); 225 $class = 'wikilink2'; 226 $more = ' rel="nofollow"'; 227 } 228 return '<a href="'.$url.'" class="'.$class.'" title="'.hsc($langname).'"'.$more.'>'.hsc($text).'</a>'; 229 } 230 231 /** Returns HTML with translation links for all enabled languages */ 232 public function translationLinksAll() { 233 global $INFO, $ID; 234 if (!$INFO['exists']) return; 235 if (!$this->isTranslatable()) return; 236 $orig = $this->getOriginal(); 237 $origlang = $this->getPageLanguage($orig); 238 239 // If no permission to translate, hide links to untranslated languages 240 if ($this->hasPermissionTranslate()) { 241 $langs = $this->getEnabledLanguages(); 242 } else { 243 // Show only languages with existing translations 244 $langs = array_values($this->getTranslations($orig)); 245 if (empty($langs)) return; // no translations exist 246 } 247 248 // Add link to the original language, if not present 249 if (!in_array($origlang, $langs)) array_unshift($langs, $origlang); 250 251 $out = '<div class="plugin_translate">'.DOKU_LF; 252 $out .= '<ul>'.DOKU_LF; 253 foreach ($langs as $lang) { 254 $out .= '<li>'.DOKU_LF; 255 $out .= '<div class="li">'.$this->translationLink($lang).'</div>'.DOKU_LF; 256 $out .= '</li>'.DOKU_LF; 257 } 258 $out .= '</ul>'.DOKU_LF; 259 $out .= '</div>'.DOKU_LF; 260 return $out; 261 } 262 263 /** 264 * Returns HTML with links to all existing translations of the current 265 * page and a link to create additional translations 266 */ 267 public function translationLinksExisting() { 268 global $INFO,$ID; 269 if (!$INFO['exists']) return; 270 if (!$this->isTranslatable()) return; 271 $orig = $this->getOriginal(); 272 $origlang = $this->getPageLanguage($orig); 273 $langs = array_values($this->getTranslations($orig)); 274 275 $has_permission_translate = $this->hasPermissionTranslate(); 276 277 if (count($langs) == 0 && !$has_permission_translate) return; 278 279 // Add the original language if not present 280 if (count($langs) > 0 && !in_array($origlang, $langs)) { 281 array_unshift($langs, $origlang); 282 } 283 284 $out = '<div class="plugin_translate">'.DOKU_LF; 285 $out .= '<ul>'.DOKU_LF; 286 foreach ($langs as $lang) { 287 $out .= '<li>'.DOKU_LF; 288 $out .= '<div class="li">'.$this->translationLink($lang).'</div>'.DOKU_LF; 289 $out .= '</li>'.DOKU_LF; 290 } 291 if ($has_permission_translate) { 292 // "Translate this page" link 293 $text = $this->getLang('translate_this_page'); 294 $url = wl($orig, 'do=translate'); 295 $link = '<a href="'.$url.'" class="translate" title="'.$text.'">'.$text.'</a>'; 296 $out .= '<li>'.DOKU_LF; 297 $out .= '<div class="li">'.$link.'</div>'.DOKU_LF; 298 $out .= '</li>'.DOKU_LF; 299 } 300 $out .= '</ul>'.DOKU_LF; 301 $out .= '</div>'.DOKU_LF; 302 return $out; 303 } 304 305 /** 306 * returns HTML with links to translations of the current page in all 307 * available languages 308 */ 309 public function translationLinks() { 310 global $INFO; 311 if (!$INFO['exists']) return; 312 if (!$this->isTranslatable()) return; 313 $langs = $this->getEnabledLanguages(); 314 if (count($langs) > 10) // FIXME configuration 315 return $this->translationLinksExisting(); 316 else 317 return $this->translationLinksAll(); 318 } 319 320 /** 321 * Returns an suggested ID of a translation of the current page, 322 * in the same or another namespace, according to configuration 323 */ 324 public function suggestTranslationId($title, $language, $from_language=null) { 325 global $INFO; 326 $ns = $INFO['namespace']; 327 if ($this->getConf('use_language_namespace')) { 328 if (!isset($from_language)) { 329 $from_language = $this->getPageLanguage(); 330 } 331 list ($ns1,$ns_tail) = explode(':',$ns,2); 332 if ($ns1 === $from_language) { 333 // replace language as first part of ns 334 $ns = isset($ns_tail) ? $language.':'.$ns_tail : $language; 335 } 336 else { 337 // prepend language to ns 338 $ns = !empty($ns) ? $language.':'.$ns : $language; 339 } 340 } 341 return (empty($ns) ? '' : $ns.':') . cleanID($title); 342 } 343 344 public function suggestPageId($title, $language) { 345 return $this->getConf('use_language_namespace') ? 346 $language.':'.cleanID($title) : cleanID($title); 347 } 348 349 /** Returns an array of language codes */ 350 public function getEnabledLanguages() { 351 if (!isset($this->enabledLangs)) { 352 $langs = $this->getConf('enabled_languages'); 353 if (empty($langs)) { 354 // Not set. Use all DokuWiki's supported languages. 355 $langs = array(); 356 if ($handle = opendir(DOKU_INC.'inc/lang')) { 357 while (false !== ($file = readdir($handle))) { 358 if ($file[0] == '.') continue; 359 if (is_dir(DOKU_INC.'inc/lang/'.$file)) 360 array_push($langs,$file); 361 } 362 closedir($handle); 363 } 364 sort($langs); 365 } 366 else { 367 $langs = array_map('trim', explode(',', strtolower($langs))); 368 } 369 $this->enabledLangs = $langs; 370 } 371 return $this->enabledLangs; 372 } 373 374 /** 375 * do=translate 376 */ 377 public function printActionTranslatePage() { 378 global $ID, $INFO; 379 $target_title = $_REQUEST['title']; 380 $target_lang = $_REQUEST['to']; 381 382 // Get source language form metadata or from namespace according to configuration 383 $source_lang = $this->getPageLanguage(); 384 385 // Start of page 386 echo $this->locale_xhtml('newtrans'); 387 388 if (!isset($source_lang)) { 389 // Can't translate in this case. No form. 390 return; 391 } 392 393 // build form 394 $form = new Doku_Form('translate__plugin'); 395 $form->startFieldset($this->getLang('translation')); 396 $form->addHidden('id',$ID); 397 $form->addHidden('do','translate'); 398 399 $class = ''; // a class could be used on fields for form validation feedback (TODO) 400 401 $form->addElement(form_makeTextField('',$this->getLanguageName($source_lang), 402 $this->getLang('original_language'), 403 '','',array('readonly'=>'readonly'))); 404 405 $langs = $this->getEnabledLanguages(); 406 $options = array(''=>''); 407 foreach ($langs as $lang) { 408 if ($lang == $source_lang) continue; 409 $options[$lang] = $this->getLanguageName($lang); 410 } 411 $form->addElement(form_makeListboxField('to',$options,$target_lang,$this->getLang('translate_to'),'',$class)); 412 413 $form->addElement(form_makeTextField('',p_get_first_heading($ID), 414 $this->getLang('original_title'), 415 '','',array('readonly'=>'readonly'))); 416 417 $form->addElement(form_makeTextField('title',$target_title,$this->getLang('translated_title'),'',$class)); 418 $form->addElement(form_makeButton('submit','', $this->getLang('create_translation'))); 419 $form->printForm(); 420 } 421 422 /** 423 * do=createpage 424 */ 425 public function printActionCreatepagePage() { 426 global $INFO; 427 428 // Start of page 429 echo $this->locale_xhtml('newpage'); 430 431 // build form 432 $form = new Doku_Form('translate__plugin'); 433 $form->startFieldset($this->getLang('create_new_page')); 434 $form->addHidden('do','createpage'); 435 $class = ''; 436 $langs = $this->getEnabledLanguages(); 437 $options = array(''=>''); 438 foreach ($langs as $lang) { 439 if ($lang == $_REQUEST['lang']) continue; 440 $options[$lang] = $this->getLanguageName($lang); 441 } 442 $form->addElement(form_makeListboxField('lang',$options,$_REQUEST['lang'],$this->getLang('language'),'',$class)); 443 $form->addElement(form_makeTextField('title',$_REQUEST['title'],$this->getLang('title'),'',$class)); 444 $form->addElement(form_makeButton('submit','', $GLOBALS['lang']['btn_create'])); 445 $form->printForm(); 446 } 447 448 /** 449 * Returns an associative of translations on the form page-id => language-code. 450 */ 451 private function getTranslations($id) { 452 global $INFO, $ID; 453 $meta = $id === $ID ? $INFO['meta'] : p_get_metadata($id); 454 if (empty($meta['relation']['translations'])) return array(); // no translations exist 455 // Check if any of the translations have been deleted 456 foreach ($meta['relation']['translations'] as $page_id => $lang) { 457 if (!page_exists($page_id)) { 458 unset($meta['relation']['translations'][$page_id]); 459 $has_deleted = true; 460 } 461 } 462 if ($has_deleted) { 463 // Store the updated list of translations in metadata 464 $set_metadata['relation']['translations'] = $meta['relation']['translations']; 465 p_set_metadata($id, $set_metadata); 466 } 467 return $meta['relation']['translations']; 468 } 469} 470// vim:ts=4:sw=4:et:enc=utf-8: 471