xref: /plugin/autotranslation/helper.php (revision 4e6ef383ac1884452ab2c927a4d59fb9ab09072a)
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(empty($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->translationsNs = $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->translationsNs) ) { return $this->translationsNs; }
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->translations) )
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->translationsNs . '(' . join('|', $this->translations) . '):(.*)/';
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->translations) . ')($|,|:|;|-)/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->translationsNs . $lng . ':' . $idpart;
156            $name = $lng;
157        } else {
158            $link = ':' . $this->translationsNs . $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->translationsNs && strpos($id, $this->translationsNs) !== 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->translations 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->translations 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="&rarr;" />';
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 .= '&nbsp';
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 = '&nbsp;';
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->translationsNs . '((' . join('|', $this->translations) . '):)?/';
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->translations 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