1<?php
2
3/**
4 * Translation Plugin: Simple multilanguage plugin
5 *
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author     Andreas Gohr <andi@splitbrain.org>
8 * @author     Guy Brand <gb@isis.u-strasbg.fr>
9 */
10class action_plugin_translation extends DokuWiki_Action_Plugin
11{
12
13    /**
14     * For the helper plugin
15     * @var helper_plugin_translation
16     */
17    protected $helper = null;
18
19    /**
20     * Constructor. Load helper plugin
21     */
22    public function __construct()
23    {
24        $this->helper = plugin_load('helper', 'translation');
25    }
26
27    /**
28     * Registers a callback function for a given event
29     *
30     * @param Doku_Event_Handler $controller
31     */
32    public function register(Doku_Event_Handler $controller)
33    {
34        if ($this->getConf('translateui')) {
35            $controller->register_hook('INIT_LANG_LOAD', 'BEFORE', $this, 'translateUI');
36            $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'translateJS');
37            $controller->register_hook('JS_CACHE_USE', 'BEFORE', $this, 'translateJSCache');
38        } else {
39            $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'addLanguageAttributes');
40        }
41
42        if ($this->getConf('redirectstart')) {
43            $controller->register_hook('DOKUWIKI_STARTED', 'BEFORE', $this, 'redirectStartPage');
44        }
45
46        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addHrefLangAttributes');
47        $controller->register_hook('COMMON_PAGETPL_LOAD', 'AFTER', $this, 'handlePageTemplates');
48        $controller->register_hook('SEARCH_QUERY_PAGELOOKUP', 'AFTER', $this, 'sortSearchResults');
49    }
50
51    /**
52     * Hook Callback. Set the language for the UI
53     *
54     * @param Doku_Event $event INIT_LANG_LOAD
55     */
56    public function translateUI(Doku_Event $event)
57    {
58        global $conf;
59        global $ACT;
60        global $INPUT;
61
62        // $ID is not set yet, so we have to get it ourselves
63        $id = getID();
64
65        // For ID based access, get the language from the ID, try request param or session otherwise
66        if (
67            isset($ACT) &&
68            in_array(act_clean($ACT), ['show', 'recent', 'diff', 'edit', 'preview', 'source', 'subscribe'])
69        ) {
70            $locale = $this->helper->getLangPart($id ?? '');
71            $_SESSION[DOKU_COOKIE]['translationlc'] = $locale; // IDs always reset the language
72        } elseif ($INPUT->has('lang')) {
73            $locale = $INPUT->str('lang');
74        } else {
75            $locale = $_SESSION[DOKU_COOKIE]['translationlc'] ?? '';
76        }
77
78        // if the language is not the default language, set the language
79        if ($locale && $locale !== $conf['lang']) {
80            $conf['lang_before_translation'] = $conf['lang']; //store for later access in syntax plugin
81            $event->data = $locale;
82            $conf['lang'] = $locale;
83        }
84    }
85
86    /**
87     * Hook Callback. Pass language code to JavaScript dispatcher
88     *
89     * @param Doku_Event $event TPL_METAHEADER_OUTPUT
90     */
91    public function translateJS(Doku_Event $event)
92    {
93        global $conf;
94
95        $count = count($event->data['script']);
96        for ($i = 0; $i < $count; $i++) {
97            if (strpos($event->data['script'][$i]['src'], '/lib/exe/js.php') !== false) {
98                $event->data['script'][$i]['src'] .= '&lang=' . hsc($conf['lang']);
99            }
100        }
101    }
102
103    /**
104     * Hook Callback. Cache JavaScript per language
105     *
106     * @param Doku_Event $event JS_CACHE_USE
107     */
108    public function translateJSCache(Doku_Event $event)
109    {
110        global $conf;
111
112        // reuse the constructor to reinitialize the cache key
113        $event->data->__construct(
114            $event->data->key . $conf['lang'],
115            $event->data->ext
116        );
117    }
118
119    /**
120     * Hook Callback. Add lang and dir attributes when UI isn't translated
121     *
122     * @param Doku_Event $event TPL_CONTENT_DISPLAY
123     */
124    public function addLanguageAttributes(Doku_Event $event)
125    {
126        global $ID;
127        global $conf;
128
129        if (!$this->helper->istranslatable($ID)) return;
130        $locale = $this->helper->getLangPart($ID ?? '');
131
132        if ($locale && $locale !== $conf['lang']) {
133            if (file_exists(DOKU_INC . 'inc/lang/' . $locale . '/lang.php')) {
134                $lang = [];
135                include(DOKU_INC . 'inc/lang/' . $locale . '/lang.php');
136                $direction = $lang['direction'] ?? 'ltr';
137
138                $event->data = '<div lang="' . hsc($locale) . '" dir="' . hsc($direction) . '">' .
139                    $event->data .
140                    '</div>';
141            }
142        }
143    }
144
145    /**
146     * Hook Callback. Redirect to translated start page
147     *
148     * @param Doku_Event $event DOKUWIKI_STARTED
149     */
150    public function redirectStartPage(Doku_Event $event)
151    {
152        global $ID;
153        global $ACT;
154        global $conf;
155
156        if ($ID == $conf['start'] && $ACT == 'show') {
157            $lc = $this->helper->getBrowserLang();
158
159            list($translatedStartpage,) = $this->helper->buildTransID($lc, $conf['start']);
160            if (cleanID($translatedStartpage) !== cleanID($ID)) {
161                send_redirect(wl(cleanID($translatedStartpage), '', true));
162            }
163        }
164    }
165
166    /**
167     * Hook Callback. Add hreflang attributes to the page header
168     *
169     * @param Doku_Event $event TPL_METAHEADER_OUTPUT
170     */
171    public function addHrefLangAttributes(Doku_Event $event)
172    {
173        global $ID;
174        global $conf;
175
176        if (!$this->helper->isTranslatable($ID)) return;
177
178        $translations = $this->helper->getAvailableTranslations($ID);
179        if ($translations) {
180            foreach ($translations as $lc => $translation) {
181                $event->data['link'][] = [
182                    'rel' => 'alternate',
183                    'hreflang' => $lc,
184                    'href' => wl(cleanID($translation), '', true),
185                ];
186            }
187        }
188
189        $default = $conf['lang_before_translation'] ?? $conf['lang'];
190        $defaultlink = $this->helper->buildTransID($default, ($this->helper->getTransParts($ID))[1])[0];
191        $event->data['link'][] = [
192            'rel' => 'alternate',
193            'hreflang' => 'x-default',
194            'href' => wl(cleanID($defaultlink), '', true),
195        ];
196    }
197
198    /**
199     * Hook Callback. Make current language available as page template placeholder and handle
200     * original language copying
201     *
202     * @param Doku_Event $event COMMON_PAGETPL_LOAD
203     */
204    public function handlePageTemplates(Doku_Event $event)
205    {
206        global $ID;
207
208        // load orginal content as template?
209        if ($this->getConf('copytrans') && $this->helper->istranslatable($ID, false)) {
210            // look for existing translations
211            $translations = $this->helper->getAvailableTranslations($ID);
212            if ($translations) {
213                // find original language (might've been provided via parameter or use first translation)
214                $orig = (string)$_REQUEST['fromlang'];
215                if (!$orig) $orig = array_key_first($translations);
216
217                // load file
218                $origfile = $translations[$orig];
219                $event->data['tpl'] = io_readFile(wikiFN($origfile));
220
221                // prefix with warning
222                $warn = io_readFile($this->localFN('totranslate'));
223                if ($warn) $warn .= "\n\n";
224                $event->data['tpl'] = $warn . $event->data['tpl'];
225
226                // show user a choice of translations if any
227                if (count($translations) > 1) {
228                    $links = array();
229                    foreach ($translations as $t => $l) {
230                        $links[] = '<a href="' . wl($ID, array(
231                                'do' => 'edit',
232                                'fromlang' => $t,
233                            )) . '">' . $this->helper->getLocalName($t) . '</a>';
234                    }
235
236                    msg(
237                        sprintf(
238                            $this->getLang('transloaded'),
239                            $this->helper->getLocalName($orig),
240                            join(', ', $links)
241                        )
242                    );
243                }
244
245            }
246        }
247
248        // apply placeholders
249        $event->data['tpl'] = str_replace('@LANG@', $this->helper->realLC(''), $event->data['tpl']);
250        $event->data['tpl'] = str_replace('@TRANS@', $this->helper->getLangPart($ID), $event->data['tpl']);
251    }
252
253    /**
254     * Hook Callback.  Resort page match results so that results are ordered by translation, having the
255     * default language first
256     *
257     * @param Doku_Event $event SEARCH_QUERY_PAGELOOKUP
258     */
259    public function sortSearchResults(Doku_Event $event)
260    {
261        // sort into translation slots
262        $res = [];
263        foreach ($event->result as $r => $t) {
264            $tr = $this->helper->getLangPart($r);
265            if (!is_array($res["x$tr"])) $res["x$tr"] = [];
266            $res["x$tr"][] = array($r, $t);
267        }
268        // sort by translations
269        ksort($res);
270        // combine
271        $event->result = [];
272        foreach ($res as $r) {
273            foreach ($r as $l) {
274                $event->result[$l[0]] = $l[1];
275            }
276        }
277    }
278}
279