Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
98.35% |
119 / 121 |
|
84.62% |
11 / 13 |
CRAP | |
0.00% |
0 / 1 |
LanguageHelper | |
98.35% |
119 / 121 |
|
84.62% |
11 / 13 |
42 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getHelpFiles | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
getLanguages | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
findMissingLanguageStrings | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
compareLanguages | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getLangName | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
5 | |||
getTextDomains | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
8.02 | |||
loadLanguage | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
6 | |||
findDuplicatedValues | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getLanguageDetails | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
getAllLanguageDetails | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
summarizeData | |
95.24% |
20 / 21 |
|
0.00% |
0 / 1 |
5 | |||
getAllDetails | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | /** |
4 | * Language Helper for Development Tools Controller |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2015. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package DevTools |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/indexing:alphabetical_heading_browse Wiki |
28 | */ |
29 | |
30 | namespace VuFindDevTools; |
31 | |
32 | use Laminas\I18n\Translator\TextDomain; |
33 | use VuFind\I18n\Translator\Loader\ExtendedIni; |
34 | |
35 | use function count; |
36 | use function in_array; |
37 | |
38 | /** |
39 | * Language Helper for Development Tools Controller |
40 | * |
41 | * @category VuFind |
42 | * @package DevTools |
43 | * @author Demian Katz <demian.katz@villanova.edu> |
44 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
45 | * @link https://vufind.org/wiki/indexing:alphabetical_heading_browse Wiki |
46 | */ |
47 | class LanguageHelper |
48 | { |
49 | /** |
50 | * Language loader |
51 | * |
52 | * @var ExtendedIni |
53 | */ |
54 | protected $loader; |
55 | |
56 | /** |
57 | * Configured languages (code => description) |
58 | * |
59 | * @var string[] |
60 | */ |
61 | protected $configuredLanguages; |
62 | |
63 | /** |
64 | * Constructor |
65 | * |
66 | * @param ExtendedIni $loader Language loader |
67 | * @param array $langs Configured languages (code => description) |
68 | */ |
69 | public function __construct(ExtendedIni $loader, array $langs = []) |
70 | { |
71 | $this->loader = $loader; |
72 | $this->configuredLanguages = $langs; |
73 | } |
74 | |
75 | /** |
76 | * Get a list of help files in the specified language. |
77 | * |
78 | * @param string $language Language to check. |
79 | * |
80 | * @return array |
81 | */ |
82 | protected function getHelpFiles($language) |
83 | { |
84 | $dir = APPLICATION_PATH |
85 | . '/themes/root/templates/HelpTranslations/' . $language; |
86 | if (!file_exists($dir) || !is_dir($dir)) { |
87 | return []; |
88 | } |
89 | $handle = opendir($dir); |
90 | $files = []; |
91 | while ($file = readdir($handle)) { |
92 | if (str_ends_with($file, '.phtml')) { |
93 | $files[] = $file; |
94 | } |
95 | } |
96 | closedir($handle); |
97 | return $files; |
98 | } |
99 | |
100 | /** |
101 | * Get a list of languages supported by VuFind: |
102 | * |
103 | * @return array |
104 | */ |
105 | protected function getLanguages(): array |
106 | { |
107 | $langs = []; |
108 | $dir = opendir(APPLICATION_PATH . '/languages'); |
109 | while ($file = readdir($dir)) { |
110 | if (str_ends_with($file, '.ini')) { |
111 | $lang = current(explode('.', $file)); |
112 | if ('native' != $lang) { |
113 | $langs[] = $lang; |
114 | } |
115 | } |
116 | } |
117 | closedir($dir); |
118 | return $langs; |
119 | } |
120 | |
121 | /** |
122 | * Find strings that are absent from a language file. |
123 | * |
124 | * @param TextDomain $lang1 Left side of comparison |
125 | * @param TextDomain $lang2 Right side of comparison |
126 | * |
127 | * @return array |
128 | */ |
129 | protected function findMissingLanguageStrings(TextDomain $lang1, TextDomain $lang2): array |
130 | { |
131 | // Find strings missing from language 2: |
132 | return array_values( |
133 | array_diff(array_keys((array)$lang1), array_keys((array)$lang2)) |
134 | ); |
135 | } |
136 | |
137 | /** |
138 | * Compare two languages and return an array of details about how they differ. |
139 | * |
140 | * @param TextDomain $lang1 Left side of comparison |
141 | * @param TextDomain $lang2 Right side of comparison |
142 | * @param TextDomain $lang1NoAliases Left side of comparison (with aliases disabled) |
143 | * @param TextDomain $lang2NoAliases Right side of comparison (with aliases disabled) |
144 | * |
145 | * @return array |
146 | */ |
147 | public function compareLanguages( |
148 | TextDomain $lang1, |
149 | TextDomain $lang2, |
150 | TextDomain $lang1NoAliases, |
151 | TextDomain $lang2NoAliases |
152 | ): array { |
153 | // We don't want to double-count aliased terms, nor do we want to count alias |
154 | // overrides as "extra lines". Thus, we find meaningful differences by subtracting |
155 | // the aliased data of one language from the non-aliased data of the other. |
156 | return [ |
157 | 'notInL1' => $this->findMissingLanguageStrings($lang2NoAliases, $lang1), |
158 | 'notInL2' => $this->findMissingLanguageStrings($lang1NoAliases, $lang2), |
159 | 'l1Percent' => number_format(count($lang1) / count($lang2) * 100, 2), |
160 | 'l2Percent' => number_format(count($lang2) / count($lang1) * 100, 2), |
161 | ]; |
162 | } |
163 | |
164 | /** |
165 | * Get English name of language |
166 | * |
167 | * @param string $lang Language code |
168 | * |
169 | * @return string |
170 | */ |
171 | public function getLangName($lang) |
172 | { |
173 | if (isset($this->configuredLanguages[$lang])) { |
174 | return $this->configuredLanguages[$lang]; |
175 | } |
176 | switch ($lang) { |
177 | case 'en-gb': |
178 | return 'British English'; |
179 | case 'pt-br': |
180 | return 'Brazilian Portuguese'; |
181 | default: |
182 | return $lang; |
183 | } |
184 | } |
185 | |
186 | /** |
187 | * Get text domains for a language. |
188 | * |
189 | * @param bool $includeOptional Include optional translations (e.g. DDC23) |
190 | * |
191 | * @return array |
192 | */ |
193 | protected function getTextDomains($includeOptional) |
194 | { |
195 | static $domains = false; |
196 | if (!$domains) { |
197 | $filter = $includeOptional |
198 | ? [] |
199 | : ['CallNumberFirst', 'CreatorRoles', 'DDC23', 'ISO639-3']; |
200 | $base = APPLICATION_PATH . '/languages'; |
201 | $dir = opendir($base); |
202 | $domains = []; |
203 | while ($current = readdir($dir)) { |
204 | if ( |
205 | $current != '.' && $current != '..' |
206 | && is_dir("$base/$current") |
207 | && !in_array($current, $filter) |
208 | ) { |
209 | $domains[] = $current; |
210 | } |
211 | } |
212 | closedir($dir); |
213 | } |
214 | return $domains; |
215 | } |
216 | |
217 | /** |
218 | * Load a language, including text domains. |
219 | * |
220 | * @param string $lang Language to load |
221 | * @param bool $includeOptional Include optional translations (e.g. DDC23) |
222 | * @param bool $includeAliases Include alias details |
223 | * |
224 | * @return array |
225 | */ |
226 | protected function loadLanguage($lang, $includeOptional, $includeAliases = true) |
227 | { |
228 | $includeAliases ? $this->loader->enableAliases() : $this->loader->disableAliases(); |
229 | $base = $this->loader->load($lang, null); |
230 | foreach ($this->getTextDomains($includeOptional) as $domain) { |
231 | $current = $this->loader->load($lang, $domain); |
232 | foreach ($current as $k => $v) { |
233 | if ($k != '@parent_ini') { |
234 | $base["$domain::$k"] = $v; |
235 | } |
236 | } |
237 | } |
238 | if (isset($base['@parent_ini'])) { |
239 | // don't count macros in comparison: |
240 | unset($base['@parent_ini']); |
241 | } |
242 | return $base; |
243 | } |
244 | |
245 | /** |
246 | * Find duplicated values within the language. |
247 | * |
248 | * @param TextDomain $lang Language to analyze. |
249 | * |
250 | * @return array |
251 | */ |
252 | protected function findDuplicatedValues(TextDomain $lang): array |
253 | { |
254 | $index = []; |
255 | foreach ($lang as $key => $val) { |
256 | $index[$val] = array_merge($index[$val] ?? [], [$key]); |
257 | } |
258 | $callback = function ($set) { |
259 | return count($set) > 1; |
260 | }; |
261 | return array_filter($index, $callback); |
262 | } |
263 | |
264 | /** |
265 | * Return details on how $langCode differs from $main. |
266 | * |
267 | * @param TextDomain $main The main language (full details) |
268 | * @param TextDomain $mainNoAliases The main language (with aliases disabled) |
269 | * @param string $langCode The code of a language to compare against $main |
270 | * @param bool $includeOptional Include optional translations (e.g. DDC23) |
271 | * |
272 | * @return array |
273 | */ |
274 | protected function getLanguageDetails( |
275 | TextDomain $main, |
276 | TextDomain $mainNoAliases, |
277 | string $langCode, |
278 | bool $includeOptional |
279 | ): array { |
280 | $lang = $this->loadLanguage($langCode, $includeOptional); |
281 | $langNoAliases = $this->loadLanguage($langCode, $includeOptional, false); |
282 | $details = $this->compareLanguages($main, $lang, $mainNoAliases, $langNoAliases); |
283 | $details['dupes'] = $this->findDuplicatedValues($langNoAliases); |
284 | $details['object'] = $lang; |
285 | $details['name'] = $this->getLangName($langCode); |
286 | $details['helpFiles'] = $this->getHelpFiles($langCode); |
287 | return $details; |
288 | } |
289 | |
290 | /** |
291 | * Return details on how all languages differ from $main. |
292 | * |
293 | * @param TextDomain $main The main language (full details) |
294 | * @param TextDomain $mainNoAliases The main language (with aliases disabled) |
295 | * @param bool $includeOptional Include optional translations (e.g. DDC23) |
296 | * |
297 | * @return array |
298 | */ |
299 | protected function getAllLanguageDetails(TextDomain $main, TextDomain $mainNoAliases, bool $includeOptional): array |
300 | { |
301 | $details = []; |
302 | $allLangs = $this->getLanguages(); |
303 | sort($allLangs); |
304 | foreach ($allLangs as $langCode) { |
305 | $details[$langCode] = $this |
306 | ->getLanguageDetails($main, $mainNoAliases, $langCode, $includeOptional); |
307 | } |
308 | return $details; |
309 | } |
310 | |
311 | /** |
312 | * Create summary data for use in the tabular display. |
313 | * |
314 | * @param array $details Full details from getAllLanguageDetails() |
315 | * |
316 | * @return array |
317 | */ |
318 | protected function summarizeData($details) |
319 | { |
320 | $data = []; |
321 | foreach ($details as $langCode => $diffs) { |
322 | if ($diffs['l2Percent'] > 90) { |
323 | $progressLevel = 'info'; |
324 | } elseif ($diffs['l2Percent'] > 70) { |
325 | $progressLevel = 'warning'; |
326 | } else { |
327 | $progressLevel = 'danger'; |
328 | } |
329 | $data[] = [ |
330 | 'lang' => $langCode, |
331 | 'name' => $diffs['name'], |
332 | 'dupes' => $diffs['dupes'], |
333 | 'langtitle' => $langCode . (($langCode != $diffs['name']) |
334 | ? ' (' . $diffs['name'] . ')' : ''), |
335 | 'missing' => count($diffs['notInL2']), |
336 | 'extra' => count($diffs['notInL1']), |
337 | 'percent' => $diffs['l2Percent'], |
338 | 'countfiles' => count($diffs['helpFiles']), |
339 | 'files' => $diffs['helpFiles'], |
340 | 'progresslevel' => $progressLevel, |
341 | ]; |
342 | } |
343 | return $data; |
344 | } |
345 | |
346 | /** |
347 | * Return language comparison information, using $mainLanguage as the |
348 | * baseline. |
349 | * |
350 | * @param string $mainLanguage Language code |
351 | * @param bool $includeOptional Include optional translations (e.g. DDC23) |
352 | * |
353 | * @return array |
354 | */ |
355 | public function getAllDetails($mainLanguage, $includeOptional = true) |
356 | { |
357 | $main = $this->loadLanguage($mainLanguage, $includeOptional); |
358 | $mainNoAliases = $this->loadLanguage($mainLanguage, $includeOptional, false); |
359 | $details = $this->getAllLanguageDetails($main, $mainNoAliases, $includeOptional); |
360 | $dirHelpParts = [ |
361 | APPLICATION_PATH, 'themes', 'root', 'templates', 'HelpTranslations', |
362 | ]; |
363 | $dirLangParts = [APPLICATION_PATH, 'languages']; |
364 | return compact('details', 'main', 'includeOptional') + [ |
365 | 'dirHelp' => implode(DIRECTORY_SEPARATOR, $dirHelpParts) |
366 | . DIRECTORY_SEPARATOR, |
367 | 'dirLang' => implode(DIRECTORY_SEPARATOR, $dirLangParts) |
368 | . DIRECTORY_SEPARATOR, |
369 | 'mainCode' => $mainLanguage, |
370 | 'mainName' => $this->getLangName($mainLanguage), |
371 | 'summaryData' => $this->summarizeData($details), |
372 | ]; |
373 | } |
374 | } |