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