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