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