Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
100.00% |
1 / 1 |
|
100.00% |
8 / 8 |
CRAP | |
100.00% |
66 / 66 |
App\Core\I18n\_m | |
100.00% |
1 / 1 |
8 | |
100.00% |
12 / 12 |
|||
I18n | |
100.00% |
1 / 1 |
|
100.00% |
7 / 7 |
26 | |
100.00% |
54 / 54 |
setTranslator | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
_mdomain | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
clientPreferredLanguage | |
100.00% |
1 / 1 |
9 | |
100.00% |
14 / 14 |
|||
getNiceLanguageList | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
isRTL | |
100.00% |
1 / 1 |
3 | |
100.00% |
4 / 4 |
|||
getAllLanguages | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
formatICU | |
100.00% |
1 / 1 |
8 | |
100.00% |
23 / 23 |
1 | <?php |
2 | |
3 | // {{{ License |
4 | |
5 | // This file is part of GNU social - https://www.gnu.org/software/social |
6 | // |
7 | // GNU social is free software: you can redistribute it and/or modify |
8 | // it under the terms of the GNU Affero General Public License as published by |
9 | // the Free Software Foundation, either version 3 of the License, or |
10 | // (at your option) any later version. |
11 | // |
12 | // GNU social is distributed in the hope that it will be useful, |
13 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 | // GNU Affero General Public License for more details. |
16 | // |
17 | // You should have received a copy of the GNU Affero General Public License |
18 | // along with GNU social. If not, see <http://www.gnu.org/licenses/>. |
19 | |
20 | // }}} |
21 | |
22 | /** |
23 | * Utility functions for i18n |
24 | * |
25 | * @category I18n |
26 | * @package GNU social |
27 | * |
28 | * @author Matthew Gregg <matthew.gregg@gmail.com> |
29 | * @author Ciaran Gultnieks <ciaran@ciarang.com> |
30 | * @author Evan Prodromou <evan@status.net> |
31 | * @author Diogo Cordeiro <diogo@fc.up.pt> |
32 | * @author Hugo Sales <hugo@hsal.es> |
33 | * @copyright 2010, 2018-2021 Free Software Foundation, Inc http://www.fsf.org |
34 | * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later |
35 | */ |
36 | |
37 | namespace App\Core\I18n; |
38 | |
39 | use App\Util\Exception\ServerException; |
40 | use App\Util\Formatting; |
41 | use InvalidArgumentException; |
42 | use Symfony\Contracts\Translation\TranslatorInterface; |
43 | |
44 | // Locale category constants are usually predefined, but may not be |
45 | // on some systems such as Win32. |
46 | $LC_CATEGORIES = [ |
47 | 'LC_CTYPE', |
48 | 'LC_NUMERIC', |
49 | 'LC_TIME', |
50 | 'LC_COLLATE', |
51 | 'LC_MONETARY', |
52 | 'LC_MESSAGES', |
53 | 'LC_ALL', |
54 | ]; |
55 | foreach ($LC_CATEGORIES as $key => $name) { |
56 | if (!defined($name)) { |
57 | define($name, $key); |
58 | } |
59 | } |
60 | |
61 | abstract class I18n |
62 | { |
63 | public static ?TranslatorInterface $translator = null; |
64 | |
65 | public static function setTranslator($trans): void |
66 | { |
67 | self::$translator = $trans; |
68 | } |
69 | |
70 | /** |
71 | * Looks for which plugin we've been called from to get the gettext domain; |
72 | * if not in a plugin subdirectory, we'll use the default 'core+intl-icu'. |
73 | * |
74 | * @param string $path |
75 | * |
76 | * @throws ServerException |
77 | * |
78 | * @return string |
79 | */ |
80 | public static function _mdomain(string $path): string |
81 | { |
82 | /* |
83 | 0 => |
84 | array |
85 | 'file' => string '/var/www/mublog/plugins/FeedSub/FeedSubPlugin.php' (length=49) |
86 | 'line' => int 77 |
87 | 'function' => string '_m' (length=2) |
88 | 'args' => |
89 | array |
90 | 0 => &string 'Feeds' (length=5) |
91 | */ |
92 | static $cached; |
93 | if (!isset($cached[$path])) { |
94 | $path = Formatting::normalizePath($path); |
95 | $cached[$path] = Formatting::moduleFromPath($path); |
96 | } |
97 | return $cached[$path] ?? 'core+intl-icu'; |
98 | } |
99 | |
100 | /** |
101 | * Content negotiation for language codes. Gets our highest rated translation language that the client accepts |
102 | * |
103 | * @param string $http_accept_lang_header HTTP Accept-Language header |
104 | * |
105 | * @return string language code for best language match, false otherwise |
106 | */ |
107 | public static function clientPreferredLanguage(string $http_accept_lang_header): string | bool |
108 | { |
109 | $client_langs = []; |
110 | $all_languages = self::getAllLanguages(); |
111 | |
112 | preg_match_all('"(((\S\S)-?(\S\S)?)(;q=([0-9.]+))?)\s*(,\s*|$)"', |
113 | mb_strtolower($http_accept_lang_header), $http_langs); |
114 | |
115 | for ($i = 0; $i < count($http_langs); ++$i) { |
116 | if (!empty($http_langs[2][$i])) { |
117 | // if no q default to 1.0 |
118 | $client_langs[$http_langs[2][$i]] = ($http_langs[6][$i] ? (float) $http_langs[6][$i] : 1.0 - ($i * 0.01)); |
119 | } |
120 | if (!empty($http_langs[3][$i]) && empty($client_langs[$http_langs[3][$i]])) { |
121 | // if a catchall default 0.01 lower |
122 | $client_langs[$http_langs[3][$i]] = ($http_langs[6][$i] ? (float) $http_langs[6][$i] - 0.01 : 0.99); |
123 | } |
124 | } |
125 | // sort in descending q |
126 | arsort($client_langs); |
127 | |
128 | foreach ($client_langs as $lang => $q) { |
129 | if (isset($all_languages[$lang])) { |
130 | return $all_languages[$lang]['lang']; |
131 | } |
132 | } |
133 | return false; |
134 | } |
135 | |
136 | /** |
137 | * returns a simple code -> name mapping for languages |
138 | * |
139 | * @return array map of available languages by code to language name. |
140 | */ |
141 | public static function getNiceLanguageList(): array |
142 | { |
143 | $nice_lang = []; |
144 | $all_languages = self::getAllLanguages(); |
145 | |
146 | foreach ($all_languages as $lang) { |
147 | $nice_lang[$lang['lang']] = $lang['name']; |
148 | } |
149 | return $nice_lang; |
150 | } |
151 | |
152 | /** |
153 | * Check whether a language is right-to-left |
154 | * |
155 | * @param string $lang_value language code of the language to check |
156 | * |
157 | * @return bool true if language is rtl |
158 | */ |
159 | public static function isRTL(string $lang_value): bool |
160 | { |
161 | foreach (self::getAllLanguages() as $code => $info) { |
162 | if ($lang_value == $info['lang']) { |
163 | return $info['direction'] == 'rtl'; |
164 | } |
165 | } |
166 | throw new InvalidArgumentException('is_rtl function received an invalid lang to test. Lang was: ' . $lang_value); |
167 | } |
168 | |
169 | /** |
170 | * Get a list of all languages that are enabled in the default config |
171 | * |
172 | * @return array mapping of language codes to language info |
173 | */ |
174 | public static function getAllLanguages(): array |
175 | { |
176 | return [ |
177 | 'af' => ['q' => 0.8, 'lang' => 'af', 'name' => 'Afrikaans', 'direction' => 'ltr'], |
178 | 'ar' => ['q' => 0.8, 'lang' => 'ar', 'name' => 'Arabic', 'direction' => 'rtl'], |
179 | 'ast' => ['q' => 1, 'lang' => 'ast', 'name' => 'Asturian', 'direction' => 'ltr'], |
180 | 'eu' => ['q' => 1, 'lang' => 'eu', 'name' => 'Basque', 'direction' => 'ltr'], |
181 | 'be-tarask' => ['q' => 0.5, 'lang' => 'be-tarask', 'name' => 'Belarusian (Taraškievica orthography)', 'direction' => 'ltr'], |
182 | 'br' => ['q' => 0.8, 'lang' => 'br', 'name' => 'Breton', 'direction' => 'ltr'], |
183 | 'bg' => ['q' => 0.8, 'lang' => 'bg', 'name' => 'Bulgarian', 'direction' => 'ltr'], |
184 | 'my' => ['q' => 1, 'lang' => 'my', 'name' => 'Burmese', 'direction' => 'ltr'], |
185 | 'ca' => ['q' => 0.5, 'lang' => 'ca', 'name' => 'Catalan', 'direction' => 'ltr'], |
186 | 'zh-cn' => ['q' => 0.9, 'lang' => 'zh_CN', 'name' => 'Chinese (Simplified)', 'direction' => 'ltr'], |
187 | 'zh-hant' => ['q' => 0.2, 'lang' => 'zh_TW', 'name' => 'Chinese (Taiwanese)', 'direction' => 'ltr'], |
188 | 'ksh' => ['q' => 1, 'lang' => 'ksh', 'name' => 'Colognian', 'direction' => 'ltr'], |
189 | 'cs' => ['q' => 0.5, 'lang' => 'cs', 'name' => 'Czech', 'direction' => 'ltr'], |
190 | 'da' => ['q' => 0.8, 'lang' => 'da', 'name' => 'Danish', 'direction' => 'ltr'], |
191 | 'nl' => ['q' => 0.5, 'lang' => 'nl', 'name' => 'Dutch', 'direction' => 'ltr'], |
192 | 'arz' => ['q' => 0.8, 'lang' => 'arz', 'name' => 'Egyptian Spoken Arabic', 'direction' => 'rtl'], |
193 | 'en' => ['q' => 1, 'lang' => 'en', 'name' => 'English', 'direction' => 'ltr'], |
194 | 'en-us' => ['q' => 1, 'lang' => 'en', 'name' => 'English (US)', 'direction' => 'ltr'], |
195 | 'en-gb' => ['q' => 1, 'lang' => 'en_GB', 'name' => 'English (UK)', 'direction' => 'ltr'], |
196 | 'eo' => ['q' => 0.8, 'lang' => 'eo', 'name' => 'Esperanto', 'direction' => 'ltr'], |
197 | 'fi' => ['q' => 1, 'lang' => 'fi', 'name' => 'Finnish', 'direction' => 'ltr'], |
198 | 'fr' => ['q' => 1, 'lang' => 'fr', 'name' => 'French', 'direction' => 'ltr'], |
199 | 'fr-fr' => ['q' => 1, 'lang' => 'fr', 'name' => 'French (France)', 'direction' => 'ltr'], |
200 | 'fur' => ['q' => 0.8, 'lang' => 'fur', 'name' => 'Friulian', 'direction' => 'ltr'], |
201 | 'gl' => ['q' => 0.8, 'lang' => 'gl', 'name' => 'Galician', 'direction' => 'ltr'], |
202 | 'ka' => ['q' => 0.8, 'lang' => 'ka', 'name' => 'Georgian', 'direction' => 'ltr'], |
203 | 'de' => ['q' => 0.8, 'lang' => 'de', 'name' => 'German', 'direction' => 'ltr'], |
204 | 'el' => ['q' => 0.1, 'lang' => 'el', 'name' => 'Greek', 'direction' => 'ltr'], |
205 | 'he' => ['q' => 0.5, 'lang' => 'he', 'name' => 'Hebrew', 'direction' => 'rtl'], |
206 | 'hu' => ['q' => 0.8, 'lang' => 'hu', 'name' => 'Hungarian', 'direction' => 'ltr'], |
207 | 'is' => ['q' => 0.1, 'lang' => 'is', 'name' => 'Icelandic', 'direction' => 'ltr'], |
208 | 'id' => ['q' => 1, 'lang' => 'id', 'name' => 'Indonesian', 'direction' => 'ltr'], |
209 | 'ia' => ['q' => 0.8, 'lang' => 'ia', 'name' => 'Interlingua', 'direction' => 'ltr'], |
210 | 'ga' => ['q' => 0.5, 'lang' => 'ga', 'name' => 'Irish', 'direction' => 'ltr'], |
211 | 'it' => ['q' => 1, 'lang' => 'it', 'name' => 'Italian', 'direction' => 'ltr'], |
212 | 'ja' => ['q' => 0.5, 'lang' => 'ja', 'name' => 'Japanese', 'direction' => 'ltr'], |
213 | 'ko' => ['q' => 0.9, 'lang' => 'ko', 'name' => 'Korean', 'direction' => 'ltr'], |
214 | 'lv' => ['q' => 1, 'lang' => 'lv', 'name' => 'Latvian', 'direction' => 'ltr'], |
215 | 'lt' => ['q' => 1, 'lang' => 'lt', 'name' => 'Lithuanian', 'direction' => 'ltr'], |
216 | 'lb' => ['q' => 1, 'lang' => 'lb', 'name' => 'Luxembourgish', 'direction' => 'ltr'], |
217 | 'mk' => ['q' => 0.5, 'lang' => 'mk', 'name' => 'Macedonian', 'direction' => 'ltr'], |
218 | 'mg' => ['q' => 1, 'lang' => 'mg', 'name' => 'Malagasy', 'direction' => 'ltr'], |
219 | 'ms' => ['q' => 1, 'lang' => 'ms', 'name' => 'Malay', 'direction' => 'ltr'], |
220 | 'ml' => ['q' => 0.5, 'lang' => 'ml', 'name' => 'Malayalam', 'direction' => 'ltr'], |
221 | 'ne' => ['q' => 1, 'lang' => 'ne', 'name' => 'Nepali', 'direction' => 'ltr'], |
222 | 'nb' => ['q' => 0.1, 'lang' => 'nb', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'], |
223 | 'no' => ['q' => 0.1, 'lang' => 'nb', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'], |
224 | 'nn' => ['q' => 1, 'lang' => 'nn', 'name' => 'Norwegian (Nynorsk)', 'direction' => 'ltr'], |
225 | 'fa' => ['q' => 1, 'lang' => 'fa', 'name' => 'Persian', 'direction' => 'rtl'], |
226 | 'pl' => ['q' => 0.5, 'lang' => 'pl', 'name' => 'Polish', 'direction' => 'ltr'], |
227 | 'pt' => ['q' => 1, 'lang' => 'pt', 'name' => 'Portuguese', 'direction' => 'ltr'], |
228 | 'pt-br' => ['q' => 0.9, 'lang' => 'pt_BR', 'name' => 'Brazilian Portuguese', 'direction' => 'ltr'], |
229 | 'ru' => ['q' => 0.9, 'lang' => 'ru', 'name' => 'Russian', 'direction' => 'ltr'], |
230 | 'sr-ec' => ['q' => 1, 'lang' => 'sr-ec', 'name' => 'Serbian', 'direction' => 'ltr'], |
231 | 'es' => ['q' => 1, 'lang' => 'es', 'name' => 'Spanish', 'direction' => 'ltr'], |
232 | 'sv' => ['q' => 0.8, 'lang' => 'sv', 'name' => 'Swedish', 'direction' => 'ltr'], |
233 | 'tl' => ['q' => 0.8, 'lang' => 'tl', 'name' => 'Tagalog', 'direction' => 'ltr'], |
234 | 'ta' => ['q' => 1, 'lang' => 'ta', 'name' => 'Tamil', 'direction' => 'ltr'], |
235 | 'te' => ['q' => 0.3, 'lang' => 'te', 'name' => 'Telugu', 'direction' => 'ltr'], |
236 | 'tr' => ['q' => 0.5, 'lang' => 'tr', 'name' => 'Turkish', 'direction' => 'ltr'], |
237 | 'uk' => ['q' => 1, 'lang' => 'uk', 'name' => 'Ukrainian', 'direction' => 'ltr'], |
238 | 'hsb' => ['q' => 0.8, 'lang' => 'hsb', 'name' => 'Upper Sorbian', 'direction' => 'ltr'], |
239 | 'ur' => ['q' => 1, 'lang' => 'ur_PK', 'name' => 'Urdu (Pakistan)', 'direction' => 'rtl'], |
240 | 'vi' => ['q' => 0.8, 'lang' => 'vi', 'name' => 'Vietnamese', 'direction' => 'ltr'], |
241 | ]; |
242 | } |
243 | |
244 | /** |
245 | * Format the given associative array $messages in the ICU |
246 | * translation format, with the given $params. Allows for a |
247 | * declarative use of the translation engine, for example |
248 | * `formatICU(['she' => ['She has one foo', 'She has many foo'], |
249 | * 'he' => ['He has one foo', 'He has many foo']], ['she' => 1])` |
250 | * |
251 | * @see http://userguide.icu-project.org/formatparse/messages |
252 | */ |
253 | public static function formatICU(array $messages, array $params): string |
254 | { |
255 | $res = ''; |
256 | foreach (array_slice($params, 0, 1, true) as $var => $type) { |
257 | if (is_int($type)) { |
258 | $pref = '='; |
259 | $op = 'plural'; |
260 | } elseif (is_string($type)) { |
261 | $pref = ''; |
262 | $op = 'select'; |
263 | } else { |
264 | throw new InvalidArgumentException('Invalid variable type. (int|string) only'); |
265 | } |
266 | |
267 | $res = "{$var}, {$op}, "; |
268 | $i = 0; |
269 | $cnt = count($messages) - 1; |
270 | foreach ($messages as $val => $m) { |
271 | if ($i !== $cnt) { |
272 | $res .= "{$pref}{$val}"; |
273 | } else { |
274 | $res .= 'other'; |
275 | } |
276 | |
277 | if (is_array($m)) { |
278 | $res .= ' {' . self::formatICU($m, array_slice($params, 1, null, true)) . '} '; |
279 | } elseif (is_string($m)) { |
280 | $res .= " {{$m}} "; |
281 | } else { |
282 | throw new InvalidArgumentException('Invalid message array'); |
283 | } |
284 | ++$i; |
285 | } |
286 | } |
287 | return "{{$res}}"; |
288 | } |
289 | } |
290 | |
291 | /** |
292 | * Wrapper for symfony translation with smart domain detection. |
293 | * |
294 | * If calling from a plugin, this function checks which plugin it was |
295 | * being called from and uses that as text domain, which will have |
296 | * been set up during plugin initialization. |
297 | * |
298 | * Also handles plurals and contexts depending on what parameters |
299 | * are passed to it: |
300 | * |
301 | * _m(string $msg) -- simple message |
302 | * _m(string $ctx, string $msg) -- message with context |
303 | * _m(string|string[] $msg, array $params) -- message |
304 | * _m(string $ctx, string|string[] $msg, array $params) -- combination of the previous two |
305 | * |
306 | * @param mixed ...$args |
307 | * |
308 | * @throws ServerException |
309 | * |
310 | * @return string |
311 | * |
312 | * @todo add parameters |
313 | */ |
314 | function _m(...$args): string |
315 | { |
316 | // Get the file where this function was called from, reducing the |
317 | // memory and performance impact by not returning the arguments, |
318 | // and only 2 frames (this and previous) |
319 | $domain = I18n::_mdomain(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)[0]['file'], 2); |
320 | switch (count($args)) { |
321 | case 1: |
322 | // Empty parameters, simple message |
323 | return I18n::$translator->trans($args[0], [], $domain); |
324 | case 3: |
325 | // @codeCoverageIgnoreStart |
326 | if (is_int($args[2])) { |
327 | throw new InvalidArgumentException('Calling `_m()` with a number for pluralization is deprecated, ' . |
328 | 'use an explicit parameter'); |
329 | } |
330 | // @codeCoverageIgnoreEnd |
331 | // Falthrough |
332 | // no break |
333 | case 2: |
334 | if (is_array($args[0])) { |
335 | $args[0] = I18n::formatICU($args[0], $args[1]); |
336 | } |
337 | |
338 | if (is_string($args[0])) { |
339 | $msg = $args[0]; |
340 | $params = $args[1] ?? []; |
341 | return I18n::$translator->trans($msg, $params, $domain); |
342 | } |
343 | // Fallthrough |
344 | // no break |
345 | default: |
346 | // @codeCoverageIgnoreStart |
347 | throw new InvalidArgumentException("Bad parameters to `_m()` for domain {$domain}"); |
348 | // @codeCoverageIgnoreEnd |
349 | } |
350 | } |