xref: /plugin/dev/LangProcessor.php (revision fe060d0d4c4c42403a614910f1d8cdcd8d360b96)
1<?php
2
3namespace dokuwiki\plugin\dev;
4
5use splitbrain\phpcli\CLI;
6
7class LangProcessor
8{
9    /** @var CLI */
10    protected $logger;
11
12    /** @var array The language keys used in the code */
13    protected $codeKeys;
14
15    /** @var array The language keys matching the configuration settings */
16    protected $settingKeys;
17
18    public function __construct(CLI $logger)
19    {
20        $this->logger = $logger;
21        $this->codeKeys = $this->findLanguageKeysInCode();
22        $this->settingKeys = $this->findLanguageKeysInSettings();
23    }
24
25    /**
26     * Remove the obsolete strings from the given lang.php
27     *
28     * @param string $file
29     * @return void
30     */
31    public function processLangFile($file)
32    {
33        $lang = [];
34        include $file;
35
36        $drop = array_diff_key($lang, $this->codeKeys);
37        if (isset($found['js']) && isset($lang['js'])) {
38            $drop['js'] = array_diff_key($lang['js'], $found['js']);
39            if ($drop['js'] === []) unset($drop['js']);
40        }
41
42        foreach ($drop as $key => $value) {
43            if (is_array($value)) {
44                foreach (array_keys($value) as $subkey) {
45                    $this->removeLangKey($file, $subkey, $key);
46                }
47            } else {
48                $this->removeLangKey($file, $key);
49            }
50        }
51    }
52
53    /**
54     * Remove obsolete string from the given settings.php
55     *
56     * @param string $file
57     * @return void
58     */
59    public function processSettingsFile($file)
60    {
61        $lang = [];
62        include $file;
63
64        $drop = array_diff_key($lang, $this->settingKeys);
65        foreach (array_keys($drop) as $key) {
66            $this->removeLangKey($file, $key);
67        }
68    }
69
70    /**
71     * Remove the given key from the given language file
72     *
73     * @param string $file
74     * @param string $key
75     * @param string $sub
76     * @return void
77     */
78    protected function removeLangKey($file, $key, $sub = '')
79    {
80        $q = '[\'"]';
81
82        if ($sub) {
83            $re = '/\$lang\[' . $q . $sub . $q . '\]\[' . $q . $key . $q . '\]/';
84        } else {
85            $re = '/\$lang\[' . $q . $key . $q . '\]/';
86        }
87
88        if (io_deleteFromFile($file, $re, true)) {
89            $this->logger->success('{key} removed from {file}', [
90                'key' => $sub ? "$sub.$key" : $key,
91                'file' => $file,
92            ]);
93        }
94    }
95
96    /**
97     * @return array
98     */
99    public function findLanguageKeysInSettings()
100    {
101        if (file_exists('./conf/metadata.php')) {
102            return $this->metaExtract('./conf/metadata.php');
103        }
104        return [];
105    }
106
107    /**
108     * Find used language keys in the actual code
109     * @return array
110     */
111    public function findLanguageKeysInCode()
112    {
113        // get all non-hidden php and js files
114        $ite = new \RecursiveIteratorIterator(
115            new \RecursiveCallbackFilterIterator(
116                new \RecursiveDirectoryIterator('.', \RecursiveDirectoryIterator::SKIP_DOTS),
117                function ($file) {
118                    /** @var \SplFileInfo $file */
119                    if ($file->isFile() && $file->getExtension() != 'php' && $file->getExtension() != 'js') {
120                        return false;
121                    }
122                    return $file->getFilename()[0] !== '.';
123                }
124            )
125        );
126
127        $found = [];
128        foreach ($ite as $file) {
129            /** @var \SplFileInfo $file */
130            $path = str_replace('\\', '/', $file->getPathname());
131            if (str_starts_with($path, './lang/')) continue; // skip language files
132            if (str_starts_with($path, './vendor/')) continue; // skip vendor files
133
134            if ($file->getExtension() == 'php') {
135                $found = array_merge($found, $this->phpExtract($path));
136            } elseif ($file->getExtension() == 'js') {
137                if (!isset($found['js'])) $found['js'] = [];
138                $found['js'] = array_merge($found['js'], $this->jsExtract($path));
139            }
140        }
141
142        // admin menu entry
143        if (is_dir('admin')) {
144            $found['menu'] = 'admin/';
145        }
146        if (file_exists('admin.php')) {
147            $found['menu'] = 'admin.php';
148        }
149
150        return $found;
151    }
152
153    /**
154     * Extract language keys from given settings file
155     *
156     * @param string $file
157     * @return array
158     */
159    public function metaExtract($file)
160    {
161        $meta = [];
162        include $file;
163
164        $found = [];
165        foreach ($meta as $key => $info) {
166            $found[$key] = $file;
167
168            if (isset($info['_choices'])) {
169                foreach ($info['_choices'] as $choice) {
170                    $found[$key . '_o_' . $choice] = $file;
171                }
172            }
173        }
174
175        return $found;
176    }
177
178    /**
179     * Extract language keys from given javascript file
180     *
181     * @param string $file
182     * @return array (key => file:line)
183     */
184    public function jsExtract($file)
185    {
186        $sep = '[\[\]\.\'"]+'; // closes and opens brackets and dots - we don't care yet
187        $any = '[^\[\]\.\'"]+'; // stuff we don't care for
188        $close = '[\]\'"]*'; // closes brackets
189
190        $dotvalue = '\.(\w+)';
191        $bracketvalue = '\[[\'"](.*?)[\'"]\]';
192
193        // https://regex101.com/r/uTjHwc/1
194        $regex = '/\bLANG' . $sep . 'plugins' . $sep . $any . $close . '(?:' . $dotvalue . '|' . $bracketvalue . ')/';
195        // echo "\n\n$regex\n\n";
196
197        return $this->extract($file, $regex);
198    }
199
200    /**
201     * Extract language keys from given php file
202     *
203     * @param string $file
204     * @return array (key => file:line)
205     */
206    public function phpExtract($file)
207    {
208        $regex = '/(?:tpl_getLang|->getLang) ?\((.*?)\)/';
209        return $this->extract($file, $regex);
210    }
211
212    /**
213     * Use the given regex to extract language keys from the given file
214     *
215     * @param string $file
216     * @param string $regex
217     * @return array
218     */
219    private function extract($file, $regex)
220    {
221        $found = [];
222        $lines = file($file);
223        foreach ($lines as $lno => $line) {
224            if (!preg_match_all($regex, $line, $matches, PREG_SET_ORDER)) {
225                continue;
226            }
227            foreach ($matches as $match) {
228                $key = $match[2] ?? $match[1];
229                $key = trim($key, '\'"');
230                if (!isset($found[$key])) {
231                    $found[$key] = $file . ':' . ($lno + 1);
232                }
233            }
234        }
235
236        return $found;
237    }
238}
239