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 | } |