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